117 lines
3.3 KiB
Go
117 lines
3.3 KiB
Go
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")
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|