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.