From 407e61ecd477ee4aaea3b0fab58a480bcf6fe45f Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 16 Apr 2024 13:31:56 +0100 Subject: [PATCH] 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 --- cli/support.go | 9 + coderd/apidoc/docs.go | 25 +- coderd/apidoc/swagger.json | 17 +- coderd/healthcheck/derphealth/derp.go | 2 +- coderd/healthcheck/healthcheck_test.go | 380 +++++++++++++------- coderd/healthcheck/websocket.go | 2 +- codersdk/healthsdk/healthsdk.go | 149 +++++--- codersdk/healthsdk/healthsdk_test.go | 127 +++++++ docs/api/debug.md | 7 +- docs/api/schemas.md | 64 +++- scripts/apitypings/main.go | 2 +- site/src/api/typesGenerated.ts | 44 +-- site/src/pages/HealthPage/WebsocketPage.tsx | 4 +- 13 files changed, 590 insertions(+), 242 deletions(-) create mode 100644 codersdk/healthsdk/healthsdk_test.go diff --git a/cli/support.go b/cli/support.go index 5d375c66d8..55f8e209da 100644 --- a/cli/support.go +++ b/cli/support.go @@ -184,6 +184,15 @@ func (r *RootCmd) supportBundle() *serpent.Command { _ = os.Remove(outputPath) // best effort return xerrors.Errorf("create support bundle: %w", err) } + deployHealthSummary := bun.Deployment.HealthReport.Summarize() + if len(deployHealthSummary) > 0 { + cliui.Warn(inv.Stdout, "Deployment health issues detected:", deployHealthSummary...) + } + clientNetcheckSummary := bun.Network.Netcheck.Summarize("Client netcheck:") + if len(clientNetcheckSummary) > 0 { + cliui.Warn(inv.Stdout, "Networking issues detected:", deployHealthSummary...) + } + bun.CLILogs = cliLogBuf.Bytes() if err := writeBundle(bun, zwr); err != nil { diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index bcd2b3b15c..60ed16632c 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -13834,7 +13834,16 @@ const docTemplate = `{ } }, "severity": { - "$ref": "#/definitions/health.Severity" + "enum": [ + "ok", + "warning", + "error" + ], + "allOf": [ + { + "$ref": "#/definitions/health.Severity" + } + ] }, "warnings": { "type": "array", @@ -13917,7 +13926,7 @@ const docTemplate = `{ "warnings": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/health.Message" } } } @@ -13932,10 +13941,20 @@ const docTemplate = `{ "type": "string" }, "healthy": { + "description": "Healthy is deprecated and left for backward compatibility purposes, use ` + "`" + `Severity` + "`" + ` instead.", "type": "boolean" }, "severity": { - "$ref": "#/definitions/health.Severity" + "enum": [ + "ok", + "warning", + "error" + ], + "allOf": [ + { + "$ref": "#/definitions/health.Severity" + } + ] }, "warnings": { "type": "array", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 47bac4fc4e..785450be71 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -12574,7 +12574,12 @@ } }, "severity": { - "$ref": "#/definitions/health.Severity" + "enum": ["ok", "warning", "error"], + "allOf": [ + { + "$ref": "#/definitions/health.Severity" + } + ] }, "warnings": { "type": "array", @@ -12653,7 +12658,7 @@ "warnings": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/health.Message" } } } @@ -12668,10 +12673,16 @@ "type": "string" }, "healthy": { + "description": "Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.", "type": "boolean" }, "severity": { - "$ref": "#/definitions/health.Severity" + "enum": ["ok", "warning", "error"], + "allOf": [ + { + "$ref": "#/definitions/health.Severity" + } + ] }, "warnings": { "type": "array", diff --git a/coderd/healthcheck/derphealth/derp.go b/coderd/healthcheck/derphealth/derp.go index 9e79b5e116..65d905f169 100644 --- a/coderd/healthcheck/derphealth/derp.go +++ b/coderd/healthcheck/derphealth/derp.go @@ -156,8 +156,8 @@ func (r *RegionReport) Run(ctx context.Context) { node = node nodeReport = NodeReport{ DERPNodeReport: healthsdk.DERPNodeReport{ - Node: node, Healthy: true, + Node: node, }, } ) diff --git a/coderd/healthcheck/healthcheck_test.go b/coderd/healthcheck/healthcheck_test.go index bb5cd581db..58fbe73053 100644 --- a/coderd/healthcheck/healthcheck_test.go +++ b/coderd/healthcheck/healthcheck_test.go @@ -58,27 +58,39 @@ func TestHealthcheck(t *testing.T) { name: "OK", checker: &testChecker{ DERPReport: healthsdk.DERPHealthReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, AccessURLReport: healthsdk.AccessURLReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, WebsocketReport: healthsdk.WebsocketReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, DatabaseReport: healthsdk.DatabaseReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{ - Severity: health.SeverityOK, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, }, healthy: true, @@ -88,27 +100,39 @@ func TestHealthcheck(t *testing.T) { name: "DERPFail", checker: &testChecker{ DERPReport: healthsdk.DERPHealthReport{ - Healthy: false, - Severity: health.SeverityError, + Healthy: false, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityError, + }, }, AccessURLReport: healthsdk.AccessURLReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, WebsocketReport: healthsdk.WebsocketReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, DatabaseReport: healthsdk.DatabaseReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{ - Severity: health.SeverityOK, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, }, healthy: false, @@ -118,28 +142,40 @@ func TestHealthcheck(t *testing.T) { name: "DERPWarning", checker: &testChecker{ DERPReport: healthsdk.DERPHealthReport{ - Healthy: true, - Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}}, - Severity: health.SeverityWarning, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}}, + Severity: health.SeverityWarning, + }, }, AccessURLReport: healthsdk.AccessURLReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, WebsocketReport: healthsdk.WebsocketReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, DatabaseReport: healthsdk.DatabaseReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{ - Severity: health.SeverityOK, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, }, healthy: true, @@ -149,27 +185,39 @@ func TestHealthcheck(t *testing.T) { name: "AccessURLFail", checker: &testChecker{ DERPReport: healthsdk.DERPHealthReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, AccessURLReport: healthsdk.AccessURLReport{ - Healthy: false, - Severity: health.SeverityWarning, + Healthy: false, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityWarning, + }, }, WebsocketReport: healthsdk.WebsocketReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, DatabaseReport: healthsdk.DatabaseReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{ - Severity: health.SeverityOK, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, }, healthy: false, @@ -179,27 +227,39 @@ func TestHealthcheck(t *testing.T) { name: "WebsocketFail", checker: &testChecker{ DERPReport: healthsdk.DERPHealthReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, AccessURLReport: healthsdk.AccessURLReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, WebsocketReport: healthsdk.WebsocketReport{ - Healthy: false, - Severity: health.SeverityError, + Healthy: false, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityError, + }, }, DatabaseReport: healthsdk.DatabaseReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{ - Severity: health.SeverityOK, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, }, healthy: false, @@ -209,27 +269,39 @@ func TestHealthcheck(t *testing.T) { name: "DatabaseFail", checker: &testChecker{ DERPReport: healthsdk.DERPHealthReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, AccessURLReport: healthsdk.AccessURLReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, WebsocketReport: healthsdk.WebsocketReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, DatabaseReport: healthsdk.DatabaseReport{ - Healthy: false, - Severity: health.SeverityError, + Healthy: false, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityError, + }, }, WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{ - Severity: health.SeverityOK, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, }, healthy: false, @@ -239,27 +311,39 @@ func TestHealthcheck(t *testing.T) { name: "ProxyFail", checker: &testChecker{ DERPReport: healthsdk.DERPHealthReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, AccessURLReport: healthsdk.AccessURLReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, WebsocketReport: healthsdk.WebsocketReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, DatabaseReport: healthsdk.DatabaseReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{ - Healthy: false, - Severity: health.SeverityError, + Healthy: false, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityError, + }, }, ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{ - Severity: health.SeverityOK, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, }, severity: health.SeverityError, @@ -269,28 +353,40 @@ func TestHealthcheck(t *testing.T) { name: "ProxyWarn", checker: &testChecker{ DERPReport: healthsdk.DERPHealthReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, AccessURLReport: healthsdk.AccessURLReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, WebsocketReport: healthsdk.WebsocketReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, DatabaseReport: healthsdk.DatabaseReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{ - Healthy: true, - Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}}, - Severity: health.SeverityWarning, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}}, + Severity: health.SeverityWarning, + }, }, ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{ - Severity: health.SeverityOK, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, }, severity: health.SeverityWarning, @@ -300,27 +396,39 @@ func TestHealthcheck(t *testing.T) { name: "ProvisionerDaemonsFail", checker: &testChecker{ DERPReport: healthsdk.DERPHealthReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, AccessURLReport: healthsdk.AccessURLReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, WebsocketReport: healthsdk.WebsocketReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, DatabaseReport: healthsdk.DatabaseReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{ - Severity: health.SeverityError, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityError, + }, }, }, severity: health.SeverityError, @@ -330,28 +438,40 @@ func TestHealthcheck(t *testing.T) { name: "ProvisionerDaemonsWarn", checker: &testChecker{ DERPReport: healthsdk.DERPHealthReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, AccessURLReport: healthsdk.AccessURLReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, WebsocketReport: healthsdk.WebsocketReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, DatabaseReport: healthsdk.DatabaseReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{ - Healthy: true, - Severity: health.SeverityOK, + Healthy: true, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityOK, + }, }, ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{ - Severity: health.SeverityWarning, - Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}}, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityWarning, + Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}}, + }, }, }, severity: health.SeverityWarning, @@ -362,27 +482,39 @@ func TestHealthcheck(t *testing.T) { healthy: false, checker: &testChecker{ DERPReport: healthsdk.DERPHealthReport{ - Healthy: false, - Severity: health.SeverityError, + Healthy: false, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityError, + }, }, AccessURLReport: healthsdk.AccessURLReport{ - Healthy: false, - Severity: health.SeverityError, + Healthy: false, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityError, + }, }, WebsocketReport: healthsdk.WebsocketReport{ - Healthy: false, - Severity: health.SeverityError, + Healthy: false, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityError, + }, }, DatabaseReport: healthsdk.DatabaseReport{ - Healthy: false, - Severity: health.SeverityError, + Healthy: false, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityError, + }, }, WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{ - Healthy: false, - Severity: health.SeverityError, + Healthy: false, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityError, + }, }, ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{ - Severity: health.SeverityError, + BaseReport: healthsdk.BaseReport{ + Severity: health.SeverityError, + }, }, }, severity: health.SeverityError, diff --git a/coderd/healthcheck/websocket.go b/coderd/healthcheck/websocket.go index 75ee3cdf05..f195b01f0f 100644 --- a/coderd/healthcheck/websocket.go +++ b/coderd/healthcheck/websocket.go @@ -31,7 +31,7 @@ func (r *WebsocketReport) Run(ctx context.Context, opts *WebsocketReportOptions) defer cancel() r.Severity = health.SeverityOK - r.Warnings = []string{} + r.Warnings = []health.Message{} r.Dismissed = opts.Dismissed u, err := opts.AccessURL.Parse("/api/v2/debug/ws") diff --git a/codersdk/healthsdk/healthsdk.go b/codersdk/healthsdk/healthsdk.go index 26e72e4e5f..51cf4b6957 100644 --- a/codersdk/healthsdk/healthsdk.go +++ b/codersdk/healthsdk/healthsdk.go @@ -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"` } diff --git a/codersdk/healthsdk/healthsdk_test.go b/codersdk/healthsdk/healthsdk_test.go new file mode 100644 index 0000000000..d89c999125 --- /dev/null +++ b/codersdk/healthsdk/healthsdk_test.go @@ -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) + }) + } +} diff --git a/docs/api/debug.md b/docs/api/debug.md index 917088de57..0ae74b5012 100644 --- a/docs/api/debug.md +++ b/docs/api/debug.md @@ -325,7 +325,12 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "error": "string", "healthy": true, "severity": "ok", - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] }, "workspace_proxy": { "dismissed": true, diff --git a/docs/api/schemas.md b/docs/api/schemas.md index efc3a38f01..1d00ac18c3 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -8134,7 +8134,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "error": "string", "healthy": true, "severity": "ok", - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] }, "workspace_proxy": { "dismissed": true, @@ -8251,6 +8256,14 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `severity` | [health.Severity](#healthseverity) | false | | | | `warnings` | array of [health.Message](#healthmessage) | false | | | +#### Enumerated Values + +| Property | Value | +| ---------- | --------- | +| `severity` | `ok` | +| `severity` | `warning` | +| `severity` | `error` | + ## healthsdk.ProvisionerDaemonsReportItem ```json @@ -8326,21 +8339,26 @@ If the schedule is empty, the user will be updated to use the default schedule.| "error": "string", "healthy": true, "severity": "ok", - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| ----------- | ---------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------- | -| `body` | string | false | | | -| `code` | integer | false | | | -| `dismissed` | boolean | false | | | -| `error` | string | false | | | -| `healthy` | boolean | false | | Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead. | -| `severity` | [health.Severity](#healthseverity) | false | | | -| `warnings` | array of string | false | | | +| Name | Type | Required | Restrictions | Description | +| ----------- | ----------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------- | +| `body` | string | false | | | +| `code` | integer | false | | | +| `dismissed` | boolean | false | | | +| `error` | string | false | | | +| `healthy` | boolean | false | | Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead. | +| `severity` | [health.Severity](#healthseverity) | false | | | +| `warnings` | array of [health.Message](#healthmessage) | false | | | #### Enumerated Values @@ -8396,14 +8414,22 @@ If the schedule is empty, the user will be updated to use the default schedule.| ### Properties -| Name | Type | Required | Restrictions | Description | -| ------------------- | ---------------------------------------------------------------------------------------------------- | -------- | ------------ | ----------- | -| `dismissed` | boolean | false | | | -| `error` | string | false | | | -| `healthy` | boolean | false | | | -| `severity` | [health.Severity](#healthseverity) | false | | | -| `warnings` | array of [health.Message](#healthmessage) | false | | | -| `workspace_proxies` | [codersdk.RegionsResponse-codersdk_WorkspaceProxy](#codersdkregionsresponse-codersdk_workspaceproxy) | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------------- | ---------------------------------------------------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------- | +| `dismissed` | boolean | false | | | +| `error` | string | false | | | +| `healthy` | boolean | false | | Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead. | +| `severity` | [health.Severity](#healthseverity) | false | | | +| `warnings` | array of [health.Message](#healthmessage) | false | | | +| `workspace_proxies` | [codersdk.RegionsResponse-codersdk_WorkspaceProxy](#codersdkregionsresponse-codersdk_workspaceproxy) | false | | | + +#### Enumerated Values + +| Property | Value | +| ---------- | --------- | +| `severity` | `ok` | +| `severity` | `warning` | +| `severity` | `error` | ## key.NodePublic diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 9d46759ecf..0b4571a6af 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -544,7 +544,7 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err tag := reflect.StructTag(st.Tag(i)) // Adding a json struct tag causes the json package to consider // the field unembedded. - if field.Embedded() && tag.Get("json") == "" && field.Pkg().Name() == "codersdk" { + if field.Embedded() && tag.Get("json") == "" { extendedFields[i] = true extends = append(extends, field.Name()) } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 54f4e57d77..13971a0345 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -2282,31 +2282,31 @@ export type RegionTypes = Region | WorkspaceProxy; // The code below is generated from codersdk/healthsdk. // From healthsdk/healthsdk.go -export interface AccessURLReport { +export interface AccessURLReport extends BaseReport { readonly healthy: boolean; - readonly severity: HealthSeverity; - readonly warnings: readonly HealthMessage[]; - readonly dismissed: boolean; readonly access_url: string; readonly reachable: boolean; readonly status_code: number; readonly healthz_response: string; - readonly error?: string; } // From healthsdk/healthsdk.go -export interface DERPHealthReport { - readonly healthy: boolean; +export interface BaseReport { + readonly error?: string; readonly severity: HealthSeverity; readonly warnings: readonly HealthMessage[]; readonly dismissed: boolean; +} + +// From healthsdk/healthsdk.go +export interface DERPHealthReport extends BaseReport { + readonly healthy: boolean; readonly regions: Record; // Named type "tailscale.com/net/netcheck.Report" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type readonly netcheck?: any; readonly netcheck_err?: string; readonly netcheck_logs: readonly string[]; - readonly error?: string; } // From healthsdk/healthsdk.go @@ -2314,6 +2314,7 @@ export interface DERPNodeReport { readonly healthy: boolean; readonly severity: HealthSeverity; readonly warnings: readonly HealthMessage[]; + readonly error?: string; // Named type "tailscale.com/tailcfg.DERPNode" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type readonly node?: any; @@ -2326,7 +2327,6 @@ export interface DERPNodeReport { readonly uses_websocket: boolean; readonly client_logs: readonly (readonly string[])[]; readonly client_errs: readonly (readonly string[])[]; - readonly error?: string; readonly stun: STUNReport; } @@ -2335,24 +2335,20 @@ export interface DERPRegionReport { readonly healthy: boolean; readonly severity: HealthSeverity; readonly warnings: readonly HealthMessage[]; + readonly error?: string; // Named type "tailscale.com/tailcfg.DERPRegion" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type readonly region?: any; readonly node_reports: readonly DERPNodeReport[]; - readonly error?: string; } // From healthsdk/healthsdk.go -export interface DatabaseReport { +export interface DatabaseReport extends BaseReport { readonly healthy: boolean; - readonly severity: HealthSeverity; - readonly warnings: readonly HealthMessage[]; - readonly dismissed: boolean; readonly reachable: boolean; readonly latency: string; readonly latency_ms: number; readonly threshold_ms: number; - readonly error?: string; } // From healthsdk/healthsdk.go @@ -2376,11 +2372,7 @@ export interface HealthcheckReport { } // From healthsdk/healthsdk.go -export interface ProvisionerDaemonsReport { - readonly severity: HealthSeverity; - readonly warnings: readonly HealthMessage[]; - readonly dismissed: boolean; - readonly error?: string; +export interface ProvisionerDaemonsReport extends BaseReport { readonly items: readonly ProvisionerDaemonsReportItem[]; } @@ -2403,23 +2395,15 @@ export interface UpdateHealthSettings { } // From healthsdk/healthsdk.go -export interface WebsocketReport { +export interface WebsocketReport extends BaseReport { readonly healthy: boolean; - readonly severity: HealthSeverity; - readonly warnings: readonly string[]; - readonly dismissed: boolean; readonly body: string; readonly code: number; - readonly error?: string; } // From healthsdk/healthsdk.go -export interface WorkspaceProxyReport { +export interface WorkspaceProxyReport extends BaseReport { readonly healthy: boolean; - readonly severity: HealthSeverity; - readonly warnings: readonly HealthMessage[]; - readonly dismissed: boolean; - readonly error?: string; readonly workspace_proxies: RegionsResponse; } diff --git a/site/src/pages/HealthPage/WebsocketPage.tsx b/site/src/pages/HealthPage/WebsocketPage.tsx index 97fac9e867..ddc37bc971 100644 --- a/site/src/pages/HealthPage/WebsocketPage.tsx +++ b/site/src/pages/HealthPage/WebsocketPage.tsx @@ -41,8 +41,8 @@ export const WebsocketPage = () => { {websocket.warnings.map((warning) => { return ( - - {warning} + + {warning.message} ); })}