diff --git a/internal/test/resolvers/main.resolvers.go b/internal/test/resolvers/main.resolvers.go index ee254e8..3ac813b 100644 --- a/internal/test/resolvers/main.resolvers.go +++ b/internal/test/resolvers/main.resolvers.go @@ -22,6 +22,9 @@ func (r *queryResolver) User(ctx context.Context, name string) (*model.User, err if name == "forbidden" { return nil, ForbiddenError{} } + if name == "not_found" { + return nil, &NotFoundError{} + } age := 17 return &model.User{Name: name, Age: &age}, nil } diff --git a/internal/test/resolvers/resolver.go b/internal/test/resolvers/resolver.go index 373769a..cdefdc6 100644 --- a/internal/test/resolvers/resolver.go +++ b/internal/test/resolvers/resolver.go @@ -11,3 +11,7 @@ type ForbiddenError struct{} func (ForbiddenError) Error() string { return "forbidden" } + +type NotFoundError struct{} + +func (NotFoundError) Error() string { return "not found" } diff --git a/tracer.go b/tracer.go index c5103bc..f197876 100644 --- a/tracer.go +++ b/tracer.go @@ -27,6 +27,7 @@ type config struct { tracerProvider trace.TracerProvider complexityExtensionName string traceStructFields bool + errorSelector ErrorSelector } type Option func(c *config) @@ -55,6 +56,15 @@ func TraceStructFields(v bool) Option { } } +// ErrorSelector is a predicate that the error should be recorded. +// +// The span records only errors that the function returns true. +// If the function returns false against all of the errors in the gqlgen response, the span status will be Unset instead of Error. +type ErrorSelector func(err error) bool + +// WithErrorSelector creates an Option that tells Tracer uses the given selector. +func WithErrorSelector(fn ErrorSelector) Option { return func(c *config) { c.errorSelector = fn } } + // New returns a new Tracer with given options. func New(opts ...Option) Tracer { cfg := &config{} @@ -68,10 +78,14 @@ func New(opts ...Option) Tracer { tracer: cfg.tracerProvider.Tracer(tracerName), complexityExtensionName: cfg.complexityExtensionName, traceStructFields: cfg.traceStructFields, + errorSelector: cfg.errorSelector, } if t.complexityExtensionName == "" { t.complexityExtensionName = defaultComplexityExtensionName } + if t.errorSelector == nil { + t.errorSelector = func(_ error) bool { return true } + } return t } @@ -80,6 +94,7 @@ type Tracer struct { tracer trace.Tracer complexityExtensionName string traceStructFields bool + errorSelector ErrorSelector } var _ interface { @@ -143,9 +158,9 @@ func (t Tracer) InterceptResponse(ctx context.Context, next graphql.ResponseHand span.SetAttributes(attrs...) resp := next(ctx) if resp != nil && len(resp.Errors) > 0 { - recordGQLErrors(span, resp.Errors) + recordGQLErrors(span, resp.Errors, t.errorSelector) if parentSpan.SpanContext().IsValid() { - recordGQLErrors(parentSpan, resp.Errors) + recordGQLErrors(parentSpan, resp.Errors, t.errorSelector) } } return resp @@ -203,7 +218,7 @@ func (t Tracer) InterceptField(ctx context.Context, next graphql.Resolver) (any, resp, err := next(ctx) if errs := graphql.GetFieldErrors(ctx, fieldCtx); len(errs) > 0 { - recordGQLErrors(span, errs) + recordGQLErrors(span, errs, t.errorSelector) } return resp, err } @@ -223,15 +238,20 @@ func operationName(ctx context.Context) string { return string(op.Operation) } -func recordGQLErrors(span trace.Span, errs gqlerror.List) { - span.SetStatus(codes.Error, errs.Error()) - for _, e := range errs { - attrs := []attribute.KeyValue{ - keyErrorPath.String(e.Path.String()), +func recordGQLErrors(span trace.Span, errs gqlerror.List, selector ErrorSelector) { + var recorded bool + for _, gqlErr := range errs { + if !selector(gqlErr) { + continue } - err := unwrapErr(e) - span.RecordError(err, trace.WithStackTrace(true), trace.WithAttributes(attrs...)) + recorded = true + attrErrorPath := keyErrorPath.String(gqlErr.Path.String()) + span.RecordError(unwrapErr(gqlErr), trace.WithStackTrace(true), trace.WithAttributes(attrErrorPath)) + } + if !recorded { + return } + span.SetStatus(codes.Error, errs.Error()) } func unwrapErr(err error) error { diff --git a/tracer_test.go b/tracer_test.go index 24bbcbe..ae295da 100644 --- a/tracer_test.go +++ b/tracer_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "io" "net/http" "net/http/httptest" @@ -310,6 +311,51 @@ func TestTracer(t *testing.T) { }, }, }, + { + name: "error that must be ignored from root field", + options: []otelgqlgen.Option{ + otelgqlgen.WithErrorSelector(func(err error) bool { + return !errors.Is(err, &resolvers.NotFoundError{}) + }), + }, + params: &graphql.RawParams{ + Query: `query($name: String!) {user(name: $name) {name}}`, + Variables: map[string]any{"name": "not_found"}, + }, + spans: tracetest.SpanStubs{ + {Name: "read", SpanKind: trace.SpanKindServer}, + {Name: "parsing", SpanKind: trace.SpanKindServer}, + {Name: "validation", SpanKind: trace.SpanKindServer}, + { + Name: "Query/user", + SpanKind: trace.SpanKindServer, + Attributes: []attribute.KeyValue{ + attribute.String("graphql.resolver.object", "Query"), + attribute.String("graphql.resolver.field", "user"), + attribute.String("graphql.resolver.alias", "user"), + attribute.String("graphql.resolver.args.name", "$name"), + attribute.String("graphql.resolver.path", "user"), + attribute.Bool("graphql.resolver.is_method", true), + attribute.Bool("graphql.resolver.is_resolver", true), + }, + }, + { + Name: "query", + SpanKind: trace.SpanKindServer, + Attributes: []attribute.KeyValue{ + attribute.String("graphql.operation.name", "query"), + attribute.String("graphql.operation.type", "query"), + attribute.String("graphql.operation.variables.name", "not_found"), + attribute.Int("graphql.operation.complexity.limit", 1000), + attribute.Int("graphql.operation.complexity.calculated", 2), + }, + }, + { + Name: "http_handler", + SpanKind: trace.SpanKindInternal, + }, + }, + }, { name: "error from edge fields", params: &graphql.RawParams{