github.com/geneva/gqlgen@v0.17.7-0.20230801155730-7b9317164836/graphql/handler/apollofederatedtracingv1/tracing.go (about) 1 package apollofederatedtracingv1 2 3 import ( 4 "context" 5 "encoding/base64" 6 "fmt" 7 8 "github.com/geneva/gqlgen/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.HasOperationContext(ctx) && 45 graphql.GetOperationContext(ctx).Headers.Get("apollo-federation-include-trace") == "ftv1" 46 } 47 48 func (t *Tracer) getTreeBuilder(ctx context.Context) *TreeBuilder { 49 val := ctx.Value(key) 50 if val == nil { 51 return nil 52 } 53 if tb, ok := val.(*TreeBuilder); ok { 54 return tb 55 } 56 return nil 57 } 58 59 // InterceptOperation acts on each Graph operation; on each operation, start a tree builder and start the tree's timer for tracing 60 func (t *Tracer) InterceptOperation(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler { 61 if !t.shouldTrace(ctx) { 62 return next(ctx) 63 } 64 return next(context.WithValue(ctx, key, NewTreeBuilder())) 65 } 66 67 // InterceptField is called on each field's resolution, including information about the path and parent node. 68 // This information is then used to build the relevant Node Tree used in the FTV1 tracing format 69 func (t *Tracer) InterceptField(ctx context.Context, next graphql.Resolver) (interface{}, error) { 70 if !t.shouldTrace(ctx) { 71 return next(ctx) 72 } 73 if tb := t.getTreeBuilder(ctx); tb != nil { 74 tb.WillResolveField(ctx) 75 } 76 77 return next(ctx) 78 } 79 80 // InterceptResponse is called before the overall response is sent, but before each field resolves; as a result 81 // the final marshaling is deferred to happen at the end of the operation 82 func (t *Tracer) InterceptResponse(ctx context.Context, next graphql.ResponseHandler) *graphql.Response { 83 if !t.shouldTrace(ctx) { 84 return next(ctx) 85 } 86 tb := t.getTreeBuilder(ctx) 87 if tb != nil { 88 tb.StartTimer(ctx) 89 } 90 91 val := new(string) 92 graphql.RegisterExtension(ctx, "ftv1", val) 93 94 // now that fields have finished resolving, it stops the timer to calculate trace duration 95 defer func(val *string) { 96 tb.StopTimer(ctx) 97 98 // marshal the protobuf ... 99 p, err := proto.Marshal(tb.Trace) 100 if err != nil { 101 fmt.Print(err) 102 } 103 104 // ... then set the previously instantiated string as the base64 formatted string as required 105 *val = base64.StdEncoding.EncodeToString(p) 106 }(val) 107 resp := next(ctx) 108 return resp 109 }