Skip to content

Commit

Permalink
Merge pull request #402 from 99designs/feat-opencensus
Browse files Browse the repository at this point in the history
add Tracer for OpenCensus
  • Loading branch information
vektah authored Nov 2, 2018
2 parents 926ad17 + 7286e24 commit d264858
Show file tree
Hide file tree
Showing 5 changed files with 336 additions and 0 deletions.
17 changes: 17 additions & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions gqlopencensus/datadog.go
Original file line number Diff line number Diff line change
@@ -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
}
10 changes: 10 additions & 0 deletions gqlopencensus/option.go
Original file line number Diff line number Diff line change
@@ -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)
125 changes: 125 additions & 0 deletions gqlopencensus/tracer.go
Original file line number Diff line number Diff line change
@@ -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
}
150 changes: 150 additions & 0 deletions gqlopencensus/tracer_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
})
}
}

0 comments on commit d264858

Please sign in to comment.