From 9bf47770d5bcae3dd261fba54e98ef56571af401 Mon Sep 17 00:00:00 2001 From: Roberto Bampi <bampi.roberto@gmail.com> Date: Sat, 1 Nov 2014 19:55:40 +0100 Subject: [PATCH 1/6] added sentry logger --- hooks/sentry/README.md | 29 +++++++++++++++ hooks/sentry/sentry.go | 84 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 hooks/sentry/README.md create mode 100644 hooks/sentry/sentry.go diff --git a/hooks/sentry/README.md b/hooks/sentry/README.md new file mode 100644 index 0000000..bf0e950 --- /dev/null +++ b/hooks/sentry/README.md @@ -0,0 +1,29 @@ +# Sentry Hook for Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:" /> + +[Sentry](https://getsentry.com) provides both self-hosted and hostes solutions for exception tracking. +Both client and server are [open source](https://github.com/getsentry/sentry). + +## Usage + +Every sentry application defined on the server gets a different [DNS](https://www.getsentry.com/docs/). In the example below replace `YOUR_DSN` with the one created for your application. + + +```go +import ( + "github.com/Sirupsen/logrus" + "github.com/Sirupsen/logrus/hooks/sentry" +) + +func main() { + log := logrus.New() + hook, err := logrus_sentry.NewSentryHook(YOUR_DSN, []logrus.Level{ + logrus.PanicLevel, + logrus.FatalLevel, + logrus.ErrorLevel, + }) + + if err == nil { + log.Hooks.Add(hook) + } +} +``` diff --git a/hooks/sentry/sentry.go b/hooks/sentry/sentry.go new file mode 100644 index 0000000..5c07eb2 --- /dev/null +++ b/hooks/sentry/sentry.go @@ -0,0 +1,84 @@ +package sentry_hook + +import ( + "github.com/Sirupsen/logrus" + "github.com/getsentry/raven-go" +) + +var ( + severityMap = map[logrus.Level]raven.Severity{ + logrus.DebugLevel: raven.DEBUG, + logrus.InfoLevel: raven.INFO, + logrus.WarnLevel: raven.WARNING, + logrus.ErrorLevel: raven.ERROR, + logrus.FatalLevel: raven.FATAL, + logrus.PanicLevel: raven.FATAL, + } +) + +func getAndDel(d logrus.Fields, key string) (string, bool) { + var ( + ok bool + v interface{} + val string + ) + if v, ok = d[key]; !ok { + return "", false + } + + if val, ok = v.(string); !ok { + return "", false + } + delete(d, key) + return val, true +} + +// SentryHook delivers logs to a sentry server. +type SentryHook struct { + // DSN for this application + // Modifications to this field after the call to NewSentryHook have no effect + DSN string + + client *raven.Client + levels []logrus.Level +} + +// NewSentryHook creates a hook to be added to an instance of logger and initializes the raven client. +func NewSentryHook(DSN string, levels []logrus.Level) (*SentryHook, error) { + client, err := raven.NewClient(DSN, nil) + if err != nil { + return nil, err + } + return &SentryHook{DSN, client, levels}, nil +} + +// Called when an event should be sent to sentry +// Special fields that sentry uses to give more information to the server +// are extracted from entry.Data (if they are found) +// These fields are: logger and server_name +func (hook *SentryHook) Fire(entry *logrus.Entry) error { + packet := &raven.Packet{ + Message: entry.Message, + Timestamp: raven.Timestamp(entry.Time), + Level: severityMap[entry.Level], + Platform: "go", + } + + d := entry.Data + + if logger, ok := getAndDel(d, "logger"); ok { + packet.Logger = logger + } + if serverName, ok := getAndDel(d, "server_name"); ok { + packet.ServerName = serverName + } + packet.Extra = map[string]interface{}(d) + + _, errCh := hook.client.Capture(packet, nil) + return <-errCh +} + +// Levels returns the available logging levels. +func (hook *SentryHook) Levels() []logrus.Level { + return hook.levels +} From 4e014d4268d34b3c861a227e6ad966512ac47883 Mon Sep 17 00:00:00 2001 From: Roberto Bampi <bampi.roberto@gmail.com> Date: Sun, 2 Nov 2014 19:31:15 +0100 Subject: [PATCH 2/6] added tests --- hooks/sentry/sentry.go | 2 +- hooks/sentry/sentry_test.go | 93 +++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 hooks/sentry/sentry_test.go diff --git a/hooks/sentry/sentry.go b/hooks/sentry/sentry.go index 5c07eb2..74e8bbe 100644 --- a/hooks/sentry/sentry.go +++ b/hooks/sentry/sentry.go @@ -1,4 +1,4 @@ -package sentry_hook +package logrus_sentry import ( "github.com/Sirupsen/logrus" diff --git a/hooks/sentry/sentry_test.go b/hooks/sentry/sentry_test.go new file mode 100644 index 0000000..747d75f --- /dev/null +++ b/hooks/sentry/sentry_test.go @@ -0,0 +1,93 @@ +package logrus_sentry + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/Sirupsen/logrus" + "github.com/getsentry/raven-go" +) + +const ( + message = "error message" + server_name = "testserver.internal" + logger_name = "test.logger" +) + +func getTestLogger() *logrus.Logger { + l := logrus.New() + l.Out = ioutil.Discard + return l +} + +func getTestDSN(t *testing.T) (string, <-chan *raven.Packet, func()) { + pch := make(chan *raven.Packet, 1) + s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + defer req.Body.Close() + d := json.NewDecoder(req.Body) + p := &raven.Packet{} + err := d.Decode(p) + if err != nil { + t.Fatal(err.Error()) + } + + pch <- p + })) + + fragments := strings.SplitN(s.URL, "://", 2) + dsn := "%s://public:secret@%s/sentry/project-id" + + return fmt.Sprintf(dsn, fragments[0], fragments[1]), pch, s.Close +} + +func TestSpecialFields(t *testing.T) { + logger := getTestLogger() + dsn, pch, closeFn := getTestDSN(t) + defer closeFn() + + hook, err := NewSentryHook(dsn, []logrus.Level{ + logrus.ErrorLevel, + }) + + if err != nil { + t.Fatal(err.Error()) + } + logger.Hooks.Add(hook) + logger.WithFields(logrus.Fields{ + "server_name": server_name, + "logger": logger_name, + }).Error(message) + + packet := <-pch + if packet.Logger != logger_name { + t.Errorf("logger should have been %s, was %s", logger_name, packet.Logger) + } + + if packet.ServerName != server_name { + t.Errorf("server_name should have been %s, was %s", server_name, packet.ServerName) + } +} + +func TestSentryHandler(t *testing.T) { + logger := getTestLogger() + dsn, pch, closeFn := getTestDSN(t) + defer closeFn() + hook, err := NewSentryHook(dsn, []logrus.Level{ + logrus.ErrorLevel, + }) + if err != nil { + t.Fatal(err.Error()) + } + logger.Hooks.Add(hook) + + logger.Error(message) + packet := <-pch + if packet.Message != message { + t.Errorf("message should have been %s, was %s", message, packet.Message) + } +} From 0e6a27180fbe01789fd181452d949952ec327e86 Mon Sep 17 00:00:00 2001 From: Roberto Bampi <bampi.roberto@gmail.com> Date: Sun, 2 Nov 2014 19:39:51 +0100 Subject: [PATCH 3/6] added raven-go to .travis.yml --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index d5a559f..c3af3ce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,3 +7,4 @@ install: - go get github.com/stretchr/testify - go get github.com/stvp/go-udp-testing - go get github.com/tobi/airbrake-go + - go get github.com/getsentry/raven-go From 8bbc74e9fbb88cf146be80c65ad0c5bbc87507a7 Mon Sep 17 00:00:00 2001 From: Roberto Bampi <bampi.roberto@gmail.com> Date: Thu, 6 Nov 2014 10:07:41 +0100 Subject: [PATCH 4/6] various fixes --- hooks/sentry/README.md | 21 +++++++-- hooks/sentry/sentry.go | 13 +++--- hooks/sentry/sentry_test.go | 88 +++++++++++++++++++------------------ 3 files changed, 68 insertions(+), 54 deletions(-) diff --git a/hooks/sentry/README.md b/hooks/sentry/README.md index bf0e950..ba64ed4 100644 --- a/hooks/sentry/README.md +++ b/hooks/sentry/README.md @@ -1,12 +1,15 @@ # Sentry Hook for Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:" /> -[Sentry](https://getsentry.com) provides both self-hosted and hostes solutions for exception tracking. -Both client and server are [open source](https://github.com/getsentry/sentry). +[Sentry](https://getsentry.com) provides both self-hosted and hosted +solutions for exception tracking. +Both client and server are +[open source](https://github.com/getsentry/sentry). ## Usage -Every sentry application defined on the server gets a different [DNS](https://www.getsentry.com/docs/). In the example below replace `YOUR_DSN` with the one created for your application. - +Every sentry application defined on the server gets a different +[DSN](https://www.getsentry.com/docs/). In the example below replace +`YOUR_DSN` with the one created for your application. ```go import ( @@ -27,3 +30,13 @@ func main() { } } ``` + +## Special fields + +Some logrus fields have a special meaning in this hook, +these are server_name and logger. +When logs are sent to sentry these fields are treated differently. +- server_name (also known as hostname) is the name of the server which +is logging the event (hostname.example.com) +- logger is the part of the application which is logging the event. +In go this usually means setting it to the name of the package. diff --git a/hooks/sentry/sentry.go b/hooks/sentry/sentry.go index 74e8bbe..392601e 100644 --- a/hooks/sentry/sentry.go +++ b/hooks/sentry/sentry.go @@ -35,21 +35,18 @@ func getAndDel(d logrus.Fields, key string) (string, bool) { // SentryHook delivers logs to a sentry server. type SentryHook struct { - // DSN for this application - // Modifications to this field after the call to NewSentryHook have no effect - DSN string - client *raven.Client levels []logrus.Level } -// NewSentryHook creates a hook to be added to an instance of logger and initializes the raven client. +// NewSentryHook creates a hook to be added to an instance of logger +// and initializes the raven client. func NewSentryHook(DSN string, levels []logrus.Level) (*SentryHook, error) { client, err := raven.NewClient(DSN, nil) if err != nil { return nil, err } - return &SentryHook{DSN, client, levels}, nil + return &SentryHook{client, levels}, nil } // Called when an event should be sent to sentry @@ -74,8 +71,8 @@ func (hook *SentryHook) Fire(entry *logrus.Entry) error { } packet.Extra = map[string]interface{}(d) - _, errCh := hook.client.Capture(packet, nil) - return <-errCh + hook.client.Capture(packet, nil) + return nil } // Levels returns the available logging levels. diff --git a/hooks/sentry/sentry_test.go b/hooks/sentry/sentry_test.go index 747d75f..45f18d1 100644 --- a/hooks/sentry/sentry_test.go +++ b/hooks/sentry/sentry_test.go @@ -25,7 +25,7 @@ func getTestLogger() *logrus.Logger { return l } -func getTestDSN(t *testing.T) (string, <-chan *raven.Packet, func()) { +func WithTestDSN(t *testing.T, tf func(string, <-chan *raven.Packet)) { pch := make(chan *raven.Packet, 1) s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { defer req.Body.Close() @@ -38,56 +38,60 @@ func getTestDSN(t *testing.T) (string, <-chan *raven.Packet, func()) { pch <- p })) + defer s.Close() fragments := strings.SplitN(s.URL, "://", 2) - dsn := "%s://public:secret@%s/sentry/project-id" - - return fmt.Sprintf(dsn, fragments[0], fragments[1]), pch, s.Close + dsn := fmt.Sprintf( + "%s://public:secret@%s/sentry/project-id", + fragments[0], + fragments[1], + ) + tf(dsn, pch) } func TestSpecialFields(t *testing.T) { - logger := getTestLogger() - dsn, pch, closeFn := getTestDSN(t) - defer closeFn() + WithTestDSN(t, func(dsn string, pch <-chan *raven.Packet) { + logger := getTestLogger() - hook, err := NewSentryHook(dsn, []logrus.Level{ - logrus.ErrorLevel, + hook, err := NewSentryHook(dsn, []logrus.Level{ + logrus.ErrorLevel, + }) + + if err != nil { + t.Fatal(err.Error()) + } + logger.Hooks.Add(hook) + logger.WithFields(logrus.Fields{ + "server_name": server_name, + "logger": logger_name, + }).Error(message) + + packet := <-pch + if packet.Logger != logger_name { + t.Errorf("logger should have been %s, was %s", logger_name, packet.Logger) + } + + if packet.ServerName != server_name { + t.Errorf("server_name should have been %s, was %s", server_name, packet.ServerName) + } }) - - if err != nil { - t.Fatal(err.Error()) - } - logger.Hooks.Add(hook) - logger.WithFields(logrus.Fields{ - "server_name": server_name, - "logger": logger_name, - }).Error(message) - - packet := <-pch - if packet.Logger != logger_name { - t.Errorf("logger should have been %s, was %s", logger_name, packet.Logger) - } - - if packet.ServerName != server_name { - t.Errorf("server_name should have been %s, was %s", server_name, packet.ServerName) - } } func TestSentryHandler(t *testing.T) { - logger := getTestLogger() - dsn, pch, closeFn := getTestDSN(t) - defer closeFn() - hook, err := NewSentryHook(dsn, []logrus.Level{ - logrus.ErrorLevel, - }) - if err != nil { - t.Fatal(err.Error()) - } - logger.Hooks.Add(hook) + WithTestDSN(t, func(dsn string, pch <-chan *raven.Packet) { + logger := getTestLogger() + hook, err := NewSentryHook(dsn, []logrus.Level{ + logrus.ErrorLevel, + }) + if err != nil { + t.Fatal(err.Error()) + } + logger.Hooks.Add(hook) - logger.Error(message) - packet := <-pch - if packet.Message != message { - t.Errorf("message should have been %s, was %s", message, packet.Message) - } + logger.Error(message) + packet := <-pch + if packet.Message != message { + t.Errorf("message should have been %s, was %s", message, packet.Message) + } + }) } From 383ab1fb69bd03d3f0a3bfbe69714050e833e196 Mon Sep 17 00:00:00 2001 From: Roberto Bampi <bampi.roberto@gmail.com> Date: Thu, 6 Nov 2014 21:17:29 +0100 Subject: [PATCH 5/6] added delivery timeout --- hooks/sentry/sentry.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/hooks/sentry/sentry.go b/hooks/sentry/sentry.go index 392601e..c91a4b6 100644 --- a/hooks/sentry/sentry.go +++ b/hooks/sentry/sentry.go @@ -1,10 +1,17 @@ package logrus_sentry import ( + "fmt" + "time" + "github.com/Sirupsen/logrus" "github.com/getsentry/raven-go" ) +const ( + timeout = 100 * time.Millisecond +) + var ( severityMap = map[logrus.Level]raven.Severity{ logrus.DebugLevel: raven.DEBUG, @@ -71,7 +78,14 @@ func (hook *SentryHook) Fire(entry *logrus.Entry) error { } packet.Extra = map[string]interface{}(d) - hook.client.Capture(packet, nil) + _, errCh := hook.client.Capture(packet, nil) + timeoutCh := time.After(timeout) + select { + case err := <-errCh: + return err + case <-timeoutCh: + return fmt.Errorf("no response from sentry server in %s", timeout) + } return nil } From 5df4b882d06b8506a1e5b4f15ff2651b68423c26 Mon Sep 17 00:00:00 2001 From: Roberto Bampi <bampi.roberto@gmail.com> Date: Tue, 25 Nov 2014 13:36:08 +0100 Subject: [PATCH 6/6] timeout is now configurable and documented --- hooks/sentry/README.md | 19 +++++++++++++++++++ hooks/sentry/sentry.go | 27 ++++++++++++++++----------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/hooks/sentry/README.md b/hooks/sentry/README.md index ba64ed4..a409f3b 100644 --- a/hooks/sentry/README.md +++ b/hooks/sentry/README.md @@ -40,3 +40,22 @@ When logs are sent to sentry these fields are treated differently. is logging the event (hostname.example.com) - logger is the part of the application which is logging the event. In go this usually means setting it to the name of the package. + +## Timeout + +`Timeout` is the time the sentry hook will wait for a response +from the sentry server. + +If this time elapses with no response from +the server an error will be returned. + +If `Timeout` is set to 0 the SentryHook will not wait for a reply +and will assume a correct delivery. + +The SentryHook has a default timeout of `100 milliseconds` when created +with a call to `NewSentryHook`. This can be changed by assigning a value to the `Timeout` field: + +```go +hook, _ := logrus_sentry.NewSentryHook(...) +hook.Timeout = 20*time.Seconds +``` diff --git a/hooks/sentry/sentry.go b/hooks/sentry/sentry.go index c91a4b6..379f281 100644 --- a/hooks/sentry/sentry.go +++ b/hooks/sentry/sentry.go @@ -8,10 +8,6 @@ import ( "github.com/getsentry/raven-go" ) -const ( - timeout = 100 * time.Millisecond -) - var ( severityMap = map[logrus.Level]raven.Severity{ logrus.DebugLevel: raven.DEBUG, @@ -42,18 +38,24 @@ func getAndDel(d logrus.Fields, key string) (string, bool) { // SentryHook delivers logs to a sentry server. type SentryHook struct { + // Timeout sets the time to wait for a delivery error from the sentry server. + // If this is set to zero the server will not wait for any response and will + // consider the message correctly sent + Timeout time.Duration + client *raven.Client levels []logrus.Level } // NewSentryHook creates a hook to be added to an instance of logger // and initializes the raven client. +// This method sets the timeout to 100 milliseconds. func NewSentryHook(DSN string, levels []logrus.Level) (*SentryHook, error) { client, err := raven.NewClient(DSN, nil) if err != nil { return nil, err } - return &SentryHook{client, levels}, nil + return &SentryHook{100 * time.Millisecond, client, levels}, nil } // Called when an event should be sent to sentry @@ -79,12 +81,15 @@ func (hook *SentryHook) Fire(entry *logrus.Entry) error { packet.Extra = map[string]interface{}(d) _, errCh := hook.client.Capture(packet, nil) - timeoutCh := time.After(timeout) - select { - case err := <-errCh: - return err - case <-timeoutCh: - return fmt.Errorf("no response from sentry server in %s", timeout) + timeout := hook.Timeout + if timeout != 0 { + timeoutCh := time.After(timeout) + select { + case err := <-errCh: + return err + case <-timeoutCh: + return fmt.Errorf("no response from sentry server in %s", timeout) + } } return nil }