diff --git a/client.go b/client.go index 57458b0..283fca6 100644 --- a/client.go +++ b/client.go @@ -9,9 +9,9 @@ import ( "io" "net/http" "os" + "reflect" "regexp" "runtime" - "reflect" ) // A Client can be used to interact with Rollbar via the configured Transport. @@ -163,11 +163,26 @@ func (c *Client) SetTransform(transform func(map[string]interface{})) { c.configuration.transform = transform } -// SetStackTracer sets the stackTracer function which is called to extract the stack -// trace from enhanced error types. Return nil if no trace information is available. -// Return true if the error type can be handled and false otherwise. -// This feature can be used to add support for custom error type stack trace extraction. -func (c *Client) SetStackTracer(stackTracer func(err error) ([]runtime.Frame, bool)) { +// SetUnwrapper sets the UnwrapperFunc used by the client. The unwrapper function +// is used to extract wrapped errors from enhanced error types. This feature can be used to add +// support for custom error types that do not yet implement the Unwrap method specified in Go 1.13. +// See the documentation of UnwrapperFunc for more details. +// +// In order to preserve the default unwrapping behavior, callers of SetUnwrapper may wish to include +// a call to DefaultUnwrapper in their custom unwrapper function. See the example on the SetUnwrapper function. +func (c *Client) SetUnwrapper(unwrapper UnwrapperFunc) { + c.configuration.unwrapper = unwrapper +} + +// SetStackTracer sets the StackTracerFunc used by the client. The stack tracer +// function is used to extract the stack trace from enhanced error types. This feature can be used +// to add support for custom error types that do not implement the Stacker interface. +// See the documentation of StackTracerFunc for more details. +// +// In order to preserve the default stack tracing behavior, callers of SetStackTracer may wish +// to include a call to DefaultStackTracer in their custom tracing function. See the example +// on the SetStackTracer function. +func (c *Client) SetStackTracer(stackTracer StackTracerFunc) { c.configuration.stackTracer = stackTracer } @@ -605,7 +620,8 @@ type configuration struct { scrubFields *regexp.Regexp checkIgnore func(string) bool transform func(map[string]interface{}) - stackTracer func(error) ([]runtime.Frame, bool) + unwrapper UnwrapperFunc + stackTracer StackTracerFunc person Person captureIp captureIp } @@ -629,6 +645,8 @@ func createConfiguration(token, environment, codeVersion, serverHost, serverRoot fingerprint: false, checkIgnore: func(_s string) bool { return false }, transform: func(_d map[string]interface{}) {}, + unwrapper: DefaultUnwrapper, + stackTracer: DefaultStackTracer, person: Person{}, captureIp: CaptureIpFull, } diff --git a/doc.go b/doc.go index 304d524..fb681c0 100644 --- a/doc.go +++ b/doc.go @@ -3,6 +3,8 @@ Package rollbar is a Golang Rollbar client that makes it easy to report errors t Basic Usage +This package is designed to be used via the functions exposed at the root of the `rollbar` package. These work by managing a single instance of the `Client` type that is configurable via the setter functions at the root of the package. + package main import ( @@ -12,10 +14,10 @@ Basic Usage func main() { rollbar.SetToken("MY_TOKEN") - rollbar.SetEnvironment("production") // defaults to "development" - rollbar.SetCodeVersion("v2") // optional Git hash/branch/tag (required for GitHub integration) - rollbar.SetServerHost("web.1") // optional override; defaults to hostname - rollbar.SetServerRoot("/") // local path of project (required for GitHub integration and non-project stacktrace collapsing) + rollbar.SetEnvironment("production") // defaults to "development" + rollbar.SetCodeVersion("v2") // optional Git hash/branch/tag (required for GitHub integration) + rollbar.SetServerHost("web.1") // optional override; defaults to hostname + rollbar.SetServerRoot("/") // local path of project (required for GitHub integration and non-project stacktrace collapsing) rollbar.Info("Message body goes here") rollbar.WrapAndWait(doSomething) @@ -26,13 +28,12 @@ Basic Usage timer.Reset(10) // this will panic } - -This package is designed to be used via the functions exposed at the root of the `rollbar` package. These work by managing a single instance of the `Client` type that is configurable via the setter functions at the root of the package. - If you wish for more fine grained control over the client or you wish to have multiple independent clients then you can create and manage your own instances of the `Client` type. We provide two implementations of the `Transport` interface, `AsyncTransport` and `SyncTransport`. These manage the communication with the network layer. The Async version uses a buffered channel to communicate with the Rollbar API in a separate go routine. The Sync version is fully synchronous. It is possible to create your own `Transport` and configure a Client to use your preferred implementation. +Handling Panics + Go does not provide a mechanism for handling all panics automatically, therefore we provide two functions `Wrap` and `WrapAndWait` to make working with panics easier. They both take a function and then report to Rollbar if that function panics. They use the recover mechanism to capture the panic, and therefore if you wish your process to have the normal behaviour on panic (i.e. to crash), you will need to re-panic the result of calling `Wrap`. For example, package main @@ -60,15 +61,22 @@ Go does not provide a mechanism for handling all panics automatically, therefore The above pattern of calling `Wrap(...)` and then `Wait(...)` can be combined via `WrapAndWait(...)`. When `WrapAndWait(...)` returns if there was a panic it has already been sent to the Rollbar API. The error is still returned by this function if there is one. +Tracing Errors + +Due to the nature of the `error` type in Go, it can be difficult to attribute errors to their original origin without doing some extra work. To account for this, we provide multiple ways of configuring the client to unwrap errors and extract stack traces. + +The client will automatically unwrap any error type which implements the `Unwrap() error` method specified in Go 1.13. (See https://golang.org/pkg/errors/ for details.) This behavior can be extended for other types of errors by calling `SetUnwrapper`. -Due to the nature of the `error` type in Go, it can be difficult to attribute errors to their original origin without doing some extra work. To account for this, we define the interface `CauseStacker`: +For stack traces, we provide the `Stacker` interface, which can be implemented on custom error types: - type CauseStacker interface { - error - Cause() error + type Stacker interface { Stack() []runtime.Frame } -One can implement this interface for custom Error types to be able to build up a chain of stack traces. In order to get the correct stack, callers are required to call runtime.Callers and build the runtime.Frame slice on their own at the time the cause is wrapped. This is the least intrusive mechanism for gathering this information due to the decisions made by the Go runtime to not track this information. +If you cannot implement the `Stacker` interface on your error type (which is common for third-party error libraries), you can provide a custom tracing function by calling `SetStackTracer`. + +See the documentation of `SetUnwrapper` and `SetStackTracer` for more information and examples. + +Finally, users of github.com/pkg/errors can use the utilities provided in the `errors` sub-package. */ package rollbar diff --git a/errors/go.mod b/errors/go.mod index befc1a6..64f58c8 100644 --- a/errors/go.mod +++ b/errors/go.mod @@ -1,3 +1,5 @@ module github.com/rollbar/rollbar-go/errors +go 1.13 + require github.com/pkg/errors v0.8.1 diff --git a/go.mod b/go.mod index 075a9ce..ac50a32 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,3 @@ module github.com/rollbar/rollbar-go + +go 1.13 diff --git a/rollbar.go b/rollbar.go index 8db60e8..1fa734a 100644 --- a/rollbar.go +++ b/rollbar.go @@ -36,6 +36,60 @@ var ( nilErrTitle = "" ) +// An UnwrapperFunc is used to extract wrapped errors when building an error chain. It should return +// the wrapped error if available, or nil otherwise. +// +// The client will use DefaultUnwrapper by default, and a user can override the default behavior +// by calling SetUnwrapper. See SetUnwrapper for more details. +type UnwrapperFunc func(error) error + +// A StackTracerFunc is used to extract stack traces when building an error chain. The first return +// value should be the extracted stack trace, if available. The second return value should be +// whether the function was able to extract a stack trace (even if the extracted stack trace was +// empty or nil). +// +// The client will use DefaultStackTracer by default, and a user can override the default +// behavior by calling SetStackTracer. See SetStackTracer for more details. +type StackTracerFunc func(error) ([]runtime.Frame, bool) + +// DefaultUnwrapper is the default UnwrapperFunc used by rollbar-go clients. It can unwrap any +// error types with the Unwrap method specified in Go 1.13, or any error type implementing the +// legacy CauseStacker interface. +// +// It also implicitly supports errors from github.com/pkg/errors. However, users of pkg/errors may +// wish to also use the stack trace extraction features provided in the +// github.com/rollbar/rollbar-go/errors package. +var DefaultUnwrapper UnwrapperFunc = func(err error) error { + type causer interface { + Cause() error + } + type wrapper interface { // matches the new Go 1.13 Unwrap() method, copied from xerrors + Unwrap() error + } + + if e, ok := err.(causer); ok { + return e.Cause() + } + if e, ok := err.(wrapper); ok { + return e.Unwrap() + } + + return nil +} + +// DefaultStackTracer is the default StackTracerFunc used by rollbar-go clients. It can extract +// stack traces from error types implementing the Stacker interface (and by extension, the legacy +// CauseStacker interface). +// +// To support stack trace extraction for other types of errors, see SetStackTracer. +var DefaultStackTracer StackTracerFunc = func(err error) ([]runtime.Frame, bool) { + if s, ok := err.(Stacker); ok { + return s.Stack(), true + } + + return nil, false +} + // SetEnabled sets whether or not the managed Client instance is enabled. // If this is true then this library works as normal. // If this is false then no calls will be made to the network. @@ -126,12 +180,25 @@ func SetTransform(transform func(map[string]interface{})) { std.SetTransform(transform) } -// SetStackTracer sets the stackTracer function on the managed Client instance. -// StackTracer is called to extract the stack trace from enhanced error types. -// Return nil if no trace information is available. Return true if the error type -// can be handled and false otherwise. -// This feature can be used to add support for custom error type stack trace extraction. -func SetStackTracer(stackTracer func(err error) ([]runtime.Frame, bool)) { +// SetUnwrapper sets the UnwrapperFunc used by the managed Client instance. The unwrapper function +// is used to extract wrapped errors from enhanced error types. This feature can be used to add +// support for custom error types that do not yet implement the Unwrap method specified in Go 1.13. +// See the documentation of UnwrapperFunc for more details. +// +// In order to preserve the default unwrapping behavior, callers of SetUnwrapper may wish to include +// a call to DefaultUnwrapper in their custom unwrapper function. See the provided example. +func SetUnwrapper(unwrapper UnwrapperFunc) { + std.SetUnwrapper(unwrapper) +} + +// SetStackTracer sets the StackTracerFunc used by the managed Client instance. The stack tracer +// function is used to extract the stack trace from enhanced error types. This feature can be used +// to add support for custom error types that do not implement the Stacker interface. +// See the documentation of StackTracerFunc for more details. +// +// In order to preserve the default stack tracing behavior, callers of SetStackTracer may wish +// to include a call to DefaultStackTracer in their custom tracing function. See the provided example. +func SetStackTracer(stackTracer StackTracerFunc) { std.SetStackTracer(stackTracer) } @@ -541,11 +608,19 @@ func LambdaWrapper(handlerFunc interface{}) interface{} { return std.LambdaWrapper(handlerFunc) } +// Stacker is an interface that errors can implement to allow the extraction of stack traces. +// To generate a stack trace, users are required to call runtime.Callers and build the runtime.Frame slice +// at the time the error is created. +type Stacker interface { + Stack() []runtime.Frame +} + // CauseStacker is an interface that errors can implement to create a trace_chain. -// Callers are required to call runtime.Callers and build the runtime.Frame slice -// on their own at the time the cause is wrapped. +// +// Deprecated: For unwrapping, use the `Unwrap() error` method specified in Go 1.13. (See https://golang.org/pkg/errors/ for more information). +// For stack traces, use the `Stacker` interface directly. type CauseStacker interface { error Cause() error - Stack() []runtime.Frame + Stacker } diff --git a/rollbar_example_set_stack_tracer_test.go b/rollbar_example_set_stack_tracer_test.go new file mode 100644 index 0000000..711e364 --- /dev/null +++ b/rollbar_example_set_stack_tracer_test.go @@ -0,0 +1,31 @@ +package rollbar_test + +import ( + "runtime" + + "github.com/rollbar/rollbar-go" +) + +type CustomTraceError struct { + error + trace []runtime.Frame +} + +func (e CustomTraceError) GetTrace() []runtime.Frame { + return e.trace +} + +func ExampleSetStackTracer() { + rollbar.SetStackTracer(func(err error) ([]runtime.Frame, bool) { + // preserve the default behavior for other types of errors + if trace, ok := rollbar.DefaultStackTracer(err); ok { + return trace, ok + } + + if cerr, ok := err.(CustomTraceError); ok { + return cerr.GetTrace(), true + } + + return nil, false + }) +} diff --git a/rollbar_example_set_unwrapper_test.go b/rollbar_example_set_unwrapper_test.go new file mode 100644 index 0000000..effb3e7 --- /dev/null +++ b/rollbar_example_set_unwrapper_test.go @@ -0,0 +1,27 @@ +package rollbar_test + +import "github.com/rollbar/rollbar-go" + +type CustomWrappingError struct { + error + wrapped error +} + +func (e CustomWrappingError) GetWrappedError() error { + return e.wrapped +} + +func ExampleSetUnwrapper() { + rollbar.SetUnwrapper(func(err error) error { + // preserve the default behavior for other types of errors + if unwrapped := rollbar.DefaultUnwrapper(err); unwrapped != nil { + return unwrapped + } + + if ex, ok := err.(CustomWrappingError); ok { + return ex.GetWrappedError() + } + + return nil + }) +} diff --git a/rollbar_test.go b/rollbar_test.go index e2bc9fd..0892094 100644 --- a/rollbar_test.go +++ b/rollbar_test.go @@ -45,7 +45,7 @@ func testErrorStackWithSkipGeneric2(s string) { func TestErrorClass(t *testing.T) { errors := map[string]error{ // generic error - "errors.errorString": fmt.Errorf("something is broken"), + "errors.errorString": fmt.Errorf("something is broken"), // custom error "rollbar.CustomError": &CustomError{"terrible mistakes were made"}, } @@ -394,6 +394,9 @@ type cs struct { stack []runtime.Frame } +var _ Stacker = cs{} +var _ CauseStacker = cs{} + func (cs cs) Cause() error { return cs.cause } @@ -402,58 +405,120 @@ func (cs cs) Stack() []runtime.Frame { return cs.stack } -func TestGetCauseOfStdErr(t *testing.T) { - if nil != getCause(fmt.Errorf("")) { - t.Error("cause should be nil for standard error") - } +type uw struct { + error + wrapped error } -func TestGetCauseOfCauseStacker(t *testing.T) { - cause := fmt.Errorf("cause") - effect := cs{fmt.Errorf("effect"), cause, nil} - if cause != getCause(effect) { - t.Error("effect should return cause") - } +func (uw uw) Unwrap() error { + return uw.wrapped } -func TestGetOrBuildStackOfStdErrWithoutParent(t *testing.T) { - err := cs{fmt.Errorf(""), nil, getCallersFrames(0)} - if nil == getOrBuildFrames(err, nil, 0, nil) { - t.Error("should build stack if parent is not a CauseStacker") - } +func TestDefaultUnwrapper(t *testing.T) { + t.Run("standard error", func(t *testing.T) { + if nil != DefaultUnwrapper(fmt.Errorf("")) { + t.Error("unwrapping a standard error should get nil") + } + }) + t.Run("unwrap", func(t *testing.T) { + wrapped := fmt.Errorf("wrapped") + parent := uw{fmt.Errorf("parent"), wrapped} + if wrapped != DefaultUnwrapper(parent) { + t.Error("parent should return wrapped") + } + }) + t.Run("CauseStacker", func(t *testing.T) { + cause := fmt.Errorf("cause") + effect := cs{fmt.Errorf("effect"), cause, nil} + if cause != DefaultUnwrapper(effect) { + t.Error("effect should return cause") + } + }) } -func TestGetOrBuildStackOfStdErrWithParent(t *testing.T) { - cause := fmt.Errorf("cause") - effect := cs{fmt.Errorf("effect"), cause, getCallersFrames(0)} - if 0 != len(getOrBuildFrames(cause, effect, 0, nil)) { - t.Error("should return empty stack of stadard error if parent is CauseStacker") - } +func TestDefaultStackTracer(t *testing.T) { + t.Run("standard error", func(t *testing.T) { + trace, ok := DefaultStackTracer(fmt.Errorf("standard error")) + if trace != nil { + t.Error("standard errors should not return a trace") + } + if ok { + t.Errorf("standard errors should not be handled") + } + }) + t.Run("Stacker", func(t *testing.T) { + trace := getCallersFrames(0) + err := cs{fmt.Errorf("cause"), nil, trace} + extractedTrace, ok := DefaultStackTracer(err) + if extractedTrace == nil { + t.Error("Stackers should return a trace") + } else if extractedTrace[0] != trace[0] { + t.Error("the trace from the error must be extracted") + } + if !ok { + t.Error("Stackers should be handled") + } + }) } -func TestGetOrBuildStackOfCauseStackerWithoutParent(t *testing.T) { - cause := fmt.Errorf("cause") - effect := cs{fmt.Errorf("effect"), cause, getCallersFrames(0)} - if len(effect.Stack()) == 0 { - t.Fatal("stack should not be empty") - } - if effect.Stack()[0] != getOrBuildFrames(effect, nil, 0, nil)[0] { - t.Error("should use stack from effect") - } -} +func TestGetOrBuildFrames(t *testing.T) { + // These tests all use the default stack tracer. The logic this is testing doesn't really + // depend on how the stack trace is extracted. -func TestGetOrBuildStackOfCauseStackerWithParent(t *testing.T) { - cause := fmt.Errorf("cause") - effect := cs{fmt.Errorf("effect"), cause, getCallersFrames(0)} - effect2 := cs{fmt.Errorf("effect2"), effect, getCallersFrames(0)} - if effect2.Stack()[0] != getOrBuildFrames(effect2, effect, 0, nil)[0] { - t.Error("should use stack from effect2") - } + t.Run("standard error without parent", func(t *testing.T) { + err := fmt.Errorf("") + trace := getOrBuildFrames(err, nil, 0, DefaultStackTracer) + if nil == trace { + t.Error("should build a new stack trace if error has no stack and parent is nil") + } + }) + t.Run("standard error with traceable parent", func(t *testing.T) { + cause := fmt.Errorf("cause") + effect := cs{fmt.Errorf("effect"), cause, getCallersFrames(0)} + if nil != getOrBuildFrames(cause, effect, 0, DefaultStackTracer) { + t.Error("should return nil if child is not traceable but parent is") + } + }) + t.Run("standard error with non-traceable parent", func(t *testing.T) { + child := fmt.Errorf("child") + parent := uw{fmt.Errorf("parent"), child} + trace := getOrBuildFrames(child, parent, 0, DefaultStackTracer) + if nil == trace { + t.Error("should build a new stack trace if parent is not traceable") + } + }) + t.Run("traceable error without parent", func(t *testing.T) { + cause := fmt.Errorf("cause") + effect := cs{fmt.Errorf("effect"), cause, getCallersFrames(0)} + if effect.Stack()[0] != getOrBuildFrames(effect, nil, 0, DefaultStackTracer)[0] { + t.Error("should use stack trace from effect") + } + }) + t.Run("traceable error with traceable parent", func(t *testing.T) { + cause := fmt.Errorf("cause") + effect := cs{fmt.Errorf("effect"), cause, getCallersFrames(0)} + effect2 := cs{fmt.Errorf("effect2"), effect, getCallersFrames(0)} + if effect.Stack()[0] != getOrBuildFrames(effect, effect2, 0, DefaultStackTracer)[0] { + t.Error("should use stack from child, not parent") + } + }) + t.Run("traceable error with non-traceable parent", func(t *testing.T) { + cause := fmt.Errorf("cause") + effect := cs{fmt.Errorf("effect"), cause, getCallersFrames(0)} + effect2 := uw{fmt.Errorf("effect2"), effect} + if effect.Stack()[0] != getOrBuildFrames(effect, effect2, 0, DefaultStackTracer)[0] { + t.Error("should use stack from child") + } + }) } func TestErrorBodyWithoutChain(t *testing.T) { err := fmt.Errorf("ERR") - errorBody, fingerprint := errorBody(configuration{fingerprint: true}, err, 0) + errorBody, fingerprint := errorBody(configuration{ + fingerprint: true, + unwrapper: DefaultUnwrapper, + stackTracer: DefaultStackTracer, + }, err, 0) if nil != errorBody["trace"] { t.Error("should not have trace element") } @@ -476,7 +541,11 @@ func TestErrorBodyWithChain(t *testing.T) { cause := fmt.Errorf("cause") effect := cs{fmt.Errorf("effect1"), cause, getCallersFrames(0)} effect2 := cs{fmt.Errorf("effect2"), effect, getCallersFrames(0)} - errorBody, fingerprint := errorBody(configuration{fingerprint: true}, effect2, 0) + errorBody, fingerprint := errorBody(configuration{ + fingerprint: true, + unwrapper: DefaultUnwrapper, + stackTracer: DefaultStackTracer, + }, effect2, 0) if nil != errorBody["trace"] { t.Error("should not have trace element") } @@ -501,3 +570,62 @@ func TestErrorBodyWithChain(t *testing.T) { t.Error("fingerprint should be the fingerprints in chain concatenated together. got: ", fingerprint) } } + +func TestSetUnwrapper(t *testing.T) { + type myCustomError struct { + error + wrapped error + } + + client := NewAsync("example", "test", "0.0.0", "", "") + child := fmt.Errorf("child") + parent := myCustomError{fmt.Errorf("parent"), child} + + if client.configuration.unwrapper(parent) != nil { + t.Fatal("bad test; default unwrapper must not recognize the custom error type") + } + + client.SetUnwrapper(func(err error) error { + if e, ok := err.(myCustomError); ok { + return e.wrapped + } + + return nil + }) + + if client.configuration.unwrapper(parent) != child { + t.Error("error did not unwrap correctly") + } +} + +func TestSetStackTracer(t *testing.T) { + type myCustomError struct { + error + trace []runtime.Frame + } + + client := NewAsync("example", "test", "0.0.0", "", "") + err := myCustomError{fmt.Errorf("some error"), getCallersFrames(0)} + + if trace, ok := client.configuration.stackTracer(err); ok || trace != nil { + t.Fatal("bad test; default stack tracer must not recognize the custom error type") + } + + client.SetStackTracer(func(err error) (frames []runtime.Frame, b bool) { + if e, ok := err.(myCustomError); ok { + return e.trace, true + } + + return nil, false + }) + + trace, ok := client.configuration.stackTracer(err) + if !ok { + t.Error("error was not handled by custom stack tracer") + } + if trace == nil { + t.Errorf("custom tracer failed to extract trace") + } else if trace[0] != err.trace[0] { + t.Errorf("custom tracer got the wrong trace") + } +} diff --git a/transforms.go b/transforms.go index d999073..f262cbd 100644 --- a/transforms.go +++ b/transforms.go @@ -215,7 +215,7 @@ func errorBody(configuration configuration, err error, skip int) (map[string]int fingerprint = fingerprint + stack.Fingerprint() } parent = err - err = getCause(err) + err = configuration.unwrapper(err) if err == nil { break } @@ -239,44 +239,16 @@ func buildTrace(err error, stack stack) map[string]interface{} { } } -func getCause(err error) error { - type causer interface { - Cause() error - } - - if cs, ok := err.(causer); ok { - return cs.Cause() - } - return nil -} - // getOrBuildFrames gets stack frames from errors that provide one of their own // otherwise, it builds a new stack trace. It returns the stack frames if the error // is of a compatible type. If the error is not, but the parent error is, it assumes // the parent error will be processed later and therefore returns nil. -func getOrBuildFrames(err error, parent error, skip int, - tracer func(error) ([]runtime.Frame, bool)) []runtime.Frame { - - switch x := err.(type) { - case CauseStacker: - return x.Stack() - default: - if tracer != nil { - if st, ok := tracer(err); ok && st != nil { - return st - } - } +func getOrBuildFrames(err error, parent error, skip int, tracer StackTracerFunc) []runtime.Frame { + if st, ok := tracer(err); ok && st != nil { + return st } - - switch parent.(type) { - case CauseStacker: + if _, ok := tracer(parent); ok { return nil - default: - if tracer != nil { - if _, ok := tracer(parent); ok { - return nil - } - } } return getCallersFrames(1 + skip)