github.com/mstephano/gqlgen-schemagen@v0.0.0-20230113041936-dd2cd4ea46aa/graphql/handler/apollofederatedtracingv1/tracing.go (about) 1 package apollofederatedtracingv1 2 3 import ( 4 "context" 5 "encoding/base64" 6 "fmt" 7 8 "github.com/mstephano/gqlgen-schemagen/graphql" 9 "google.golang.org/protobuf/proto" 10 ) 11 12 type ( 13 Tracer struct { 14 ClientName string 15 Version string 16 Hostname string 17 } 18 19 treeBuilderKey string 20 ) 21 22 const ( 23 key = treeBuilderKey("treeBuilder") 24 ) 25 26 var _ interface { 27 graphql.HandlerExtension 28 graphql.ResponseInterceptor 29 graphql.FieldInterceptor 30 graphql.OperationInterceptor 31 } = &Tracer{} 32 33 // ExtensionName returns the name of the extension 34 func (Tracer) ExtensionName() string { 35 return "ApolloFederatedTracingV1" 36 } 37 38 // Validate returns errors based on the schema; since this extension doesn't require validation, we return nil 39 func (Tracer) Validate(graphql.ExecutableSchema) error { 40 return nil 41 } 42 43 func (t *Tracer) shouldTrace(ctx context.Context) bool { 44 return graphql.GetOperationContext(ctx).Headers.Get("apollo-federation-include-trace") == "ftv1" 45 } 46 47 func (t *Tracer) getTreeBuilder(ctx context.Context) *TreeBuilder { 48 val := ctx.Value(key) 49 if val == nil { 50 return nil 51 } 52 if tb, ok := val.(*TreeBuilder); ok { 53 return tb 54 } 55 return nil 56 } 57 58 // InterceptOperation acts on each Graph operation; on each operation, start a tree builder and start the tree's timer for tracing 59 func (t *Tracer) InterceptOperation(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler { 60 if !t.shouldTrace(ctx) { 61 return next(ctx) 62 } 63 return next(context.WithValue(ctx, key, NewTreeBuilder())) 64 } 65 66 // InterceptField is called on each field's resolution, including information about the path and parent node. 67 // This information is then used to build the relevant Node Tree used in the FTV1 tracing format 68 func (t *Tracer) InterceptField(ctx context.Context, next graphql.Resolver) (interface{}, error) { 69 if !t.shouldTrace(ctx) { 70 return next(ctx) 71 } 72 if tb := t.getTreeBuilder(ctx); tb != nil { 73 tb.WillResolveField(ctx) 74 } 75 76 return next(ctx) 77 } 78 79 // InterceptResponse is called before the overall response is sent, but before each field resolves; as a result 80 // the final marshaling is deferred to happen at the end of the operation 81 func (t *Tracer) InterceptResponse(ctx context.Context, next graphql.ResponseHandler) *graphql.Response { 82 if !t.shouldTrace(ctx) { 83 return next(ctx) 84 } 85 tb := t.getTreeBuilder(ctx) 86 if tb != nil { 87 tb.StartTimer(ctx) 88 } 89 90 val := new(string) 91 graphql.RegisterExtension(ctx, "ftv1", val) 92 93 // now that fields have finished resolving, it stops the timer to calculate trace duration 94 defer func(val *string) { 95 tb.StopTimer(ctx) 96 97 // marshal the protobuf ... 98 p, err := proto.Marshal(tb.Trace) 99 if err != nil { 100 fmt.Print(err) 101 } 102 103 // ... then set the previously instantiated string as the base64 formatted string as required 104 *val = base64.StdEncoding.EncodeToString(p) 105 }(val) 106 resp := next(ctx) 107 return resp 108 }