From 83752ed3c519dc89614efade9b28cd995eca9544 Mon Sep 17 00:00:00 2001 From: Burke Libbey Date: Mon, 16 Mar 2015 15:29:39 -0400 Subject: [PATCH 1/2] hooks: Add BugSnag hook --- hooks/bugsnag/bugsnag.go | 53 +++++++++++++++++++++++++++++ hooks/bugsnag/bugsnag_test.go | 64 +++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 hooks/bugsnag/bugsnag.go create mode 100644 hooks/bugsnag/bugsnag_test.go diff --git a/hooks/bugsnag/bugsnag.go b/hooks/bugsnag/bugsnag.go new file mode 100644 index 0000000..a0cfe84 --- /dev/null +++ b/hooks/bugsnag/bugsnag.go @@ -0,0 +1,53 @@ +package logrus_bugsnag + +import ( + "github.com/Sirupsen/logrus" + "github.com/bugsnag/bugsnag-go" +) + +// BugsnagHook sends exceptions to an exception-tracking service compatible +// with the Bugsnag API. Before using this hook, you must call +// bugsnag.Configure(). +// +// Entries that trigger an Error, Fatal or Panic should now include an "error" +// field to send to Bugsnag +type BugsnagHook struct{} + +// Fire forwards an error to Bugsnag. Given a logrus.Entry, it extracts the +// implicitly-required "error" field and sends it off. +func (hook *BugsnagHook) Fire(entry *logrus.Entry) error { + if entry.Data["error"] == nil { + entry.Logger.WithFields(logrus.Fields{ + "source": "bugsnag", + }).Warn("Exceptions sent to Bugsnag must have an 'error' key with the error") + return nil + } + + err, ok := entry.Data["error"].(error) + if !ok { + entry.Logger.WithFields(logrus.Fields{ + "source": "bugsnag", + }).Warn("Exceptions sent to Bugsnag must have an `error` key of type `error`") + return nil + } + + bugsnagErr := bugsnag.Notify(err) + if bugsnagErr != nil { + entry.Logger.WithFields(logrus.Fields{ + "source": "bugsnag", + "error": bugsnagErr, + }).Warn("Failed to send error to Bugsnag") + } + + return nil +} + +// Levels enumerates the log levels on which the error should be forwarded to +// bugsnag: everything at or above the "Error" level. +func (hook *BugsnagHook) Levels() []logrus.Level { + return []logrus.Level{ + logrus.ErrorLevel, + logrus.FatalLevel, + logrus.PanicLevel, + } +} diff --git a/hooks/bugsnag/bugsnag_test.go b/hooks/bugsnag/bugsnag_test.go new file mode 100644 index 0000000..7db5136 --- /dev/null +++ b/hooks/bugsnag/bugsnag_test.go @@ -0,0 +1,64 @@ +package logrus_bugsnag + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/Sirupsen/logrus" + "github.com/bugsnag/bugsnag-go" +) + +type notice struct { + Events []struct { + Exceptions []struct { + Message string `json:"message"` + } `json:"exceptions"` + } `json:"events"` +} + +func TestNoticeReceived(t *testing.T) { + msg := make(chan string, 1) + expectedMsg := "foo" + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var notice notice + data, _ := ioutil.ReadAll(r.Body) + if err := json.Unmarshal(data, ¬ice); err != nil { + t.Error(err) + } + _ = r.Body.Close() + + msg <- notice.Events[0].Exceptions[0].Message + })) + defer ts.Close() + + hook := &BugsnagHook{} + + bugsnag.Configure(bugsnag.Configuration{ + Endpoint: ts.URL, + ReleaseStage: "production", + APIKey: "12345678901234567890123456789012", + Synchronous: true, + }) + + log := logrus.New() + log.Hooks.Add(hook) + + log.WithFields(logrus.Fields{ + "error": errors.New(expectedMsg), + }).Error("Bugsnag will not see this string") + + select { + case received := <-msg: + if received != expectedMsg { + t.Errorf("Unexpected message received: %s", received) + } + case <-time.After(time.Second): + t.Error("Timed out; no notice received by Bugsnag API") + } +} From d96cee72fa69ef226c2d3c1594a829aa539cd902 Mon Sep 17 00:00:00 2001 From: Burke Libbey Date: Thu, 19 Mar 2015 11:17:22 -0400 Subject: [PATCH 2/2] Code review changes --- README.md | 6 ++++ hooks/bugsnag/bugsnag.go | 65 +++++++++++++++++++++-------------- hooks/bugsnag/bugsnag_test.go | 2 +- 3 files changed, 47 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index e755e7c..e3b5afa 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ import ( "os" log "github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus/hooks/airbrake" + "github.com/Sirupsen/logrus/hooks/bugsnag" ) func init() { @@ -84,6 +85,11 @@ func init() { // an exception tracker. You can create custom hooks, see the Hooks section. log.AddHook(&logrus_airbrake.AirbrakeHook{}) + // Use the Bugsnag hook to report errors that have Error severity or above to + // an exception tracker. You can create custom hooks, see the Hooks section. + bugsnagHook, _ = logrus_bugsnag.NewBugsnagHook() + log.AddHook(bugsnagHook) + // Output to stderr instead of stdout, could also be a file. log.SetOutput(os.Stderr) diff --git a/hooks/bugsnag/bugsnag.go b/hooks/bugsnag/bugsnag.go index a0cfe84..d20a0f5 100644 --- a/hooks/bugsnag/bugsnag.go +++ b/hooks/bugsnag/bugsnag.go @@ -1,42 +1,57 @@ package logrus_bugsnag import ( + "errors" + "github.com/Sirupsen/logrus" "github.com/bugsnag/bugsnag-go" ) -// BugsnagHook sends exceptions to an exception-tracking service compatible -// with the Bugsnag API. Before using this hook, you must call -// bugsnag.Configure(). +type bugsnagHook struct{} + +// ErrBugsnagUnconfigured is returned if NewBugsnagHook is called before +// bugsnag.Configure. Bugsnag must be configured before the hook. +var ErrBugsnagUnconfigured = errors.New("bugsnag must be configured before installing this logrus hook") + +// ErrBugsnagSendFailed indicates that the hook failed to submit an error to +// bugsnag. The error was successfully generated, but `bugsnag.Notify()` +// failed. +type ErrBugsnagSendFailed struct { + err error +} + +func (e ErrBugsnagSendFailed) Error() string { + return "failed to send error to Bugsnag: " + e.err.Error() +} + +// NewBugsnagHook initializes a logrus hook which sends exceptions to an +// exception-tracking service compatible with the Bugsnag API. Before using +// this hook, you must call bugsnag.Configure(). The returned object should be +// registered with a log via `AddHook()` // // Entries that trigger an Error, Fatal or Panic should now include an "error" -// field to send to Bugsnag -type BugsnagHook struct{} +// field to send to Bugsnag. +func NewBugsnagHook() (*bugsnagHook, error) { + if bugsnag.Config.APIKey == "" { + return nil, ErrBugsnagUnconfigured + } + return &bugsnagHook{}, nil +} // Fire forwards an error to Bugsnag. Given a logrus.Entry, it extracts the -// implicitly-required "error" field and sends it off. -func (hook *BugsnagHook) Fire(entry *logrus.Entry) error { - if entry.Data["error"] == nil { - entry.Logger.WithFields(logrus.Fields{ - "source": "bugsnag", - }).Warn("Exceptions sent to Bugsnag must have an 'error' key with the error") - return nil - } - +// "error" field (or the Message if the error isn't present) and sends it off. +func (hook *bugsnagHook) Fire(entry *logrus.Entry) error { + var notifyErr error err, ok := entry.Data["error"].(error) - if !ok { - entry.Logger.WithFields(logrus.Fields{ - "source": "bugsnag", - }).Warn("Exceptions sent to Bugsnag must have an `error` key of type `error`") - return nil + if ok { + notifyErr = err + } else { + notifyErr = errors.New(entry.Message) } - bugsnagErr := bugsnag.Notify(err) + bugsnagErr := bugsnag.Notify(notifyErr) if bugsnagErr != nil { - entry.Logger.WithFields(logrus.Fields{ - "source": "bugsnag", - "error": bugsnagErr, - }).Warn("Failed to send error to Bugsnag") + return ErrBugsnagSendFailed{bugsnagErr} } return nil @@ -44,7 +59,7 @@ func (hook *BugsnagHook) Fire(entry *logrus.Entry) error { // Levels enumerates the log levels on which the error should be forwarded to // bugsnag: everything at or above the "Error" level. -func (hook *BugsnagHook) Levels() []logrus.Level { +func (hook *bugsnagHook) Levels() []logrus.Level { return []logrus.Level{ logrus.ErrorLevel, logrus.FatalLevel, diff --git a/hooks/bugsnag/bugsnag_test.go b/hooks/bugsnag/bugsnag_test.go index 7db5136..e9ea298 100644 --- a/hooks/bugsnag/bugsnag_test.go +++ b/hooks/bugsnag/bugsnag_test.go @@ -37,7 +37,7 @@ func TestNoticeReceived(t *testing.T) { })) defer ts.Close() - hook := &BugsnagHook{} + hook := &bugsnagHook{} bugsnag.Configure(bugsnag.Configuration{ Endpoint: ts.URL,