feat: add gqlgen Recover that logs panics as one structured record #206

Merged
argoyle merged 1 commits from feat/graphql-recover into main 2026-10-16 23:12:45 +00:00
Owner

Summary

gqlgen's graphql.DefaultRecover prints a recovered resolver panic and debug.PrintStack() to stderr, so the log collector stores every stack line as a separate, level-less entry (~60 entries per panic in production).

Adds otelsetup.Recover (func(ctx context.Context, err any) error, a graphql.RecoverFunc for gqlgen v0.17.95):

  • logs the panic as one ERROR record via slog.Default().ErrorContext (the default logger services set up through logging.SetupLogger), with panic, stack (debug.Stack()) and path (GraphQL field path, when available) attributes
  • records the error on the active span (RecordError with stack trace, SetStatus(codes.Error))
  • returns the generic internal system error gqlerror to the client, without the panic text

Services install it with srv.SetRecoverFunc(otelsetup.Recover).

Test plan

  • TestRecover: error and non-error panic values; asserts the generic gqlerror, no panic text leak, exactly one ERROR log record with panic/stack attributes, and span status Error with an exception event carrying message and stacktrace (in-memory tracetest.SpanRecorder)
  • go test -race ./... passes, prek run --all-files passes

🤖 Generated with Claude Code

https://claude.ai/code/session_01H6eVuR4Yt3m7TbDTDwyZJi

## Summary gqlgen's `graphql.DefaultRecover` prints a recovered resolver panic and `debug.PrintStack()` to stderr, so the log collector stores every stack line as a separate, level-less entry (~60 entries per panic in production). Adds `otelsetup.Recover` (`func(ctx context.Context, err any) error`, a `graphql.RecoverFunc` for gqlgen v0.17.95): - logs the panic as **one** ERROR record via `slog.Default().ErrorContext` (the default logger services set up through `logging.SetupLogger`), with `panic`, `stack` (`debug.Stack()`) and `path` (GraphQL field path, when available) attributes - records the error on the active span (`RecordError` with stack trace, `SetStatus(codes.Error)`) - returns the generic `internal system error` gqlerror to the client, without the panic text Services install it with `srv.SetRecoverFunc(otelsetup.Recover)`. ## Test plan - `TestRecover`: error and non-error panic values; asserts the generic gqlerror, no panic text leak, exactly one ERROR log record with panic/stack attributes, and span status Error with an `exception` event carrying message and stacktrace (in-memory `tracetest.SpanRecorder`) - `go test -race ./...` passes, `prek run --all-files` passes 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01H6eVuR4Yt3m7TbDTDwyZJi
argoyle added 1 commit 2026-10-16 23:05:01 +00:00
feat: add gqlgen Recover that logs panics as one structured record
otelsetup / vulnerabilities (pull_request) Successful in 31s
otelsetup / test (pull_request) Successful in 3m41s
pre-commit / pre-commit (pull_request) Successful in 3m2s
ff0585ad68
gqlgen's DefaultRecover prints the panic and debug.PrintStack() to stderr, so the log collector stores every stack line as a separate, level-less entry. Recover logs the panic as a single ERROR record via slog.Default() with panic, stack and path attributes, records it on the active span with status Error, and returns the generic "internal system error" without leaking the panic text. Install with srv.SetRecoverFunc(otelsetup.Recover).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H6eVuR4Yt3m7TbDTDwyZJi
argoyle scheduled this pull request to auto merge when all checks succeed 2026-10-16 23:09:06 +00:00

Coverage Report

Total coverage: 37%

## Coverage Report Total coverage: **37%**
argoyle merged commit 17130005c9 into main 2026-10-16 23:12:45 +00:00
argoyle deleted branch feat/graphql-recover 2026-10-16 23:12:45 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: shiny/otelsetup#206