diff --git a/openapi.go b/openapi.go index 6dc427f..3c1c240 100644 --- a/openapi.go +++ b/openapi.go @@ -35,6 +35,8 @@ type SpecBuilder struct { LicenseURL string ExternalDocsDescription string ExternalDocsURL string + PprofEnabled bool + ExpvarEnabled bool } type preparedRouteGroup struct { @@ -197,6 +199,14 @@ func (sb *SpecBuilder) buildPaths(groups []preparedRouteGroup) map[string]any { paths[ssePath] = ssePathItem(ssePath, operationIDs) } + if sb.PprofEnabled { + paths["/debug/pprof"] = pprofPathItem(operationIDs) + } + + if sb.ExpvarEnabled { + paths["/debug/vars"] = expvarPathItem(operationIDs) + } + for _, g := range groups { for _, rd := range g.descs { fullPath := joinOpenAPIPath(g.group.BasePath(), rd.Path) @@ -598,6 +608,14 @@ func (sb *SpecBuilder) buildTags(groups []preparedRouteGroup) []map[string]any { seen["events"] = true } + if (sb.PprofEnabled || sb.ExpvarEnabled) && !seen["debug"] { + tags = append(tags, map[string]any{ + "name": "debug", + "description": "Runtime debug endpoints", + }) + seen["debug"] = true + } + for _, g := range groups { name := strings.TrimSpace(g.group.Name()) if name != "" && !seen[name] { @@ -703,6 +721,113 @@ func ssePathItem(path string, operationIDs map[string]int) map[string]any { } } +func pprofPathItem(operationIDs map[string]int) map[string]any { + return map[string]any{ + "get": map[string]any{ + "summary": "pprof index", + "description": "Lists the available Go runtime profiles", + "tags": []string{"debug"}, + "operationId": operationID("get", "/debug/pprof", operationIDs), + "security": []any{ + map[string]any{ + "bearerAuth": []any{}, + }, + }, + "responses": map[string]any{ + "200": map[string]any{ + "description": "pprof index", + "content": map[string]any{ + "text/html": map[string]any{ + "schema": map[string]any{ + "type": "string", + }, + }, + }, + "headers": standardResponseHeaders(), + }, + "401": map[string]any{ + "description": "Unauthorised", + "content": map[string]any{ + "application/json": map[string]any{ + "schema": map[string]any{ + "type": "object", + "additionalProperties": true, + }, + }, + }, + "headers": standardResponseHeaders(), + }, + "403": map[string]any{ + "description": "Forbidden", + "content": map[string]any{ + "application/json": map[string]any{ + "schema": map[string]any{ + "type": "object", + "additionalProperties": true, + }, + }, + }, + "headers": standardResponseHeaders(), + }, + }, + }, + } +} + +func expvarPathItem(operationIDs map[string]int) map[string]any { + return map[string]any{ + "get": map[string]any{ + "summary": "Runtime metrics", + "description": "Returns expvar metrics as JSON", + "tags": []string{"debug"}, + "operationId": operationID("get", "/debug/vars", operationIDs), + "security": []any{ + map[string]any{ + "bearerAuth": []any{}, + }, + }, + "responses": map[string]any{ + "200": map[string]any{ + "description": "Runtime metrics", + "content": map[string]any{ + "application/json": map[string]any{ + "schema": map[string]any{ + "type": "object", + "additionalProperties": true, + }, + }, + }, + "headers": standardResponseHeaders(), + }, + "401": map[string]any{ + "description": "Unauthorised", + "content": map[string]any{ + "application/json": map[string]any{ + "schema": map[string]any{ + "type": "object", + "additionalProperties": true, + }, + }, + }, + "headers": standardResponseHeaders(), + }, + "403": map[string]any{ + "description": "Forbidden", + "content": map[string]any{ + "application/json": map[string]any{ + "schema": map[string]any{ + "type": "object", + "additionalProperties": true, + }, + }, + }, + "headers": standardResponseHeaders(), + }, + }, + }, + } +} + func graphqlRequestSchema() map[string]any { return map[string]any{ "type": "object", diff --git a/spec_builder_helper.go b/spec_builder_helper.go index f10c9cd..708de6d 100644 --- a/spec_builder_helper.go +++ b/spec_builder_helper.go @@ -36,6 +36,8 @@ func (e *Engine) OpenAPISpecBuilder() *SpecBuilder { if e.sseBroker != nil { builder.SSEPath = resolveSSEPath(e.ssePath) } + builder.PprofEnabled = e.pprofEnabled + builder.ExpvarEnabled = e.expvarEnabled return builder } diff --git a/spec_builder_helper_test.go b/spec_builder_helper_test.go index 94e4982..6958d7a 100644 --- a/spec_builder_helper_test.go +++ b/spec_builder_helper_test.go @@ -25,6 +25,8 @@ func TestEngine_Good_OpenAPISpecBuilderCarriesEngineMetadata(t *testing.T) { api.WithGraphQL(newTestSchema(), api.WithGraphQLPath("/gql")), api.WithSSE(broker), api.WithSSEPath("/events"), + api.WithPprof(), + api.WithExpvar(), ) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -107,4 +109,10 @@ func TestEngine_Good_OpenAPISpecBuilderCarriesEngineMetadata(t *testing.T) { if _, ok := paths["/events"]; !ok { t.Fatal("expected SSE path from engine metadata in generated spec") } + if _, ok := paths["/debug/pprof"]; !ok { + t.Fatal("expected pprof path from engine metadata in generated spec") + } + if _, ok := paths["/debug/vars"]; !ok { + t.Fatal("expected expvar path from engine metadata in generated spec") + } }