feat: mount pprof and metrics to /api/v2/debug for admins (#20353)

Adds the following debug routes for people with the `debug_info:read`
permission:
- `/api/v2/debug/pprof` for `net/http/pprof`
    - `/`
    - `/cmdline`
    - `/profile`
    - `/symbol`
    - `/trace`
    - `/*`
- `/api/v2/debug/metrics` for Prometheus metrics
This commit is contained in:
Dean Sheather
2025-10-21 14:13:11 +11:00
committed by GitHub
parent 5a18cf4c86
commit 0652b18ebc
5 changed files with 348 additions and 3 deletions
+132
View File
@@ -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": [
+120
View File
@@ -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": [
+39 -1
View File
@@ -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) {
+3 -2
View File
@@ -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
}
+54
View File
@@ -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