feat(cli): support bundle: dump healthcheck summary (#12963)

* refactor(codersdk): extract common fields from HealthReport and friends
* feat(codersdk/healthsdk): add Summarize() method
* feat(cli): support bundle: dump healthcheck summary
This commit is contained in:
Cian Johnston
2024-04-16 13:31:56 +01:00
committed by GitHub
parent 06e042acfa
commit 407e61ecd4
13 changed files with 590 additions and 242 deletions

View File

@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"net/http"
"strings"
"time"
"golang.org/x/xerrors"
@ -95,6 +96,7 @@ func (c *HealthClient) PutHealthSettings(ctx context.Context, settings HealthSet
return nil
}
// HealthcheckReport contains information about the health status of a Coder deployment.
type HealthcheckReport struct {
// Time is the time the report was generated at.
Time time.Time `json:"time" format:"date-time"`
@ -117,52 +119,96 @@ type HealthcheckReport struct {
CoderVersion string `json:"coder_version"`
}
// Summarize returns a summary of all errors and warnings of components of HealthcheckReport.
func (r *HealthcheckReport) Summarize() []string {
var msgs []string
msgs = append(msgs, r.AccessURL.Summarize("Access URL:")...)
msgs = append(msgs, r.Database.Summarize("Database:")...)
msgs = append(msgs, r.DERP.Summarize("DERP:")...)
msgs = append(msgs, r.ProvisionerDaemons.Summarize("Provisioner Daemons:")...)
msgs = append(msgs, r.Websocket.Summarize("Websocket:")...)
msgs = append(msgs, r.WorkspaceProxy.Summarize("Workspace Proxies:")...)
return msgs
}
// BaseReport holds fields common to various health reports.
type BaseReport struct {
Error *string `json:"error"`
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
Warnings []health.Message `json:"warnings"`
Dismissed bool `json:"dismissed"`
}
// Summarize returns a list of strings containing the errors and warnings of BaseReport, if present.
// All strings are prefixed with prefix.
func (b *BaseReport) Summarize(prefix string) []string {
if b == nil {
return []string{}
}
var msgs []string
if b.Error != nil {
var sb strings.Builder
if prefix != "" {
_, _ = sb.WriteString(prefix)
_, _ = sb.WriteString(" ")
}
_, _ = sb.WriteString("Error: ")
_, _ = sb.WriteString(*b.Error)
msgs = append(msgs, sb.String())
}
for _, warn := range b.Warnings {
var sb strings.Builder
if prefix != "" {
_, _ = sb.WriteString(prefix)
_, _ = sb.WriteString(" ")
}
_, _ = sb.WriteString("Warn: ")
_, _ = sb.WriteString(warn.String())
msgs = append(msgs, sb.String())
}
return msgs
}
// AccessURLReport shows the results of performing a HTTP_GET to the /healthz endpoint through the configured access URL.
type AccessURLReport struct {
BaseReport
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
Healthy bool `json:"healthy"`
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
Warnings []health.Message `json:"warnings"`
Dismissed bool `json:"dismissed"`
AccessURL string `json:"access_url"`
Reachable bool `json:"reachable"`
StatusCode int `json:"status_code"`
HealthzResponse string `json:"healthz_response"`
Error *string `json:"error"`
Healthy bool `json:"healthy"`
AccessURL string `json:"access_url"`
Reachable bool `json:"reachable"`
StatusCode int `json:"status_code"`
HealthzResponse string `json:"healthz_response"`
}
// DERPHealthReport includes health details of each configured DERP/STUN region.
type DERPHealthReport struct {
BaseReport
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
Healthy bool `json:"healthy"`
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
Warnings []health.Message `json:"warnings"`
Dismissed bool `json:"dismissed"`
Regions map[int]*DERPRegionReport `json:"regions"`
Netcheck *netcheck.Report `json:"netcheck"`
NetcheckErr *string `json:"netcheck_err"`
NetcheckLogs []string `json:"netcheck_logs"`
Error *string `json:"error"`
Healthy bool `json:"healthy"`
Regions map[int]*DERPRegionReport `json:"regions"`
Netcheck *netcheck.Report `json:"netcheck"`
NetcheckErr *string `json:"netcheck_err"`
NetcheckLogs []string `json:"netcheck_logs"`
}
// DERPHealthReport includes health details of each node in a single region.
type DERPRegionReport struct {
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
Healthy bool `json:"healthy"`
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
Warnings []health.Message `json:"warnings"`
Healthy bool `json:"healthy"`
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
Warnings []health.Message `json:"warnings"`
Error *string `json:"error"`
Region *tailcfg.DERPRegion `json:"region"`
NodeReports []*DERPNodeReport `json:"node_reports"`
Error *string `json:"error"`
}
// DERPHealthReport includes health details of a single node in a single region.
type DERPNodeReport struct {
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
Healthy bool `json:"healthy"`
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
Warnings []health.Message `json:"warnings"`
Error *string `json:"error"`
Node *tailcfg.DERPNode `json:"node"`
@ -173,37 +219,31 @@ type DERPNodeReport struct {
UsesWebsocket bool `json:"uses_websocket"`
ClientLogs [][]string `json:"client_logs"`
ClientErrs [][]string `json:"client_errs"`
Error *string `json:"error"`
STUN STUNReport `json:"stun"`
}
// STUNReport contains information about a given node's STUN capabilities.
type STUNReport struct {
Enabled bool
CanSTUN bool
Error *string
}
// DatabaseReport shows the results of pinging the configured database.Conn.
type DatabaseReport struct {
BaseReport
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
Healthy bool `json:"healthy"`
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
Warnings []health.Message `json:"warnings"`
Dismissed bool `json:"dismissed"`
Reachable bool `json:"reachable"`
Latency string `json:"latency"`
LatencyMS int64 `json:"latency_ms"`
ThresholdMS int64 `json:"threshold_ms"`
Error *string `json:"error"`
Healthy bool `json:"healthy"`
Reachable bool `json:"reachable"`
Latency string `json:"latency"`
LatencyMS int64 `json:"latency_ms"`
ThresholdMS int64 `json:"threshold_ms"`
}
// ProvisionerDaemonsReport includes health details of each connected provisioner daemon.
type ProvisionerDaemonsReport struct {
Severity health.Severity `json:"severity"`
Warnings []health.Message `json:"warnings"`
Dismissed bool `json:"dismissed"`
Error *string `json:"error"`
BaseReport
Items []ProvisionerDaemonsReportItem `json:"items"`
}
@ -212,24 +252,19 @@ type ProvisionerDaemonsReportItem struct {
Warnings []health.Message `json:"warnings"`
}
// WebsocketReport shows if the configured access URL allows establishing WebSocket connections.
type WebsocketReport struct {
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
Healthy bool `json:"healthy"`
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
Warnings []string `json:"warnings"`
Dismissed bool `json:"dismissed"`
Body string `json:"body"`
Code int `json:"code"`
Error *string `json:"error"`
Healthy bool `json:"healthy"`
BaseReport
Body string `json:"body"`
Code int `json:"code"`
}
// WorkspaceProxyReport includes health details of each connected workspace proxy.
type WorkspaceProxyReport struct {
Healthy bool `json:"healthy"`
Severity health.Severity `json:"severity"`
Warnings []health.Message `json:"warnings"`
Dismissed bool `json:"dismissed"`
Error *string `json:"error"`
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
Healthy bool `json:"healthy"`
BaseReport
WorkspaceProxies codersdk.RegionsResponse[codersdk.WorkspaceProxy] `json:"workspace_proxies"`
}

View File

@ -0,0 +1,127 @@
package healthsdk_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/coder/coder/v2/coderd/healthcheck/health"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk/healthsdk"
)
func TestSummarize(t *testing.T) {
t.Parallel()
t.Run("HealthcheckReport", func(t *testing.T) {
unhealthy := healthsdk.BaseReport{
Error: ptr.Ref("test error"),
Warnings: []health.Message{{Code: "TEST", Message: "testing"}},
}
hr := healthsdk.HealthcheckReport{
AccessURL: healthsdk.AccessURLReport{
BaseReport: unhealthy,
},
Database: healthsdk.DatabaseReport{
BaseReport: unhealthy,
},
DERP: healthsdk.DERPHealthReport{
BaseReport: unhealthy,
},
ProvisionerDaemons: healthsdk.ProvisionerDaemonsReport{
BaseReport: unhealthy,
},
Websocket: healthsdk.WebsocketReport{
BaseReport: unhealthy,
},
WorkspaceProxy: healthsdk.WorkspaceProxyReport{
BaseReport: unhealthy,
},
}
expected := []string{
"Access URL: Error: test error",
"Access URL: Warn: TEST: testing",
"Database: Error: test error",
"Database: Warn: TEST: testing",
"DERP: Error: test error",
"DERP: Warn: TEST: testing",
"Provisioner Daemons: Error: test error",
"Provisioner Daemons: Warn: TEST: testing",
"Websocket: Error: test error",
"Websocket: Warn: TEST: testing",
"Workspace Proxies: Error: test error",
"Workspace Proxies: Warn: TEST: testing",
}
actual := hr.Summarize()
assert.Equal(t, expected, actual)
})
for _, tt := range []struct {
name string
br healthsdk.BaseReport
pfx string
expected []string
}{
{
name: "empty",
br: healthsdk.BaseReport{},
pfx: "",
expected: []string{},
},
{
name: "no prefix",
br: healthsdk.BaseReport{
Error: ptr.Ref("testing"),
Warnings: []health.Message{
{
Code: "TEST01",
Message: "testing one",
},
{
Code: "TEST02",
Message: "testing two",
},
},
},
pfx: "",
expected: []string{
"Error: testing",
"Warn: TEST01: testing one",
"Warn: TEST02: testing two",
},
},
{
name: "prefix",
br: healthsdk.BaseReport{
Error: ptr.Ref("testing"),
Warnings: []health.Message{
{
Code: "TEST01",
Message: "testing one",
},
{
Code: "TEST02",
Message: "testing two",
},
},
},
pfx: "TEST:",
expected: []string{
"TEST: Error: testing",
"TEST: Warn: TEST01: testing one",
"TEST: Warn: TEST02: testing two",
},
},
} {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
actual := tt.br.Summarize(tt.pfx)
if len(tt.expected) == 0 {
assert.Empty(t, actual)
return
}
assert.Equal(t, tt.expected, actual)
})
}
}