From 674e1cfa6c402104b6b374d842f6e49e42c07b17 Mon Sep 17 00:00:00 2001 From: Pavel Nikolov Date: Tue, 1 Feb 2022 20:58:29 +1100 Subject: [PATCH] Refactor trace package Remove dependency for graphql-go on OpenTracing and OpenTelemetry except where those tracers are explicitly configured for use. --- README.md | 46 ++++++++++- go.mod | 9 +-- go.sum | 27 ++++--- graphql.go | 29 +++---- graphql_test.go | 24 +++--- internal/exec/exec.go | 4 +- trace/noop/trace.go | 24 ++++++ trace/noop/trace_test.go | 22 ++++++ trace/opentracing/trace.go | 79 +++++++++++++++++++ trace/opentracing/trace_test.go | 22 ++++++ trace/{otel_trace.go => otel/trace.go} | 24 +++--- trace/otel/trace_test.go | 29 +++++++ trace/trace.go | 101 ++++--------------------- trace/trace_test.go | 35 ++++++++- trace/tracer/tracer.go | 34 +++++++++ trace/validation_trace.go | 23 ++---- 16 files changed, 367 insertions(+), 165 deletions(-) create mode 100644 trace/noop/trace.go create mode 100644 trace/noop/trace_test.go create mode 100644 trace/opentracing/trace.go create mode 100644 trace/opentracing/trace_test.go rename trace/{otel_trace.go => otel/trace.go} (72%) create mode 100644 trace/otel/trace_test.go create mode 100644 trace/tracer/tracer.go diff --git a/README.md b/README.md index 69d017ce..f9c73861 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ safe for production use. - minimal API - support for `context.Context` -- support for the `OpenTracing` standard +- support for the `OpenTelemetry` and `OpenTracing` standards - schema type-checking against resolvers - resolvers are matched to the schema based on method sets (can resolve a GraphQL schema with a Go interface or Go struct). - handles panics in resolvers @@ -108,8 +108,7 @@ func (r *helloWorldResolver) Hello(ctx context.Context) (string, error) { - `UseFieldResolvers()` specifies whether to use struct field resolvers. - `MaxDepth(n int)` specifies the maximum field nesting depth in a query. The default is 0 which disables max depth checking. - `MaxParallelism(n int)` specifies the maximum number of resolvers per request allowed to run in parallel. The default is 10. -- `Tracer(tracer trace.Tracer)` is used to trace queries and fields. It defaults to `trace.OpenTracingTracer`. -- `ValidationTracer(tracer trace.ValidationTracer)` is used to trace validation errors. It defaults to `trace.NoopValidationTracer`. +- `Tracer(tracer trace.Tracer)` is used to trace queries and fields. It defaults to `noop.Tracer`. - `Logger(logger log.Logger)` is used to log panics during query execution. It defaults to `exec.DefaultLogger`. - `PanicHandler(panicHandler errors.PanicHandler)` is used to transform panics into errors during query execution. It defaults to `errors.DefaultPanicHandler`. - `DisableIntrospection()` disables introspection queries. @@ -165,6 +164,47 @@ Which could produce a GraphQL error such as: } ``` +### Tracing + +By default the library uses `noop.Tracer`. If you want to change that you can use the OpenTelemetry or the OpenTracing implementations, respectively: + +```go +// OpenTelemetry tracer +package main + +import ( + "github.com/graph-gophers/graphql-go" + "github.com/graph-gophers/graphql-go/example/starwars" + otelgraphql "github.com/graph-gophers/graphql-go/trace/otel" + "github.com/graph-gophers/graphql-go/trace/tracer" +) +// ... +_, err := graphql.ParseSchema(starwars.Schema, nil, graphql.Tracer(otelgraphql.DefaultTracer())) +// ... +``` +Alternatively you can pass an existing trace.Tracer instance: +```go +_, err = graphql.ParseSchema(starwars.Schema, nil, graphql.Tracer(&otelgraphql.Tracer{Tracer: otel.Tracer("example")})) +``` + + +```go +// OpenTracing tracer +package main + +import ( + "github.com/graph-gophers/graphql-go" + "github.com/graph-gophers/graphql-go/example/starwars" + "github.com/graph-gophers/graphql-go/trace/opentracing" + "github.com/graph-gophers/graphql-go/trace/tracer" +) +// ... +_, err := graphql.ParseSchema(starwars.Schema, nil, graphql.Tracer(opentracing.Tracer{})) + +// ... +``` + + ### [Examples](https://github.com/graph-gophers/graphql-go/wiki/Examples) ### [Companies that use this library](https://github.com/graph-gophers/graphql-go/wiki/Users) diff --git a/go.mod b/go.mod index 2873065c..423043e8 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,9 @@ module github.com/graph-gophers/graphql-go +go 1.13 + require ( - github.com/go-logr/stdr v1.2.2 // indirect github.com/opentracing/opentracing-go v1.2.0 - go.opentelemetry.io/otel v1.3.0 - go.opentelemetry.io/otel/trace v1.3.0 + go.opentelemetry.io/otel v1.6.3 + go.opentelemetry.io/otel/trace v1.6.3 ) - -go 1.13 diff --git a/go.sum b/go.sum index fd8a1af5..b987a5d2 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,27 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -go.opentelemetry.io/otel v1.3.0 h1:APxLf0eiBwLl+SOXiJJCVYzA1OOJNyAoV8C5RNRyy7Y= -go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs= -go.opentelemetry.io/otel/trace v1.3.0 h1:doy8Hzb1RJ+I3yFhtDmwNc7tIyw1tNMOIsyPzp1NOGY= -go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.opentelemetry.io/otel v1.6.3 h1:FLOfo8f9JzFVFVyU+MSRJc2HdEAXQgm7pIv2uFKRSZE= +go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI= +go.opentelemetry.io/otel/trace v1.6.3 h1:IqN4L+5b0mPNjdXIiZ90Ni4Bl5BRkDQywePLWemd9bc= +go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/graphql.go b/graphql.go index 76a6434d..ccdf605a 100644 --- a/graphql.go +++ b/graphql.go @@ -16,7 +16,8 @@ import ( "github.com/graph-gophers/graphql-go/internal/validation" "github.com/graph-gophers/graphql-go/introspection" "github.com/graph-gophers/graphql-go/log" - "github.com/graph-gophers/graphql-go/trace" + "github.com/graph-gophers/graphql-go/trace/noop" + "github.com/graph-gophers/graphql-go/trace/tracer" "github.com/graph-gophers/graphql-go/types" ) @@ -27,7 +28,7 @@ func ParseSchema(schemaString string, resolver interface{}, opts ...SchemaOpt) ( s := &Schema{ schema: schema.New(), maxParallelism: 10, - tracer: trace.OpenTracingTracer{}, + tracer: noop.Tracer{}, logger: &log.DefaultLogger{}, panicHandler: &errors.DefaultPanicHandler{}, } @@ -36,10 +37,10 @@ func ParseSchema(schemaString string, resolver interface{}, opts ...SchemaOpt) ( } if s.validationTracer == nil { - if tracer, ok := s.tracer.(trace.ValidationTracerContext); ok { - s.validationTracer = tracer + if t, ok := s.tracer.(tracer.ValidationTracer); ok { + s.validationTracer = t } else { - s.validationTracer = &validationBridgingTracer{tracer: trace.NoopValidationTracer{}} + s.validationTracer = &validationBridgingTracer{tracer: tracer.LegacyNoopValidationTracer{}} //nolint:staticcheck } } @@ -75,8 +76,8 @@ type Schema struct { maxDepth int maxParallelism int - tracer trace.Tracer - validationTracer trace.ValidationTracerContext + tracer tracer.Tracer + validationTracer tracer.ValidationTracer logger log.Logger panicHandler errors.PanicHandler useStringDescriptions bool @@ -122,16 +123,16 @@ func MaxParallelism(n int) SchemaOpt { } } -// Tracer is used to trace queries and fields. It defaults to trace.OpenTracingTracer. -func Tracer(tracer trace.Tracer) SchemaOpt { +// Tracer is used to trace queries and fields. It defaults to tracer.Noop. +func Tracer(tracer tracer.Tracer) SchemaOpt { return func(s *Schema) { s.tracer = tracer } } -// ValidationTracer is used to trace validation errors. It defaults to trace.NoopValidationTracer. -// Deprecated: context is needed to support tracing correctly. Use a Tracer which implements trace.ValidationTracerContext. -func ValidationTracer(tracer trace.ValidationTracer) SchemaOpt { //nolint:staticcheck +// ValidationTracer is used to trace validation errors. It defaults to tracer.LegacyNoopValidationTracer. +// Deprecated: context is needed to support tracing correctly. Use a Tracer which implements tracer.ValidationTracer. +func ValidationTracer(tracer tracer.LegacyValidationTracer) SchemaOpt { //nolint:staticcheck return func(s *Schema) { s.validationTracer = &validationBridgingTracer{tracer: tracer} } @@ -296,10 +297,10 @@ func (s *Schema) validateSchema() error { } type validationBridgingTracer struct { - tracer trace.ValidationTracer //nolint:staticcheck + tracer tracer.LegacyValidationTracer //nolint:staticcheck } -func (t *validationBridgingTracer) TraceValidation(context.Context) trace.TraceValidationFinishFunc { +func (t *validationBridgingTracer) TraceValidation(context.Context) func([]*errors.QueryError) { return t.tracer.TraceValidation() } diff --git a/graphql_test.go b/graphql_test.go index 91d0a562..d78ad310 100644 --- a/graphql_test.go +++ b/graphql_test.go @@ -13,7 +13,7 @@ import ( "github.com/graph-gophers/graphql-go/example/starwars" "github.com/graph-gophers/graphql-go/gqltesting" "github.com/graph-gophers/graphql-go/introspection" - "github.com/graph-gophers/graphql-go/trace" + "github.com/graph-gophers/graphql-go/trace/tracer" ) type helloWorldResolver1 struct{} @@ -4053,7 +4053,7 @@ type queryTrace struct { errors []*gqlerrors.QueryError } -func (t *testTracer) TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, trace.TraceFieldFinishFunc) { +func (t *testTracer) TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, func(*gqlerrors.QueryError)) { return ctx, func(qe *gqlerrors.QueryError) { t.mu.Lock() defer t.mu.Unlock() @@ -4071,7 +4071,7 @@ func (t *testTracer) TraceField(ctx context.Context, label, typeName, fieldName } } -func (t *testTracer) TraceQuery(ctx context.Context, document string, opName string, vars map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, trace.TraceQueryFinishFunc) { +func (t *testTracer) TraceQuery(ctx context.Context, document string, opName string, vars map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, func([]*gqlerrors.QueryError)) { return ctx, func(qe []*gqlerrors.QueryError) { t.mu.Lock() defer t.mu.Unlock() @@ -4088,14 +4088,14 @@ func (t *testTracer) TraceQuery(ctx context.Context, document string, opName str } } -var _ trace.Tracer = (*testTracer)(nil) +var _ tracer.Tracer = (*testTracer)(nil) func TestTracer(t *testing.T) { t.Parallel() - tracer := &testTracer{mu: &sync.Mutex{}} + tt := &testTracer{mu: &sync.Mutex{}} - schema, err := graphql.ParseSchema(starwars.Schema, &starwars.Resolver{}, graphql.Tracer(tracer)) + schema, err := graphql.ParseSchema(starwars.Schema, &starwars.Resolver{}, graphql.Tracer(tt)) if err != nil { t.Fatalf("graphql.ParseSchema: %s", err) } @@ -4116,14 +4116,14 @@ func TestTracer(t *testing.T) { _ = schema.Exec(ctx, doc, opName, variables) - tracer.mu.Lock() - defer tracer.mu.Unlock() + tt.mu.Lock() + defer tt.mu.Unlock() - if len(tracer.queries) != 1 { - t.Fatalf("expected one query trace, but got %d: %#v", len(tracer.queries), tracer.queries) + if len(tt.queries) != 1 { + t.Fatalf("expected one query trace, but got %d: %#v", len(tt.queries), tt.queries) } - qt := tracer.queries[0] + qt := tt.queries[0] if qt.document != doc { t.Errorf("mismatched query trace document:\nwant: %q\ngot : %q", doc, qt.document) } @@ -4137,7 +4137,7 @@ func TestTracer(t *testing.T) { {fieldName: "name", typeName: "Human"}, } - checkFieldTraces(t, expectedFieldTraces, tracer.fields) + checkFieldTraces(t, expectedFieldTraces, tt.fields) } func checkFieldTraces(t *testing.T, want, have []fieldTrace) { diff --git a/internal/exec/exec.go b/internal/exec/exec.go index 6b478487..e9056c53 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -14,14 +14,14 @@ import ( "github.com/graph-gophers/graphql-go/internal/exec/selected" "github.com/graph-gophers/graphql-go/internal/query" "github.com/graph-gophers/graphql-go/log" - "github.com/graph-gophers/graphql-go/trace" + "github.com/graph-gophers/graphql-go/trace/tracer" "github.com/graph-gophers/graphql-go/types" ) type Request struct { selected.Request Limiter chan struct{} - Tracer trace.Tracer + Tracer tracer.Tracer Logger log.Logger PanicHandler errors.PanicHandler SubscribeResolverTimeout time.Duration diff --git a/trace/noop/trace.go b/trace/noop/trace.go new file mode 100644 index 00000000..c304c4f2 --- /dev/null +++ b/trace/noop/trace.go @@ -0,0 +1,24 @@ +// Package noop defines a no-op tracer implementation. +package noop + +import ( + "context" + + "github.com/graph-gophers/graphql-go/errors" + "github.com/graph-gophers/graphql-go/introspection" +) + +// Tracer is a no-op tracer that does nothing. +type Tracer struct{} + +func (Tracer) TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, func([]*errors.QueryError)) { + return ctx, func(errs []*errors.QueryError) {} +} + +func (Tracer) TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, func(*errors.QueryError)) { + return ctx, func(err *errors.QueryError) {} +} + +func (Tracer) TraceValidation(context.Context) func([]*errors.QueryError) { + return func(errs []*errors.QueryError) {} +} diff --git a/trace/noop/trace_test.go b/trace/noop/trace_test.go new file mode 100644 index 00000000..661c7c81 --- /dev/null +++ b/trace/noop/trace_test.go @@ -0,0 +1,22 @@ +package noop_test + +import ( + "testing" + + "github.com/graph-gophers/graphql-go" + "github.com/graph-gophers/graphql-go/example/starwars" + "github.com/graph-gophers/graphql-go/trace/noop" + "github.com/graph-gophers/graphql-go/trace/tracer" +) + +func TestInterfaceImplementation(t *testing.T) { + var _ tracer.ValidationTracer = &noop.Tracer{} + var _ tracer.Tracer = &noop.Tracer{} +} + +func TestTracerOption(t *testing.T) { + _, err := graphql.ParseSchema(starwars.Schema, nil, graphql.Tracer(noop.Tracer{})) + if err != nil { + t.Fatal(err) + } +} diff --git a/trace/opentracing/trace.go b/trace/opentracing/trace.go new file mode 100644 index 00000000..a44b7a80 --- /dev/null +++ b/trace/opentracing/trace.go @@ -0,0 +1,79 @@ +package opentracing + +import ( + "context" + "fmt" + + "github.com/graph-gophers/graphql-go/errors" + "github.com/graph-gophers/graphql-go/introspection" + opentracing "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/ext" + "github.com/opentracing/opentracing-go/log" +) + +// Tracer implements the graphql-go Tracer inteface and creates OpenTracing spans. +type Tracer struct{} + +func (Tracer) TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, func([]*errors.QueryError)) { + span, spanCtx := opentracing.StartSpanFromContext(ctx, "GraphQL request") + span.SetTag("graphql.query", queryString) + + if operationName != "" { + span.SetTag("graphql.operationName", operationName) + } + + if len(variables) != 0 { + span.LogFields(log.Object("graphql.variables", variables)) + } + + return spanCtx, func(errs []*errors.QueryError) { + if len(errs) > 0 { + msg := errs[0].Error() + if len(errs) > 1 { + msg += fmt.Sprintf(" (and %d more errors)", len(errs)-1) + } + ext.Error.Set(span, true) + span.SetTag("graphql.error", msg) + } + span.Finish() + } +} + +func (Tracer) TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, func(*errors.QueryError)) { + if trivial { + return ctx, noop + } + + span, spanCtx := opentracing.StartSpanFromContext(ctx, label) + span.SetTag("graphql.type", typeName) + span.SetTag("graphql.field", fieldName) + for name, value := range args { + span.SetTag("graphql.args."+name, value) + } + + return spanCtx, func(err *errors.QueryError) { + if err != nil { + ext.Error.Set(span, true) + span.SetTag("graphql.error", err.Error()) + } + span.Finish() + } +} + +func (Tracer) TraceValidation(ctx context.Context) func([]*errors.QueryError) { + span, _ := opentracing.StartSpanFromContext(ctx, "Validate Query") + + return func(errs []*errors.QueryError) { + if len(errs) > 0 { + msg := errs[0].Error() + if len(errs) > 1 { + msg += fmt.Sprintf(" (and %d more errors)", len(errs)-1) + } + ext.Error.Set(span, true) + span.SetTag("graphql.error", msg) + } + span.Finish() + } +} + +func noop(*errors.QueryError) {} diff --git a/trace/opentracing/trace_test.go b/trace/opentracing/trace_test.go new file mode 100644 index 00000000..457ec553 --- /dev/null +++ b/trace/opentracing/trace_test.go @@ -0,0 +1,22 @@ +package opentracing_test + +import ( + "testing" + + "github.com/graph-gophers/graphql-go" + "github.com/graph-gophers/graphql-go/example/starwars" + "github.com/graph-gophers/graphql-go/trace/opentracing" + "github.com/graph-gophers/graphql-go/trace/tracer" +) + +func TestInterfaceImplementation(t *testing.T) { + var _ tracer.ValidationTracer = &opentracing.Tracer{} + var _ tracer.Tracer = &opentracing.Tracer{} +} + +func TestTracerOption(t *testing.T) { + _, err := graphql.ParseSchema(starwars.Schema, nil, graphql.Tracer(opentracing.Tracer{})) + if err != nil { + t.Fatal(err) + } +} diff --git a/trace/otel_trace.go b/trace/otel/trace.go similarity index 72% rename from trace/otel_trace.go rename to trace/otel/trace.go index ed044cad..a72154b3 100644 --- a/trace/otel_trace.go +++ b/trace/otel/trace.go @@ -1,32 +1,32 @@ -package trace +package otel import ( "context" "fmt" - "github.com/graph-gophers/graphql-go/errors" - "github.com/graph-gophers/graphql-go/introspection" - "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" oteltrace "go.opentelemetry.io/otel/trace" + + "github.com/graph-gophers/graphql-go/errors" + "github.com/graph-gophers/graphql-go/introspection" ) -// DefaultOpenTelemetryTracer creates a tracer using a default name -func DefaultOpenTelemetryTracer() Tracer { - return &OpenTelemetryTracer{ +// DefaultTracer creates a tracer using a default name. +func DefaultTracer() *Tracer { + return &Tracer{ Tracer: otel.Tracer("graphql-go"), } } -// OpenTelemetryTracer is an OpenTelemetry implementation for graphql-go. Set the Tracer +// Tracer is an OpenTelemetry implementation for graphql-go. Set the Tracer // property to your tracer instance as required. -type OpenTelemetryTracer struct { +type Tracer struct { Tracer oteltrace.Tracer } -func (t *OpenTelemetryTracer) TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, TraceQueryFinishFunc) { +func (t *Tracer) TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, func([]*errors.QueryError)) { spanCtx, span := t.Tracer.Start(ctx, "GraphQL Request") var attributes []attribute.KeyValue @@ -52,7 +52,7 @@ func (t *OpenTelemetryTracer) TraceQuery(ctx context.Context, queryString string } } -func (t *OpenTelemetryTracer) TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, TraceFieldFinishFunc) { +func (t *Tracer) TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, func(*errors.QueryError)) { if trivial { return ctx, func(*errors.QueryError) {} } @@ -75,7 +75,7 @@ func (t *OpenTelemetryTracer) TraceField(ctx context.Context, label, typeName, f } } -func (t *OpenTelemetryTracer) TraceValidation(ctx context.Context) TraceValidationFinishFunc { +func (t *Tracer) TraceValidation(ctx context.Context) func([]*errors.QueryError) { _, span := t.Tracer.Start(ctx, "GraphQL Validate") return func(errs []*errors.QueryError) { diff --git a/trace/otel/trace_test.go b/trace/otel/trace_test.go new file mode 100644 index 00000000..020834c1 --- /dev/null +++ b/trace/otel/trace_test.go @@ -0,0 +1,29 @@ +package otel_test + +import ( + "testing" + + "go.opentelemetry.io/otel" + + "github.com/graph-gophers/graphql-go" + "github.com/graph-gophers/graphql-go/example/starwars" + otelgraphql "github.com/graph-gophers/graphql-go/trace/otel" + "github.com/graph-gophers/graphql-go/trace/tracer" +) + +func TestInterfaceImplementation(t *testing.T) { + var _ tracer.ValidationTracer = &otelgraphql.Tracer{} + var _ tracer.Tracer = &otelgraphql.Tracer{} +} + +func TestTracerOption(t *testing.T) { + _, err := graphql.ParseSchema(starwars.Schema, nil, graphql.Tracer(otelgraphql.DefaultTracer())) + if err != nil { + t.Fatal(err) + } + + _, err = graphql.ParseSchema(starwars.Schema, nil, graphql.Tracer(&otelgraphql.Tracer{Tracer: otel.Tracer("example")})) + if err != nil { + t.Fatal(err) + } +} diff --git a/trace/trace.go b/trace/trace.go index 8d5d8a71..954f8e0d 100644 --- a/trace/trace.go +++ b/trace/trace.go @@ -1,96 +1,25 @@ +// The trace package provides tracing functionality. +// Deprecated: this package has been deprecated. Use package trace/tracer instead. package trace import ( - "context" - "fmt" - "github.com/graph-gophers/graphql-go/errors" - "github.com/graph-gophers/graphql-go/introspection" - opentracing "github.com/opentracing/opentracing-go" - "github.com/opentracing/opentracing-go/ext" - "github.com/opentracing/opentracing-go/log" + "github.com/graph-gophers/graphql-go/trace/noop" + "github.com/graph-gophers/graphql-go/trace/opentracing" + "github.com/graph-gophers/graphql-go/trace/tracer" ) -type TraceQueryFinishFunc func([]*errors.QueryError) -type TraceFieldFinishFunc func(*errors.QueryError) - -type Tracer interface { - TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, TraceQueryFinishFunc) - TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, TraceFieldFinishFunc) -} - -type OpenTracingTracer struct{} - -func (OpenTracingTracer) TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, TraceQueryFinishFunc) { - span, spanCtx := opentracing.StartSpanFromContext(ctx, "GraphQL request") - span.SetTag("graphql.query", queryString) - - if operationName != "" { - span.SetTag("graphql.operationName", operationName) - } - - if len(variables) != 0 { - span.LogFields(log.Object("graphql.variables", variables)) - } - - return spanCtx, func(errs []*errors.QueryError) { - if len(errs) > 0 { - msg := errs[0].Error() - if len(errs) > 1 { - msg += fmt.Sprintf(" (and %d more errors)", len(errs)-1) - } - ext.Error.Set(span, true) - span.SetTag("graphql.error", msg) - } - span.Finish() - } -} - -func (OpenTracingTracer) TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, TraceFieldFinishFunc) { - if trivial { - return ctx, noop - } - - span, spanCtx := opentracing.StartSpanFromContext(ctx, label) - span.SetTag("graphql.type", typeName) - span.SetTag("graphql.field", fieldName) - for name, value := range args { - span.SetTag("graphql.args."+name, value) - } - - return spanCtx, func(err *errors.QueryError) { - if err != nil { - ext.Error.Set(span, true) - span.SetTag("graphql.error", err.Error()) - } - span.Finish() - } -} - -func (OpenTracingTracer) TraceValidation(ctx context.Context) TraceValidationFinishFunc { - span, _ := opentracing.StartSpanFromContext(ctx, "Validate Query") - - return func(errs []*errors.QueryError) { - if len(errs) > 0 { - msg := errs[0].Error() - if len(errs) > 1 { - msg += fmt.Sprintf(" (and %d more errors)", len(errs)-1) - } - ext.Error.Set(span, true) - span.SetTag("graphql.error", msg) - } - span.Finish() - } -} +// Deprecated: this type has been deprecated. Use tracer.QueryFinishFunc instead. +type TraceQueryFinishFunc = func([]*errors.QueryError) -func noop(*errors.QueryError) {} +// Deprecated: this type has been deprecated. Use tarcer.FieldFinishFunc instead. +type TraceFieldFinishFunc = func(*errors.QueryError) -type NoopTracer struct{} +// Deprecated: this interface has been deprecated. Use tracer.Tracer instead. +type Tracer = tracer.Tracer -func (NoopTracer) TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, TraceQueryFinishFunc) { - return ctx, func(errs []*errors.QueryError) {} -} +// Deprecated: this type has been deprecated. Use opentracing.Tracer instead. +type OpenTracingTracer = opentracing.Tracer -func (NoopTracer) TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, TraceFieldFinishFunc) { - return ctx, func(err *errors.QueryError) {} -} +// Deprecated: this type has been deprecated. Use noop.Tracer instead. +type NoopTracer = noop.Tracer diff --git a/trace/trace_test.go b/trace/trace_test.go index 7d748090..2c7708b5 100644 --- a/trace/trace_test.go +++ b/trace/trace_test.go @@ -3,13 +3,40 @@ package trace_test import ( "testing" + "github.com/graph-gophers/graphql-go" + "github.com/graph-gophers/graphql-go/errors" + "github.com/graph-gophers/graphql-go/example/starwars" "github.com/graph-gophers/graphql-go/trace" + "github.com/graph-gophers/graphql-go/trace/tracer" ) func TestInterfaceImplementation(t *testing.T) { - var _ trace.ValidationTracerContext = &trace.OpenTelemetryTracer{} - var _ trace.Tracer = &trace.OpenTelemetryTracer{} + var _ tracer.ValidationTracer = &trace.OpenTracingTracer{} + var _ tracer.Tracer = &trace.OpenTracingTracer{} - var _ trace.ValidationTracerContext = &trace.OpenTracingTracer{} - var _ trace.Tracer = &trace.OpenTracingTracer{} + var _ tracer.ValidationTracer = &trace.NoopTracer{} + var _ tracer.Tracer = &trace.NoopTracer{} +} + +func TestTracerOption(t *testing.T) { + _, err := graphql.ParseSchema(starwars.Schema, nil, graphql.Tracer(trace.OpenTracingTracer{})) + if err != nil { + t.Fatal(err) + } +} + +// MockVlidationTracer is a struct that implements the tracer.LegacyValidationTracer inteface. +type MockValidationTracer struct{} + +func (MockValidationTracer) TraceValidation() func([]*errors.QueryError) { + return func([]*errors.QueryError) {} +} + +func TestValidationTracer(t *testing.T) { + // test the legacy validation tracer interface (validating without using context) to ensure backwards compatibility + vt := MockValidationTracer{} + _, err := graphql.ParseSchema(starwars.Schema, nil, graphql.ValidationTracer(vt)) + if err != nil { + t.Fatal(err) + } } diff --git a/trace/tracer/tracer.go b/trace/tracer/tracer.go new file mode 100644 index 00000000..ddebc8b3 --- /dev/null +++ b/trace/tracer/tracer.go @@ -0,0 +1,34 @@ +package tracer + +import ( + "context" + + "github.com/graph-gophers/graphql-go/errors" + "github.com/graph-gophers/graphql-go/introspection" +) + +type QueryFinishFunc = func([]*errors.QueryError) +type FieldFinishFunc = func(*errors.QueryError) +type ValidationFinishFunc = func([]*errors.QueryError) + +type Tracer interface { + TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, QueryFinishFunc) + TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, FieldFinishFunc) +} + +type ValidationTracer interface { + TraceValidation(ctx context.Context) ValidationFinishFunc +} + +// Deprecated: use ValidationTracerContext instead. +type LegacyValidationTracer interface { + TraceValidation() func([]*errors.QueryError) +} + +// Deprecated: use a Tracer which implements ValidationTracerContext. +type LegacyNoopValidationTracer struct{} + +// Deprecated: use a Tracer which implements ValidationTracerContext. +func (LegacyNoopValidationTracer) TraceValidation() func([]*errors.QueryError) { + return func(errs []*errors.QueryError) {} +} diff --git a/trace/validation_trace.go b/trace/validation_trace.go index bce7a9a4..40035b51 100644 --- a/trace/validation_trace.go +++ b/trace/validation_trace.go @@ -1,25 +1,18 @@ package trace import ( - "context" - "github.com/graph-gophers/graphql-go/errors" + "github.com/graph-gophers/graphql-go/trace/tracer" ) -type TraceValidationFinishFunc = TraceQueryFinishFunc +// Deprecated: this type has been deprecated. Use tracer.ValidationFinishFunc instead. +type TraceValidationFinishFunc = func([]*errors.QueryError) // Deprecated: use ValidationTracerContext. -type ValidationTracer interface { - TraceValidation() TraceValidationFinishFunc -} - -type ValidationTracerContext interface { - TraceValidation(ctx context.Context) TraceValidationFinishFunc -} +type ValidationTracer = tracer.LegacyValidationTracer //nolint:staticcheck -type NoopValidationTracer struct{} +// Deprecated: this type has been deprecated. Use tracer.ValidationTracer instead. +type ValidationTracerContext = tracer.ValidationTracer -// Deprecated: use a Tracer which implements ValidationTracerContext. -func (NoopValidationTracer) TraceValidation() TraceValidationFinishFunc { - return func(errs []*errors.QueryError) {} -} +// Deprecated: use a tracer that implements ValidationTracerContext. +type NoopValidationTracer = tracer.LegacyNoopValidationTracer //nolint:staticcheck