Skip to content

Commit

Permalink
add Tracer for OpenCensus
Browse files Browse the repository at this point in the history
  • Loading branch information
vvakame committed Oct 29, 2018
1 parent 0d5c65b commit ececa23
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.

21 changes: 21 additions & 0 deletions gqlopencensus/datadog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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 WithStartFieldResolverExecution(func(ctx context.Context, rc *graphql.ResolverContext) context.Context {
span := trace.FromContext(ctx)
span.AddAttributes(
// key from gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext#ResourceName
trace.StringAttribute("resource.name", operationName(ctx)),
)
return ctx
})
}
64 changes: 64 additions & 0 deletions gqlopencensus/option.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package gqlopencensus

import (
"context"

"github.com/99designs/gqlgen/graphql"
)

type config struct {
tracer *tracerImpl
}

// Option is anything that can configure Tracer.
type Option interface {
apply(cfg *config)
}

type optionFunc func(cfg *config)

func (opt optionFunc) apply(cfg *config) {
opt(cfg)
}

// WithStartOperationExecution returns option that execute some process on StartOperationExecution step.
func WithStartOperationExecution(f func(ctx context.Context) context.Context) Option {
return optionFunc(func(cfg *config) {
cfg.tracer.startOperationExecutions = append(cfg.tracer.startOperationExecutions, f)
})
}

// WithStartFieldExecution returns option that execute some process on StartFieldExecution step.
func WithStartFieldExecution(f func(ctx context.Context, field graphql.CollectedField) context.Context) Option {
return optionFunc(func(cfg *config) {
cfg.tracer.startFieldExecutions = append(cfg.tracer.startFieldExecutions, f)
})
}

// WithStartFieldResolverExecution returns option that execute some process on StartFieldResolverExecution step.
func WithStartFieldResolverExecution(f func(ctx context.Context, rc *graphql.ResolverContext) context.Context) Option {
return optionFunc(func(cfg *config) {
cfg.tracer.startFieldResolverExecutions = append(cfg.tracer.startFieldResolverExecutions, f)
})
}

// WithStartFieldChildExecution returns option that execute some process on StartFieldChildExecution step.
func WithStartFieldChildExecution(f func(ctx context.Context) context.Context) Option {
return optionFunc(func(cfg *config) {
cfg.tracer.startFieldChildExecutions = append(cfg.tracer.startFieldChildExecutions, f)
})
}

// WithEndFieldExecution returns option that execute some process on EndFieldExecution step.
func WithEndFieldExecution(f func(ctx context.Context)) Option {
return optionFunc(func(cfg *config) {
cfg.tracer.endFieldExecutions = append(cfg.tracer.endFieldExecutions, f)
})
}

// WithEndOperationExecutions returns option that execute some process on EndOperationExecutions step.
func WithEndOperationExecutions(f func(ctx context.Context)) Option {
return optionFunc(func(cfg *config) {
cfg.tracer.endOperationExecutions = append(cfg.tracer.endOperationExecutions, f)
})
}
140 changes: 140 additions & 0 deletions gqlopencensus/tracer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package gqlopencensus

import (
"context"
"fmt"

"github.com/99designs/gqlgen/graphql"
"go.opencensus.io/trace"
)

var _ graphql.Tracer = (*tracerImpl)(nil)

// New returns Tracer for OpenCensus.
// see https://go.opencensus.io/trace
func New(opts ...Option) graphql.Tracer {
tracer := &tracerImpl{}
cfg := &config{tracer}

for _, opt := range opts {
opt.apply(cfg)
}

return tracer
}

type tracerImpl struct {
startOperationExecutions []func(ctx context.Context) context.Context
startFieldExecutions []func(ctx context.Context, field graphql.CollectedField) context.Context
startFieldResolverExecutions []func(ctx context.Context, rc *graphql.ResolverContext) context.Context
startFieldChildExecutions []func(ctx context.Context) context.Context
endFieldExecutions []func(ctx context.Context)
endOperationExecutions []func(ctx context.Context)
}

