Merge pull request #131 from lvillani/debuglevel-gray
Change DebugLevel color to gray
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 @@
// 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.
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 @@
```go
hook, _ := logrus_sentry.NewSentryHook(...)
-hook.Timeout = 20*time.Seconds
+hook.Timeout = 20*time.Second
```
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 @@
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..1d70873
--- /dev/null
+++ b/json_formatter_test.go
@@ -0,0 +1,120 @@
+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")
+ }
+}
+
+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")
+ }
+}
diff --git a/text_formatter.go b/text_formatter.go
index 4f50a60..71dcb66 100644
--- a/text_formatter.go
+++ b/text_formatter.go
@@ -35,20 +35,34 @@
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
+
+ // 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{}
@@ -57,7 +71,7 @@
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 +87,7 @@
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 +102,11 @@
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)
@@ -99,7 +117,7 @@
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..28a9499 100644
--- a/text_formatter_test.go
+++ b/text_formatter_test.go
@@ -25,9 +25,13 @@
checkQuoting(false, "abcd")
checkQuoting(false, "v1.0")
+ checkQuoting(false, "1234567890")
checkQuoting(true, "/foobar")
checkQuoting(true, "x y")
checkQuoting(true, "x,y")
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.