mirror of https://github.com/coder/coder.git
feat(healthcheck): add websocket report (#7689)
This commit is contained in:
parent
77b0ca0b53
commit
022372dd73
|
@ -427,6 +427,34 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/debug/ws": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Debug"
|
||||
],
|
||||
"summary": "Debug Info Websocket Test",
|
||||
"operationId": "debug-info-websocket-test",
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.Response"
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-apidocgen": {
|
||||
"skip": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"/deployment/config": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -10419,6 +10447,29 @@ const docTemplate = `{
|
|||
"time": {
|
||||
"description": "Time is the time the report was generated at.",
|
||||
"type": "string"
|
||||
},
|
||||
"websocket": {
|
||||
"$ref": "#/definitions/healthcheck.WebsocketReport"
|
||||
}
|
||||
}
|
||||
},
|
||||
"healthcheck.WebsocketReport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {},
|
||||
"response": {
|
||||
"$ref": "#/definitions/healthcheck.WebsocketResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"healthcheck.WebsocketResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"body": {
|
||||
"type": "string"
|
||||
},
|
||||
"code": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -363,6 +363,30 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/debug/ws": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Debug"],
|
||||
"summary": "Debug Info Websocket Test",
|
||||
"operationId": "debug-info-websocket-test",
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.Response"
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-apidocgen": {
|
||||
"skip": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"/deployment/config": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -9408,6 +9432,29 @@
|
|||
"time": {
|
||||
"description": "Time is the time the report was generated at.",
|
||||
"type": "string"
|
||||
},
|
||||
"websocket": {
|
||||
"$ref": "#/definitions/healthcheck.WebsocketReport"
|
||||
}
|
||||
}
|
||||
},
|
||||
"healthcheck.WebsocketReport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {},
|
||||
"response": {
|
||||
"$ref": "#/definitions/healthcheck.WebsocketResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"healthcheck.WebsocketResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"body": {
|
||||
"type": "string"
|
||||
},
|
||||
"code": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -129,7 +129,7 @@ type Options struct {
|
|||
// AppSecurityKey is the crypto key used to sign and encrypt tokens related to
|
||||
// workspace applications. It consists of both a signing and encryption key.
|
||||
AppSecurityKey workspaceapps.SecurityKey
|
||||
HealthcheckFunc func(ctx context.Context) (*healthcheck.Report, error)
|
||||
HealthcheckFunc func(ctx context.Context, apiKey string) (*healthcheck.Report, error)
|
||||
HealthcheckTimeout time.Duration
|
||||
HealthcheckRefresh time.Duration
|
||||
|
||||
|
@ -256,10 +256,11 @@ func New(options *Options) *API {
|
|||
options.TemplateScheduleStore.Store(&v)
|
||||
}
|
||||
if options.HealthcheckFunc == nil {
|
||||
options.HealthcheckFunc = func(ctx context.Context) (*healthcheck.Report, error) {
|
||||
options.HealthcheckFunc = func(ctx context.Context, apiKey string) (*healthcheck.Report, error) {
|
||||
return healthcheck.Run(ctx, &healthcheck.ReportOptions{
|
||||
AccessURL: options.AccessURL,
|
||||
DERPMap: options.DERPMap.Clone(),
|
||||
APIKey: apiKey,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -787,6 +788,7 @@ func New(options *Options) *API {
|
|||
|
||||
r.Get("/coordinator", api.debugCoordinator)
|
||||
r.Get("/health", api.debugDeploymentHealth)
|
||||
r.Get("/ws", (&healthcheck.WebsocketEchoServer{}).ServeHTTP)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -874,6 +876,7 @@ type API struct {
|
|||
Experiments codersdk.Experiments
|
||||
|
||||
healthCheckGroup *singleflight.Group[string, *healthcheck.Report]
|
||||
healthCheckCache atomic.Pointer[healthcheck.Report]
|
||||
}
|
||||
|
||||
// Close waits for all WebSocket connections to drain before returning.
|
||||
|
|
|
@ -107,7 +107,7 @@ type Options struct {
|
|||
TrialGenerator func(context.Context, string) error
|
||||
TemplateScheduleStore schedule.TemplateScheduleStore
|
||||
|
||||
HealthcheckFunc func(ctx context.Context) (*healthcheck.Report, error)
|
||||
HealthcheckFunc func(ctx context.Context, apiKey string) (*healthcheck.Report, error)
|
||||
HealthcheckTimeout time.Duration
|
||||
HealthcheckRefresh time.Duration
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/coder/coder/coderd/healthcheck"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
|
@ -29,11 +30,28 @@ func (api *API) debugCoordinator(rw http.ResponseWriter, r *http.Request) {
|
|||
// @Success 200 {object} healthcheck.Report
|
||||
// @Router /debug/health [get]
|
||||
func (api *API) debugDeploymentHealth(rw http.ResponseWriter, r *http.Request) {
|
||||
apiKey := httpmw.APITokenFromRequest(r)
|
||||
ctx, cancel := context.WithTimeout(r.Context(), api.HealthcheckTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Get cached report if it exists.
|
||||
if report := api.healthCheckCache.Load(); report != nil {
|
||||
if time.Since(report.Time) < api.HealthcheckRefresh {
|
||||
httpapi.Write(ctx, rw, http.StatusOK, report)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resChan := api.healthCheckGroup.DoChan("", func() (*healthcheck.Report, error) {
|
||||
return api.HealthcheckFunc(ctx)
|
||||
// Create a new context not tied to the request.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), api.HealthcheckTimeout)
|
||||
defer cancel()
|
||||
|
||||
report, err := api.HealthcheckFunc(ctx, apiKey)
|
||||
if err == nil {
|
||||
api.healthCheckCache.Store(report)
|
||||
}
|
||||
return report, err
|
||||
})
|
||||
|
||||
select {
|
||||
|
@ -43,13 +61,19 @@ func (api *API) debugDeploymentHealth(rw http.ResponseWriter, r *http.Request) {
|
|||
})
|
||||
return
|
||||
case res := <-resChan:
|
||||
if time.Since(res.Val.Time) > api.HealthcheckRefresh {
|
||||
api.healthCheckGroup.Forget("")
|
||||
api.debugDeploymentHealth(rw, r)
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, res.Val)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// For some reason the swagger docs need to be attached to a function.
|
||||
//
|
||||
// @Summary Debug Info Websocket Test
|
||||
// @ID debug-info-websocket-test
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Debug
|
||||
// @Success 201 {object} codersdk.Response
|
||||
// @Router /debug/ws [get]
|
||||
// @x-apidocgen {"skip": true}
|
||||
func _debugws(http.ResponseWriter, *http.Request) {} //nolint:unused
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
|
@ -14,15 +15,17 @@ import (
|
|||
"github.com/coder/coder/testutil"
|
||||
)
|
||||
|
||||
func TestDebug(t *testing.T) {
|
||||
func TestDebugHealth(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Health/OK", func(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
client = coderdtest.New(t, &coderdtest.Options{
|
||||
HealthcheckFunc: func(context.Context) (*healthcheck.Report, error) {
|
||||
ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
sessionToken string
|
||||
client = coderdtest.New(t, &coderdtest.Options{
|
||||
HealthcheckFunc: func(_ context.Context, apiKey string) (*healthcheck.Report, error) {
|
||||
assert.Equal(t, sessionToken, apiKey)
|
||||
return &healthcheck.Report{}, nil
|
||||
},
|
||||
})
|
||||
|
@ -30,6 +33,7 @@ func TestDebug(t *testing.T) {
|
|||
)
|
||||
defer cancel()
|
||||
|
||||
sessionToken = client.SessionToken()
|
||||
res, err := client.Request(ctx, "GET", "/debug/health", nil)
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
|
@ -37,14 +41,14 @@ func TestDebug(t *testing.T) {
|
|||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Health/Timeout", func(t *testing.T) {
|
||||
t.Run("Timeout", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
client = coderdtest.New(t, &coderdtest.Options{
|
||||
HealthcheckTimeout: time.Microsecond,
|
||||
HealthcheckFunc: func(context.Context) (*healthcheck.Report, error) {
|
||||
HealthcheckFunc: func(context.Context, string) (*healthcheck.Report, error) {
|
||||
t := time.NewTimer(time.Second)
|
||||
defer t.Stop()
|
||||
|
||||
|
@ -66,4 +70,48 @@ func TestDebug(t *testing.T) {
|
|||
_, _ = io.ReadAll(res.Body)
|
||||
require.Equal(t, http.StatusNotFound, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Deduplicated", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
calls int
|
||||
client = coderdtest.New(t, &coderdtest.Options{
|
||||
HealthcheckRefresh: time.Hour,
|
||||
HealthcheckTimeout: time.Hour,
|
||||
HealthcheckFunc: func(context.Context, string) (*healthcheck.Report, error) {
|
||||
calls++
|
||||
return &healthcheck.Report{
|
||||
Time: time.Now(),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
res, err := client.Request(ctx, "GET", "/api/v2/debug/health", nil)
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
_, _ = io.ReadAll(res.Body)
|
||||
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
res, err = client.Request(ctx, "GET", "/api/v2/debug/health", nil)
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
_, _ = io.ReadAll(res.Body)
|
||||
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
require.Equal(t, 1, calls)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDebugWebsocket(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -19,9 +19,7 @@ type Report struct {
|
|||
|
||||
DERP DERPReport `json:"derp"`
|
||||
AccessURL AccessURLReport `json:"access_url"`
|
||||
|
||||
// TODO:
|
||||
// Websocket WebsocketReport `json:"websocket"`
|
||||
Websocket WebsocketReport `json:"websocket"`
|
||||
}
|
||||
|
||||
type ReportOptions struct {
|
||||
|
@ -29,6 +27,7 @@ type ReportOptions struct {
|
|||
DERPMap *tailcfg.DERPMap
|
||||
AccessURL *url.URL
|
||||
Client *http.Client
|
||||
APIKey string
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, opts *ReportOptions) (*Report, error) {
|
||||
|
@ -65,11 +64,19 @@ func Run(ctx context.Context, opts *ReportOptions) (*Report, error) {
|
|||
})
|
||||
}()
|
||||
|
||||
// wg.Add(1)
|
||||
// go func() {
|
||||
// defer wg.Done()
|
||||
// report.Websocket.Run(ctx, opts.AccessURL)
|
||||
// }()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
report.Websocket.Error = xerrors.Errorf("%v", err)
|
||||
}
|
||||
}()
|
||||
report.Websocket.Run(ctx, &WebsocketReportOptions{
|
||||
APIKey: opts.APIKey,
|
||||
AccessURL: opts.AccessURL,
|
||||
})
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
report.Time = time.Now()
|
||||
|
|
|
@ -2,11 +2,149 @@ package healthcheck
|
|||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
"nhooyr.io/websocket"
|
||||
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
)
|
||||
|
||||
type WebsocketReport struct{}
|
||||
|
||||
func (*WebsocketReport) Run(ctx context.Context, accessURL *url.URL) {
|
||||
_, _ = ctx, accessURL
|
||||
type WebsocketReportOptions struct {
|
||||
APIKey string
|
||||
AccessURL *url.URL
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
type WebsocketReport struct {
|
||||
Response WebsocketResponse `json:"response"`
|
||||
Error error `json:"error"`
|
||||
}
|
||||
|
||||
type WebsocketResponse struct {
|
||||
Body string `json:"body"`
|
||||
Code int `json:"code"`
|
||||
}
|
||||
|
||||
func (r *WebsocketReport) Run(ctx context.Context, opts *WebsocketReportOptions) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
u, err := opts.AccessURL.Parse("/api/v2/debug/ws")
|
||||
if err != nil {
|
||||
r.Error = xerrors.Errorf("parse access url: %w", err)
|
||||
return
|
||||
}
|
||||
if u.Scheme == "https" {
|
||||
u.Scheme = "wss"
|
||||
} else {
|
||||
u.Scheme = "ws"
|
||||
}
|
||||
|
||||
//nolint:bodyclose // websocket package closes this for you
|
||||
c, res, err := websocket.Dial(ctx, u.String(), &websocket.DialOptions{
|
||||
HTTPClient: opts.HTTPClient,
|
||||
HTTPHeader: http.Header{"Coder-Session-Token": []string{opts.APIKey}},
|
||||
})
|
||||
if res != nil {
|
||||
var body string
|
||||
if res.Body != nil {
|
||||
b, err := io.ReadAll(res.Body)
|
||||
if err == nil {
|
||||
body = string(b)
|
||||
}
|
||||
}
|
||||
|
||||
r.Response = WebsocketResponse{
|
||||
Body: body,
|
||||
Code: res.StatusCode,
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
r.Error = xerrors.Errorf("websocket dial: %w", err)
|
||||
return
|
||||
}
|
||||
defer c.Close(websocket.StatusGoingAway, "goodbye")
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
msg := strconv.Itoa(i)
|
||||
err := c.Write(ctx, websocket.MessageText, []byte(msg))
|
||||
if err != nil {
|
||||
r.Error = xerrors.Errorf("write message: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
ty, got, err := c.Read(ctx)
|
||||
if err != nil {
|
||||
r.Error = xerrors.Errorf("read message: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ty != websocket.MessageText {
|
||||
r.Error = xerrors.Errorf("received incorrect message type: %v", ty)
|
||||
return
|
||||
}
|
||||
|
||||
if string(got) != msg {
|
||||
r.Error = xerrors.Errorf("received incorrect message: wanted %q, got %q", msg, string(got))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Close(websocket.StatusGoingAway, "goodbye")
|
||||
}
|
||||
|
||||
type WebsocketEchoServer struct {
|
||||
Error error
|
||||
Code int
|
||||
}
|
||||
|
||||
func (s *WebsocketEchoServer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
if s.Error != nil {
|
||||
rw.WriteHeader(s.Code)
|
||||
_, _ = rw.Write([]byte(s.Error.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
c, err := websocket.Accept(rw, r, &websocket.AcceptOptions{})
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, "unable to accept: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer c.Close(websocket.StatusGoingAway, "goodbye")
|
||||
|
||||
echo := func() error {
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
typ, r, err := c.Reader(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get reader: %w", err)
|
||||
}
|
||||
|
||||
w, err := c.Writer(ctx, typ)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get writer: %w", err)
|
||||
}
|
||||
|
||||
_, err = io.Copy(w, r)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("echo message: %w", err)
|
||||
}
|
||||
|
||||
err = w.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
err := echo()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
package healthcheck_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/healthcheck"
|
||||
"github.com/coder/coder/testutil"
|
||||
)
|
||||
|
||||
func TestWebsocket(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
srv := httptest.NewServer(&healthcheck.WebsocketEchoServer{})
|
||||
defer srv.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
u, err := url.Parse(srv.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
wsReport := healthcheck.WebsocketReport{}
|
||||
wsReport.Run(ctx, &healthcheck.WebsocketReportOptions{
|
||||
AccessURL: u,
|
||||
HTTPClient: srv.Client(),
|
||||
APIKey: "test",
|
||||
})
|
||||
|
||||
require.NoError(t, wsReport.Error)
|
||||
})
|
||||
|
||||
t.Run("Error", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
srv := httptest.NewServer(&healthcheck.WebsocketEchoServer{
|
||||
Error: xerrors.New("test error"),
|
||||
Code: http.StatusBadRequest,
|
||||
})
|
||||
defer srv.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
u, err := url.Parse(srv.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
wsReport := healthcheck.WebsocketReport{}
|
||||
wsReport.Run(ctx, &healthcheck.WebsocketReportOptions{
|
||||
AccessURL: u,
|
||||
HTTPClient: srv.Client(),
|
||||
APIKey: "test",
|
||||
})
|
||||
|
||||
require.Error(t, wsReport.Error)
|
||||
assert.Equal(t, wsReport.Response.Body, "test error")
|
||||
assert.Equal(t, wsReport.Response.Code, http.StatusBadRequest)
|
||||
})
|
||||
}
|
|
@ -207,7 +207,14 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \
|
|||
}
|
||||
},
|
||||
"pass": true,
|
||||
"time": "string"
|
||||
"time": "string",
|
||||
"websocket": {
|
||||
"error": null,
|
||||
"response": {
|
||||
"body": "string",
|
||||
"code": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -6377,7 +6377,14 @@ Parameter represents a set value for the scope.
|
|||
}
|
||||
},
|
||||
"pass": true,
|
||||
"time": "string"
|
||||
"time": "string",
|
||||
"websocket": {
|
||||
"error": null,
|
||||
"response": {
|
||||
"body": "string",
|
||||
"code": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -6389,6 +6396,42 @@ Parameter represents a set value for the scope.
|
|||
| `derp` | [healthcheck.DERPReport](#healthcheckderpreport) | false | | |
|
||||
| `pass` | boolean | false | | Healthy is true if the report returns no errors. |
|
||||
| `time` | string | false | | Time is the time the report was generated at. |
|
||||
| `websocket` | [healthcheck.WebsocketReport](#healthcheckwebsocketreport) | false | | |
|
||||
|
||||
## healthcheck.WebsocketReport
|
||||
|
||||
```json
|
||||
{
|
||||
"error": null,
|
||||
"response": {
|
||||
"body": "string",
|
||||
"code": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ---------- | -------------------------------------------------------------- | -------- | ------------ | ----------- |
|
||||
| `error` | any | false | | |
|
||||
| `response` | [healthcheck.WebsocketResponse](#healthcheckwebsocketresponse) | false | | |
|
||||
|
||||
## healthcheck.WebsocketResponse
|
||||
|
||||
```json
|
||||
{
|
||||
"body": "string",
|
||||
"code": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ------ | ------- | -------- | ------------ | ----------- |
|
||||
| `body` | string | false | | |
|
||||
| `code` | integer | false | | |
|
||||
|
||||
## netcheck.Report
|
||||
|
||||
|
|
Loading…
Reference in New Issue