Files
otelsetup/recover.go
T

41 lines
1.3 KiB
Go
Raw Normal View History

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")
}