feat(healthcheck): add failing sections to report (#7789)

This commit is contained in:
Colin Adler
2023-06-01 19:21:24 -05:00
committed by GitHub
parent 9b8e5c2d8a
commit 2b63492649
11 changed files with 217 additions and 38 deletions

View File

@@ -25,7 +25,6 @@ import (
)
type DERPReport struct {
mu sync.Mutex
Healthy bool `json:"healthy"`
Regions map[int]*DERPRegionReport `json:"regions"`
@@ -78,6 +77,7 @@ func (r *DERPReport) Run(ctx context.Context, opts *DERPReportOptions) {
r.Regions = map[int]*DERPRegionReport{}
wg := &sync.WaitGroup{}
mu := sync.Mutex{}
wg.Add(len(opts.DERPMap.Regions))
for _, region := range opts.DERPMap.Regions {
@@ -97,19 +97,19 @@ func (r *DERPReport) Run(ctx context.Context, opts *DERPReportOptions) {
regionReport.Run(ctx)
r.mu.Lock()
mu.Lock()
r.Regions[region.RegionID] = &regionReport
if !regionReport.Healthy {
r.Healthy = false
}
r.mu.Unlock()
mu.Unlock()
}()
}
ncLogf := func(format string, args ...interface{}) {
r.mu.Lock()
mu.Lock()
r.NetcheckLogs = append(r.NetcheckLogs, fmt.Sprintf(format, args...))
r.mu.Unlock()
mu.Unlock()
}
nc := &netcheck.Client{
PortMapper: portmapper.NewClient(tslogger.WithPrefix(ncLogf, "portmap: "), nil),

View File

@@ -11,11 +11,24 @@ import (
"tailscale.com/tailcfg"
)
const (
SectionDERP string = "DERP"
SectionAccessURL string = "AccessURL"
SectionWebsocket string = "Websocket"
)
type Checker interface {
DERP(ctx context.Context, opts *DERPReportOptions) DERPReport
AccessURL(ctx context.Context, opts *AccessURLOptions) AccessURLReport
Websocket(ctx context.Context, opts *WebsocketReportOptions) WebsocketReport
}
type Report struct {
// Time is the time the report was generated at.
Time time.Time `json:"time"`
// Healthy is true if the report returns no errors.
Healthy bool `json:"healthy"`
Healthy bool `json:"healthy"`
FailingSections []string `json:"failing_sections"`
DERP DERPReport `json:"derp"`
AccessURL AccessURLReport `json:"access_url"`
@@ -28,12 +41,36 @@ type ReportOptions struct {
AccessURL *url.URL
Client *http.Client
APIKey string
Checker Checker
}
func Run(ctx context.Context, opts *ReportOptions) (*Report, error) {
var report Report
type defaultChecker struct{}
wg := &sync.WaitGroup{}
func (defaultChecker) DERP(ctx context.Context, opts *DERPReportOptions) (report DERPReport) {
report.Run(ctx, opts)
return report
}
func (defaultChecker) AccessURL(ctx context.Context, opts *AccessURLOptions) (report AccessURLReport) {
report.Run(ctx, opts)
return report
}
func (defaultChecker) Websocket(ctx context.Context, opts *WebsocketReportOptions) (report WebsocketReport) {
report.Run(ctx, opts)
return report
}
func Run(ctx context.Context, opts *ReportOptions) *Report {
var (
wg sync.WaitGroup
report Report
)
if opts.Checker == nil {
opts.Checker = defaultChecker{}
}
wg.Add(1)
go func() {
@@ -44,7 +81,7 @@ func Run(ctx context.Context, opts *ReportOptions) (*Report, error) {
}
}()
report.DERP.Run(ctx, &DERPReportOptions{
report.DERP = opts.Checker.DERP(ctx, &DERPReportOptions{
DERPMap: opts.DERPMap,
})
}()
@@ -58,7 +95,7 @@ func Run(ctx context.Context, opts *ReportOptions) (*Report, error) {
}
}()
report.AccessURL.Run(ctx, &AccessURLOptions{
report.AccessURL = opts.Checker.AccessURL(ctx, &AccessURLOptions{
AccessURL: opts.AccessURL,
Client: opts.Client,
})
@@ -72,7 +109,8 @@ func Run(ctx context.Context, opts *ReportOptions) (*Report, error) {
report.Websocket.Error = xerrors.Errorf("%v", err)
}
}()
report.Websocket.Run(ctx, &WebsocketReportOptions{
report.Websocket = opts.Checker.Websocket(ctx, &WebsocketReportOptions{
APIKey: opts.APIKey,
AccessURL: opts.AccessURL,
})
@@ -80,8 +118,16 @@ func Run(ctx context.Context, opts *ReportOptions) (*Report, error) {
wg.Wait()
report.Time = time.Now()
report.Healthy = report.DERP.Healthy &&
report.AccessURL.Healthy &&
report.Websocket.Healthy
return &report, nil
if !report.DERP.Healthy {
report.FailingSections = append(report.FailingSections, SectionDERP)
}
if !report.AccessURL.Healthy {
report.FailingSections = append(report.FailingSections, SectionAccessURL)
}
if !report.Websocket.Healthy {
report.FailingSections = append(report.FailingSections, SectionWebsocket)
}
report.Healthy = len(report.FailingSections) == 0
return &report
}

View File

@@ -0,0 +1,120 @@
package healthcheck_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/coder/coder/coderd/healthcheck"
)
type testChecker struct {
DERPReport healthcheck.DERPReport
AccessURLReport healthcheck.AccessURLReport
WebsocketReport healthcheck.WebsocketReport
}
func (c *testChecker) DERP(context.Context, *healthcheck.DERPReportOptions) healthcheck.DERPReport {
return c.DERPReport
}
func (c *testChecker) AccessURL(context.Context, *healthcheck.AccessURLOptions) healthcheck.AccessURLReport {
return c.AccessURLReport
}
func (c *testChecker) Websocket(context.Context, *healthcheck.WebsocketReportOptions) healthcheck.WebsocketReport {
return c.WebsocketReport
}
func TestHealthcheck(t *testing.T) {
t.Parallel()
for _, c := range []struct {
name string
checker *testChecker
healthy bool
failingSections []string
}{{
name: "OK",
checker: &testChecker{
DERPReport: healthcheck.DERPReport{
Healthy: true,
},
AccessURLReport: healthcheck.AccessURLReport{
Healthy: true,
},
WebsocketReport: healthcheck.WebsocketReport{
Healthy: true,
},
},
healthy: true,
failingSections: nil,
}, {
name: "DERPFail",
checker: &testChecker{
DERPReport: healthcheck.DERPReport{
Healthy: false,
},
AccessURLReport: healthcheck.AccessURLReport{
Healthy: true,
},
WebsocketReport: healthcheck.WebsocketReport{
Healthy: true,
},
},
healthy: false,
failingSections: []string{healthcheck.SectionDERP},
}, {
name: "AccessURLFail",
checker: &testChecker{
DERPReport: healthcheck.DERPReport{
Healthy: true,
},
AccessURLReport: healthcheck.AccessURLReport{
Healthy: false,
},
WebsocketReport: healthcheck.WebsocketReport{
Healthy: true,
},
},
healthy: false,
failingSections: []string{healthcheck.SectionAccessURL},
}, {
name: "WebsocketFail",
checker: &testChecker{
DERPReport: healthcheck.DERPReport{
Healthy: true,
},
AccessURLReport: healthcheck.AccessURLReport{
Healthy: true,
},
WebsocketReport: healthcheck.WebsocketReport{
Healthy: false,
},
},
healthy: false,
failingSections: []string{healthcheck.SectionWebsocket},
}, {
name: "AllFail",
checker: &testChecker{},
healthy: false,
failingSections: []string{healthcheck.SectionDERP, healthcheck.SectionAccessURL, healthcheck.SectionWebsocket},
}} {
c := c
t.Run(c.name, func(t *testing.T) {
t.Parallel()
report := healthcheck.Run(context.Background(), &healthcheck.ReportOptions{
Checker: c.checker,
})
assert.Equal(t, c.healthy, report.Healthy)
assert.Equal(t, c.failingSections, report.FailingSections)
assert.Equal(t, c.checker.DERPReport.Healthy, report.DERP.Healthy)
assert.Equal(t, c.checker.AccessURLReport.Healthy, report.AccessURL.Healthy)
assert.Equal(t, c.checker.WebsocketReport.Healthy, report.Websocket.Healthy)
assert.NotZero(t, report.Time)
})
}
}