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
 }