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

Context based error reporting #37

wants to merge 19 commits into from

Conversation

seanhagen
Copy link

Update Client.SetContext to require context.Context
To keep the Honeybadger context scoped to a single request, store
the *contextSync value in a context.Context. This way there's no possibility
of the request context data being overwritten due to the function being called
in a concurrent setting.

Update Client.Notify to require context.Context
In order to get the context stored in the Client.SetContext call, need to
require a context as the first argument.

Update Monitor to require context.Context
Same as other functions, client.Notify requires a context.Context as the
first argument so Monitor has to be updated as well. Does mean that a panic
won't have the context information unless it's called within the function that
might panic instead of at the top level.

Ie:

func main(){
  defer honeybadger.Monitor()
  // set up http handlers and whatnot
}

Won't be able to get the values set unless it's called like so:

func handler(w http.ResponseWriter, r *http.Request){
  defer honeybadger.MonitorCtx(r.Context())
  // rest of handler
}

No more need for Client.context, so removing it

Update Client.Handler to use r.Context()

Update functions to require context.Context

Use context.Background in Monitor()

Version bump -- v0.5.0

To keep the Honeybadger context scoped to a single request, store
the `*contextSync` value in a `context.Context`. This way there's no possibility
of the request context data being overwritten due to the function being called
in a concurrent setting.
In order to get the context stored in the `Client.SetContext` call, need to
require a context as the first argument.
Same as other functions, `client.Notify` requires a `context.Context` as the
first argument so Monitor has to be updated as well. Does mean that a panic
won't have the context information unless it's called within the function that
might panic instead of at the top level.

Ie:

```
func main(){
  ctx := context.Background()
  defer honeybadger.Monitor(ctx)
  // set up http handlers and whatnot
}
```

Won't be able to get the values set unless it's called like so:

```
func handler(w http.ResponseWriter, r *http.Request){
  defer honeybadger.Monitor(r.Context())
  // rest of handler
}
```
@seanhagen
Copy link
Author

This should address #35

@seanhagen
Copy link
Author

Not sure why this is failing or what can be done to fix:

1.48s$ make deps
# dependencies
go get github.com/pborman/uuid
# github.com/pborman/uuid
../../pborman/uuid/time.go:15: syntax error: unexpected = in type declaration
make: *** [deps] Error 2

@joshuap
Copy link
Member

joshuap commented Nov 1, 2018

@seanhagen super sorry for the delay in reviewing this--thanks for submitting it! I'm looking over it now. Prepare for some dumb questions; context passing in go is pretty new to me. :)

It sounds like this solves the problem of scoping context to the request when using the honeybadger.Handler implementation. Is that correct?

My first question is how to get ctx if I'm just reporting a regular error from say, an executable script (no concurrency, outside of a request). For instance:

honeybadger.Notify(ctx, err)

Where does ctx come from? Looking at the test changes, it looks like I would have to do ctx := context.Background() to get the context? If I want to use honeybadger.SetContext in that same script, will it work with that context object?

If that's the case, is there anything we can do to smooth the user interface for users who don't want to think about shared context (i.e., say they just want to call honeybadger.Notify(err))? Or will they always have to grab a default context.Context from somewhere, and pass it in as the first argument?

Because contexts are not modifiable in-place (eg, calling `context.WithValue`
returns a new context, it doesn't return the same context you've passed in with
the value set), need to set some data in the Honeybadger HTTP middleware.

Unfortunately, any additional info set with `SetContext` within the handler is
lost if the handler panics.
@seanhagen
Copy link
Author

Yeah, this will prevent the honeybadger context ( ie, setting the information that should be reported if Notify is called -- having two things named context gets confusing ) from getting clobbered when honeybadger is used within goroutines.

I also just updated the honeybadger.Handler so that it adds some additional info.

Basically, think of the context.Context as where the values get stored, instead of within a global state in the honeybadger package. So yeah, if you're writing a script, you'd use ctx := context.Background() or context.TODO()to get a context that you then pass into every call to honeybadger.SetContext & to honeybadger.Notify if an error occurs.

Unfortunately, in the case of the http handler, because the request context can't be replaced if SetContext is within the handler ( or in a middleware lower down the chain ), the panic catching won't see any of that info. So fixing the "overwriting-context-in-goroutines" has the side effect of "the panic handler in honeybadger.Handler doesn't see anything set via honeybadger.SetContext once it calls h.ServeHTTP(w, req)".

I don't think that's too clear, so here's a code example:

mux := http.NewServeMux()

