diff --git a/CHANGELOG.md b/CHANGELOG.md index de81ae7..b8bf1dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ CHANGELOG](http://keepachangelog.com/) for how to update this file. This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased][unreleased] +### Added +- Added `honeybadger.FromContext` to retrieve a honeybadger.Context from a + context.Context. +- Added `Context.WithContext` for storing a honeybadger.Context into a + context.Context. ### Changed - Removed honeybadger.SetContext and client.SetContext (#35) -@gaffneyc diff --git a/README.md b/README.md index 9a49fcf..b745f88 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,26 @@ honeybadger.Notify(err, honeybadger.Tags{"timeout", "http"}) --- +When using Go's context.Context you can store a honeybadger.Context to build it +up across multiple middleware. Be aware that honeybadger.Context is not thread +safe. +```go +func(resp http.ResponseWriter, req *http.Request) { + // To store a honeybadger.Context (or use honeybadger.Handler which does this for you) + hbCtx := honeybadger.Context{} + req = req.WithContext(hbCtx.WithContext(req.Context())) + + // To add to an existing context + hbCtx = honeybadger.FromContext(req.Context()) + hbCtx["user_id"] = "ID" + + // To add the context when sending you can just pass the context.Context + honeybadger.Notify(err, ctx) +} +``` + +--- + ### ``defer honeybadger.Monitor()``: Automatically report panics from your functions To automatically report panics in your functions or methods, add diff --git a/client.go b/client.go index c356c23..79549c4 100644 --- a/client.go +++ b/client.go @@ -88,14 +88,20 @@ func (client *Client) Handler(h http.Handler) http.Handler { if h == nil { h = http.DefaultServeMux } - fn := func(w http.ResponseWriter, r *http.Request) { + fn := func(w http.ResponseWriter, req *http.Request) { defer func() { if err := recover(); err != nil { - client.Notify(newError(err, 2), Params(r.Form), getCGIData(r), *r.URL) + client.Notify(newError(err, 2), Params(req.Form), getCGIData(req), *req.URL) panic(err) } }() - h.ServeHTTP(w, r) + + // Add a fresh Context to the request if one is not already set + if hbCtx := FromContext(req.Context()); hbCtx == nil { + req = req.WithContext(Context{}.WithContext(req.Context())) + } + + h.ServeHTTP(w, req) } return http.HandlerFunc(fn) } diff --git a/client_test.go b/client_test.go index 0979697..4caa8b6 100644 --- a/client_test.go +++ b/client_test.go @@ -1,6 +1,8 @@ package honeybadger import ( + "context" + "fmt" "testing" ) @@ -84,3 +86,37 @@ func mockClient(c Configuration) (Client, *mockWorker, *mockBackend) { return client, worker, backend } + +func TestClientContext(t *testing.T) { + backend := NewMemoryBackend() + + client := New(Configuration{ + APIKey: "badgers", + Backend: backend, + }) + + err := NewError(fmt.Errorf("which context is which")) + + hbCtx := Context{"user_id": 1} + goCtx := Context{"request_id": "1234"}.WithContext(context.Background()) + + _, nErr := client.Notify(err, hbCtx, goCtx) + if nErr != nil { + t.Fatal(nErr) + } + + // Flush otherwise backend.Notices will be empty + client.Flush() + + if len(backend.Notices) != 1 { + t.Fatalf("Notices expected=%d actual=%d", 1, len(backend.Notices)) + } + + notice := backend.Notices[0] + if notice.Context["user_id"] != 1 { + t.Errorf("notice.Context[user_id] expected=%d actual=%v", 1, notice.Context["user_id"]) + } + if notice.Context["request_id"] != "1234" { + t.Errorf("notice.Context[request_id] expected=%q actual=%v", "1234", notice.Context["request_id"]) + } +} diff --git a/context.go b/context.go index 0f828d3..a70f9b9 100644 --- a/context.go +++ b/context.go @@ -1,11 +1,33 @@ package honeybadger +import "context" + // Context is used to send extra data to Honeybadger. type Context hash +// ctxKey is use in WithContext and FromContext to store and load the +// honeybadger.Context into a context.Context. +type ctxKey struct{} + // Update applies the values in other Context to context. -func (context Context) Update(other Context) { +func (c Context) Update(other Context) { for k, v := range other { - context[k] = v + c[k] = v + } +} + +// WithContext adds the honeybadger.Context to the given context.Context and +// returns the new context.Context. +func (c Context) WithContext(ctx context.Context) context.Context { + return context.WithValue(ctx, ctxKey{}, c) +} + +// FromContext retrieves a honeybadger.Context from the context.Context. +// FromContext will return nil if no Honeybadger context exists in ctx. +func FromContext(ctx context.Context) Context { + if c, ok := ctx.Value(ctxKey{}).(Context); ok { + return c } + + return nil } diff --git a/context_test.go b/context_test.go index 83cf23a..b585d6c 100644 --- a/context_test.go +++ b/context_test.go @@ -1,6 +1,9 @@ package honeybadger -import "testing" +import ( + "context" + "testing" +) func TestContextUpdate(t *testing.T) { c := Context{"foo": "bar"} @@ -9,3 +12,26 @@ func TestContextUpdate(t *testing.T) { t.Errorf("Context should update values. expected=%#v actual=%#v", "baz", c["foo"]) } } + +func TestContext(t *testing.T) { + t.Run("setting values is allowed between reads", func(t *testing.T) { + ctx := context.Background() + ctx = Context{"foo": "bar"}.WithContext(ctx) + + stored := FromContext(ctx) + if stored == nil { + t.Fatalf("FromContext returned nil") + } + if stored["foo"] != "bar" { + t.Errorf("stored[foo] expected=%q actual=%v", "bar", stored["foo"]) + } + + // Write a new key then we'll read from the ctx again and make sure it is + // still set. + stored["baz"] = "qux" + stored = FromContext(ctx) + if stored["baz"] != "qux" { + t.Errorf("stored[baz] expected=%q actual=%v", "qux", stored["baz"]) + } + }) +} diff --git a/memory_backend.go b/memory_backend.go new file mode 100644 index 0000000..6064386 --- /dev/null +++ b/memory_backend.go @@ -0,0 +1,45 @@ +package honeybadger + +import ( + "fmt" + "reflect" + "sync" +) + +// MemoryBackend is a Backend that writes error notices to a slice. The +// MemoryBackend is mainly useful for testing and will cause issues if used in +// production. MemoryBackend is thread safe but order can't be guaranteed. +type MemoryBackend struct { + Notices []*Notice + mu sync.Mutex +} + +// NewMemoryBackend creates a new MemoryBackend. +func NewMemoryBackend() *MemoryBackend { + return &MemoryBackend{ + Notices: make([]*Notice, 0), + } +} + +// Notify adds the given payload (if it is a Notice) to Notices. +func (b *MemoryBackend) Notify(_ Feature, payload Payload) error { + notice, ok := payload.(*Notice) + if !ok { + return fmt.Errorf("memory backend does not support payload of type %q", reflect.TypeOf(payload)) + } + + b.mu.Lock() + defer b.mu.Unlock() + + b.Notices = append(b.Notices, notice) + + return nil +} + +// Reset clears the set of Notices +func (b *MemoryBackend) Reset() { + b.mu.Lock() + defer b.mu.Unlock() + + b.Notices = b.Notices[:0] +} diff --git a/notice.go b/notice.go index b780b58..a1db516 100644 --- a/notice.go +++ b/notice.go @@ -1,6 +1,7 @@ package honeybadger import ( + "context" "encoding/json" "net/url" "os" @@ -174,6 +175,11 @@ func newNotice(config *Configuration, err Error, extra ...interface{}) *Notice { notice.CGIData = t case url.URL: notice.URL = t.String() + case context.Context: + context := FromContext(t) + if context != nil { + notice.setContext(context) + } } } diff --git a/notice_test.go b/notice_test.go index 2c15f48..6c2222c 100644 --- a/notice_test.go +++ b/notice_test.go @@ -1,6 +1,7 @@ package honeybadger import ( + "context" "encoding/json" "errors" "testing" @@ -57,6 +58,18 @@ func TestNewNotice(t *testing.T) { if notice.Context["foo"] != "bar" { t.Errorf("Expected notice to contain context. expected=%#v result=%#v", "bar", notice.Context["foo"]) } + + // Notices can take the Context that is stored in a context.Context + notice = newNotice(&Configuration{}, err, Context{"foo": "bar"}.WithContext(context.Background())) + if notice.Context["foo"] != "bar" { + t.Errorf("Expected notice to contain context. expected=%#v result=%#v", "bar", notice.Context["foo"]) + } + + // Notices given a context.Context without a Context don't set notice.Context + notice = newNotice(&Configuration{}, err, context.Background()) + if len(notice.Context) != 0 { + t.Errorf("Expected notice to contain empty context. result=%#v", notice.Context) + } } func TestToJSON(t *testing.T) {