Merge pull request #92 from rasky/no-extra-quoting

Avoid extra quotes where not strictly necessary.
diff --git a/.travis.yml b/.travis.yml
index d5a559f..c3af3ce 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,3 +7,4 @@
   - go get github.com/stretchr/testify
   - go get github.com/stvp/go-udp-testing
   - go get github.com/tobi/airbrake-go
+  - go get github.com/getsentry/raven-go
diff --git a/README.md b/README.md
index cabd027..b6aa84c 100644
--- a/README.md
+++ b/README.md
@@ -235,6 +235,9 @@
 * [`github.com/nubo/hiprus`](https://github.com/nubo/hiprus)
   Send errors to a channel in hipchat.
 
+* [`github.com/sebest/logrusly`](https://github.com/sebest/logrusly)
+  Send logs to Loggly (https://www.loggly.com/)
+
 #### Level logging
 
 Logrus has six logging levels: Debug, Info, Warning, Error, Fatal and Panic.
diff --git a/formatter.go b/formatter.go
index 74c49a0..038ce9f 100644
--- a/formatter.go
+++ b/formatter.go
@@ -26,19 +26,19 @@
 //
 // It's not exported because it's still using Data in an opinionated way. It's to
 // avoid code duplication between the two default formatters.
-func prefixFieldClashes(entry *Entry) {
-	_, ok := entry.Data["time"]
+func prefixFieldClashes(data Fields) {
+	_, ok := data["time"]
 	if ok {
-		entry.Data["fields.time"] = entry.Data["time"]
+		data["fields.time"] = data["time"]
 	}
 
-	_, ok = entry.Data["msg"]
+	_, ok = data["msg"]
 	if ok {
-		entry.Data["fields.msg"] = entry.Data["msg"]
+		data["fields.msg"] = data["msg"]
 	}
 
-	_, ok = entry.Data["level"]
+	_, ok = data["level"]
 	if ok {
-		entry.Data["fields.level"] = entry.Data["level"]
+		data["fields.level"] = data["level"]
 	}
 }
diff --git a/hooks/papertrail/papertrail.go b/hooks/papertrail/papertrail.go
index 48e2fea..12c56f2 100644
--- a/hooks/papertrail/papertrail.go
+++ b/hooks/papertrail/papertrail.go
@@ -30,7 +30,7 @@
 // Fire is called when a log event is fired.
 func (hook *PapertrailHook) Fire(entry *logrus.Entry) error {
 	date := time.Now().Format(format)
-	payload := fmt.Sprintf("<22> %s %s: [%s] %s", date, hook.AppName, entry.Data["level"], entry.Message)
+	payload := fmt.Sprintf("<22> %s %s: [%s] %s", date, hook.AppName, entry.Level, entry.Message)
 
 	bytesWritten, err := hook.UDPConn.Write([]byte(payload))
 	if err != nil {
diff --git a/hooks/sentry/README.md b/hooks/sentry/README.md
new file mode 100644
index 0000000..a409f3b
--- /dev/null
+++ b/hooks/sentry/README.md
@@ -0,0 +1,61 @@
+# Sentry Hook for Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:" />
+
+[Sentry](https://getsentry.com) provides both self-hosted and hosted
+solutions for exception tracking.
+Both client and server are
+[open source](https://github.com/getsentry/sentry).
+
+## Usage
+
+Every sentry application defined on the server gets a different
+[DSN](https://www.getsentry.com/docs/). In the example below replace
+`YOUR_DSN` with the one created for your application.
+
+```go
+import (
+  "github.com/Sirupsen/logrus"
+  "github.com/Sirupsen/logrus/hooks/sentry"
+)
+
+func main() {
+  log       := logrus.New()
+  hook, err := logrus_sentry.NewSentryHook(YOUR_DSN, []logrus.Level{
+    logrus.PanicLevel,
+    logrus.FatalLevel,
+    logrus.ErrorLevel,
+  })
+
+  if err == nil {
+    log.Hooks.Add(hook)
+  }
+}
+```
+
+## Special fields
+
+Some logrus fields have a special meaning in this hook,
+these are server_name and logger.
+When logs are sent to sentry these fields are treated differently.
+- server_name (also known as hostname) is the name of the server which
+is logging the event (hostname.example.com)
+- logger is the part of the application which is logging the event.
+In go this usually means setting it to the name of the package.
+
+## Timeout
+
+`Timeout` is the time the sentry hook will wait for a response
+from the sentry server.
+
+If this time elapses with no response from
+the server an error will be returned.
+
+If `Timeout` is set to 0 the SentryHook will not wait for a reply
+and will assume a correct delivery.
+
+The SentryHook has a default timeout of `100 milliseconds` when created
+with a call to `NewSentryHook`. This can be changed by assigning a value to the `Timeout` field:
+
+```go
+hook, _ := logrus_sentry.NewSentryHook(...)
+hook.Timeout = 20*time.Seconds
+```
diff --git a/hooks/sentry/sentry.go b/hooks/sentry/sentry.go
new file mode 100644
index 0000000..379f281
--- /dev/null
+++ b/hooks/sentry/sentry.go
@@ -0,0 +1,100 @@
+package logrus_sentry
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/Sirupsen/logrus"
+	"github.com/getsentry/raven-go"
+)
+
+var (
+	severityMap = map[logrus.Level]raven.Severity{
+		logrus.DebugLevel: raven.DEBUG,
+		logrus.InfoLevel:  raven.INFO,
+		logrus.WarnLevel:  raven.WARNING,
+		logrus.ErrorLevel: raven.ERROR,
+		logrus.FatalLevel: raven.FATAL,
+		logrus.PanicLevel: raven.FATAL,
+	}
+)
+
+func getAndDel(d logrus.Fields, key string) (string, bool) {
+	var (
+		ok  bool
+		v   interface{}
+		val string
+	)
+	if v, ok = d[key]; !ok {
+		return "", false
+	}
+
+	if val, ok = v.(string); !ok {
+		return "", false
+	}
+	delete(d, key)
+	return val, true
+}
+
+// SentryHook delivers logs to a sentry server.
+type SentryHook struct {
+	// Timeout sets the time to wait for a delivery error from the sentry server.
+	// If this is set to zero the server will not wait for any response and will
+	// consider the message correctly sent
+	Timeout time.Duration
+
+	client *raven.Client
+	levels []logrus.Level
+}
+
+// NewSentryHook creates a hook to be added to an instance of logger
+// and initializes the raven client.
+// This method sets the timeout to 100 milliseconds.
+func NewSentryHook(DSN string, levels []logrus.Level) (*SentryHook, error) {
+	client, err := raven.NewClient(DSN, nil)
+	if err != nil {
+		return nil, err
+	}
+	return &SentryHook{100 * time.Millisecond, client, levels}, nil
+}
+
+// Called when an event should be sent to sentry
+// Special fields that sentry uses to give more information to the server
+// are extracted from entry.Data (if they are found)
+// These fields are: logger and server_name
+func (hook *SentryHook) Fire(entry *logrus.Entry) error {
+	packet := &raven.Packet{
+		Message:   entry.Message,
+		Timestamp: raven.Timestamp(entry.Time),
+		Level:     severityMap[entry.Level],
+		Platform:  "go",
+	}
+
+	d := entry.Data
+
+	if logger, ok := getAndDel(d, "logger"); ok {
+		packet.Logger = logger
+	}
+	if serverName, ok := getAndDel(d, "server_name"); ok {
+		packet.ServerName = serverName
+	}
+	packet.Extra = map[string]interface{}(d)
+
+	_, errCh := hook.client.Capture(packet, nil)
+	timeout := hook.Timeout
+	if timeout != 0 {
+		timeoutCh := time.After(timeout)
+		select {
+		case err := <-errCh:
+			return err
+		case <-timeoutCh:
+			return fmt.Errorf("no response from sentry server in %s", timeout)
+		}
+	}
+	return nil
+}
+
+// Levels returns the available logging levels.
+func (hook *SentryHook) Levels() []logrus.Level {
+	return hook.levels
+}
diff --git a/hooks/sentry/sentry_test.go b/hooks/sentry/sentry_test.go
new file mode 100644
index 0000000..45f18d1
--- /dev/null
+++ b/hooks/sentry/sentry_test.go
@@ -0,0 +1,97 @@
+package logrus_sentry
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"github.com/Sirupsen/logrus"
+	"github.com/getsentry/raven-go"
+)
+
+const (
+	message     = "error message"
+	server_name = "testserver.internal"
+	logger_name = "test.logger"
+)
+
+func getTestLogger() *logrus.Logger {
+	l := logrus.New()
+	l.Out = ioutil.Discard
+	return l
+}
+
+func WithTestDSN(t *testing.T, tf func(string, <-chan *raven.Packet)) {
+	pch := make(chan *raven.Packet, 1)
+	s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		defer req.Body.Close()
+		d := json.NewDecoder(req.Body)
+		p := &raven.Packet{}
+		err := d.Decode(p)
+		if err != nil {
+			t.Fatal(err.Error())
+		}
+
+		pch <- p
+	}))
+	defer s.Close()
+
+	fragments := strings.SplitN(s.URL, "://", 2)
+	dsn := fmt.Sprintf(
+		"%s://public:secret@%s/sentry/project-id",
+		fragments[0],
+		fragments[1],
+	)
+	tf(dsn, pch)
+}
+
+func TestSpecialFields(t *testing.T) {
+	WithTestDSN(t, func(dsn string, pch <-chan *raven.Packet) {
+		logger := getTestLogger()
+
+		hook, err := NewSentryHook(dsn, []logrus.Level{
+			logrus.ErrorLevel,
+		})
+
+		if err != nil {
+			t.Fatal(err.Error())
+		}
+		logger.Hooks.Add(hook)
+		logger.WithFields(logrus.Fields{
+			"server_name": server_name,
+			"logger":      logger_name,
+		}).Error(message)
+
+		packet := <-pch
+		if packet.Logger != logger_name {
+			t.Errorf("logger should have been %s, was %s", logger_name, packet.Logger)
+		}
+
+		if packet.ServerName != server_name {
+			t.Errorf("server_name should have been %s, was %s", server_name, packet.ServerName)
+		}
+	})
+}
+
+func TestSentryHandler(t *testing.T) {
+	WithTestDSN(t, func(dsn string, pch <-chan *raven.Packet) {
+		logger := getTestLogger()
+		hook, err := NewSentryHook(dsn, []logrus.Level{
+			logrus.ErrorLevel,
+		})
+		if err != nil {
+			t.Fatal(err.Error())
+		}
+		logger.Hooks.Add(hook)
+
+		logger.Error(message)
+		packet := <-pch
+		if packet.Message != message {
+			t.Errorf("message should have been %s, was %s", message, packet.Message)
+		}
+	})
+}
diff --git a/hooks/syslog/README.md b/hooks/syslog/README.md
index cd706bc..4dbb8e7 100644
--- a/hooks/syslog/README.md
+++ b/hooks/syslog/README.md
@@ -6,7 +6,7 @@
 import (
   "log/syslog"
   "github.com/Sirupsen/logrus"
-  "github.com/Sirupsen/logrus/hooks/syslog"
+  logrus_syslog "github.com/Sirupsen/logrus/hooks/syslog"
 )
 
 func main() {
@@ -17,4 +17,4 @@
     log.Hooks.Add(hook)
   }
 }
-```
\ No newline at end of file
+```
diff --git a/json_formatter.go b/json_formatter.go
index 9d11b64..b09227c 100644
--- a/json_formatter.go
+++ b/json_formatter.go
@@ -9,12 +9,16 @@
 type JSONFormatter struct{}
 
 func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
-	prefixFieldClashes(entry)
-	entry.Data["time"] = entry.Time.Format(time.RFC3339)
-	entry.Data["msg"] = entry.Message
-	entry.Data["level"] = entry.Level.String()
+	data := make(Fields, len(entry.Data)+3)
+	for k, v := range entry.Data {
+		data[k] = v
+	}
+	prefixFieldClashes(data)
+	data["time"] = entry.Time.Format(time.RFC3339)
+	data["msg"] = entry.Message
+	data["level"] = entry.Level.String()
 
-	serialized, err := json.Marshal(entry.Data)
+	serialized, err := json.Marshal(data)
 	if err != nil {
 		return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
 	}
diff --git a/logger.go b/logger.go
index 7374fe3..b392e54 100644
--- a/logger.go
+++ b/logger.go
@@ -38,7 +38,7 @@
 //      Out: os.Stderr,
 //      Formatter: new(JSONFormatter),
 //      Hooks: make(levelHooks),
-//      Level: logrus.Debug,
+//      Level: logrus.DebugLevel,
 //    }
 //
 // It's recommended to make this a global instance called `log`.
diff --git a/logrus_test.go b/logrus_test.go
index 0682281..5302542 100644
--- a/logrus_test.go
+++ b/logrus_test.go
@@ -208,6 +208,38 @@
 	})
 }
 
