diff --git a/hooks/raygun/raygun.go b/hooks/raygun/raygun.go new file mode 100644 index 0000000..ec16719 --- /dev/null +++ b/hooks/raygun/raygun.go @@ -0,0 +1,50 @@ +package raygun + +import ( + "errors" + "net/http" + + "github.com/Sirupsen/logrus" + "github.com/sditools/goraygun" +) + +type raygunHook struct { + client *goraygun.Client +} + +func NewHook(Endpoint string, ApiKey string, Enabled bool) *raygunHook { + client := goraygun.Init(goraygun.Settings{ + ApiKey: ApiKey, + Endpoint: Endpoint, + Enabled: Enabled, + }, goraygun.Entry{}) + return &raygunHook{client} +} + +func (hook *raygunHook) Fire(logEntry *logrus.Entry) error { + // Start with a copy of the default entry + raygunEntry := hook.client.Entry + + if request, ok := logEntry.Data["request"]; ok { + raygunEntry.Details.Request.Populate(*(request.(*http.Request))) + } + + var reportErr error + if err, ok := logEntry.Data["error"]; ok { + reportErr = err.(error) + } else { + reportErr = errors.New(logEntry.Message) + } + + hook.client.Report(reportErr, raygunEntry) + + return nil +} + +func (hook *raygunHook) Levels() []logrus.Level { + return []logrus.Level{ + logrus.ErrorLevel, + logrus.FatalLevel, + logrus.PanicLevel, + } +} diff --git a/hooks/raygun/raygun_test.go b/hooks/raygun/raygun_test.go new file mode 100644 index 0000000..eda4b83 --- /dev/null +++ b/hooks/raygun/raygun_test.go @@ -0,0 +1,97 @@ +package raygun + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/Sirupsen/logrus" + "github.com/sditools/goraygun" +) + +type customErr struct { + msg string +} + +func (e *customErr) Error() string { + return e.msg +} + +const ( + testAPIKey = "abcxyz" + expectedClass = "github.com/Sirupsen/logrus/hooks/raygun" + expectedMsg = "oh no some error occured." + unintendedMsg = "Airbrake will not see this string" +) + +var ( + entryCh = make(chan goraygun.Entry, 1) +) + +// TestLogEntryMessageReceived checks if invoking Logrus' log.Error +// method causes an XML payload containing the log entry message is received +// by a HTTP server emulating an Airbrake-compatible endpoint. +func TestLogEntryMessageReceived(t *testing.T) { + log := logrus.New() + ts := startRaygunServer(t) + defer ts.Close() + + hook := NewHook(ts.URL, testAPIKey, true) + log.Hooks.Add(hook) + + log.Error(expectedMsg) + + select { + case received := <-entryCh: + if received.Details.Error.Message != expectedMsg { + t.Errorf("Unexpected message received: %s", received) + } + case <-time.After(time.Second): + t.Error("Timed out; no notice received by Raygun API") + } +} + +// TestLogEntryMessageReceived confirms that, when passing an error type using +// logrus.Fields, a HTTP server emulating an Airbrake endpoint receives the +// error message returned by the Error() method on the error interface +// rather than the logrus.Entry.Message string. +func TestLogEntryWithErrorReceived(t *testing.T) { + log := logrus.New() + ts := startRaygunServer(t) + defer ts.Close() + + hook := NewHook(ts.URL, testAPIKey, true) + log.Hooks.Add(hook) + + log.WithFields(logrus.Fields{ + "error": &customErr{expectedMsg}, + }).Error(unintendedMsg) + + select { + case received := <-entryCh: + if received.Details.Error.Message != expectedMsg { + t.Errorf("Unexpected message received: %s", received.Details.Error.Message) + } + if received.Details.Error.ClassName != expectedClass { + t.Errorf("Unexpected error class: %s", received.Details.Error.ClassName) + } + case <-time.After(time.Second): + t.Error("Timed out; no notice received by Airbrake API") + } +} + +func startRaygunServer(t *testing.T) *httptest.Server { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var entry goraygun.Entry + if err := json.NewDecoder(r.Body).Decode(&entry); err != nil { + t.Error(err) + } + r.Body.Close() + + entryCh <- entry + })) + + return ts +}