diff --git a/executor.go b/executor.go index 10d3799f..0de6cb9d 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) { @@ -30,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{} @@ -51,6 +58,7 @@ func Execute(p ExecuteParams) (result *Result) { Args: p.Args, Result: result, Context: p.Context, + Tracer: p.Tracer, }) if err != nil { @@ -82,16 +90,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 +147,7 @@ func buildExecutionContext(p buildExecutionCtxParams) (*executionContext, error) eCtx.Operation = operation eCtx.VariableValues = variableValues eCtx.Context = p.Context + eCtx.Tracer = p.Tracer return eCtx, nil } @@ -239,7 +250,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 } @@ -255,7 +266,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) @@ -265,7 +276,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{}{} } @@ -276,7 +287,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 } @@ -572,17 +585,7 @@ 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) { - // catch panic from resolveFn - var returnType Output - defer func() (interface{}, resolveFieldResultState) { - if r := recover(); r != nil { - handleFieldError(r, FieldASTsToNodeASTs(fieldASTs), path, returnType, eCtx) - return result, resultState - } - return result, resultState - }() - +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 { @@ -594,7 +597,22 @@ func resolveField(eCtx *executionContext, parentType *Object, source interface{} resultState.hasNoFieldDefs = true return nil, resultState } - returnType = fieldDef.Type + returnType := fieldDef.Type + + traceCtx, finishTraceField := eCtx.Tracer.TraceField(ctx, fieldName, + returnType.Name()) + + // catch panic from resolveFn + defer func() (interface{}, resolveFieldResultState) { + if r := recover(); r != nil { + handleFieldError(r, FieldASTsToNodeASTs(fieldASTs), path, returnType, eCtx) + finishTraceField(eCtx.Errors) + return result, resultState + } + finishTraceField([]gqlerrors.FormattedError{}) + return result, resultState + }() + resolveFn := fieldDef.Resolve if resolveFn == nil { resolveFn = DefaultResolveFn @@ -631,11 +649,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 { @@ -646,26 +666,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), @@ -684,7 +704,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, @@ -699,15 +719,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. @@ -720,7 +740,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() { @@ -742,17 +762,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 @@ -785,11 +805,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 @@ -833,7 +853,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. @@ -846,7 +866,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() @@ -869,7 +889,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 diff --git a/graphql.go b/graphql.go index c9bdb168..e57a3d9b 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,23 @@ func Do(p Params) *Result { } } - return Execute(ExecuteParams{ + 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: p.Context, + Context: ctx, + Tracer: p.Tracer, }) + + traceFinish(result.Errors) + + return result } diff --git a/trace.go b/trace.go new file mode 100644 index 00000000..f5d6275a --- /dev/null +++ b/trace.go @@ -0,0 +1,25 @@ +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, 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(errs []gqlerrors.FormattedError) {} +}