forked from mirror/logrus
Rework the Airbrake hook
Rework the Airbrake hook to: a) change the interface so that the Airbrake credentials are stored in an unexported struct, `airbrakeHook`, which is instantiated using the `NewHook()` method b) send log entries where no 'error' field is set to Airbrake, using the `entry.Message` string as the message sent to Airbrake but continue to allow the passing of error types using the 'error' field Update the tests accordingly, assuring that the correct message is received by the Airbrake server. Also update the examples in the README, which would not have worked with the previous implementation of the Airbrake hook.
This commit is contained in:
parent
78dee3c0ba
commit
83a820d91e
|
@ -82,7 +82,7 @@ func init() {
|
||||||
|
|
||||||
// Use the Airbrake hook to report errors that have Error severity or above to
|
// Use the Airbrake hook to report errors that have Error severity or above to
|
||||||
// an exception tracker. You can create custom hooks, see the Hooks section.
|
// an exception tracker. You can create custom hooks, see the Hooks section.
|
||||||
log.AddHook(&logrus_airbrake.AirbrakeHook{})
|
log.AddHook(airbrake.NewHook("https://example.com", "xyz", "development"))
|
||||||
|
|
||||||
// Output to stderr instead of stdout, could also be a file.
|
// Output to stderr instead of stdout, could also be a file.
|
||||||
log.SetOutput(os.Stderr)
|
log.SetOutput(os.Stderr)
|
||||||
|
@ -211,7 +211,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
log.AddHook(new(logrus_airbrake.AirbrakeHook))
|
log.AddHook(airbrake.NewHook("https://example.com", "xyz", "development"))
|
||||||
|
|
||||||
hook, err := logrus_syslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
|
hook, err := logrus_syslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -3,21 +3,16 @@ package main
|
||||||
import (
|
import (
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
"github.com/Sirupsen/logrus/hooks/airbrake"
|
"github.com/Sirupsen/logrus/hooks/airbrake"
|
||||||
"github.com/tobi/airbrake-go"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var log = logrus.New()
|
var log = logrus.New()
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
log.Formatter = new(logrus.TextFormatter) // default
|
log.Formatter = new(logrus.TextFormatter) // default
|
||||||
log.Hooks.Add(new(logrus_airbrake.AirbrakeHook))
|
log.Hooks.Add(airbrake.NewHook("https://example.com", "xyz", "development"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
airbrake.Endpoint = "https://exceptions.whatever.com/notifier_api/v2/notices.xml"
|
|
||||||
airbrake.ApiKey = "whatever"
|
|
||||||
airbrake.Environment = "production"
|
|
||||||
|
|
||||||
log.WithFields(logrus.Fields{
|
log.WithFields(logrus.Fields{
|
||||||
"animal": "walrus",
|
"animal": "walrus",
|
||||||
"size": 10,
|
"size": 10,
|
||||||
|
|
|
@ -1,51 +1,51 @@
|
||||||
package airbrake
|
package airbrake
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
"github.com/tobi/airbrake-go"
|
"github.com/tobi/airbrake-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AirbrakeHook to send exceptions to an exception-tracking service compatible
|
// AirbrakeHook to send exceptions to an exception-tracking service compatible
|
||||||
// with the Airbrake API. You must set:
|
// with the Airbrake API.
|
||||||
// * airbrake.Endpoint
|
type airbrakeHook struct {
|
||||||
// * airbrake.ApiKey
|
APIKey string
|
||||||
// * airbrake.Environment
|
Endpoint string
|
||||||
//
|
Environment string
|
||||||
// Before using this hook, to send an error. Entries that trigger an Error,
|
}
|
||||||
// Fatal or Panic should now include an "error" field to send to Airbrake.
|
|
||||||
type AirbrakeHook struct{}
|
|
||||||
|
|
||||||
func (hook *AirbrakeHook) Fire(entry *logrus.Entry) error {
|
func NewHook(endpoint, apiKey, env string) *airbrakeHook {
|
||||||
if entry.Data["error"] == nil {
|
return &airbrakeHook{
|
||||||
entry.Logger.WithFields(logrus.Fields{
|
APIKey: apiKey,
|
||||||
"source": "airbrake",
|
Endpoint: endpoint,
|
||||||
"endpoint": airbrake.Endpoint,
|
Environment: env,
|
||||||
}).Warn("Exceptions sent to Airbrake must have an 'error' key with the error")
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hook *airbrakeHook) Fire(entry *logrus.Entry) error {
|
||||||
|
airbrake.ApiKey = hook.APIKey
|
||||||
|
airbrake.Endpoint = hook.Endpoint
|
||||||
|
airbrake.Environment = hook.Environment
|
||||||
|
|
||||||
|
var notifyErr error
|
||||||
err, ok := entry.Data["error"].(error)
|
err, ok := entry.Data["error"].(error)
|
||||||
if !ok {
|
if ok {
|
||||||
entry.Logger.WithFields(logrus.Fields{
|
notifyErr = err
|
||||||
"source": "airbrake",
|
} else {
|
||||||
"endpoint": airbrake.Endpoint,
|
notifyErr = errors.New(entry.Message)
|
||||||
}).Warn("Exceptions sent to Airbrake must have an `error` key of type `error`")
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
airErr := airbrake.Notify(err)
|
airErr := airbrake.Notify(notifyErr)
|
||||||
if airErr != nil {
|
if airErr != nil {
|
||||||
entry.Logger.WithFields(logrus.Fields{
|
return fmt.Errorf("Failed to send error to Airbrake: %s", airErr)
|
||||||
"source": "airbrake",
|
|
||||||
"endpoint": airbrake.Endpoint,
|
|
||||||
"error": airErr,
|
|
||||||
}).Warn("Failed to send error to Airbrake")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hook *AirbrakeHook) Levels() []logrus.Level {
|
func (hook *airbrakeHook) Levels() []logrus.Level {
|
||||||
return []logrus.Level{
|
return []logrus.Level{
|
||||||
logrus.ErrorLevel,
|
logrus.ErrorLevel,
|
||||||
logrus.FatalLevel,
|
logrus.FatalLevel,
|
||||||
|
|
|
@ -2,26 +2,123 @@ package airbrake
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
"github.com/tobi/airbrake-go"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type notice struct {
|
type notice struct {
|
||||||
Error struct {
|
Error NoticeError `xml:"error"`
|
||||||
Message string `xml:"message"`
|
}
|
||||||
} `xml:"error"`
|
type NoticeError struct {
|
||||||
|
Class string `xml:"class"`
|
||||||
|
Message string `xml:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNoticeReceived(t *testing.T) {
|
type customErr struct {
|
||||||
msg := make(chan string, 1)
|
msg string
|
||||||
expectedMsg := "foo"
|
}
|
||||||
|
|
||||||
|
func (e *customErr) Error() string {
|
||||||
|
return e.msg
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
testAPIKey = "abcxyz"
|
||||||
|
testEnv = "development"
|
||||||
|
expectedClass = "*airbrake.customErr"
|
||||||
|
expectedMsg = "foo"
|
||||||
|
unintendedMsg = "Airbrake will not see this string"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
noticeError = make(chan NoticeError, 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 := startAirbrakeServer(t)
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
hook := NewHook(ts.URL, testAPIKey, "production")
|
||||||
|
log.Hooks.Add(hook)
|
||||||
|
|
||||||
|
log.Error(expectedMsg)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case received := <-noticeError:
|
||||||
|
if received.Message != expectedMsg {
|
||||||
|
t.Errorf("Unexpected message received: %s", received.Message)
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Error("Timed out; no notice received by Airbrake 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 := startAirbrakeServer(t)
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
hook := NewHook(ts.URL, testAPIKey, "production")
|
||||||
|
log.Hooks.Add(hook)
|
||||||
|
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"error": &customErr{expectedMsg},
|
||||||
|
}).Error(unintendedMsg)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case received := <-noticeError:
|
||||||
|
if received.Message != expectedMsg {
|
||||||
|
t.Errorf("Unexpected message received: %s", received.Message)
|
||||||
|
}
|
||||||
|
if received.Class != expectedClass {
|
||||||
|
t.Errorf("Unexpected error class: %s", received.Class)
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Error("Timed out; no notice received by Airbrake API")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLogEntryWithNonErrorTypeNotReceived confirms that, when passing a
|
||||||
|
// non-error type using logrus.Fields, a HTTP server emulating an Airbrake
|
||||||
|
// endpoint receives the logrus.Entry.Message string.
|
||||||
|
//
|
||||||
|
// Only error types are supported when setting the 'error' field using
|
||||||
|
// logrus.WithFields().
|
||||||
|
func TestLogEntryWithNonErrorTypeNotReceived(t *testing.T) {
|
||||||
|
log := logrus.New()
|
||||||
|
ts := startAirbrakeServer(t)
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
hook := NewHook(ts.URL, testAPIKey, "production")
|
||||||
|
log.Hooks.Add(hook)
|
||||||
|
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"error": expectedMsg,
|
||||||
|
}).Error(unintendedMsg)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case received := <-noticeError:
|
||||||
|
if received.Message != unintendedMsg {
|
||||||
|
t.Errorf("Unexpected message received: %s", received.Message)
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Error("Timed out; no notice received by Airbrake API")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startAirbrakeServer(t *testing.T) *httptest.Server {
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
var notice notice
|
var notice notice
|
||||||
if err := xml.NewDecoder(r.Body).Decode(¬ice); err != nil {
|
if err := xml.NewDecoder(r.Body).Decode(¬ice); err != nil {
|
||||||
|
@ -29,29 +126,8 @@ func TestNoticeReceived(t *testing.T) {
|
||||||
}
|
}
|
||||||
r.Body.Close()
|
r.Body.Close()
|
||||||
|
|
||||||
msg <- notice.Error.Message
|
noticeError <- notice.Error
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
hook := &AirbrakeHook{}
|
return ts
|
||||||
|
|
||||||
airbrake.Environment = "production"
|
|
||||||
airbrake.Endpoint = ts.URL
|
|
||||||
airbrake.ApiKey = "foo"
|
|
||||||
|
|
||||||
log := logrus.New()
|
|
||||||
log.Hooks.Add(hook)
|
|
||||||
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"error": errors.New(expectedMsg),
|
|
||||||
}).Error("Airbrake 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 Airbrake API")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue