From 0ba4cac44e5bd2e47e4976dd0a3f242558db6b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Ram=C3=B3n?= Date: Sun, 9 Dec 2018 21:16:08 -0500 Subject: [PATCH 1/6] WIP: trace support --- executor.go | 36 ++++++++++++++++++++++++++---------- graphql.go | 20 ++++++++++++++++++-- trace.go | 15 +++++++++++++++ 3 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 trace.go diff --git a/executor.go b/executor.go index 10d3799f..32f23eb6 100644 --- a/executor.go +++ b/executor.go @@ -21,6 +21,9 @@ type ExecuteParams struct { // Context may be provided to pass application-specific per-request // information to resolve functions. Context context.Context + + // Tracer ... + Tracer Tracer } func Execute(p ExecuteParams) (result *Result) { @@ -51,6 +54,7 @@ func Execute(p ExecuteParams) (result *Result) { Args: p.Args, Result: result, Context: p.Context, + Tracer: p.Tracer, }) if err != nil { @@ -82,16 +86,18 @@ type buildExecutionCtxParams struct { Args map[string]interface{} Result *Result Context context.Context + Tracer Tracer } type executionContext struct { - Schema Schema + Context context.Context + Errors []gqlerrors.FormattedError Fragments map[string]ast.Definition - Root interface{} Operation ast.Definition + Root interface{} + Schema Schema + Tracer Tracer VariableValues map[string]interface{} - Errors []gqlerrors.FormattedError - Context context.Context } func buildExecutionContext(p buildExecutionCtxParams) (*executionContext, error) { @@ -137,6 +143,7 @@ func buildExecutionContext(p buildExecutionCtxParams) (*executionContext, error) eCtx.Operation = operation eCtx.VariableValues = variableValues eCtx.Context = p.Context + eCtx.Tracer = p.Tracer return eCtx, nil } @@ -573,22 +580,31 @@ func handleFieldError(r interface{}, fieldNodes []ast.Node, path *ResponsePath, // then calls completeValue to complete promises, serialize scalars, or execute // the sub-selection-set for objects. func resolveField(eCtx *executionContext, parentType *Object, source interface{}, fieldASTs []*ast.Field, path *ResponsePath) (result interface{}, resultState resolveFieldResultState) { + fieldAST := fieldASTs[0] + fieldName := "" + if fieldAST.Name != nil { + fieldName = fieldAST.Name.Value + } + + var finishTraceField TraceFieldFinishFunc + if eCtx.Tracer != nil { + finishTraceField = eCtx.Tracer.TraceField(eCtx.Context, fieldName) + finishTraceField(gqlerrors.FormattedError{}) + } + // catch panic from resolveFn var returnType Output defer func() (interface{}, resolveFieldResultState) { if r := recover(); r != nil { handleFieldError(r, FieldASTsToNodeASTs(fieldASTs), path, returnType, eCtx) + if eCtx.Tracer != nil { + finishTraceField(gqlerrors.FormatError(errors.New("todo"))) + } return result, resultState } return result, resultState }() - fieldAST := fieldASTs[0] - fieldName := "" - if fieldAST.Name != nil { - fieldName = fieldAST.Name.Value - } - fieldDef := getFieldDef(eCtx.Schema, parentType, fieldName) if fieldDef == nil { resultState.hasNoFieldDefs = true diff --git a/graphql.go b/graphql.go index c9bdb168..1c7da580 100644 --- a/graphql.go +++ b/graphql.go @@ -31,6 +31,9 @@ type Params struct { // Context may be provided to pass application-specific per-request // information to resolve functions. Context context.Context + + // Tracer ... + Tracer Tracer } func Do(p Params) *Result { @@ -52,12 +55,25 @@ func Do(p Params) *Result { } } - return Execute(ExecuteParams{ + var traceFinish TraceQueryFinishFunc + ctx2 := p.Context + if p.Tracer != nil { + ctx2, traceFinish = p.Tracer.TraceQuery(p.Context, p.RequestString, p.OperationName) + } + + result := Execute(ExecuteParams{ Schema: p.Schema, Root: p.RootObject, AST: AST, OperationName: p.OperationName, Args: p.VariableValues, - Context: p.Context, + Context: ctx2, + Tracer: p.Tracer, }) + + if p.Tracer != nil { + traceFinish(result.Errors) + } + + return result } diff --git a/trace.go b/trace.go new file mode 100644 index 00000000..a5e3a448 --- /dev/null +++ b/trace.go @@ -0,0 +1,15 @@ +package graphql + +import ( + "context" + + "github.com/graphql-go/graphql/gqlerrors" +) + +type TraceQueryFinishFunc func([]gqlerrors.FormattedError) +type TraceFieldFinishFunc func(gqlerrors.FormattedError) + +type Tracer interface { + TraceQuery(ctx context.Context, queryString string, operationName string) (context.Context, TraceQueryFinishFunc) + TraceField(ctx context.Context, fieldName string) TraceFieldFinishFunc +} From 61df404d2e3efc960f397fc6f740644e499b3e4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Ram=C3=B3n?= Date: Tue, 1 Jan 2019 20:46:33 -0500 Subject: [PATCH 2/6] trace: Adds NoopTracer to be used as opt-out tracing --- trace.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/trace.go b/trace.go index a5e3a448..a494b0cb 100644 --- a/trace.go +++ b/trace.go @@ -10,6 +10,16 @@ type TraceQueryFinishFunc func([]gqlerrors.FormattedError) type TraceFieldFinishFunc func(gqlerrors.FormattedError) type Tracer interface { - TraceQuery(ctx context.Context, queryString string, operationName string) (context.Context, TraceQueryFinishFunc) - TraceField(ctx context.Context, fieldName string) TraceFieldFinishFunc + TraceQuery(ctx context.Context, queryString string, operationName string) (context.Context, TraceQueryFinishFunc) + TraceField(ctx context.Context, fieldName string, typeName string) (context.Context, TraceFieldFinishFunc) +} + +type NoopTracer struct{} + +func (NoopTracer) TraceQuery(ctx context.Context, queryString string, operationName string) (context.Context, TraceQueryFinishFunc) { + return ctx, func(errs []gqlerrors.FormattedError) {} +} + +func (NoopTracer) TraceField(ctx context.Context, fieldName string, typeName string) (context.Context, TraceFieldFinishFunc) { + return ctx, func(err gqlerrors.FormattedError) {} } From 1481740e3a9bec905c114cde526ca55189c8d8b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Ram=C3=B3n?= Date: Tue, 1 Jan 2019 20:47:04 -0500 Subject: [PATCH 3/6] graphql: Adds support for optional tracing via NoopTracer --- graphql.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/graphql.go b/graphql.go index 1c7da580..e57a3d9b 100644 --- a/graphql.go +++ b/graphql.go @@ -55,25 +55,23 @@ func Do(p Params) *Result { } } - var traceFinish TraceQueryFinishFunc - ctx2 := p.Context - if p.Tracer != nil { - ctx2, traceFinish = p.Tracer.TraceQuery(p.Context, p.RequestString, p.OperationName) + if p.Tracer == nil { + p.Tracer = NoopTracer{} } + ctx, traceFinish := p.Tracer.TraceQuery(p.Context, p.RequestString, p.OperationName) + result := Execute(ExecuteParams{ Schema: p.Schema, Root: p.RootObject, AST: AST, OperationName: p.OperationName, Args: p.VariableValues, - Context: ctx2, + Context: ctx, Tracer: p.Tracer, }) - if p.Tracer != nil { - traceFinish(result.Errors) - } + traceFinish(result.Errors) return result } From 358381793128b2589f65d45cd42dc84a2f012b1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Ram=C3=B3n?= Date: Tue, 1 Jan 2019 20:56:22 -0500 Subject: [PATCH 4/6] trace: TraceFieldFinishFunc might return multiple formatted errors --- trace.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/trace.go b/trace.go index a494b0cb..f5d6275a 100644 --- a/trace.go +++ b/trace.go @@ -7,7 +7,7 @@ import ( ) type TraceQueryFinishFunc func([]gqlerrors.FormattedError) -type TraceFieldFinishFunc func(gqlerrors.FormattedError) +type TraceFieldFinishFunc func([]gqlerrors.FormattedError) type Tracer interface { TraceQuery(ctx context.Context, queryString string, operationName string) (context.Context, TraceQueryFinishFunc) @@ -21,5 +21,5 @@ func (NoopTracer) TraceQuery(ctx context.Context, queryString string, operationN } func (NoopTracer) TraceField(ctx context.Context, fieldName string, typeName string) (context.Context, TraceFieldFinishFunc) { - return ctx, func(err gqlerrors.FormattedError) {} + return ctx, func(errs []gqlerrors.FormattedError) {} } From c3a19799d746c4b05fdf54ac0f665100783299ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Ram=C3=B3n?= Date: Tue, 1 Jan 2019 20:57:24 -0500 Subject: [PATCH 5/6] executor: working TraceField & propagate tracing context --- executor.go | 78 ++++++++++++++++++++++++++--------------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/executor.go b/executor.go index 32f23eb6..02bbefb8 100644 --- a/executor.go +++ b/executor.go @@ -246,7 +246,7 @@ func executeFieldsSerially(p executeFieldsParams) *Result { finalResults := make(map[string]interface{}, len(p.Fields)) for responseName, fieldASTs := range p.Fields { fieldPath := p.Path.WithKey(responseName) - resolved, state := resolveField(p.ExecutionContext, p.ParentType, p.Source, fieldASTs, fieldPath) + resolved, state := resolveField(p.ExecutionContext.Context, p.ExecutionContext, p.ParentType, p.Source, fieldASTs, fieldPath) if state.hasNoFieldDefs { continue } @@ -262,7 +262,7 @@ func executeFieldsSerially(p executeFieldsParams) *Result { // Implements the "Evaluating selection sets" section of the spec for "read" mode. func executeFields(p executeFieldsParams) *Result { - finalResults := executeSubFields(p) + finalResults := executeSubFields(p.ExecutionContext.Context, p) dethunkMapWithBreadthFirstTraversal(finalResults) @@ -272,7 +272,7 @@ func executeFields(p executeFieldsParams) *Result { } } -func executeSubFields(p executeFieldsParams) map[string]interface{} { +func executeSubFields(ctx context.Context, p executeFieldsParams) map[string]interface{} { if p.Source == nil { p.Source = map[string]interface{}{} } @@ -283,7 +283,9 @@ func executeSubFields(p executeFieldsParams) map[string]interface{} { finalResults := make(map[string]interface{}, len(p.Fields)) for responseName, fieldASTs := range p.Fields { fieldPath := p.Path.WithKey(responseName) - resolved, state := resolveField(p.ExecutionContext, p.ParentType, p.Source, fieldASTs, fieldPath) + + resolved, state := resolveField(ctx, p.ExecutionContext, p.ParentType, p.Source, + fieldASTs, fieldPath) if state.hasNoFieldDefs { continue } @@ -579,38 +581,34 @@ func handleFieldError(r interface{}, fieldNodes []ast.Node, path *ResponsePath, // figures out the value that the field returns by calling its resolve function, // then calls completeValue to complete promises, serialize scalars, or execute // the sub-selection-set for objects. -func resolveField(eCtx *executionContext, parentType *Object, source interface{}, fieldASTs []*ast.Field, path *ResponsePath) (result interface{}, resultState resolveFieldResultState) { +func resolveField(ctx context.Context, eCtx *executionContext, parentType *Object, source interface{}, fieldASTs []*ast.Field, path *ResponsePath) (result interface{}, resultState resolveFieldResultState) { fieldAST := fieldASTs[0] fieldName := "" if fieldAST.Name != nil { fieldName = fieldAST.Name.Value } - var finishTraceField TraceFieldFinishFunc - if eCtx.Tracer != nil { - finishTraceField = eCtx.Tracer.TraceField(eCtx.Context, fieldName) - finishTraceField(gqlerrors.FormattedError{}) + fieldDef := getFieldDef(eCtx.Schema, parentType, fieldName) + if fieldDef == nil { + resultState.hasNoFieldDefs = true + return nil, resultState } + returnType := fieldDef.Type + + traceCtx, finishTraceField := eCtx.Tracer.TraceField(ctx, fieldName, + returnType.Name()) // catch panic from resolveFn - var returnType Output defer func() (interface{}, resolveFieldResultState) { if r := recover(); r != nil { handleFieldError(r, FieldASTsToNodeASTs(fieldASTs), path, returnType, eCtx) - if eCtx.Tracer != nil { - finishTraceField(gqlerrors.FormatError(errors.New("todo"))) - } + finishTraceField(eCtx.Errors) return result, resultState } + finishTraceField([]gqlerrors.FormattedError{}) return result, resultState }() - fieldDef := getFieldDef(eCtx.Schema, parentType, fieldName) - if fieldDef == nil { - resultState.hasNoFieldDefs = true - return nil, resultState - } - returnType = fieldDef.Type resolveFn := fieldDef.Resolve if resolveFn == nil { resolveFn = DefaultResolveFn @@ -647,11 +645,13 @@ func resolveField(eCtx *executionContext, parentType *Object, source interface{} panic(resolveFnError) } - completed := completeValueCatchingError(eCtx, returnType, fieldASTs, info, path, result) + completed := completeValueCatchingError(traceCtx, eCtx, returnType, fieldASTs, + info, path, result) + return completed, resultState } -func completeValueCatchingError(eCtx *executionContext, returnType Type, fieldASTs []*ast.Field, info ResolveInfo, path *ResponsePath, result interface{}) (completed interface{}) { +func completeValueCatchingError(ctx context.Context, eCtx *executionContext, returnType Type, fieldASTs []*ast.Field, info ResolveInfo, path *ResponsePath, result interface{}) (completed interface{}) { // catch panic defer func() interface{} { if r := recover(); r != nil { @@ -662,26 +662,26 @@ func completeValueCatchingError(eCtx *executionContext, returnType Type, fieldAS }() if returnType, ok := returnType.(*NonNull); ok { - completed := completeValue(eCtx, returnType, fieldASTs, info, path, result) + completed := completeValue(ctx, eCtx, returnType, fieldASTs, info, path, result) return completed } - completed = completeValue(eCtx, returnType, fieldASTs, info, path, result) + completed = completeValue(ctx, eCtx, returnType, fieldASTs, info, path, result) return completed } -func completeValue(eCtx *executionContext, returnType Type, fieldASTs []*ast.Field, info ResolveInfo, path *ResponsePath, result interface{}) interface{} { +func completeValue(ctx context.Context, eCtx *executionContext, returnType Type, fieldASTs []*ast.Field, info ResolveInfo, path *ResponsePath, result interface{}) interface{} { resultVal := reflect.ValueOf(result) if resultVal.IsValid() && resultVal.Kind() == reflect.Func { return func() interface{} { - return completeThunkValueCatchingError(eCtx, returnType, fieldASTs, info, path, result) + return completeThunkValueCatchingError(ctx, eCtx, returnType, fieldASTs, info, path, result) } } // If field type is NonNull, complete for inner type, and throw field error // if result is null. if returnType, ok := returnType.(*NonNull); ok { - completed := completeValue(eCtx, returnType.OfType, fieldASTs, info, path, result) + completed := completeValue(ctx, eCtx, returnType.OfType, fieldASTs, info, path, result) if completed == nil { err := NewLocatedErrorWithPath( fmt.Sprintf("Cannot return null for non-nullable field %v.%v.", info.ParentType, info.FieldName), @@ -700,7 +700,7 @@ func completeValue(eCtx *executionContext, returnType Type, fieldASTs []*ast.Fie // If field type is List, complete each item in the list with the inner type if returnType, ok := returnType.(*List); ok { - return completeListValue(eCtx, returnType, fieldASTs, info, path, result) + return completeListValue(ctx, eCtx, returnType, fieldASTs, info, path, result) } // If field type is a leaf type, Scalar or Enum, serialize to a valid value, @@ -715,15 +715,15 @@ func completeValue(eCtx *executionContext, returnType Type, fieldASTs []*ast.Fie // If field type is an abstract type, Interface or Union, determine the // runtime Object type and complete for that type. if returnType, ok := returnType.(*Union); ok { - return completeAbstractValue(eCtx, returnType, fieldASTs, info, path, result) + return completeAbstractValue(ctx, eCtx, returnType, fieldASTs, info, path, result) } if returnType, ok := returnType.(*Interface); ok { - return completeAbstractValue(eCtx, returnType, fieldASTs, info, path, result) + return completeAbstractValue(ctx, eCtx, returnType, fieldASTs, info, path, result) } // If field type is Object, execute and complete all sub-selections. if returnType, ok := returnType.(*Object); ok { - return completeObjectValue(eCtx, returnType, fieldASTs, info, path, result) + return completeObjectValue(ctx, eCtx, returnType, fieldASTs, info, path, result) } // Not reachable. All possible output types have been considered. @@ -736,7 +736,7 @@ func completeValue(eCtx *executionContext, returnType Type, fieldASTs []*ast.Fie return nil } -func completeThunkValueCatchingError(eCtx *executionContext, returnType Type, fieldASTs []*ast.Field, info ResolveInfo, path *ResponsePath, result interface{}) (completed interface{}) { +func completeThunkValueCatchingError(ctx context.Context, eCtx *executionContext, returnType Type, fieldASTs []*ast.Field, info ResolveInfo, path *ResponsePath, result interface{}) (completed interface{}) { // catch any panic invoked from the propertyFn (thunk) defer func() { @@ -758,17 +758,17 @@ func completeThunkValueCatchingError(eCtx *executionContext, returnType Type, fi result = fnResult if returnType, ok := returnType.(*NonNull); ok { - completed := completeValue(eCtx, returnType, fieldASTs, info, path, result) + completed := completeValue(ctx, eCtx, returnType, fieldASTs, info, path, result) return completed } - completed = completeValue(eCtx, returnType, fieldASTs, info, path, result) + completed = completeValue(ctx, eCtx, returnType, fieldASTs, info, path, result) return completed } // completeAbstractValue completes value of an Abstract type (Union / Interface) by determining the runtime type // of that value, then completing based on that type. -func completeAbstractValue(eCtx *executionContext, returnType Abstract, fieldASTs []*ast.Field, info ResolveInfo, path *ResponsePath, result interface{}) interface{} { +func completeAbstractValue(ctx context.Context, eCtx *executionContext, returnType Abstract, fieldASTs []*ast.Field, info ResolveInfo, path *ResponsePath, result interface{}) interface{} { var runtimeType *Object @@ -801,11 +801,11 @@ func completeAbstractValue(eCtx *executionContext, returnType Abstract, fieldAST )) } - return completeObjectValue(eCtx, runtimeType, fieldASTs, info, path, result) + return completeObjectValue(ctx, eCtx, runtimeType, fieldASTs, info, path, result) } // completeObjectValue complete an Object value by executing all sub-selections. -func completeObjectValue(eCtx *executionContext, returnType *Object, fieldASTs []*ast.Field, info ResolveInfo, path *ResponsePath, result interface{}) interface{} { +func completeObjectValue(ctx context.Context, eCtx *executionContext, returnType *Object, fieldASTs []*ast.Field, info ResolveInfo, path *ResponsePath, result interface{}) interface{} { // If there is an isTypeOf predicate function, call it with the // current result. If isTypeOf returns false, then raise an error rather @@ -849,7 +849,7 @@ func completeObjectValue(eCtx *executionContext, returnType *Object, fieldASTs [ Fields: subFieldASTs, Path: path, } - return executeSubFields(executeFieldsParams) + return executeSubFields(ctx, executeFieldsParams) } // completeLeafValue complete a leaf value (Scalar / Enum) by serializing to a valid value, returning nil if serialization is not possible. @@ -862,7 +862,7 @@ func completeLeafValue(returnType Leaf, result interface{}) interface{} { } // completeListValue complete a list value by completing each item in the list with the inner type -func completeListValue(eCtx *executionContext, returnType *List, fieldASTs []*ast.Field, info ResolveInfo, path *ResponsePath, result interface{}) interface{} { +func completeListValue(ctx context.Context, eCtx *executionContext, returnType *List, fieldASTs []*ast.Field, info ResolveInfo, path *ResponsePath, result interface{}) interface{} { resultVal := reflect.ValueOf(result) if resultVal.Kind() == reflect.Ptr { resultVal = resultVal.Elem() @@ -885,7 +885,7 @@ func completeListValue(eCtx *executionContext, returnType *List, fieldASTs []*as for i := 0; i < resultVal.Len(); i++ { val := resultVal.Index(i).Interface() fieldPath := path.WithKey(i) - completedItem := completeValueCatchingError(eCtx, itemType, fieldASTs, info, fieldPath, val) + completedItem := completeValueCatchingError(ctx, eCtx, itemType, fieldASTs, info, fieldPath, val) completedResults = append(completedResults, completedItem) } return completedResults From acd9fcd03fbd683488f796c554e2b288f5a7b1f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Ram=C3=B3n?= Date: Tue, 1 Jan 2019 21:22:04 -0500 Subject: [PATCH 6/6] executor: tracer emptiness checking --- executor.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/executor.go b/executor.go index 02bbefb8..0de6cb9d 100644 --- a/executor.go +++ b/executor.go @@ -33,6 +33,10 @@ func Execute(p ExecuteParams) (result *Result) { ctx = context.Background() } + if p.Tracer == nil { + p.Tracer = NoopTracer{} + } + resultChannel := make(chan *Result) result = &Result{}