func (t *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),
)
for key, val := range requestContext.Variables {
span.AddAttributes(
trace.StringAttribute(fmt.Sprintf("request.variables.%s", key), fmt.Sprintf("%+v", val)),
)
}
for _, f := range t.startOperationExecutions {
ctx = f(ctx)
}

return ctx
}

func (t *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()),
)
}
}

for _, f := range t.startFieldExecutions {
ctx = f(ctx, field)
}

return ctx
}

func (t *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())),
)
for _, f := range t.startFieldResolverExecutions {
ctx = f(ctx, rc)
}

return ctx
}

func (t *tracerImpl) StartFieldChildExecution(ctx context.Context) context.Context {
span := trace.FromContext(ctx)
if !span.IsRecordingEvents() {
return ctx
}
for _, f := range t.startFieldChildExecutions {
ctx = f(ctx)
}
return ctx
}

func (t *tracerImpl) EndFieldExecution(ctx context.Context) {
span := trace.FromContext(ctx)
defer span.End()
if !span.IsRecordingEvents() {
return
}
for _, f := range t.endFieldExecutions {
f(ctx)
}
}

func (t *tracerImpl) EndOperationExecution(ctx context.Context) {
span := trace.FromContext(ctx)
defer span.End()
if !span.IsRecordingEvents() {
return
}
for _, f := range t.endOperationExecutions {
f(ctx)
}
}

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
}
94 changes: 94 additions & 0 deletions gqlopencensus/tracer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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"
)

func TestTracer(t *testing.T) {
var logs []string
var mu sync.Mutex

tracer := gqlopencensus.New(
gqlopencensus.WithStartOperationExecution(func(ctx context.Context) context.Context {
logs = append(logs, "StartOperationExecution")
return ctx
}),
gqlopencensus.WithStartFieldExecution(func(ctx context.Context, field graphql.CollectedField) context.Context {
logs = append(logs, "StartFieldExecution")
return ctx
}),
gqlopencensus.WithStartFieldResolverExecution(func(ctx context.Context, rc *graphql.ResolverContext) context.Context {
logs = append(logs, "StartFieldResolverExecution")
return ctx
}),
gqlopencensus.WithStartFieldChildExecution(func(ctx context.Context) context.Context {
logs = append(logs, "StartFieldChildExecution")
return ctx
}),
gqlopencensus.WithEndFieldExecution(func(ctx context.Context) {
logs = append(logs, "EndFieldExecution")
}),
gqlopencensus.WithEndOperationExecutions(func(ctx context.Context) {
logs = append(logs, "EndOperationExecution")
}),
)

specs := []struct {
SpecName string
Sampler trace.Sampler
Expected []string
}{
{
SpecName: "with sampling",
Sampler: trace.AlwaysSample(),
Expected: []string{
"StartOperationExecution",
"StartFieldExecution",
"StartFieldResolverExecution",
"StartFieldChildExecution",
"EndFieldExecution",
"EndOperationExecution",
},
},
{
SpecName: "without sampling",
Sampler: trace.NeverSample(),
Expected: nil,
},
}

for _, spec := range specs {
t.Run(spec.SpecName, func(t *testing.T) {
mu.Lock()
defer mu.Unlock()
logs = nil

ctx := context.Background()
ctx = graphql.WithRequestContext(ctx, &graphql.RequestContext{})
ctx, _ = trace.StartSpan(ctx, "test", trace.WithSampler(spec.Sampler))
ctx = tracer.StartOperationExecution(ctx)
ctx = tracer.StartFieldExecution(ctx, graphql.CollectedField{
Field: &ast.Field{
Name: "F",
ObjectDefinition: &ast.Definition{
Name: "OD",
},
},
})
ctx = tracer.StartFieldResolverExecution(ctx, &graphql.ResolverContext{})
ctx = tracer.StartFieldChildExecution(ctx)
tracer.EndFieldExecution(ctx)
tracer.EndOperationExecution(ctx)

assert.Equal(t, spec.Expected, logs)
})
}
}

0 comments on commit ececa23

Please sign in to comment.