Merge pull request #145 from alphagov/add_tests_to_airbrake_hook

Add integration test to Airbrake hook
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 @@
 func init() {
 	log.Formatter = new(logrus.JSONFormatter)
 	log.Formatter = new(logrus.TextFormatter) // default
+	log.Level = logrus.DebugLevel
 }
 
 func main() {
@@ -25,6 +26,11 @@
 
 	log.WithFields(logrus.Fields{
 		"animal": "walrus",
+		"number": 8,
+	}).Debug("Started observing beach")
+
+	log.WithFields(logrus.Fields{
+		"animal": "walrus",
 		"size":   10,
 	}).Info("A group of walrus emerges from the ocean")
 
@@ -34,6 +40,10 @@
 	}).Warn("The group's number increased tremendously!")
 
 	log.WithFields(logrus.Fields{
+		"temperature": -4,
+	}).Debug("Temperature changes")
+
+	log.WithFields(logrus.Fields{
 		"animal": "orca",
 		"size":   9009,
 	}).Panic("It's over 9000!")
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/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 e44f1e1..71dcb66 100644
--- a/text_formatter.go
+++ b/text_formatter.go
@@ -15,6 +15,7 @@
 	green   = 32
 	yellow  = 33
 	blue    = 34
+	gray    = 37
 )
 
 var (
@@ -46,15 +47,22 @@
 	// 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{}
 
@@ -82,6 +90,8 @@
 func (f *TextFormatter) 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:
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 @@
 	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.