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") + } +}