diff --git a/graphql.go b/graphql.go index d688234631..acf1a1b410 100644 --- a/graphql.go +++ b/graphql.go @@ -17,6 +17,7 @@ import ( "github.com/neelance/graphql-go/internal/schema" "github.com/neelance/graphql-go/internal/validation" "github.com/neelance/graphql-go/introspection" + "github.com/neelance/graphql-go/log" "github.com/neelance/graphql-go/trace" ) @@ -49,6 +50,7 @@ func ParseSchema(schemaString string, resolver interface{}, opts ...SchemaOpt) ( schema: schema.New(), maxParallelism: 10, tracer: trace.OpenTracingTracer{}, + logger: &log.DefaultLogger{}, } for _, opt := range opts { opt(s) @@ -85,6 +87,7 @@ type Schema struct { maxParallelism int tracer trace.Tracer + logger log.Logger } // SchemaOpt is an option to pass to ParseSchema or MustParseSchema. @@ -104,6 +107,13 @@ func Tracer(tracer trace.Tracer) SchemaOpt { } } +// Logger is used to log panics durring query execution. It defaults to exec.DefaultLogger. +func Logger(logger log.Logger) SchemaOpt { + return func(s *Schema) { + s.logger = logger + } +} + // Response represents a typical response of a GraphQL server. It may be encoded to JSON directly or // it may be further processed to a custom response type, for example to include custom error data. type Response struct { @@ -146,6 +156,7 @@ func (s *Schema) exec(ctx context.Context, queryString string, operationName str }, Limiter: make(chan struct{}, s.maxParallelism), Tracer: s.tracer, + Logger: s.logger, } varTypes := make(map[string]*introspection.Type) for _, v := range op.Vars { diff --git a/internal/exec/exec.go b/internal/exec/exec.go index 7b06942cdc..e6f1a4ebd0 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -3,9 +3,7 @@ package exec import ( "bytes" "context" - "log" "reflect" - "runtime" "strconv" "sync" @@ -15,6 +13,7 @@ import ( "github.com/neelance/graphql-go/internal/exec/selected" "github.com/neelance/graphql-go/internal/query" "github.com/neelance/graphql-go/internal/schema" + "github.com/neelance/graphql-go/log" "github.com/neelance/graphql-go/trace" ) @@ -22,6 +21,7 @@ type Request struct { selected.Request Limiter chan struct{} Tracer trace.Tracer + Logger log.Logger } type fieldResult struct { @@ -29,25 +29,21 @@ type fieldResult struct { value []byte } -func (r *Request) handlePanic() { - if err := recover(); err != nil { - r.AddError(makePanicError(err)) +func (r *Request) handlePanic(ctx context.Context) { + if value := recover(); value != nil { + r.Logger.LogPanic(ctx, value) + r.AddError(makePanicError(value)) } } func makePanicError(value interface{}) *errors.QueryError { - err := errors.Errorf("graphql: panic occurred: %v", value) - const size = 64 << 10 - buf := make([]byte, size) - buf = buf[:runtime.Stack(buf, false)] - log.Printf("%s\n%s", err, buf) - return err + return errors.Errorf("graphql: panic occurred: %v", value) } func (r *Request) Execute(ctx context.Context, s *resolvable.Schema, op *query.Operation) ([]byte, []*errors.QueryError) { var out bytes.Buffer func() { - defer r.handlePanic() + defer r.handlePanic(ctx) sels := selected.ApplyOperation(&r.Request, s, op) r.execSelections(ctx, sels, s.Resolver, &out, op.Type == query.Mutation) }() @@ -76,7 +72,7 @@ func (r *Request) execSelections(ctx context.Context, sels []selected.Selection, wg.Add(len(fields)) for _, f := range fields { go func(f *fieldWithResolver) { - defer r.handlePanic() + defer r.handlePanic(ctx) r.execFieldSelection(ctx, f.field, f.resolver, &f.out, false) wg.Done() }(f) @@ -158,6 +154,7 @@ func (r *Request) execFieldSelection(ctx context.Context, field *selected.Schema err = func() (err *errors.QueryError) { defer func() { if panicValue := recover(); panicValue != nil { + r.Logger.LogPanic(ctx, panicValue) err = makePanicError(panicValue) } }() @@ -236,7 +233,7 @@ func (r *Request) execSelectionSet(ctx context.Context, sels []selected.Selectio entryouts := make([]bytes.Buffer, l) for i := 0; i < l; i++ { go func(i int) { - defer r.handlePanic() + defer r.handlePanic(ctx) r.execSelectionSet(ctx, sels, t.OfType, resolver.Index(i), &entryouts[i]) wg.Done() }(i) diff --git a/log/log.go b/log/log.go new file mode 100644 index 0000000000..aaab4342b1 --- /dev/null +++ b/log/log.go @@ -0,0 +1,23 @@ +package log + +import ( + "context" + "log" + "runtime" +) + +// Logger is the interface used to log panics that occur durring query execution. It is setable via graphql.ParseSchema +type Logger interface { + LogPanic(ctx context.Context, value interface{}) +} + +// DefaultLogger is the default logger used to log panics that occur durring query execution +type DefaultLogger struct{} + +// LogPanic is used to log recovered panic values that occur durring query execution +func (l *DefaultLogger) LogPanic(_ context.Context, value interface{}) { + const size = 64 << 10 + buf := make([]byte, size) + buf = buf[:runtime.Stack(buf, false)] + log.Printf("graphql: panic occurred: %v\n%s", value, buf) +}