Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Context based error reporting #37

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ To report an error manually, use `honeybadger.Notify`:

```go
if err != nil {
honeybadger.Notify(err)
honeybadger.Notify(ctx, err)
}
```

Expand Down Expand Up @@ -113,50 +113,50 @@ 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.

#### Examples:

```go
if err != nil {
honeybadger.Notify(err)
honeybadger.Notify(ctx, err)
}
```

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
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.

Expand All @@ -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,
})
```
Expand Down
65 changes: 53 additions & 12 deletions client.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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
}
Expand All @@ -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.
Expand All @@ -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 {
Expand All @@ -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)
}
Expand All @@ -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(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this data is great, but we'll want to send it as cgi_data (with special keys) instead of context--that's what we use to display request data in error reports. For an example, search for "cgi_data" on this page: https://docs.honeybadger.io/api/exceptions.html#sample-payload

Here's how we construct it in Elixir, for instance: https://github.com/honeybadger-io/honeybadger-elixir/blob/94c6c1f29cef70ae754bf12aa99779849ddd0b70/lib/honeybadger/plug_data.ex#L49

I'd probably hold off on including this in the current PR (since it's kind of a separate feature) and then submit a subsequent PR after we get the new ctx API worked out.

"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)
}
Expand All @@ -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
Expand Down
43 changes: 14 additions & 29 deletions client_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package honeybadger

import (
"context"
"testing"
"sync"
)

func TestNewConfig(t *testing.T) {
Expand Down Expand Up @@ -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"])
}
}
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
38 changes: 32 additions & 6 deletions honeybadger.go
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand Down
Loading