diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index f6a15f4c44..95413dff67 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -954,6 +954,138 @@ const docTemplate = `{ } } }, + "/debug/metrics": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": [ + "Debug" + ], + "summary": "Debug metrics", + "operationId": "debug-metrics", + "responses": { + "200": { + "description": "OK" + } + }, + "x-apidocgen": { + "skip": true + } + } + }, + "/debug/pprof": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": [ + "Debug" + ], + "summary": "Debug pprof index", + "operationId": "debug-pprof-index", + "responses": { + "200": { + "description": "OK" + } + }, + "x-apidocgen": { + "skip": true + } + } + }, + "/debug/pprof/cmdline": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": [ + "Debug" + ], + "summary": "Debug pprof cmdline", + "operationId": "debug-pprof-cmdline", + "responses": { + "200": { + "description": "OK" + } + }, + "x-apidocgen": { + "skip": true + } + } + }, + "/debug/pprof/profile": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": [ + "Debug" + ], + "summary": "Debug pprof profile", + "operationId": "debug-pprof-profile", + "responses": { + "200": { + "description": "OK" + } + }, + "x-apidocgen": { + "skip": true + } + } + }, + "/debug/pprof/symbol": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": [ + "Debug" + ], + "summary": "Debug pprof symbol", + "operationId": "debug-pprof-symbol", + "responses": { + "200": { + "description": "OK" + } + }, + "x-apidocgen": { + "skip": true + } + } + }, + "/debug/pprof/trace": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": [ + "Debug" + ], + "summary": "Debug pprof trace", + "operationId": "debug-pprof-trace", + "responses": { + "200": { + "description": "OK" + } + }, + "x-apidocgen": { + "skip": true + } + } + }, "/debug/tailnet": { "get": { "security": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 780f89d876..a3f57d4fa4 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -840,6 +840,126 @@ } } }, + "/debug/metrics": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": ["Debug"], + "summary": "Debug metrics", + "operationId": "debug-metrics", + "responses": { + "200": { + "description": "OK" + } + }, + "x-apidocgen": { + "skip": true + } + } + }, + "/debug/pprof": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": ["Debug"], + "summary": "Debug pprof index", + "operationId": "debug-pprof-index", + "responses": { + "200": { + "description": "OK" + } + }, + "x-apidocgen": { + "skip": true + } + } + }, + "/debug/pprof/cmdline": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": ["Debug"], + "summary": "Debug pprof cmdline", + "operationId": "debug-pprof-cmdline", + "responses": { + "200": { + "description": "OK" + } + }, + "x-apidocgen": { + "skip": true + } + } + }, + "/debug/pprof/profile": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": ["Debug"], + "summary": "Debug pprof profile", + "operationId": "debug-pprof-profile", + "responses": { + "200": { + "description": "OK" + } + }, + "x-apidocgen": { + "skip": true + } + } + }, + "/debug/pprof/symbol": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": ["Debug"], + "summary": "Debug pprof symbol", + "operationId": "debug-pprof-symbol", + "responses": { + "200": { + "description": "OK" + } + }, + "x-apidocgen": { + "skip": true + } + } + }, + "/debug/pprof/trace": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": ["Debug"], + "summary": "Debug pprof trace", + "operationId": "debug-pprof-trace", + "responses": { + "200": { + "description": "OK" + } + }, + "x-apidocgen": { + "skip": true + } + } + }, "/debug/tailnet": { "get": { "security": [ diff --git a/coderd/coderd.go b/coderd/coderd.go index 15f2767c22..122018f96d 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -11,6 +11,7 @@ import ( "fmt" "io" "net/http" + httppprof "net/http/pprof" "net/url" "path/filepath" "regexp" @@ -32,6 +33,7 @@ import ( "github.com/google/uuid" "github.com/klauspost/compress/zstd" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" httpSwagger "github.com/swaggo/http-swagger/v2" "go.opentelemetry.io/otel/trace" "golang.org/x/xerrors" @@ -1512,7 +1514,8 @@ func New(options *Options) *API { r.Route("/debug", func(r chi.Router) { r.Use( apiKeyMiddleware, - // Ensure only owners can access debug endpoints. + // Ensure only users with the debug_info:read (e.g. only owners) + // can view debug endpoints. func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { if !api.Authorize(r, policy.ActionRead, rbac.ResourceDebugInfo) { @@ -1545,6 +1548,41 @@ func New(options *Options) *API { }) } r.Method("GET", "/expvar", expvar.Handler()) // contains DERP metrics as well as cmdline and memstats + + r.Route("/pprof", func(r chi.Router) { + r.Use(func(next http.Handler) http.Handler { + // Some of the pprof handlers strip the `/debug/pprof` + // prefix, so we need to strip our additional prefix as + // well. + return http.StripPrefix("/api/v2", next) + }) + + // Serve the index HTML page. + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + // Redirect to include a trailing slash, otherwise links on + // the generated HTML page will be broken. + if !strings.HasSuffix(r.URL.Path, "/") { + http.Redirect(w, r, "/api/v2/debug/pprof/", http.StatusTemporaryRedirect) + return + } + httppprof.Index(w, r) + }) + + // Handle any out of the box pprof handlers that don't get + // dealt with by the default index handler. See httppprof.init. + r.Get("/cmdline", httppprof.Cmdline) + r.Get("/profile", httppprof.Profile) + r.Get("/symbol", httppprof.Symbol) + r.Get("/trace", httppprof.Trace) + + // Index will handle any standard and custom runtime/pprof + // profiles. + r.Get("/*", httppprof.Index) + }) + + r.Get("/metrics", promhttp.InstrumentMetricHandler( + options.PrometheusRegistry, promhttp.HandlerFor(options.PrometheusRegistry, promhttp.HandlerOpts{}), + ).ServeHTTP) }) // Manage OAuth2 applications that can use Coder as an OAuth2 provider. r.Route("/oauth2-provider", func(r chi.Router) { diff --git a/coderd/coderdtest/swaggerparser.go b/coderd/coderdtest/swaggerparser.go index 4a0b6744a9..cac6fdf7a9 100644 --- a/coderd/coderdtest/swaggerparser.go +++ b/coderd/coderdtest/swaggerparser.go @@ -160,8 +160,9 @@ func VerifySwaggerDefinitions(t *testing.T, router chi.Router, swaggerComments [ t.Run(method+" "+route, func(t *testing.T) { t.Parallel() - // This route is for compatibility purposes and is not documented. - if route == "/workspaceagents/me/metadata" { + // Wildcard routes break the swaggo parser, so we do not document + // them. + if strings.HasSuffix(route, "/*") { return } diff --git a/coderd/debug.go b/coderd/debug.go index 64c7c9e632..4c0eff7f33 100644 --- a/coderd/debug.go +++ b/coderd/debug.go @@ -325,3 +325,57 @@ func loadDismissedHealthchecks(ctx context.Context, db database.Store, logger sl } return dismissedHealthchecks } + +// @Summary Debug pprof index +// @ID debug-pprof-index +// @Security CoderSessionToken +// @Success 200 +// @Tags Debug +// @Router /debug/pprof [get] +// @x-apidocgen {"skip": true} +func _debugPprofIndex(http.ResponseWriter, *http.Request) {} //nolint:unused + +// @Summary Debug pprof cmdline +// @ID debug-pprof-cmdline +// @Security CoderSessionToken +// @Success 200 +// @Tags Debug +// @Router /debug/pprof/cmdline [get] +// @x-apidocgen {"skip": true} +func _debugPprofCmdline(http.ResponseWriter, *http.Request) {} //nolint:unused + +// @Summary Debug pprof profile +// @ID debug-pprof-profile +// @Security CoderSessionToken +// @Success 200 +// @Tags Debug +// @Router /debug/pprof/profile [get] +// @x-apidocgen {"skip": true} +func _debugPprofProfile(http.ResponseWriter, *http.Request) {} //nolint:unused + +// @Summary Debug pprof symbol +// @ID debug-pprof-symbol +// @Security CoderSessionToken +// @Success 200 +// @Tags Debug +// @Router /debug/pprof/symbol [get] +// @x-apidocgen {"skip": true} +func _debugPprofSymbol(http.ResponseWriter, *http.Request) {} //nolint:unused + +// @Summary Debug pprof trace +// @ID debug-pprof-trace +// @Security CoderSessionToken +// @Success 200 +// @Tags Debug +// @Router /debug/pprof/trace [get] +// @x-apidocgen {"skip": true} +func _debugPprofTrace(http.ResponseWriter, *http.Request) {} //nolint:unused + +// @Summary Debug metrics +// @ID debug-metrics +// @Security CoderSessionToken +// @Success 200 +// @Tags Debug +// @Router /debug/metrics [get] +// @x-apidocgen {"skip": true} +func _debugMetrics(http.ResponseWriter, *http.Request) {} //nolint:unused