package otelsetup import ( "context" "fmt" "log/slog" "runtime/debug" "github.com/99designs/gqlgen/graphql" "github.com/vektah/gqlparser/v2/gqlerror" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" ) // Recover is a gqlgen recover function, installed with srv.SetRecoverFunc(otelsetup.Recover). // Unlike graphql.DefaultRecover, which prints the stack to stderr line by line, it logs the // panic as a single structured ERROR record (via slog.Default) with the stack as an attribute, // records it on the active span and marks the span as failed. The client gets the same generic // "internal system error" as with the default, without the panic text. func Recover(ctx context.Context, err any) error { stack := string(debug.Stack()) panicErr, ok := err.(error) if !ok { panicErr = fmt.Errorf("%v", err) } attrs := []any{ slog.String("panic", panicErr.Error()), slog.String("stack", stack), } if fc := graphql.GetFieldContext(ctx); fc != nil { attrs = append(attrs, slog.String("path", fc.Path().String())) } slog.Default().ErrorContext(ctx, "recovered panic in graphql resolver", attrs...) span := trace.SpanFromContext(ctx) span.RecordError(panicErr, trace.WithStackTrace(true)) span.SetStatus(codes.Error, "panic: "+panicErr.Error()) return gqlerror.Errorf("internal system error") }