+func TestDoubleLoggingDoesntPrefixPreviousFields(t *testing.T) {
+
+	var buffer bytes.Buffer
+	var fields Fields
+
+	logger := New()
+	logger.Out = &buffer
+	logger.Formatter = new(JSONFormatter)
+
+	llog := logger.WithField("context", "eating raw fish")
+
+	llog.Info("looks delicious")
+
+	err := json.Unmarshal(buffer.Bytes(), &fields)
+	assert.NoError(t, err, "should have decoded first message")
+	assert.Len(t, fields, 4, "should only have msg/time/level/context fields")
+	assert.Equal(t, fields["msg"], "looks delicious")
+	assert.Equal(t, fields["context"], "eating raw fish")
+
+	buffer.Reset()
+
+	llog.Warn("omg it is!")
+
+	err = json.Unmarshal(buffer.Bytes(), &fields)
+	assert.NoError(t, err, "should have decoded second message")
+	assert.Len(t, fields, 4, "should only have msg/time/level/context fields")
+	assert.Equal(t, fields["msg"], "omg it is!")
+	assert.Equal(t, fields["context"], "eating raw fish")
+	assert.Nil(t, fields["fields.msg"], "should not have prefixed previous `msg` entry")
+
+}
+
 func TestConvertLevelToString(t *testing.T) {
 	assert.Equal(t, "debug", DebugLevel.String())
 	assert.Equal(t, "info", InfoLevel.String())
diff --git a/text_formatter.go b/text_formatter.go
index 658df9b..78e7889 100644
--- a/text_formatter.go
+++ b/text_formatter.go
@@ -36,6 +36,9 @@
 	// Set to true to bypass checking for a TTY before outputting colors.
 	ForceColors   bool
 	DisableColors bool
+	// Set to true to disable timestamp logging (useful when the output
+	// is redirected to a logging system already adding a timestamp)
+	DisableTimestamp bool
 }
 
 func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
@@ -48,14 +51,16 @@
 
 	b := &bytes.Buffer{}
 
-	prefixFieldClashes(entry)
+	prefixFieldClashes(entry.Data)
 
 	isColored := (f.ForceColors || isTerminal) && !f.DisableColors
 
 	if isColored {
 		printColored(b, entry, keys)
 	} else {
-		f.appendKeyValue(b, "time", entry.Time.Format(time.RFC3339))
+		if !f.DisableTimestamp {
+			f.appendKeyValue(b, "time", entry.Time.Format(time.RFC3339))
+		}
 		f.appendKeyValue(b, "level", entry.Level.String())
 		f.appendKeyValue(b, "msg", entry.Message)
 		for _, key := range keys {