mux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request){
  ctx := honeybadger.SetContext(r.Context(), map[string]interface{}{"value": "something important"})

  // important code

  if err != nil {
    // this error in honeybadger will have "value" = "something important" in the context
    honeybadger.Notify(ctx, err)
  }
})

mux.HandleFunc("/panics", func(w http.ResponseWriter, r *http.Request){
  // this data won't be seen if the function panics
  // because it's a new context, not the same one that the panic handler in client.go
  // has access to
  ctx := honeybadger.SetContext(r.Context(), map[string]interface{}{"value": "very important"})

  // important code

  if err != nil {
    panic(err)
  }
})

http.ListenAndServe(":8080", honeybadger.Handler(mux))

In the route /test, we'll get all the context set in client.go in Handler ( line 121 ) when we call honeybadger.Notify(ctx, err), along with the {"value": "something important"} context info.

However, in /panics, it'll only have the context set within client.go, because it doesn't have the context returned from SetContext

It's a downside, for sure, but there's no way around it without global values -- and then we're right back to data getting clobbered when it's run in goroutines ( which is just unavoidable in web servers, http.ListenAndServe uses goroutines to handle incoming requests, so do GRPC servers ).

Re: still being able to call honeybadger.Notify(err)

I could change the new Notify to be named NotifyCtx. It would mean that the new behaviour for Notify would be "sends the error and only extra info passed in at the time of calling Notify", because without the context.Context it can't grab previously stored information. Can't put it into global state, because that's what was causing the "overwrite-context-in-goroutine" problem in the first place 😞

@seanhagen
Copy link
Author

I've been using my fork in production for a bit now, and it works fine and no data gets clobbered or shows up where it shouldn't in the Honeybadger app.

@joshuap
Copy link
Member

joshuap commented Nov 30, 2018

@seanhagen Take a look at how Bugsnag handles context--they do something similar to what you're doing here: https://docs.bugsnag.com/platforms/go/net-http/#manage-http-session-data-with-context-context

In their Notify method, they use the trailing splat argument (rawData ... interface {}) to collect extra (optional) data like we do, including ctx (I'm going to refer to go-context as ctx from now on, and Honeybadger context as context). They use the same approach in their AutoNotify and Recover methods (which are like our Monitor method). https://github.com/bugsnag/bugsnag-go/blob/master/bugsnag.go#L70

I'd like to take the same approach (adding ctx as an optional trailing argument to all methods) here. This way, existing users won't have to change their existing apps. To take advantage of context, they'll start passing ctx as an optional end-argument to Notify, Monitor, and Context. This should also make having a second notify method (like NotifyCtx unnecessary).

What do you think?

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

@rzaharenkov
Copy link

What is the status of this PR? It would be really nice to get it merged soon.

@joshuap
Copy link
Member

joshuap commented Apr 4, 2019

I can't merge this PR as-is, unfortunately. See my comment here (and my review).

I don't have time to do it myself atm; PRs are welcome of course, but otherwise I could schedule this to happen in the next couple months. If anyone does want to work on this, comment here/on #35 to make sure that we don't duplicate effort.

gaffneyc added a commit to gaffneyc/honeybadger-go that referenced this pull request Jul 10, 2019
This allows building up a honeybadger.Context riding along with a
context.Context so it can be sent with errors. The goal is to make it so
that the context sent to Honeybadger is tied to the request (or stack)
rather than multiple requests clobbering the global state (see honeybadger-io#35).

Fixes honeybadger-io#35
Closes honeybadger-io#37
@joshuap
Copy link
Member

joshuap commented Nov 5, 2019

Closing this in favor of #39 (which is also hanging atm, but a better direction).

@joshuap joshuap closed this Nov 5, 2019
gaffneyc added a commit to gaffneyc/honeybadger-go that referenced this pull request Jan 9, 2020
This allows building up a honeybadger.Context riding along with a
context.Context so it can be sent with errors. The goal is to make it so
that the context sent to Honeybadger is tied to the request (or stack)
rather than multiple requests clobbering the global state (see honeybadger-io#35).

Fixes honeybadger-io#35
Closes honeybadger-io#37
gaffneyc added a commit to gaffneyc/honeybadger-go that referenced this pull request Jun 18, 2020
This allows building up a honeybadger.Context riding along with a
context.Context so it can be sent with errors. The goal is to make it so
that the context sent to Honeybadger is tied to the request (or stack)
rather than multiple requests clobbering the global state (see honeybadger-io#35).

Fixes honeybadger-io#35
Closes honeybadger-io#37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants