diff --git a/README.md b/README.md index f44730d..b47b010 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ To report an error manually, use `honeybadger.Notify`: ```go if err != nil { - honeybadger.Notify(err) + honeybadger.Notify(ctx, err) } ``` @@ -113,7 +113,7 @@ The following options are available to you: ## Public Interface -### `honeybadger.Notify()`: Send an error to Honeybadger. +### `honeybadger.Notify(context,Context, interface{}, ...interface{})`: Send an error to Honeybadger. If you've handled a panic in your code, but would still like to report the error to Honeybadger, this is the method for you. @@ -121,7 +121,7 @@ If you've handled a panic in your code, but would still like to report the error ```go if err != nil { - honeybadger.Notify(err) + honeybadger.Notify(ctx, err) } ``` @@ -129,7 +129,7 @@ You can also add local context using an optional second argument when calling `honeybadger.Notify`: ```go -honeybadger.Notify(err, honeybadger.Context{"user_id": 2}) +honeybadger.Notify(ctx, err, honeybadger.Context{"user_id": 2}) ``` Honeybadger uses the error's class name to group similar errors together. If @@ -137,26 +137,26 @@ your error classes are often generic (such as `errors.errorString`), you can improve grouping by overriding the default with something more unique: ```go -honeybadger.Notify(err, honeybadger.ErrorClass{"CustomClassName"}) +honeybadger.Notify(ctx, err, honeybadger.ErrorClass{"CustomClassName"}) ``` To override grouping entirely, you can send a custom fingerprint. All errors with the same fingerprint will be grouped together: ```go -honeybadger.Notify(err, honeybadger.Fingerprint{"A unique string"}) +honeybadger.Notify(ctx, err, honeybadger.Fingerprint{"A unique string"}) ``` To tag errors in Honeybadger: ```go -honeybadger.Notify(err, honeybadger.Tags{"timeout", "http"}) +honeybadger.Notify(ctx, err, honeybadger.Tags{"timeout", "http"}) ``` --- -### `honeybadger.SetContext()`: Set metadata to be sent if an error occurs +### `honeybadger.SetContext(context.Context, map[string]interface{})`: Set metadata to be sent if an error occurs This method lets you set context data that will be sent if an error should occur. @@ -167,7 +167,7 @@ For example, it's often useful to record the current user's ID when an error occ #### Examples: ```go -honeybadger.SetContext(honeybadger.Context{ +honeybadger.SetContext(ctx, honeybadger.Context{ "user_id": 1, }) ``` diff --git a/client.go b/client.go index cc18da7..a3f7bfc 100644 --- a/client.go +++ b/client.go @@ -1,10 +1,16 @@ package honeybadger import ( + "context" + "fmt" "net/http" "strings" ) +const honeybadgerCtxKey = "honeybadger-go-ctx" + +var noCastErr = fmt.Errorf("unable to cast value from context properly") + // The Payload interface is implemented by any type which can be handled by the // Backend interface. type Payload interface { @@ -23,7 +29,6 @@ type noticeHandler func(*Notice) error // the configuration and implements the public API. type Client struct { Config *Configuration - context *contextSync worker worker beforeNotifyHandlers []noticeHandler } @@ -34,8 +39,25 @@ func (client *Client) Configure(config Configuration) { } // SetContext updates the client context with supplied context. -func (client *Client) SetContext(context Context) { - client.context.Update(context) +func (client *Client) SetContext(ctx context.Context, val Context) context.Context { + var clientContext *contextSync + + tmp := ctx.Value(honeybadgerCtxKey) + if tmp == nil { + clientContext = &contextSync{ + internal: Context{}, + } + } else { + if cc, ok := tmp.(*contextSync); ok { + clientContext = cc + } else { + panic(noCastErr) + } + } + + clientContext.Update(val) + + return context.WithValue(ctx, honeybadgerCtxKey, clientContext) } // Flush blocks until the worker has processed its queue. @@ -51,8 +73,14 @@ func (client *Client) BeforeNotify(handler func(notice *Notice) error) { } // Notify reports the error err to the Honeybadger service. -func (client *Client) Notify(err interface{}, extra ...interface{}) (string, error) { - extra = append([]interface{}{client.context.internal}, extra...) +func (client *Client) Notify(ctx context.Context, err interface{}, extra ...interface{}) (string, error) { + val, ok := ctx.Value(honeybadgerCtxKey).(*contextSync) + if ok { + val.Lock() + extra = append([]interface{}{val.internal}, extra...) + val.Unlock() + } + notice := newNotice(client.Config, newError(err, 2), extra...) for _, handler := range client.beforeNotifyHandlers { if err := handler(notice); err != nil { @@ -74,9 +102,9 @@ func (client *Client) Notify(err interface{}, extra ...interface{}) (string, err // Monitor automatically reports panics which occur in the function it's called // from. Must be deferred. -func (client *Client) Monitor() { +func (client *Client) Monitor(ctx context.Context) { if err := recover(); err != nil { - client.Notify(newError(err, 2)) + client.Notify(ctx, newError(err, 2)) client.Flush() panic(err) } @@ -89,13 +117,27 @@ func (client *Client) Handler(h http.Handler) http.Handler { h = http.DefaultServeMux } fn := func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + ctx = client.SetContext(ctx, map[string]interface{}{ + "path": r.URL.String(), + "host": r.Host, + "header": r.Header, + "user-agent": r.UserAgent(), + "content-length": r.ContentLength, + "method": r.Method, + "proto": r.Proto, + "remote-address": r.RemoteAddr, + "request-uri": r.RequestURI, + }) + req := r.WithContext(ctx) + defer func() { if err := recover(); err != nil { - client.Notify(newError(err, 2), Params(r.Form), getCGIData(r), *r.URL) + client.Notify(req.Context(), newError(err, 2), Params(r.Form), getCGIData(r), *r.URL) panic(err) } }() - h.ServeHTTP(w, r) + h.ServeHTTP(w, req) } return http.HandlerFunc(fn) } @@ -106,9 +148,8 @@ func New(c Configuration) *Client { worker := newBufferedWorker(config) client := Client{ - Config: config, - worker: worker, - context: newContextSync(), + Config: config, + worker: worker, } return &client diff --git a/client_test.go b/client_test.go index 9f07250..6567d71 100644 --- a/client_test.go +++ b/client_test.go @@ -1,8 +1,8 @@ package honeybadger import ( + "context" "testing" - "sync" ) func TestNewConfig(t *testing.T) { @@ -32,41 +32,26 @@ func TestConfigureClientEndpoint(t *testing.T) { func TestClientContext(t *testing.T) { client := New(Configuration{}) - client.SetContext(Context{"foo": "bar"}) - client.SetContext(Context{"bar": "baz"}) + ctx := context.Background() - context := client.context.internal + ctx = client.SetContext(ctx, Context{"foo": "bar"}) + ctx = client.SetContext(ctx, Context{"bar": "baz"}) - if context["foo"] != "bar" { - t.Errorf("Expected client to merge global context. expected=%#v actual=%#v", "bar", context["foo"]) + var context Context + if tmp, ok := ctx.Value(honeybadgerCtxKey).(*contextSync); ok { + context = tmp.internal } - if context["bar"] != "baz" { - t.Errorf("Expected client to merge global context. expected=%#v actual=%#v", "baz", context["bar"]) + if context == nil { + t.Errorf("context value not placed in context.Context") + t.FailNow() } -} - -func TestClientConcurrentContext(t *testing.T) { - var wg sync.WaitGroup - - client := New(Configuration{}) - newContext := Context{"foo":"bar"} - - wg.Add(2) - - go updateContext(&wg, client, newContext) - go updateContext(&wg, client, newContext) - - wg.Wait() - - context := client.context.internal if context["foo"] != "bar" { - t.Errorf("Expected context value. expected=%#v result=%#v", "bar", context["foo"]) + t.Errorf("Expected client to merge global context. expected=%#v actual=%#v", "bar", context["foo"]) } -} -func updateContext(wg *sync.WaitGroup, client *Client, context Context) { - client.SetContext(context) - wg.Done() + if context["bar"] != "baz" { + t.Errorf("Expected client to merge global context. expected=%#v actual=%#v", "baz", context["bar"]) + } } diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f1ec191 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module github.com/seanhagen/honeybadger-go + +require ( + github.com/gofrs/uuid/v3 v3.1.2 + github.com/shirou/gopsutil v2.18.12+incompatible + golang.org/x/sys v0.0.0-20190102155601-82a175fd1598 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..84eb89f --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/gofrs/uuid/v3 v3.1.2 h1:V3IBv1oU82x6YIr5txe3azVHgmOKYdyKQTowm9moBlY= +github.com/gofrs/uuid/v3 v3.1.2/go.mod h1:xPwMqoocQ1L5G6pXX5BcE7N5jlzn2o19oqAKxwZW/kI= +github.com/shirou/gopsutil v2.18.12+incompatible h1:1eaJvGomDnH74/5cF4CTmTbLHAriGFsTZppLXDX93OM= +github.com/shirou/gopsutil v2.18.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +golang.org/x/sys v0.0.0-20190102155601-82a175fd1598 h1:S8GOgffXV1X3fpVG442QRfWOt0iFl79eHJ7OPt725bo= +golang.org/x/sys v0.0.0-20190102155601-82a175fd1598/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/honeybadger.go b/honeybadger.go index 0797d87..cc0c74c 100644 --- a/honeybadger.go +++ b/honeybadger.go @@ -1,13 +1,14 @@ package honeybadger import ( + "context" "encoding/json" "net/http" "net/url" ) // VERSION defines the version of the honeybadger package. -const VERSION = "0.4.0" +const VERSION = "0.5.0" var ( // client is a pre-defined "global" client. @@ -55,8 +56,8 @@ func Configure(c Configuration) { } // SetContext merges c Context into the Context of the global client. -func SetContext(c Context) { - DefaultClient.SetContext(c) +func SetContext(ctx context.Context, c Context) context.Context { + return DefaultClient.SetContext(ctx, c) } // Notify reports the error err to the Honeybadger service. @@ -66,8 +67,8 @@ func SetContext(c Context) { // // It returns a string UUID which can be used to reference the error from the // Honeybadger service, and an error as a second argument. -func Notify(err interface{}, extra ...interface{}) (string, error) { - return DefaultClient.Notify(newError(err, 2), extra...) +func Notify(ctx context.Context, err interface{}, extra ...interface{}) (string, error) { + return DefaultClient.Notify(ctx, newError(err, 2), extra...) } // Monitor is used to automatically notify Honeybadger service of panics which @@ -79,9 +80,32 @@ func Notify(err interface{}, extra ...interface{}) (string, error) { // } // The Monitor function re-panics after the notification has been sent, so it's // still up to the user to recover from panics if desired. +// +// The Monitor function doesn't have access to any set context from `SetContext` +// calls. func Monitor() { if err := recover(); err != nil { - DefaultClient.Notify(newError(err, 2)) + ctx := context.Background() + DefaultClient.Notify(ctx, newError(err, 2)) + DefaultClient.Flush() + panic(err) + } +} + +// MonitorCtx is used to automatically notify Honeybadger service of panics which +// happen inside the current function. In order to monitor for panics, defer a +// call to MonitorCtx. For example: +// func handler(ctx context.Context) { +// defer honeybadger.MonitorCtx(ctx) +// // Do risky stuff... +// } +// The MonitorCtx function re-panics after the notification has been sent, so it's +// still up to the user to recover from panics if desired. +// +// Has access to any context set when using `SetCtx` +func MonitorCtx(ctx context.Context) { + if err := recover(); err != nil { + DefaultClient.Notify(ctx, newError(err, 2)) DefaultClient.Flush() panic(err) } @@ -95,6 +119,8 @@ func Flush() { // Handler returns an http.Handler function which automatically reports panics // to Honeybadger and then re-panics. +// +// The request context is what's passed to notify. func Handler(h http.Handler) http.Handler { return DefaultClient.Handler(h) } diff --git a/honeybadger_test.go b/honeybadger_test.go index 2388c1b..71c1c69 100644 --- a/honeybadger_test.go +++ b/honeybadger_test.go @@ -1,6 +1,7 @@ package honeybadger import ( + "context" "encoding/json" "errors" "fmt" @@ -85,7 +86,9 @@ func TestNotify(t *testing.T) { setup(t) defer teardown() - res, _ := Notify(errors.New("Cobras!")) + ctx := context.Background() + + res, _ := Notify(ctx, errors.New("Cobras!")) if uuid.Parse(res) == nil { t.Errorf("Expected Notify() to return a UUID. actual=%#v", res) @@ -104,8 +107,9 @@ func TestNotifyWithContext(t *testing.T) { setup(t) defer teardown() + ctx := context.Background() context := Context{"foo": "bar"} - Notify("Cobras!", context) + Notify(ctx, "Cobras!", context) Flush() if !testRequestCount(t, 1) { @@ -124,7 +128,8 @@ func TestNotifyWithErrorClass(t *testing.T) { setup(t) defer teardown() - Notify("Cobras!", ErrorClass{"Badgers"}) + ctx := context.Background() + Notify(ctx, "Cobras!", ErrorClass{"Badgers"}) Flush() if !testRequestCount(t, 1) { @@ -149,7 +154,8 @@ func TestNotifyWithTags(t *testing.T) { setup(t) defer teardown() - Notify("Cobras!", Tags{"timeout", "http"}) + ctx := context.Background() + Notify(ctx, "Cobras!", Tags{"timeout", "http"}) Flush() if !testRequestCount(t, 1) { @@ -174,7 +180,8 @@ func TestNotifyWithFingerprint(t *testing.T) { setup(t) defer teardown() - Notify("Cobras!", Fingerprint{"Badgers"}) + ctx := context.Background() + Notify(ctx, "Cobras!", Fingerprint{"Badgers"}) Flush() if !testRequestCount(t, 1) { @@ -222,7 +229,8 @@ func TestNotifyWithHandler(t *testing.T) { n.Fingerprint = "foo bar baz" return nil }) - Notify(errors.New("Cobras!")) + ctx := context.Background() + Notify(ctx, errors.New("Cobras!")) Flush() payload := requests[0].decodeJSON() @@ -248,7 +256,8 @@ func TestNotifyWithHandlerError(t *testing.T) { BeforeNotify(func(n *Notice) error { return err }) - _, notifyErr := Notify(errors.New("Cobras!")) + ctx := context.Background() + _, notifyErr := Notify(ctx, errors.New("Cobras!")) Flush() if !testRequestCount(t, 0) { diff --git a/notice.go b/notice.go index b780b58..186f3fe 100644 --- a/notice.go +++ b/notice.go @@ -7,7 +7,7 @@ import ( "regexp" "time" - "github.com/pborman/uuid" + "github.com/gofrs/uuid/v3" "github.com/shirou/gopsutil/load" "github.com/shirou/gopsutil/mem" ) @@ -143,10 +143,12 @@ func composeStack(stack []*Frame, root string) (frames []*Frame) { } func newNotice(config *Configuration, err Error, extra ...interface{}) *Notice { + tkn, _ := uuid.NewV4() + notice := Notice{ APIKey: config.APIKey, Error: err, - Token: uuid.NewRandom().String(), + Token: tkn.String(), ErrorMessage: err.Message, ErrorClass: err.Class, Env: config.Env,