diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index a4e792067c..f49b91832f 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -398,6 +398,66 @@ const docTemplate = `{ } } }, + "/debug/derp/traffic": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Debug" + ], + "summary": "Debug DERP traffic", + "operationId": "debug-derp-traffic", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/derp.BytesSentRecv" + } + } + } + }, + "x-apidocgen": { + "skip": true + } + } + }, + "/debug/expvar": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Debug" + ], + "summary": "Debug expvar", + "operationId": "debug-expvar", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + }, + "x-apidocgen": { + "skip": true + } + } + }, "/debug/health": { "get": { "security": [ @@ -13266,6 +13326,25 @@ const docTemplate = `{ } } }, + "derp.BytesSentRecv": { + "type": "object", + "properties": { + "key": { + "description": "Key is the public key of the client which sent/received these bytes.", + "allOf": [ + { + "$ref": "#/definitions/key.NodePublic" + } + ] + }, + "recv": { + "type": "integer" + }, + "sent": { + "type": "integer" + } + } + }, "derp.ServerInfoMessage": { "type": "object", "properties": { @@ -13771,6 +13850,9 @@ const docTemplate = `{ } } }, + "key.NodePublic": { + "type": "object" + }, "netcheck.Report": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 1fd4acd85d..1852fd9b5a 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -338,6 +338,58 @@ } } }, + "/debug/derp/traffic": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Debug"], + "summary": "Debug DERP traffic", + "operationId": "debug-derp-traffic", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/derp.BytesSentRecv" + } + } + } + }, + "x-apidocgen": { + "skip": true + } + } + }, + "/debug/expvar": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Debug"], + "summary": "Debug expvar", + "operationId": "debug-expvar", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + }, + "x-apidocgen": { + "skip": true + } + } + }, "/debug/health": { "get": { "security": [ @@ -12079,6 +12131,25 @@ } } }, + "derp.BytesSentRecv": { + "type": "object", + "properties": { + "key": { + "description": "Key is the public key of the client which sent/received these bytes.", + "allOf": [ + { + "$ref": "#/definitions/key.NodePublic" + } + ] + }, + "recv": { + "type": "integer" + }, + "sent": { + "type": "integer" + } + } + }, "derp.ServerInfoMessage": { "type": "object", "properties": { @@ -12548,6 +12619,9 @@ } } }, + "key.NodePublic": { + "type": "object" + }, "netcheck.Report": { "type": "object", "properties": { diff --git a/coderd/coderd.go b/coderd/coderd.go index c76b9d92a1..5a8c8d828d 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "crypto/x509" "database/sql" + "expvar" "flag" "fmt" "io" @@ -85,6 +86,8 @@ func init() { globalHTTPSwaggerHandler = httpSwagger.Handler(httpSwagger.URL("/swagger/doc.json")) } +var expDERPOnce = sync.Once{} + // Options are requires parameters for Coder to start. type Options struct { AccessURL *url.URL @@ -561,6 +564,16 @@ func New(options *Options) *API { derpHandler := derphttp.Handler(api.DERPServer) derpHandler, api.derpCloseFunc = tailnet.WithWebsocketSupport(api.DERPServer, derpHandler) + // Register DERP on expvar HTTP handler, which we serve below in the router, c.f. expvar.Handler() + // These are the metrics the DERP server exposes. + // TODO: export via prometheus + expDERPOnce.Do(func() { + // We need to do this via a global Once because expvar registry is global and panics if we + // register multiple times. In production there is only one Coderd and one DERP server per + // process, but in testing, we create multiple of both, so the Once protects us from + // panicking. + expvar.Publish("derp", api.DERPServer.ExpVar()) + }) cors := httpmw.Cors(options.DeploymentValues.Dangerous.AllowAllCors.Value()) prometheusMW := httpmw.Prometheus(options.PrometheusRegistry) @@ -1038,6 +1051,10 @@ func New(options *Options) *API { r.Use(httpmw.ExtractUserParam(options.Database)) r.Get("/debug-link", api.userDebugOIDC) }) + r.Route("/derp", func(r chi.Router) { + r.Get("/traffic", options.DERPServer.ServeDebugTraffic) + }) + r.Method("GET", "/expvar", expvar.Handler()) // contains DERP metrics as well as cmdline and memstats }) }) diff --git a/coderd/debug.go b/coderd/debug.go index 0e9bb6acbb..40e3b10b82 100644 --- a/coderd/debug.go +++ b/coderd/debug.go @@ -276,7 +276,7 @@ func validateHealthSettings(settings codersdk.HealthSettings) error { } // 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 @@ -287,6 +287,26 @@ func validateHealthSettings(settings codersdk.HealthSettings) error { // @x-apidocgen {"skip": true} func _debugws(http.ResponseWriter, *http.Request) {} //nolint:unused +// @Summary Debug DERP traffic +// @ID debug-derp-traffic +// @Security CoderSessionToken +// @Produce json +// @Success 200 {array} derp.BytesSentRecv +// @Tags Debug +// @Router /debug/derp/traffic [get] +// @x-apidocgen {"skip": true} +func _debugDERPTraffic(http.ResponseWriter, *http.Request) {} //nolint:unused + +// @Summary Debug expvar +// @ID debug-expvar +// @Security CoderSessionToken +// @Produce json +// @Tags Debug +// @Success 200 {object} map[string]any +// @Router /debug/expvar [get] +// @x-apidocgen {"skip": true} +func _debugExpVar(http.ResponseWriter, *http.Request) {} //nolint:unused + func loadDismissedHealthchecks(ctx context.Context, db database.Store, logger slog.Logger) []codersdk.HealthSection { dismissedHealthchecks := []codersdk.HealthSection{} settingsJSON, err := db.GetHealthSettings(ctx) diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 364b943fc3..2ea43f292b 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -7517,6 +7517,24 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `count` | integer | false | | | | `workspaces` | array of [codersdk.Workspace](#codersdkworkspace) | false | | | +## derp.BytesSentRecv + +```json +{ + "key": {}, + "recv": 0, + "sent": 0 +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------ | -------------------------------- | -------- | ------------ | -------------------------------------------------------------------- | +| `key` | [key.NodePublic](#keynodepublic) | false | | Key is the public key of the client which sent/received these bytes. | +| `recv` | integer | false | | | +| `sent` | integer | false | | | + ## derp.ServerInfoMessage ```json @@ -8630,6 +8648,16 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `warnings` | array of [health.Message](#healthmessage) | false | | | | `workspace_proxies` | [codersdk.RegionsResponse-codersdk_WorkspaceProxy](#codersdkregionsresponse-codersdk_workspaceproxy) | false | | | +## key.NodePublic + +```json +{} +``` + +### Properties + +_None_ + ## netcheck.Report ```json