Merge pull request #154 from imkira/master
improved: move level check from entry to logger and bail out faster
diff --git a/README.md b/README.md
index e755e7c..512f26e 100644
--- a/README.md
+++ b/README.md
@@ -82,7 +82,7 @@
// 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.
- 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.
log.SetOutput(os.Stderr)
@@ -164,43 +164,8 @@
tracking service on `Error`, `Fatal` and `Panic`, info to StatsD or log to
multiple places simultaneously, e.g. syslog.
-```go
-// Not the real implementation of the Airbrake hook. Just a simple sample.
-import (
- log "github.com/Sirupsen/logrus"
-)
-
-func init() {
- log.AddHook(new(AirbrakeHook))
-}
-
-type AirbrakeHook struct{}
-
-// `Fire()` takes the entry that the hook is fired for. `entry.Data[]` contains
-// the fields for the entry. See the Fields section of the README.
-func (hook *AirbrakeHook) Fire(entry *logrus.Entry) error {
- err := airbrake.Notify(entry.Data["error"].(error))
- if err != nil {
- log.WithFields(log.Fields{
- "source": "airbrake",
- "endpoint": airbrake.Endpoint,
- }).Info("Failed to send error to Airbrake")
- }
-
- return nil
-}
-
-// `Levels()` returns a slice of `Levels` the hook is fired for.
-func (hook *AirbrakeHook) Levels() []log.Level {
- return []log.Level{
- log.ErrorLevel,
- log.FatalLevel,
- log.PanicLevel,
- }
-}
-```
-
-Logrus comes with built-in hooks. Add those, or your custom hook, in `init`:
+Logrus comes with [built-in hooks](hooks/). Add those, or your custom hook, in
+`init`:
```go
import (
@@ -211,7 +176,7 @@
)
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, "")
if err != nil {
@@ -233,6 +198,9 @@
Send errors to remote syslog server.
Uses standard library `log/syslog` behind the scenes.
+* [`github.com/Sirupsen/logrus/hooks/bugsnag`](https://github.com/Sirupsen/logrus/blob/master/hooks/bugsnag/bugsnag.go)
+ Send errors to the Bugsnag exception tracking service.
+
* [`github.com/nubo/hiprus`](https://github.com/nubo/hiprus)
Send errors to a channel in hipchat.
@@ -321,6 +289,11 @@
field to `true`. To force no colored output even if there is a TTY set the
`DisableColors` field to `true`
* `logrus.JSONFormatter`. Logs fields as JSON.
+* `logrus_logstash.LogstashFormatter`. Logs fields as Logstash Events (http://logstash.net).
+
+ ```go
+ logrus.SetFormatter(&logrus_logstash.LogstashFormatter{Type: “application_name"})
+ ```
Third party logging formatters:
diff --git a/examples/hook/hook.go b/examples/hook/hook.go
index 42e7a4c..cb5759a 100644
--- a/examples/hook/hook.go
+++ b/examples/hook/hook.go
@@ -3,21 +3,16 @@
import (
"github.com/Sirupsen/logrus"
"github.com/Sirupsen/logrus/hooks/airbrake"
- "github.com/tobi/airbrake-go"
)
var log = logrus.New()
func init() {
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() {
- airbrake.Endpoint = "https://exceptions.whatever.com/notifier_api/v2/notices.xml"
- airbrake.ApiKey = "whatever"
- airbrake.Environment = "production"
-
log.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
diff --git a/formatters/logstash/logstash.go b/formatters/logstash/logstash.go
new file mode 100644
index 0000000..34b1ccb
--- /dev/null
+++ b/formatters/logstash/logstash.go
@@ -0,0 +1,48 @@
+package logstash
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/Sirupsen/logrus"
+ "time"
+)
+
+// Formatter generates json in logstash format.
+// Logstash site: http://logstash.net/
+type LogstashFormatter struct {
+ Type string // if not empty use for logstash type field.
+}
+
+func (f *LogstashFormatter) Format(entry *logrus.Entry) ([]byte, error) {
+ entry.Data["@version"] = 1
+ entry.Data["@timestamp"] = entry.Time.Format(time.RFC3339)
+
+ // set message field
+ v, ok := entry.Data["message"]
+ if ok {
+ entry.Data["fields.message"] = v
+ }
+ entry.Data["message"] = entry.Message
+
+ // set level field
+ v, ok = entry.Data["level"]
+ if ok {
+ entry.Data["fields.level"] = v
+ }
+ entry.Data["level"] = entry.Level.String()
+
+ // set type field
+ if f.Type != "" {
+ v, ok = entry.Data["type"]
+ if ok {
+ entry.Data["fields.type"] = v
+ }
+ entry.Data["type"] = f.Type
+ }
+
+ serialized, err := json.Marshal(entry.Data)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
+ }
+ return append(serialized, '\n'), nil
+}
diff --git a/formatters/logstash/logstash_test.go b/formatters/logstash/logstash_test.go
new file mode 100644
index 0000000..d8814a0
--- /dev/null
+++ b/formatters/logstash/logstash_test.go
@@ -0,0 +1,52 @@
+package logstash
+
+import (
+ "bytes"
+ "encoding/json"
+ "github.com/Sirupsen/logrus"
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func TestLogstashFormatter(t *testing.T) {
+ assert := assert.New(t)
+
+ lf := LogstashFormatter{Type: "abc"}
+
+ fields := logrus.Fields{
+ "message": "def",
+ "level": "ijk",
+ "type": "lmn",
+ "one": 1,
+ "pi": 3.14,
+ "bool": true,
+ }
+
+ entry := logrus.WithFields(fields)
+ entry.Message = "msg"
+ entry.Level = logrus.InfoLevel
+
+ b, _ := lf.Format(entry)
+
+ var data map[string]interface{}
+ dec := json.NewDecoder(bytes.NewReader(b))
+ dec.UseNumber()
+ dec.Decode(&data)
+
+ // base fields
+ assert.Equal(json.Number("1"), data["@version"])
+ assert.NotEmpty(data["@timestamp"])
+ assert.Equal("abc", data["type"])
+ assert.Equal("msg", data["message"])
+ assert.Equal("info", data["level"])
+
+ // substituted fields
+ assert.Equal("def", data["fields.message"])
+ assert.Equal("ijk", data["fields.level"])
+ assert.Equal("lmn", data["fields.type"])
+
+ // formats
+ assert.Equal(json.Number("1"), data["one"])
+ assert.Equal(json.Number("3.14"), data["pi"])
+ assert.Equal(true, data["bool"])
+}
diff --git a/hooks/airbrake/airbrake.go b/hooks/airbrake/airbrake.go
index 75f4db1..b0502c3 100644
--- a/hooks/airbrake/airbrake.go
+++ b/hooks/airbrake/airbrake.go
@@ -1,51 +1,51 @@
-package logrus_airbrake
+package airbrake
import (
+ "errors"
+ "fmt"
+
"github.com/Sirupsen/logrus"
"github.com/tobi/airbrake-go"
)
// AirbrakeHook to send exceptions to an exception-tracking service compatible
-// with the Airbrake API. You must set:
-// * airbrake.Endpoint
-// * airbrake.ApiKey
-// * airbrake.Environment
-//
-// 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{}
+// with the Airbrake API.
+type airbrakeHook struct {
+ APIKey string
+ Endpoint string
+ Environment string
+}
-func (hook *AirbrakeHook) Fire(entry *logrus.Entry) error {
- if entry.Data["error"] == nil {
- entry.Logger.WithFields(logrus.Fields{
- "source": "airbrake",
- "endpoint": airbrake.Endpoint,
- }).Warn("Exceptions sent to Airbrake must have an 'error' key with the error")
- return nil
+func NewHook(endpoint, apiKey, env string) *airbrakeHook {
+ return &airbrakeHook{
+ APIKey: apiKey,
+ Endpoint: endpoint,
+ Environment: env,
}
+}
+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)
- if !ok {
- entry.Logger.WithFields(logrus.Fields{
- "source": "airbrake",
- "endpoint": airbrake.Endpoint,
- }).Warn("Exceptions sent to Airbrake must have an `error` key of type `error`")
- return nil
+ if ok {
+ notifyErr = err
+ } else {
+ notifyErr = errors.New(entry.Message)
}
- airErr := airbrake.Notify(err)
+ airErr := airbrake.Notify(notifyErr)
if airErr != nil {
- entry.Logger.WithFields(logrus.Fields{
- "source": "airbrake",
- "endpoint": airbrake.Endpoint,
- "error": airErr,
- }).Warn("Failed to send error to Airbrake")
+ return fmt.Errorf("Failed to send error to Airbrake: %s", airErr)
}
return nil
}
-func (hook *AirbrakeHook) Levels() []logrus.Level {
+func (hook *airbrakeHook) Levels() []logrus.Level {
return []logrus.Level{
logrus.ErrorLevel,
logrus.FatalLevel,
diff --git a/hooks/airbrake/airbrake_test.go b/hooks/airbrake/airbrake_test.go
index d2fd61d..058a91e 100644
--- a/hooks/airbrake/airbrake_test.go
+++ b/hooks/airbrake/airbrake_test.go
@@ -1,27 +1,124 @@
-package logrus_airbrake
+package airbrake
import (
"encoding/xml"
- "errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/Sirupsen/logrus"
- "github.com/tobi/airbrake-go"
)
type notice struct {
- Error struct {
- Message string `xml:"message"`
- } `xml:"error"`
+ Error NoticeError `xml:"error"`
+}
+type NoticeError struct {
+ Class string `xml:"class"`
+ Message string `xml:"message"`
}
-func TestNoticeReceived(t *testing.T) {
- msg := make(chan string, 1)
- expectedMsg := "foo"
+type customErr struct {
+ msg string
+}
+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) {
var notice notice
if err := xml.NewDecoder(r.Body).Decode(¬ice); err != nil {
@@ -29,29 +126,8 @@
}
r.Body.Close()
- msg <- notice.Error.Message
+ noticeError <- notice.Error
}))
- defer ts.Close()
- hook := &AirbrakeHook{}
-
- 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")
- }
+ return ts
}
diff --git a/hooks/bugsnag/bugsnag.go b/hooks/bugsnag/bugsnag.go
new file mode 100644
index 0000000..d20a0f5
--- /dev/null
+++ b/hooks/bugsnag/bugsnag.go
@@ -0,0 +1,68 @@
+package logrus_bugsnag
+
+import (
+ "errors"
+
+ "github.com/Sirupsen/logrus"
+ "github.com/bugsnag/bugsnag-go"
+)
+
+type bugsnagHook struct{}
+
+// ErrBugsnagUnconfigured is returned if NewBugsnagHook is called before
+// bugsnag.Configure. Bugsnag must be configured before the hook.
+var ErrBugsnagUnconfigured = errors.New("bugsnag must be configured before installing this logrus hook")
+
+// ErrBugsnagSendFailed indicates that the hook failed to submit an error to
+// bugsnag. The error was successfully generated, but `bugsnag.Notify()`
+// failed.
+type ErrBugsnagSendFailed struct {
+ err error
+}
+
+func (e ErrBugsnagSendFailed) Error() string {
+ return "failed to send error to Bugsnag: " + e.err.Error()
+}
+
+// NewBugsnagHook initializes a logrus hook which sends exceptions to an
+// exception-tracking service compatible with the Bugsnag API. Before using
+// this hook, you must call bugsnag.Configure(). The returned object should be
+// registered with a log via `AddHook()`
+//
+// Entries that trigger an Error, Fatal or Panic should now include an "error"
+// field to send to Bugsnag.
+func NewBugsnagHook() (*bugsnagHook, error) {
+ if bugsnag.Config.APIKey == "" {
+ return nil, ErrBugsnagUnconfigured
+ }
+ return &bugsnagHook{}, nil
+}
+
+// Fire forwards an error to Bugsnag. Given a logrus.Entry, it extracts the
+// "error" field (or the Message if the error isn't present) and sends it off.
+func (hook *bugsnagHook) Fire(entry *logrus.Entry) error {
+ var notifyErr error
+ err, ok := entry.Data["error"].(error)
+ if ok {
+ notifyErr = err
+ } else {
+ notifyErr = errors.New(entry.Message)
+ }
+
+ bugsnagErr := bugsnag.Notify(notifyErr)
+ if bugsnagErr != nil {
+ return ErrBugsnagSendFailed{bugsnagErr}
+ }
+
+ 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..e9ea298
--- /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")
+ }
+}
diff --git a/json_formatter.go b/json_formatter.go
index 0e38a61..5c4c44b 100644
--- a/json_formatter.go
+++ b/json_formatter.go
@@ -11,11 +11,12 @@
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
data := make(Fields, len(entry.Data)+3)
for k, v := range entry.Data {
- // Otherwise errors are ignored by `encoding/json`
- // https://github.com/Sirupsen/logrus/issues/137
- if err, ok := v.(error); ok {
- data[k] = err.Error()
- } else {
+ switch v := v.(type) {
+ case error:
+ // Otherwise errors are ignored by `encoding/json`
+ // https://github.com/Sirupsen/logrus/issues/137
+ data[k] = v.Error()
+ default:
data[k] = v
}
}
diff --git a/terminal_openbsd.go b/terminal_openbsd.go
index d238bfa..af609a5 100644
--- a/terminal_openbsd.go
+++ b/terminal_openbsd.go
@@ -1,4 +1,3 @@
-
package logrus
import "syscall"
diff --git a/text_formatter.go b/text_formatter.go
index 71dcb66..0a06a11 100644
--- a/text_formatter.go
+++ b/text_formatter.go
@@ -3,7 +3,6 @@
import (
"bytes"
"fmt"
- "regexp"
"sort"
"strings"
"time"
@@ -21,7 +20,6 @@
var (
baseTimestamp time.Time
isTerminal bool
- noQuoteNeeded *regexp.Regexp
)
func init() {
diff --git a/writer.go b/writer.go
index 90d3e01..1e30b1c 100644
--- a/writer.go
+++ b/writer.go
@@ -6,7 +6,7 @@
"runtime"
)
-func (logger *Logger) Writer() (*io.PipeWriter) {
+func (logger *Logger) Writer() *io.PipeWriter {
reader, writer := io.Pipe()
go logger.writerScanner(reader)