Merge pull request #298 from dolmen/refactor-prefixFieldClashes

formatter.go: simplify prefixFieldClashes(Fields)
diff --git a/.travis.yml b/.travis.yml
index ec64114..ff23150 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,6 +2,8 @@
 go:
   - 1.3
   - 1.4
+  - 1.5
   - tip
 install:
   - go get -t ./...
+script: GOMAXPROCS=4 GORACE="halt_on_error=1" go test -race -v ./...
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ecc8432..f2c2bc2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,21 @@
-# 0.9.0 (Unreleased)
+# 0.10.0
+
+* feature: Add a test hook (#180)
+* feature: `ParseLevel` is now case-insensitive (#326)
+* feature: `FieldLogger` interface that generalizes `Logger` and `Entry` (#308)
+* performance: avoid re-allocations on `WithFields` (#335)
+
+# 0.9.0
 
 * logrus/text_formatter: don't emit empty msg
 * logrus/hooks/airbrake: move out of main repository
 * logrus/hooks/sentry: move out of main repository
 * logrus/hooks/papertrail: move out of main repository
 * logrus/hooks/bugsnag: move out of main repository
+* logrus/core: run tests with `-race`
+* logrus/core: detect TTY based on `stderr`
+* logrus/core: support `WithError` on logger
+* logrus/core: Solaris support
 
 # 0.8.7
 
diff --git a/README.md b/README.md
index ddfe795..c43849e 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:"/>&nbsp;[![Build Status](https://travis-ci.org/Sirupsen/logrus.svg?branch=master)](https://travis-ci.org/Sirupsen/logrus)&nbsp;[![godoc reference](https://godoc.org/github.com/Sirupsen/logrus?status.png)][godoc]
+# Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:"/>&nbsp;[![Build Status](https://travis-ci.org/Sirupsen/logrus.svg?branch=master)](https://travis-ci.org/Sirupsen/logrus)&nbsp;[![GoDoc](https://godoc.org/github.com/Sirupsen/logrus?status.svg)](https://godoc.org/github.com/Sirupsen/logrus)
 
 Logrus is a structured logger for Go (golang), completely API compatible with
 the standard library logger. [Godoc][godoc]. **Please note the Logrus API is not
@@ -12,7 +12,7 @@
 
 ![Colored](http://i.imgur.com/PY7qMwd.png)
 
-With `log.Formatter = new(logrus.JSONFormatter)`, for easy parsing by logstash
+With `log.SetFormatter(&log.JSONFormatter{})`, for easy parsing by logstash
 or Splunk:
 
 ```json
@@ -32,7 +32,7 @@
 "time":"2014-03-10 19:57:38.562543128 -0400 EDT"}
 ```
 
-With the default `log.Formatter = new(&log.TextFormatter{})` when a TTY is not
+With the default `log.SetFormatter(&log.TextFormatter{})` when a TTY is not
 attached, the output is compatible with the
 [logfmt](http://godoc.org/github.com/kr/logfmt) format:
 
@@ -218,9 +218,17 @@
 | [Rollrus](https://github.com/heroku/rollrus) | Hook for sending errors to rollbar |
 | [Fluentd](https://github.com/evalphobia/logrus_fluent) | Hook for logging to fluentd |
 | [Mongodb](https://github.com/weekface/mgorus) | Hook for logging to mongodb |
+| [Influxus] (http://github.com/vlad-doru/influxus) | Hook for concurrently logging to [InfluxDB] (http://influxdata.com/) |
 | [InfluxDB](https://github.com/Abramovic/logrus_influxdb) | Hook for logging to influxdb |
 | [Octokit](https://github.com/dorajistyle/logrus-octokit-hook) | Hook for logging to github via octokit |
 | [DeferPanic](https://github.com/deferpanic/dp-logrus) | Hook for logging to DeferPanic |
+| [Redis-Hook](https://github.com/rogierlommers/logrus-redis-hook) | Hook for logging to a ELK stack (through Redis) |
+| [Amqp-Hook](https://github.com/vladoatanasov/logrus_amqp) | Hook for logging to Amqp broker (Like RabbitMQ) |
+| [KafkaLogrus](https://github.com/goibibo/KafkaLogrus) | Hook for logging to kafka |
+| [Typetalk](https://github.com/dragon3/logrus-typetalk-hook) | Hook for logging to [Typetalk](https://www.typetalk.in/) |
+| [ElasticSearch](https://github.com/sohlich/elogrus) | Hook for logging to ElasticSearch|
+| [Sumorus](https://github.com/doublefree/sumorus) | Hook for logging to [SumoLogic](https://www.sumologic.com/)|
+
 
 #### Level logging
 
@@ -301,7 +309,7 @@
 * `logrus/formatters/logstash.LogstashFormatter`. Logs fields as [Logstash](http://logstash.net) Events.
 
     ```go
-      logrus.SetFormatter(&logstash.LogstashFormatter{Type: “application_name"})
+      logrus.SetFormatter(&logstash.LogstashFormatter{Type: "application_name"})
     ```
 
 Third party logging formatters:
@@ -362,4 +370,21 @@
 | ---- | ----------- |
 |[Logrus Mate](https://github.com/gogap/logrus_mate)|Logrus mate is a tool for Logrus to manage loggers, you can initial logger's level, hook and formatter by config file, the logger will generated with different config at different environment.|
 
-[godoc]: https://godoc.org/github.com/Sirupsen/logrus
+#### Testing
+
+Logrus has a built in facility for asserting the presence of log messages. This is implemented through the `test` hook and provides:
+
+* decorators for existing logger (`test.NewLocal` and `test.NewGlobal`) which basically just add the `test` hook
+* a test logger (`test.NewNullLogger`) that just records log messages (and does not output any):
+
+```go
+logger, hook := NewNullLogger()
+logger.Error("Hello error")
+
+assert.Equal(1, len(hook.Entries))
+assert.Equal(logrus.ErrorLevel, hook.LastEntry().Level)
+assert.Equal("Hello error", hook.LastEntry().Message)
+
+hook.Reset()
+assert.Nil(hook.LastEntry())
+```
diff --git a/entry.go b/entry.go
index 9ae900b..89e966e 100644
--- a/entry.go
+++ b/entry.go
@@ -68,7 +68,7 @@
 
 // Add a map of fields to the Entry.
 func (entry *Entry) WithFields(fields Fields) *Entry {
-	data := Fields{}
+	data := make(Fields, len(entry.Data)+len(fields))
 	for k, v := range entry.Data {
 		data[k] = v
 	}
diff --git a/formatters/logstash/logstash.go b/formatters/logstash/logstash.go
index 8ea93dd..2793af8 100644
--- a/formatters/logstash/logstash.go
+++ b/formatters/logstash/logstash.go
@@ -17,38 +17,45 @@
 }
 
 func (f *LogstashFormatter) Format(entry *logrus.Entry) ([]byte, error) {
-	entry.Data["@version"] = 1
-
-	if f.TimestampFormat == "" {
-		f.TimestampFormat = logrus.DefaultTimestampFormat
+	fields := make(logrus.Fields)
+	for k, v := range entry.Data {
+		fields[k] = v
 	}
 
-	entry.Data["@timestamp"] = entry.Time.Format(f.TimestampFormat)
+	fields["@version"] = 1
+
+	timeStampFormat := f.TimestampFormat
+
+	if timeStampFormat == "" {
+		timeStampFormat = logrus.DefaultTimestampFormat
+	}
+
+	fields["@timestamp"] = entry.Time.Format(timeStampFormat)
 
 	// set message field
 	v, ok := entry.Data["message"]
 	if ok {
-		entry.Data["fields.message"] = v
+		fields["fields.message"] = v
 	}
-	entry.Data["message"] = entry.Message
+	fields["message"] = entry.Message
 
 	// set level field
 	v, ok = entry.Data["level"]
 	if ok {
-		entry.Data["fields.level"] = v
+		fields["fields.level"] = v
 	}
-	entry.Data["level"] = entry.Level.String()
+	fields["level"] = entry.Level.String()
 
 	// set type field
 	if f.Type != "" {
 		v, ok = entry.Data["type"]
 		if ok {
-			entry.Data["fields.type"] = v
+			fields["fields.type"] = v
 		}
-		entry.Data["type"] = f.Type
+		fields["type"] = f.Type
 	}
 
-	serialized, err := json.Marshal(entry.Data)
+	serialized, err := json.Marshal(fields)
 	if err != nil {
 		return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
 	}
diff --git a/hooks/syslog/syslog.go b/hooks/syslog/syslog.go
index c59f331..a36e200 100644
--- a/hooks/syslog/syslog.go
+++ b/hooks/syslog/syslog.go
@@ -50,12 +50,5 @@
 }
 
 func (hook *SyslogHook) Levels() []logrus.Level {
-	return []logrus.Level{
-		logrus.PanicLevel,
-		logrus.FatalLevel,
-		logrus.ErrorLevel,
-		logrus.WarnLevel,
-		logrus.InfoLevel,
-		logrus.DebugLevel,
-	}
+	return logrus.AllLevels
 }
diff --git a/hooks/test/test.go b/hooks/test/test.go
new file mode 100644
index 0000000..0688125
--- /dev/null
+++ b/hooks/test/test.go
@@ -0,0 +1,67 @@
+package test
+
+import (
+	"io/ioutil"
+
+	"github.com/Sirupsen/logrus"
+)
+
+// test.Hook is a hook designed for dealing with logs in test scenarios.
+type Hook struct {
+	Entries []*logrus.Entry
+}
+
+// Installs a test hook for the global logger.
+func NewGlobal() *Hook {
+
+	hook := new(Hook)
+	logrus.AddHook(hook)
+
+	return hook
+
+}
+
+// Installs a test hook for a given local logger.
+func NewLocal(logger *logrus.Logger) *Hook {
+
+	hook := new(Hook)
+	logger.Hooks.Add(hook)
+
+	return hook
+
+}
+
+// Creates a discarding logger and installs the test hook.
+func NewNullLogger() (*logrus.Logger, *Hook) {
+
+	logger := logrus.New()
+	logger.Out = ioutil.Discard
+
+	return logger, NewLocal(logger)
+
+}
+
+func (t *Hook) Fire(e *logrus.Entry) error {
+	t.Entries = append(t.Entries, e)
+	return nil
+}
+
+func (t *Hook) Levels() []logrus.Level {
+	return logrus.AllLevels
+}
+
+// LastEntry returns the last entry that was logged or nil.
+func (t *Hook) LastEntry() (l *logrus.Entry) {
+
+	if i := len(t.Entries) - 1; i < 0 {
+		return nil
+	} else {
+		return t.Entries[i]
+	}
+
+}
+
+// Reset removes all Entries from this test hook.
+func (t *Hook) Reset() {
+	t.Entries = make([]*logrus.Entry, 0)
+}
diff --git a/hooks/test/test_test.go b/hooks/test/test_test.go
new file mode 100644
index 0000000..d69455b
--- /dev/null
+++ b/hooks/test/test_test.go
@@ -0,0 +1,39 @@
+package test
+
+import (
+	"testing"
+
+	"github.com/Sirupsen/logrus"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAllHooks(t *testing.T) {
+
+	assert := assert.New(t)
+
+	logger, hook := NewNullLogger()
+	assert.Nil(hook.LastEntry())
+	assert.Equal(0, len(hook.Entries))
+
+	logger.Error("Hello error")
+	assert.Equal(logrus.ErrorLevel, hook.LastEntry().Level)
+	assert.Equal("Hello error", hook.LastEntry().Message)
+	assert.Equal(1, len(hook.Entries))
+
+	logger.Warn("Hello warning")
+	assert.Equal(logrus.WarnLevel, hook.LastEntry().Level)
+	assert.Equal("Hello warning", hook.LastEntry().Message)
+	assert.Equal(2, len(hook.Entries))
+
+	hook.Reset()
+	assert.Nil(hook.LastEntry())
+	assert.Equal(0, len(hook.Entries))
+
+	hook = NewGlobal()
+
+	logrus.Error("Hello error")
+	assert.Equal(logrus.ErrorLevel, hook.LastEntry().Level)
+	assert.Equal("Hello error", hook.LastEntry().Message)
+	assert.Equal(1, len(hook.Entries))
+
+}
diff --git a/logger.go b/logger.go
index fd9804c..2fdb231 100644
--- a/logger.go
+++ b/logger.go
@@ -64,6 +64,12 @@
 	return NewEntry(logger).WithFields(fields)
 }
 
+// Add an error as single field to the log entry.  All it does is call
+// `WithError` for the given `error`.
+func (logger *Logger) WithError(err error) *Entry {
+	return NewEntry(logger).WithError(err)
+}
+
 func (logger *Logger) Debugf(format string, args ...interface{}) {
 	if logger.Level >= DebugLevel {
 		NewEntry(logger).Debugf(format, args...)
diff --git a/logrus.go b/logrus.go
index 0c09fbc..e596691 100644
--- a/logrus.go
+++ b/logrus.go
@@ -3,6 +3,7 @@
 import (
 	"fmt"
 	"log"
+	"strings"
 )
 
 // Fields type, used to pass to `WithFields`.
@@ -33,7 +34,7 @@
 
 // ParseLevel takes a string level and returns the Logrus log level constant.
 func ParseLevel(lvl string) (Level, error) {
-	switch lvl {
+	switch strings.ToLower(lvl) {
 	case "panic":
 		return PanicLevel, nil
 	case "fatal":
@@ -52,6 +53,16 @@
 	return l, fmt.Errorf("not a valid logrus Level: %q", lvl)
 }
 
+// A constant exposing all logging levels
+var AllLevels = []Level{
+	PanicLevel,
+	FatalLevel,
+	ErrorLevel,
+	WarnLevel,
+	InfoLevel,
+	DebugLevel,
+}
+
 // These are the different logging levels. You can set the logging level to log
 // on your instance of logger, obtained with `logrus.New()`.
 const (
@@ -96,3 +107,37 @@
 	Panicf(string, ...interface{})
 	Panicln(...interface{})
 }
+
+// The FieldLogger interface generalizes the Entry and Logger types
+type FieldLogger interface {
+	WithField(key string, value interface{}) *Entry
+	WithFields(fields Fields) *Entry
+	WithError(err error) *Entry
+
+	Debugf(format string, args ...interface{})
+	Infof(format string, args ...interface{})
+	Printf(format string, args ...interface{})
+	Warnf(format string, args ...interface{})
+	Warningf(format string, args ...interface{})
+	Errorf(format string, args ...interface{})
+	Fatalf(format string, args ...interface{})
+	Panicf(format string, args ...interface{})
+
+	Debug(args ...interface{})
+	Info(args ...interface{})
+	Print(args ...interface{})
+	Warn(args ...interface{})
+	Warning(args ...interface{})
+	Error(args ...interface{})
+	Fatal(args ...interface{})
+	Panic(args ...interface{})
+
+	Debugln(args ...interface{})
+	Infoln(args ...interface{})
+	Println(args ...interface{})
+	Warnln(args ...interface{})
+	Warningln(args ...interface{})
+	Errorln(args ...interface{})
+	Fatalln(args ...interface{})
+	Panicln(args ...interface{})
+}
diff --git a/logrus_test.go b/logrus_test.go
index efaacea..bfc4780 100644
--- a/logrus_test.go
+++ b/logrus_test.go
@@ -255,30 +255,58 @@
 	assert.Nil(t, err)
 	assert.Equal(t, PanicLevel, l)
 
+	l, err = ParseLevel("PANIC")
+	assert.Nil(t, err)
+	assert.Equal(t, PanicLevel, l)
+
 	l, err = ParseLevel("fatal")
 	assert.Nil(t, err)
 	assert.Equal(t, FatalLevel, l)
 
+	l, err = ParseLevel("FATAL")
+	assert.Nil(t, err)
+	assert.Equal(t, FatalLevel, l)
+
 	l, err = ParseLevel("error")
 	assert.Nil(t, err)
 	assert.Equal(t, ErrorLevel, l)
 
+	l, err = ParseLevel("ERROR")
+	assert.Nil(t, err)
+	assert.Equal(t, ErrorLevel, l)
+
 	l, err = ParseLevel("warn")
 	assert.Nil(t, err)
 	assert.Equal(t, WarnLevel, l)
 
+	l, err = ParseLevel("WARN")
+	assert.Nil(t, err)
+	assert.Equal(t, WarnLevel, l)
+
 	l, err = ParseLevel("warning")
 	assert.Nil(t, err)
 	assert.Equal(t, WarnLevel, l)
 
+	l, err = ParseLevel("WARNING")
+	assert.Nil(t, err)
+	assert.Equal(t, WarnLevel, l)
+
 	l, err = ParseLevel("info")
 	assert.Nil(t, err)
 	assert.Equal(t, InfoLevel, l)
 
+	l, err = ParseLevel("INFO")
+	assert.Nil(t, err)
+	assert.Equal(t, InfoLevel, l)
+
 	l, err = ParseLevel("debug")
 	assert.Nil(t, err)
 	assert.Equal(t, DebugLevel, l)
 
+	l, err = ParseLevel("DEBUG")
+	assert.Nil(t, err)
+	assert.Equal(t, DebugLevel, l)
+
 	l, err = ParseLevel("invalid")
 	assert.Equal(t, "not a valid logrus Level: \"invalid\"", err.Error())
 }
@@ -299,3 +327,35 @@
 	}
 	wg.Wait()
 }
+
+func TestLoggingRace(t *testing.T) {
+	logger := New()
+
+	var wg sync.WaitGroup
+	wg.Add(100)
+
+	for i := 0; i < 100; i++ {
+		go func() {
+			logger.Info("info")
+			wg.Done()
+		}()
+	}
+	wg.Wait()
+}
+
+// Compile test
+func TestLogrusInterface(t *testing.T) {
+	var buffer bytes.Buffer
+	fn := func(l FieldLogger) {
+		b := l.WithField("key", "value")
+		b.Debug("Test")
+	}
+	// test logger
+	logger := New()
+	logger.Out = &buffer
+	fn(logger)
+
+	// test Entry
+	e := logger.WithField("another", "value")
+	fn(e)
+}
diff --git a/terminal_notwindows.go b/terminal_notwindows.go
index 4bb5376..b343b3a 100644
--- a/terminal_notwindows.go
+++ b/terminal_notwindows.go
@@ -12,9 +12,9 @@
 	"unsafe"
 )
 
-// IsTerminal returns true if the given file descriptor is a terminal.
+// IsTerminal returns true if stderr's file descriptor is a terminal.
 func IsTerminal() bool {
-	fd := syscall.Stdout
+	fd := syscall.Stderr
 	var termios Termios
 	_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
 	return err == 0
diff --git a/terminal_windows.go b/terminal_windows.go
index 2e09f6f..0146845 100644
--- a/terminal_windows.go
+++ b/terminal_windows.go
@@ -18,9 +18,9 @@
 	procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
 )
 
-// IsTerminal returns true if the given file descriptor is a terminal.
+// IsTerminal returns true if stderr's file descriptor is a terminal.
 func IsTerminal() bool {
-	fd := syscall.Stdout
+	fd := syscall.Stderr
 	var st uint32
 	r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
 	return r != 0 && e == 0