package otelsetup import ( "context" "errors" "log/slog" "strings" "sync" "testing" "github.com/99designs/gqlgen/graphql" "github.com/vektah/gqlparser/v2/gqlerror" "go.opentelemetry.io/otel/codes" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" ) type recordingHandler struct { mu sync.Mutex records []slog.Record } func (h *recordingHandler) Enabled(context.Context, slog.Level) bool { return true } func (h *recordingHandler) Handle(_ context.Context, r slog.Record) error { h.mu.Lock() defer h.mu.Unlock() h.records = append(h.records, r.Clone()) return nil } func (h *recordingHandler) WithAttrs([]slog.Attr) slog.Handler { return h } func (h *recordingHandler) WithGroup(string) slog.Handler { return h } func TestRecover(t *testing.T) { var _ graphql.RecoverFunc = Recover tests := []struct { name string panicVal any wantPanic string }{ {name: "error value", panicVal: errors.New("runtime error: invalid memory address or nil pointer dereference"), wantPanic: "runtime error: invalid memory address or nil pointer dereference"}, {name: "string value", panicVal: "boom", wantPanic: "boom"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { h := &recordingHandler{} prev := slog.Default() slog.SetDefault(slog.New(h)) t.Cleanup(func() { slog.SetDefault(prev) }) sr := tracetest.NewSpanRecorder() tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) ctx, span := tp.Tracer("test").Start(context.Background(), "resolver") err := Recover(ctx, tt.panicVal) span.End() var gqlErr *gqlerror.Error if !errors.As(err, &gqlErr) || gqlErr.Message != "internal system error" { t.Fatalf("Recover() error = %v, want gqlerror \"internal system error\"", err) } if strings.Contains(err.Error(), tt.wantPanic) { t.Errorf("Recover() error leaks panic text: %v", err) } if len(h.records) != 1 { t.Fatalf("got %d log records, want exactly 1", len(h.records)) } rec := h.records[0] if rec.Level != slog.LevelError { t.Errorf("log level = %v, want ERROR", rec.Level) } got := map[string]string{} rec.Attrs(func(a slog.Attr) bool { got[a.Key] = a.Value.String() return true }) if got["panic"] != tt.wantPanic { t.Errorf("panic attr = %q, want %q", got["panic"], tt.wantPanic) } if !strings.Contains(got["stack"], "goroutine") { t.Errorf("stack attr missing stack trace: %q", got["stack"]) } spans := sr.Ended() if len(spans) != 1 { t.Fatalf("got %d spans, want 1", len(spans)) } s := spans[0] if s.Status().Code != codes.Error { t.Errorf("span status = %v, want Error", s.Status().Code) } var exception bool for _, ev := range s.Events() { if ev.Name != "exception" { continue } exception = true attrs := map[string]string{} for _, kv := range ev.Attributes { attrs[string(kv.Key)] = kv.Value.String() } if attrs["exception.message"] != tt.wantPanic { t.Errorf("exception.message = %q, want %q", attrs["exception.message"], tt.wantPanic) } if attrs["exception.stacktrace"] == "" { t.Error("exception event has no stacktrace") } } if !exception { t.Error("span has no exception event") } }) } }