mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
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:
@ -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"`
|
||||
}
|
||||
|
127
codersdk/healthsdk/healthsdk_test.go
Normal file
127
codersdk/healthsdk/healthsdk_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user