From e15d51fef3f38c49e6b9f573419c62cdc3ac267b Mon Sep 17 00:00:00 2001 From: Alexander Demidov Date: Fri, 20 Feb 2015 21:52:53 +0600 Subject: [PATCH 01/20] formatter for logstash (http://logstash.net) --- README.md | 1 + logstash_formatter.go | 47 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 logstash_formatter.go diff --git a/README.md b/README.md index e755e7c..4b1117a 100644 --- a/README.md +++ b/README.md @@ -321,6 +321,7 @@ The built-in logging formatters are: 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.LogstashFormatter`. Logs fields as Logstash Events (http://logstash.net). Third party logging formatters: diff --git a/logstash_formatter.go b/logstash_formatter.go new file mode 100644 index 0000000..99a3f7f --- /dev/null +++ b/logstash_formatter.go @@ -0,0 +1,47 @@ +package logrus + +import ( + "encoding/json" + "fmt" + "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 *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 +} From 75cc3dd51a36addf41a68ff6dba8089e1eae293b Mon Sep 17 00:00:00 2001 From: Alex Demidov Date: Fri, 20 Feb 2015 21:02:11 +0500 Subject: [PATCH 02/20] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 4b1117a..0e39601 100644 --- a/README.md +++ b/README.md @@ -323,6 +323,10 @@ The built-in logging formatters are: * `logrus.JSONFormatter`. Logs fields as JSON. * `logrus.LogstashFormatter`. Logs fields as Logstash Events (http://logstash.net). + ```go + logrus.SetFormatter(&logrus.LogstashFormatter{Type: “application_name"}) + ``` + Third party logging formatters: * [`zalgo`](https://github.com/aybabtme/logzalgo): invoking the P͉̫o̳̼̊w̖͈̰͎e̬͔̭͂r͚̼̹̲ ̫͓͉̳͈ō̠͕͖̚f̝͍̠ ͕̲̞͖͑Z̖̫̤̫ͪa͉̬͈̗l͖͎g̳̥o̰̥̅!̣͔̲̻͊̄ ̙̘̦̹̦. From 3cc6fcc52194ca551e2a153f07eceb98c1d1ea66 Mon Sep 17 00:00:00 2001 From: Alexander Demidov Date: Thu, 5 Mar 2015 23:31:39 +0600 Subject: [PATCH 03/20] use formatters directory --- README.md | 4 ++-- .../logstash/logstash_formatter.go | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) rename logstash_formatter.go => formatters/logstash/logstash_formatter.go (87%) diff --git a/README.md b/README.md index 0e39601..f457193 100644 --- a/README.md +++ b/README.md @@ -321,10 +321,10 @@ The built-in logging formatters are: 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.LogstashFormatter`. Logs fields as Logstash Events (http://logstash.net). +* `logrus_logstash.LogstashFormatter`. Logs fields as Logstash Events (http://logstash.net). ```go - logrus.SetFormatter(&logrus.LogstashFormatter{Type: “application_name"}) + logrus.SetFormatter(&logrus_logstash.LogstashFormatter{Type: “application_name"}) ``` Third party logging formatters: diff --git a/logstash_formatter.go b/formatters/logstash/logstash_formatter.go similarity index 87% rename from logstash_formatter.go rename to formatters/logstash/logstash_formatter.go index 99a3f7f..63ac133 100644 --- a/logstash_formatter.go +++ b/formatters/logstash/logstash_formatter.go @@ -1,8 +1,9 @@ -package logrus +package logrus_logstash import ( "encoding/json" "fmt" + "github.com/Sirupsen/logrus" "time" ) @@ -12,7 +13,7 @@ type LogstashFormatter struct { Type string // if not empty use for logstash type field. } -func (f *LogstashFormatter) Format(entry *Entry) ([]byte, error) { +func (f *LogstashFormatter) Format(entry *logrus.Entry) ([]byte, error) { entry.Data["@version"] = 1 entry.Data["@timestamp"] = entry.Time.Format(time.RFC3339) From 2ec723cd5bb75924cb73668fe38b7a5e67fca768 Mon Sep 17 00:00:00 2001 From: Alexander Demidov Date: Sun, 15 Mar 2015 23:34:19 +0600 Subject: [PATCH 04/20] add logstash formatter test --- .../logstash/logstash_formatter_test.go | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 formatters/logstash/logstash_formatter_test.go diff --git a/formatters/logstash/logstash_formatter_test.go b/formatters/logstash/logstash_formatter_test.go new file mode 100644 index 0000000..7e48201 --- /dev/null +++ b/formatters/logstash/logstash_formatter_test.go @@ -0,0 +1,52 @@ +package logrus_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"]) +} From 9c9013ac4fe5695b432ec3220e7671fdff038158 Mon Sep 17 00:00:00 2001 From: Lorenzo Villani Date: Fri, 20 Feb 2015 16:32:47 +0100 Subject: [PATCH 05/20] Change DebugLevel color to gray --- text_formatter.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/text_formatter.go b/text_formatter.go index 4e51734..4f50a60 100644 --- a/text_formatter.go +++ b/text_formatter.go @@ -15,6 +15,7 @@ const ( green = 32 yellow = 33 blue = 34 + gray = 37 ) var ( @@ -75,6 +76,8 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { func printColored(b *bytes.Buffer, entry *Entry, keys []string) { var levelColor int switch entry.Level { + case DebugLevel: + levelColor = gray case WarnLevel: levelColor = yellow case ErrorLevel, FatalLevel, PanicLevel: From 115ae7564e53601c28f4e53dace98bfe515486b5 Mon Sep 17 00:00:00 2001 From: Steeve Lennmark Date: Fri, 20 Feb 2015 18:43:24 +0200 Subject: [PATCH 06/20] Add option to show full timestamp in TextFormatter Sometimes elapsed seconds just aren't enough. --- text_formatter.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/text_formatter.go b/text_formatter.go index 4f50a60..6d2740f 100644 --- a/text_formatter.go +++ b/text_formatter.go @@ -40,6 +40,7 @@ type TextFormatter struct { // Set to true to disable timestamp logging (useful when the output // is redirected to a logging system already adding a timestamp) DisableTimestamp bool + FullTimestamp bool } func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { @@ -57,7 +58,7 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { isColored := (f.ForceColors || isTerminal) && !f.DisableColors if isColored { - printColored(b, entry, keys) + f.printColored(b, entry, keys) } else { if !f.DisableTimestamp { f.appendKeyValue(b, "time", entry.Time.Format(time.RFC3339)) @@ -73,7 +74,7 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { return b.Bytes(), nil } -func printColored(b *bytes.Buffer, entry *Entry, keys []string) { +func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string) { var levelColor int switch entry.Level { case DebugLevel: @@ -88,7 +89,11 @@ func printColored(b *bytes.Buffer, entry *Entry, keys []string) { levelText := strings.ToUpper(entry.Level.String())[0:4] - fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, miniTS(), entry.Message) + if !f.FullTimestamp { + fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, miniTS(), entry.Message) + } else { + fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %-44s ", levelColor, levelText, entry.Time.Format(time.RFC3339), entry.Message) + } for _, k := range keys { v := entry.Data[k] fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=%v", levelColor, k, v) From 5be851d70636beb2697398b221774c6f23ecdaa5 Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Wed, 25 Feb 2015 19:01:02 +0000 Subject: [PATCH 07/20] text_formatter: improve comments --- text_formatter.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/text_formatter.go b/text_formatter.go index 6d2740f..def60e9 100644 --- a/text_formatter.go +++ b/text_formatter.go @@ -35,12 +35,18 @@ func miniTS() int { type TextFormatter struct { // Set to true to bypass checking for a TTY before outputting colors. - ForceColors bool + ForceColors bool + + // Force disabling colors. DisableColors bool - // Set to true to disable timestamp logging (useful when the output - // is redirected to a logging system already adding a timestamp) + + // Disable timestamp logging. useful when output is redirected to logging + // system that already adds timestamps. DisableTimestamp bool - FullTimestamp bool + + // Enable logging the full timestamp when a TTY is attached instead of just + // the time passed since beginning of execution. + FullTimestamp bool } func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { From 9aea8212000fa144e46da726521716ee35614ea6 Mon Sep 17 00:00:00 2001 From: Nikolay Kirsh Date: Mon, 2 Mar 2015 16:25:19 +0500 Subject: [PATCH 08/20] fix Second const --- hooks/sentry/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/sentry/README.md b/hooks/sentry/README.md index a409f3b..19e58bb 100644 --- a/hooks/sentry/README.md +++ b/hooks/sentry/README.md @@ -57,5 +57,5 @@ with a call to `NewSentryHook`. This can be changed by assigning a value to the ```go hook, _ := logrus_sentry.NewSentryHook(...) -hook.Timeout = 20*time.Seconds +hook.Timeout = 20*time.Second ``` From ff5ba169e84f2830c307e797974d4c8f7ad92caa Mon Sep 17 00:00:00 2001 From: Henrik Hodne Date: Wed, 4 Mar 2015 14:04:50 +0000 Subject: [PATCH 09/20] text-formatter: do not quote 9 --- text_formatter.go | 2 +- text_formatter_test.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/text_formatter.go b/text_formatter.go index def60e9..2d884c0 100644 --- a/text_formatter.go +++ b/text_formatter.go @@ -110,7 +110,7 @@ func needsQuoting(text string) bool { for _, ch := range text { if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || - (ch >= '0' && ch < '9') || + (ch >= '0' && ch <= '9') || ch == '-' || ch == '.') { return false } diff --git a/text_formatter_test.go b/text_formatter_test.go index f604f1b..396bc5f 100644 --- a/text_formatter_test.go +++ b/text_formatter_test.go @@ -25,6 +25,7 @@ func TestQuoting(t *testing.T) { checkQuoting(false, "abcd") checkQuoting(false, "v1.0") + checkQuoting(false, "1234567890") checkQuoting(true, "/foobar") checkQuoting(true, "x y") checkQuoting(true, "x,y") From e803eeed62f80d2e5128155a510e4dc955a376e4 Mon Sep 17 00:00:00 2001 From: Matt Bostock Date: Fri, 6 Mar 2015 16:15:30 +0000 Subject: [PATCH 10/20] Add integration test to Airbrake hook Add a test for the Airbrake hook to: a) document how the hook is intended to work b) test that an XML payload is received with the expected message --- hooks/airbrake/airbrake_test.go | 57 +++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 hooks/airbrake/airbrake_test.go diff --git a/hooks/airbrake/airbrake_test.go b/hooks/airbrake/airbrake_test.go new file mode 100644 index 0000000..d2fd61d --- /dev/null +++ b/hooks/airbrake/airbrake_test.go @@ -0,0 +1,57 @@ +package logrus_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"` +} + +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 + if err := xml.NewDecoder(r.Body).Decode(¬ice); err != nil { + t.Error(err) + } + r.Body.Close() + + msg <- notice.Error.Message + })) + 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") + } +} From 31897e2db52d7e9f61764ba41535f7f92cd8fd2f Mon Sep 17 00:00:00 2001 From: Matt Bostock Date: Fri, 6 Mar 2015 16:32:18 +0000 Subject: [PATCH 11/20] Remove misleading comment in Airbrake hook As far as I can tell, exceptions are always sent regardless of what `airbrake.Environment` is set to. --- hooks/airbrake/airbrake.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/airbrake/airbrake.go b/hooks/airbrake/airbrake.go index 880d21e..75f4db1 100644 --- a/hooks/airbrake/airbrake.go +++ b/hooks/airbrake/airbrake.go @@ -9,7 +9,7 @@ import ( // with the Airbrake API. You must set: // * airbrake.Endpoint // * airbrake.ApiKey -// * airbrake.Environment (only sends exceptions when set to "production") +// * 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. From 0dd045932f30d8291d8434a36702a7c2084821a4 Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Mon, 9 Mar 2015 15:15:08 +0000 Subject: [PATCH 12/20] json_formatter: always cast errors to strings Fixes #137 --- json_formatter.go | 8 +++++++- json_formatter_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 json_formatter_test.go diff --git a/json_formatter.go b/json_formatter.go index b09227c..0e38a61 100644 --- a/json_formatter.go +++ b/json_formatter.go @@ -11,7 +11,13 @@ type JSONFormatter struct{} func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { data := make(Fields, len(entry.Data)+3) for k, v := range entry.Data { - data[k] = v + // 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 { + data[k] = v + } } prefixFieldClashes(data) data["time"] = entry.Time.Format(time.RFC3339) diff --git a/json_formatter_test.go b/json_formatter_test.go new file mode 100644 index 0000000..de19bc1 --- /dev/null +++ b/json_formatter_test.go @@ -0,0 +1,46 @@ +package logrus + +import ( + "encoding/json" + "errors" + + "testing" +) + +func TestErrorNotLost(t *testing.T) { + formatter := &JSONFormatter{} + + b, err := formatter.Format(WithField("error", errors.New("wild walrus"))) + if err != nil { + t.Fatal("Unable to format entry: ", err) + } + + entry := make(map[string]interface{}) + err = json.Unmarshal(b, &entry) + if err != nil { + t.Fatal("Unable to unmarshal formatted entry: ", err) + } + + if entry["error"] != "wild walrus" { + t.Fatal("Error field not set") + } +} + +func TestErrorNotLostOnFieldNotNamedError(t *testing.T) { + formatter := &JSONFormatter{} + + b, err := formatter.Format(WithField("omg", errors.New("wild walrus"))) + if err != nil { + t.Fatal("Unable to format entry: ", err) + } + + entry := make(map[string]interface{}) + err = json.Unmarshal(b, &entry) + if err != nil { + t.Fatal("Unable to unmarshal formatted entry: ", err) + } + + if entry["omg"] != "wild walrus" { + t.Fatal("Error field not set") + } +} From 0fa54be10fd40e11f85bf916583f7e816b32932a Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Mon, 9 Mar 2015 15:19:51 +0000 Subject: [PATCH 13/20] text_formatter: add field to disable sorting --- text_formatter.go | 11 +++++++++-- text_formatter_test.go | 3 +++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/text_formatter.go b/text_formatter.go index 2d884c0..71dcb66 100644 --- a/text_formatter.go +++ b/text_formatter.go @@ -47,15 +47,22 @@ type TextFormatter struct { // Enable logging the full timestamp when a TTY is attached instead of just // the time passed since beginning of execution. FullTimestamp bool + + // The fields are sorted by default for a consistent output. For applications + // that log extremely frequently and don't use the JSON formatter this may not + // be desired. + DisableSorting bool } func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { - var keys []string = make([]string, 0, len(entry.Data)) for k := range entry.Data { keys = append(keys, k) } - sort.Strings(keys) + + if !f.DisableSorting { + sort.Strings(keys) + } b := &bytes.Buffer{} diff --git a/text_formatter_test.go b/text_formatter_test.go index 396bc5f..28a9499 100644 --- a/text_formatter_test.go +++ b/text_formatter_test.go @@ -32,3 +32,6 @@ func TestQuoting(t *testing.T) { checkQuoting(false, errors.New("invalid")) checkQuoting(true, errors.New("invalid argument")) } + +// TODO add tests for sorting etc., this requires a parser for the text +// formatter output. From 566a97d868bc50ee03e541669892bb703ebd098b Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Mon, 9 Mar 2015 15:30:43 +0000 Subject: [PATCH 14/20] json_formatter: add tests for field clashes and newline --- json_formatter_test.go | 74 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/json_formatter_test.go b/json_formatter_test.go index de19bc1..1d70873 100644 --- a/json_formatter_test.go +++ b/json_formatter_test.go @@ -44,3 +44,77 @@ func TestErrorNotLostOnFieldNotNamedError(t *testing.T) { t.Fatal("Error field not set") } } + +func TestFieldClashWithTime(t *testing.T) { + formatter := &JSONFormatter{} + + b, err := formatter.Format(WithField("time", "right now!")) + if err != nil { + t.Fatal("Unable to format entry: ", err) + } + + entry := make(map[string]interface{}) + err = json.Unmarshal(b, &entry) + if err != nil { + t.Fatal("Unable to unmarshal formatted entry: ", err) + } + + if entry["fields.time"] != "right now!" { + t.Fatal("fields.time not set to original time field") + } + + if entry["time"] != "0001-01-01T00:00:00Z" { + t.Fatal("time field not set to current time, was: ", entry["time"]) + } +} + +func TestFieldClashWithMsg(t *testing.T) { + formatter := &JSONFormatter{} + + b, err := formatter.Format(WithField("msg", "something")) + if err != nil { + t.Fatal("Unable to format entry: ", err) + } + + entry := make(map[string]interface{}) + err = json.Unmarshal(b, &entry) + if err != nil { + t.Fatal("Unable to unmarshal formatted entry: ", err) + } + + if entry["fields.msg"] != "something" { + t.Fatal("fields.msg not set to original msg field") + } +} + +func TestFieldClashWithLevel(t *testing.T) { + formatter := &JSONFormatter{} + + b, err := formatter.Format(WithField("level", "something")) + if err != nil { + t.Fatal("Unable to format entry: ", err) + } + + entry := make(map[string]interface{}) + err = json.Unmarshal(b, &entry) + if err != nil { + t.Fatal("Unable to unmarshal formatted entry: ", err) + } + + if entry["fields.level"] != "something" { + t.Fatal("fields.level not set to original level field") + } +} + +func TestJSONEntryEndsWithNewline(t *testing.T) { + formatter := &JSONFormatter{} + + b, err := formatter.Format(WithField("level", "something")) + if err != nil { + t.Fatal("Unable to format entry: ", err) + } + + if b[len(b)-1] != '\n' { + t.Fatal("Expected JSON log entry to end with a newline") + } +} From 9cc13fab16bb0966fbfe6be657ec4f68158988b3 Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Mon, 9 Mar 2015 15:40:44 +0000 Subject: [PATCH 15/20] examples/basic: add debug level --- examples/basic/basic.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/examples/basic/basic.go b/examples/basic/basic.go index a62ba45..a1623ec 100644 --- a/examples/basic/basic.go +++ b/examples/basic/basic.go @@ -9,6 +9,7 @@ var log = logrus.New() func init() { log.Formatter = new(logrus.JSONFormatter) log.Formatter = new(logrus.TextFormatter) // default + log.Level = logrus.DebugLevel } func main() { @@ -23,6 +24,11 @@ func main() { } }() + log.WithFields(logrus.Fields{ + "animal": "walrus", + "number": 8, + }).Debug("Started observing beach") + log.WithFields(logrus.Fields{ "animal": "walrus", "size": 10, @@ -33,6 +39,10 @@ func main() { "number": 122, }).Warn("The group's number increased tremendously!") + log.WithFields(logrus.Fields{ + "temperature": -4, + }).Debug("Temperature changes") + log.WithFields(logrus.Fields{ "animal": "orca", "size": 9009, From 4fcb55c7348338bda64180a0dcf7210693362404 Mon Sep 17 00:00:00 2001 From: Matt Bostock Date: Tue, 10 Mar 2015 17:45:12 +0000 Subject: [PATCH 16/20] Rename package from logrus_airbrake to airbrake Using underscores in package names in discouraged: https://golang.org/doc/effective_go.html#package-names Given that this package is in a subdirectory of the logrus package, the name `airbrake` should be sufficiently descriptive. --- hooks/airbrake/airbrake.go | 2 +- hooks/airbrake/airbrake_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hooks/airbrake/airbrake.go b/hooks/airbrake/airbrake.go index 75f4db1..9fa108a 100644 --- a/hooks/airbrake/airbrake.go +++ b/hooks/airbrake/airbrake.go @@ -1,4 +1,4 @@ -package logrus_airbrake +package airbrake import ( "github.com/Sirupsen/logrus" diff --git a/hooks/airbrake/airbrake_test.go b/hooks/airbrake/airbrake_test.go index d2fd61d..55df200 100644 --- a/hooks/airbrake/airbrake_test.go +++ b/hooks/airbrake/airbrake_test.go @@ -1,4 +1,4 @@ -package logrus_airbrake +package airbrake import ( "encoding/xml" From 7ba71bd3573102b76ae7dcf53da889eec986e5cb Mon Sep 17 00:00:00 2001 From: Matt Bostock Date: Tue, 10 Mar 2015 17:49:55 +0000 Subject: [PATCH 17/20] 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. --- README.md | 4 +- examples/hook/hook.go | 7 +- hooks/airbrake/airbrake.go | 56 ++++++------- hooks/airbrake/airbrake_test.go | 138 +++++++++++++++++++++++++------- 4 files changed, 138 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index f457193..607eab1 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ func init() { // 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) @@ -211,7 +211,7 @@ import ( ) 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 { 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 @@ package main 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/hooks/airbrake/airbrake.go b/hooks/airbrake/airbrake.go index 9fa108a..b0502c3 100644 --- a/hooks/airbrake/airbrake.go +++ b/hooks/airbrake/airbrake.go @@ -1,51 +1,51 @@ 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 55df200..058a91e 100644 --- a/hooks/airbrake/airbrake_test.go +++ b/hooks/airbrake/airbrake_test.go @@ -2,26 +2,123 @@ 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 @@ func TestNoticeReceived(t *testing.T) { } 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 } From bc1129f48e76ebe6db479f0f00807cd417c121c2 Mon Sep 17 00:00:00 2001 From: Matt Bostock Date: Tue, 10 Mar 2015 18:01:14 +0000 Subject: [PATCH 18/20] Remove outdated version of Airbrake hook It seems unnecessary to duplicate the code (which is now outdated) in the README. Instead, link to the built-in hooks where a user can see the code. --- README.md | 39 ++------------------------------------- 1 file changed, 2 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 607eab1..178123d 100644 --- a/README.md +++ b/README.md @@ -164,43 +164,8 @@ You can add hooks for logging levels. For example to send errors to an exception 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 ( From 2d359740a46f30c73eda240b6e4f4ba1c7f7c3cd Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Fri, 13 Mar 2015 11:10:11 -0700 Subject: [PATCH 19/20] text_formatter: remove unneeded regexp --- text_formatter.go | 2 -- 1 file changed, 2 deletions(-) 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 @@ package logrus import ( "bytes" "fmt" - "regexp" "sort" "strings" "time" @@ -21,7 +20,6 @@ const ( var ( baseTimestamp time.Time isTerminal bool - noQuoteNeeded *regexp.Regexp ) func init() { From e178ef4efd314fc87c4b60a2e1d374bf93c067d9 Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Thu, 19 Mar 2015 10:04:53 -0400 Subject: [PATCH 20/20] formatter/logstash: style --- formatters/logstash/{logstash_formatter.go => logstash.go} | 2 +- .../logstash/{logstash_formatter_test.go => logstash_test.go} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename formatters/logstash/{logstash_formatter.go => logstash.go} (97%) rename formatters/logstash/{logstash_formatter_test.go => logstash_test.go} (97%) diff --git a/formatters/logstash/logstash_formatter.go b/formatters/logstash/logstash.go similarity index 97% rename from formatters/logstash/logstash_formatter.go rename to formatters/logstash/logstash.go index 63ac133..34b1ccb 100644 --- a/formatters/logstash/logstash_formatter.go +++ b/formatters/logstash/logstash.go @@ -1,4 +1,4 @@ -package logrus_logstash +package logstash import ( "encoding/json" diff --git a/formatters/logstash/logstash_formatter_test.go b/formatters/logstash/logstash_test.go similarity index 97% rename from formatters/logstash/logstash_formatter_test.go rename to formatters/logstash/logstash_test.go index 7e48201..d8814a0 100644 --- a/formatters/logstash/logstash_formatter_test.go +++ b/formatters/logstash/logstash_test.go @@ -1,4 +1,4 @@ -package logrus_logstash +package logstash import ( "bytes"