diff --git a/Gopkg.lock b/Gopkg.lock index 34890156d5d..dd16953eab9 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -185,6 +185,21 @@ pruneopts = "UT" revision = "f119686bf1d4d1a68c7ed6afe35f183625443c41" +[[projects]] + digest = "1:bc4aaa0db4ea6107fd151d7c56ab48cae34d65a0f409ede7484dfd3c6f793e7e" + name = "go.opencensus.io" + packages = [ + ".", + "exemplar", + "internal", + "trace", + "trace/internal", + "trace/tracestate", + ] + pruneopts = "UT" + revision = "b7bf3cdb64150a8c8c53b769fdeb2ba581bd4d4b" + version = "v0.18.0" + [[projects]] branch = "master" digest = "1:76ee51c3f468493aff39dbacc401e8831fbb765104cbf613b89bef01cf4bad70" @@ -259,6 +274,8 @@ "github.com/vektah/gqlparser/ast", "github.com/vektah/gqlparser/gqlerror", "github.com/vektah/gqlparser/validator", + "go.opencensus.io/trace", + "golang.org/x/tools/go/ast/astutil", "golang.org/x/tools/go/loader", "golang.org/x/tools/imports", "gopkg.in/yaml.v2", diff --git a/gqlopencensus/datadog.go b/gqlopencensus/datadog.go new file mode 100644 index 00000000000..f9f9c973b93 --- /dev/null +++ b/gqlopencensus/datadog.go @@ -0,0 +1,34 @@ +package gqlopencensus + +import ( + "context" + + "github.com/99designs/gqlgen/graphql" + "go.opencensus.io/trace" +) + +// WithDataDog provides DataDog specific span attrs. +// see github.com/DataDog/opencensus-go-exporter-datadog +func WithDataDog() Option { + return func(cfg *config) { + cfg.tracer = &datadogTracerImpl{cfg.tracer} + } +} + +type datadogTracerImpl struct { + graphql.Tracer +} + +func (dt *datadogTracerImpl) StartFieldResolverExecution(ctx context.Context, rc *graphql.ResolverContext) context.Context { + ctx = dt.Tracer.StartFieldResolverExecution(ctx, rc) + span := trace.FromContext(ctx) + if !span.IsRecordingEvents() { + return ctx + } + span.AddAttributes( + // key from gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext#ResourceName + trace.StringAttribute("resource.name", operationName(ctx)), + ) + + return ctx +} diff --git a/gqlopencensus/option.go b/gqlopencensus/option.go new file mode 100644 index 00000000000..00b3037e687 --- /dev/null +++ b/gqlopencensus/option.go @@ -0,0 +1,10 @@ +package gqlopencensus + +import "github.com/99designs/gqlgen/graphql" + +type config struct { + tracer graphql.Tracer +} + +// Option is anything that can configure Tracer. +type Option func(cfg *config) diff --git a/gqlopencensus/tracer.go b/gqlopencensus/tracer.go new file mode 100644 index 00000000000..74e0cc52789 --- /dev/null +++ b/gqlopencensus/tracer.go @@ -0,0 +1,125 @@ +package gqlopencensus + +import ( + "context" + "fmt" + + "github.com/99designs/gqlgen/graphql" + "go.opencensus.io/trace" +) + +var _ graphql.Tracer = (tracerImpl)(0) + +// New returns Tracer for OpenCensus. +// see https://go.opencensus.io/trace +func New(opts ...Option) graphql.Tracer { + var tracer tracerImpl + cfg := &config{tracer} + + for _, opt := range opts { + opt(cfg) + } + + return cfg.tracer +} + +type tracerImpl int + +func (tracerImpl) StartOperationParsing(ctx context.Context) context.Context { + return ctx +} + +func (tracerImpl) EndOperationParsing(ctx context.Context) { +} + +func (tracerImpl) StartOperationValidation(ctx context.Context) context.Context { + return ctx +} + +func (tracerImpl) EndOperationValidation(ctx context.Context) { +} + +func (tracerImpl) StartOperationExecution(ctx context.Context) context.Context { + ctx, span := trace.StartSpan(ctx, operationName(ctx)) + if !span.IsRecordingEvents() { + return ctx + } + requestContext := graphql.GetRequestContext(ctx) + span.AddAttributes( + trace.StringAttribute("request.query", requestContext.RawQuery), + ) + if requestContext.ComplexityLimit > 0 { + span.AddAttributes( + trace.Int64Attribute("request.complexityLimit", int64(requestContext.ComplexityLimit)), + trace.Int64Attribute("request.operationComplexity", int64(requestContext.OperationComplexity)), + ) + } + + for key, val := range requestContext.Variables { + span.AddAttributes( + trace.StringAttribute(fmt.Sprintf("request.variables.%s", key), fmt.Sprintf("%+v", val)), + ) + } + + return ctx +} + +func (tracerImpl) StartFieldExecution(ctx context.Context, field graphql.CollectedField) context.Context { + ctx, span := trace.StartSpan(ctx, field.ObjectDefinition.Name+"/"+field.Name) + if !span.IsRecordingEvents() { + return ctx + } + span.AddAttributes( + trace.StringAttribute("resolver.object", field.ObjectDefinition.Name), + trace.StringAttribute("resolver.field", field.Name), + trace.StringAttribute("resolver.alias", field.Alias), + ) + for _, arg := range field.Arguments { + if arg.Value != nil { + span.AddAttributes( + trace.StringAttribute(fmt.Sprintf("resolver.args.%s", arg.Name), arg.Value.String()), + ) + } + } + + return ctx +} + +func (tracerImpl) StartFieldResolverExecution(ctx context.Context, rc *graphql.ResolverContext) context.Context { + span := trace.FromContext(ctx) + if !span.IsRecordingEvents() { + return ctx + } + span.AddAttributes( + trace.StringAttribute("resolver.path", fmt.Sprintf("%+v", rc.Path())), + ) + + return ctx +} + +func (tracerImpl) StartFieldChildExecution(ctx context.Context) context.Context { + return ctx +} + +func (tracerImpl) EndFieldExecution(ctx context.Context) { + span := trace.FromContext(ctx) + defer span.End() +} + +func (tracerImpl) EndOperationExecution(ctx context.Context) { + span := trace.FromContext(ctx) + defer span.End() +} + +func operationName(ctx context.Context) string { + requestContext := graphql.GetRequestContext(ctx) + requestName := "nameless-operation" + if requestContext.Doc != nil && len(requestContext.Doc.Operations) != 0 { + op := requestContext.Doc.Operations[0] + if op.Name != "" { + requestName = op.Name + } + } + + return requestName +} diff --git a/gqlopencensus/tracer_test.go b/gqlopencensus/tracer_test.go new file mode 100644 index 00000000000..bd20137faf1 --- /dev/null +++ b/gqlopencensus/tracer_test.go @@ -0,0 +1,150 @@ +package gqlopencensus_test + +import ( + "context" + "sync" + "testing" + + "github.com/99designs/gqlgen/gqlopencensus" + "github.com/99designs/gqlgen/graphql" + "github.com/stretchr/testify/assert" + "github.com/vektah/gqlparser/ast" + "go.opencensus.io/trace" +) + +var _ trace.Exporter = (*testExporter)(nil) + +type testExporter struct { + sync.Mutex + + Spans []*trace.SpanData +} + +func (te *testExporter) ExportSpan(s *trace.SpanData) { + te.Lock() + defer te.Unlock() + + te.Spans = append(te.Spans, s) +} + +func (te *testExporter) Reset() { + te.Lock() + defer te.Unlock() + + te.Spans = nil +} + +func TestTracer(t *testing.T) { + var mu sync.Mutex + + exporter := &testExporter{} + + trace.RegisterExporter(exporter) + defer trace.UnregisterExporter(exporter) + + specs := []struct { + SpecName string + Tracer graphql.Tracer + Sampler trace.Sampler + ExpectedAttrs []map[string]interface{} + }{ + { + SpecName: "with sampling", + Tracer: gqlopencensus.New(), + Sampler: trace.AlwaysSample(), + ExpectedAttrs: []map[string]interface{}{ + { + "resolver.object": "OD", + "resolver.field": "F", + "resolver.alias": "F", + "resolver.path": "[]", + }, + { + "request.query": "query { foobar }", + "request.variables.fizz": "buzz", + "request.complexityLimit": int64(1000), + "request.operationComplexity": int64(100), + }, + }, + }, + { + SpecName: "without sampling", + Tracer: gqlopencensus.New(), + Sampler: trace.NeverSample(), + ExpectedAttrs: nil, + }, + { + SpecName: "with sampling & DataDog", + Tracer: gqlopencensus.New(gqlopencensus.WithDataDog()), + Sampler: trace.AlwaysSample(), + ExpectedAttrs: []map[string]interface{}{ + { + "resolver.object": "OD", + "resolver.field": "F", + "resolver.alias": "F", + "resolver.path": "[]", + "resource.name": "nameless-operation", + }, + { + "request.query": "query { foobar }", + "request.variables.fizz": "buzz", + "request.complexityLimit": int64(1000), + "request.operationComplexity": int64(100), + }, + }, + }, + { + SpecName: "without sampling & DataDog", + Tracer: gqlopencensus.New(gqlopencensus.WithDataDog()), + Sampler: trace.NeverSample(), + ExpectedAttrs: nil, + }, + } + + for _, spec := range specs { + t.Run(spec.SpecName, func(t *testing.T) { + mu.Lock() + defer mu.Unlock() + exporter.Reset() + + tracer := spec.Tracer + ctx := context.Background() + ctx = graphql.WithRequestContext(ctx, &graphql.RequestContext{ + RawQuery: "query { foobar }", + Variables: map[string]interface{}{ + "fizz": "buzz", + }, + ComplexityLimit: 1000, + OperationComplexity: 100, + }) + ctx, _ = trace.StartSpan(ctx, "test", trace.WithSampler(spec.Sampler)) + ctx = tracer.StartOperationExecution(ctx) + { + ctx2 := tracer.StartFieldExecution(ctx, graphql.CollectedField{ + Field: &ast.Field{ + Name: "F", + Alias: "F", + ObjectDefinition: &ast.Definition{ + Name: "OD", + }, + }, + }) + ctx2 = tracer.StartFieldResolverExecution(ctx2, &graphql.ResolverContext{}) + ctx2 = tracer.StartFieldChildExecution(ctx2) + tracer.EndFieldExecution(ctx2) + } + tracer.EndOperationExecution(ctx) + + if len(spec.ExpectedAttrs) == 0 && len(exporter.Spans) != 0 { + t.Errorf("unexpected spans: %+v", exporter.Spans) + } else if len(spec.ExpectedAttrs) != len(exporter.Spans) { + assert.Equal(t, len(spec.ExpectedAttrs), len(exporter.Spans)) + } else { + for idx, expectedAttrs := range spec.ExpectedAttrs { + span := exporter.Spans[idx] + assert.Equal(t, expectedAttrs, span.Attributes) + } + } + }) + } +}