-
Notifications
You must be signed in to change notification settings - Fork 0
/
tracer.go
145 lines (127 loc) · 4.01 KB
/
tracer.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
package trace
// This schema/arch is taken from: https://github.com/99designs/gqlgen/blob/master/graphql/handler/apollotracing/tracer.go
import (
"context"
"fmt"
"github.com/scout-inc/scout-go"
"github.com/99designs/gqlgen/graphql"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/vektah/gqlparser/v2/gqlerror"
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
)
type GraphqlTracer interface {
graphql.HandlerExtension
graphql.ResponseInterceptor
graphql.FieldInterceptor
WithRequestFieldLogging() GraphqlTracer
}
type Tracer struct {
graphName string
requestFieldLogging bool
}
func NewGraphqlTracer(graphName string) GraphqlTracer {
return Tracer{graphName: graphName}
}
// WithRequestFieldLogging configures the tracer to log each graphql operation.
func (t Tracer) WithRequestFieldLogging() GraphqlTracer {
t.requestFieldLogging = true
return t
}
func (t Tracer) ExtensionName() string {
return scout.ScopedKey("Tracer", nil)
}
func (t Tracer) Validate(graphql.ExecutableSchema) error {
return nil
}
// InterceptField instruments timing of individual fields resolved.
func (t Tracer) InterceptField(ctx context.Context, next graphql.Resolver) (interface{}, error) {
fc := graphql.GetFieldContext(ctx)
fieldName := fc.Field.Name
name := fmt.Sprintf("graphql.field.%s", fieldName)
span, ctx := scout.StartTrace(ctx, name)
span.SetAttributes(
semconv.ServiceNameKey.String(t.graphName),
attribute.String("graphql.field.name", fieldName),
attribute.String("graphql.object.type", fc.Object),
)
if args := serializeVars(fc.Args); len(args) < 2048 {
span.SetAttributes(attribute.String("graphql.field.arguments", args))
}
res, err := next(ctx)
scout.RecordSpanError(
span, err,
attribute.String(scout.SourceAttribute, "InterceptField"),
)
scout.EndTrace(span)
return res, err
}
// InterceptResponse instruments timing, payload size, and error information
// of the response handler. The metric is grouped by the corresponding operation name.
func (t Tracer) InterceptResponse(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
var oc *graphql.OperationContext
if graphql.HasOperationContext(ctx) {
oc = graphql.GetOperationContext(ctx)
}
opName := "undefined"
variables := ""
if oc != nil {
opName = oc.OperationName
if vars := serializeVars(oc.Variables); len(vars) < 2048 {
variables = vars
}
}
name := fmt.Sprintf("graphql.operation.%s", opName)
span, ctx := scout.StartTrace(ctx, name)
span.SetAttributes(
semconv.ServiceNameKey.String(t.graphName),
semconv.GraphqlOperationName(opName),
attribute.String("graphql.operation.variables", variables),
)
resp := next(ctx)
// though there is a resp.Errors, we should not record it because it will
// be a duplicate of errors on individual fields.
scout.EndTrace(span)
return resp
}
func GraphQLRecoverFunc() graphql.RecoverFunc {
return func(ctx context.Context, err interface{}) error {
var ok bool
var e error
if e, ok = err.(error); !ok {
e = errors.Errorf("panic {error: %+v}", err)
}
scout.RecordError(ctx, e, attribute.String(scout.SourceAttribute, "GraphQLRecoverFunc"))
return e
}
}
func GraphQLErrorPresenter(service string) func(ctx context.Context, e error) *gqlerror.Error {
return func(ctx context.Context, e error) *gqlerror.Error {
var gqlerr *gqlerror.Error
switch t := e.(type) {
case *gqlerror.Error:
gqlerr = t
log.WithContext(ctx).WithError(t).WithFields(log.Fields{
"message": t.Message,
"rule": t.Rule,
"path": t.Path,
"extensions": t.Extensions,
"locations": t.Locations,
}).Errorf("%s graphql request failed", service)
default:
gqlerr = gqlerror.Errorf(e.Error())
log.WithContext(ctx).WithError(e).WithFields(log.Fields{
"error": e,
"path": graphql.GetPath(ctx),
}).Errorf("%s graphql request failed", service)
}
return gqlerr
}
}
func serializeVars(vars map[string]interface{}) string {
if vars == nil {
return ""
}
return fmt.Sprintf("%+v", vars)
}