Browse code

Merge pull request #11331 from jfrazelle/update-logrus

Update logrus to 0.6.6

Michael Crosby authored on 2015/03/12 08:18:22
Showing 26 changed files
... ...
@@ -5042,8 +5042,8 @@ func TestBuildSpaces(t *testing.T) {
5042 5042
 	}
5043 5043
 
5044 5044
 	// Skip over the times
5045
-	e1 := err1.Error()[strings.Index(err1.Error(), `level="`):]
5046
-	e2 := err2.Error()[strings.Index(err1.Error(), `level="`):]
5045
+	e1 := err1.Error()[strings.Index(err1.Error(), `level=`):]
5046
+	e2 := err2.Error()[strings.Index(err1.Error(), `level=`):]
5047 5047
 
5048 5048
 	// Ignore whitespace since that's what were verifying doesn't change stuff
5049 5049
 	if strings.Replace(e1, " ", "", -1) != strings.Replace(e2, " ", "", -1) {
... ...
@@ -5056,8 +5056,8 @@ func TestBuildSpaces(t *testing.T) {
5056 5056
 	}
5057 5057
 
5058 5058
 	// Skip over the times
5059
-	e1 = err1.Error()[strings.Index(err1.Error(), `level="`):]
5060
-	e2 = err2.Error()[strings.Index(err1.Error(), `level="`):]
5059
+	e1 = err1.Error()[strings.Index(err1.Error(), `level=`):]
5060
+	e2 = err2.Error()[strings.Index(err1.Error(), `level=`):]
5061 5061
 
5062 5062
 	// Ignore whitespace since that's what were verifying doesn't change stuff
5063 5063
 	if strings.Replace(e1, " ", "", -1) != strings.Replace(e2, " ", "", -1) {
... ...
@@ -5070,8 +5070,8 @@ func TestBuildSpaces(t *testing.T) {
5070 5070
 	}
5071 5071
 
5072 5072
 	// Skip over the times
5073
-	e1 = err1.Error()[strings.Index(err1.Error(), `level="`):]
5074
-	e2 = err2.Error()[strings.Index(err1.Error(), `level="`):]
5073
+	e1 = err1.Error()[strings.Index(err1.Error(), `level=`):]
5074
+	e2 = err2.Error()[strings.Index(err1.Error(), `level=`):]
5075 5075
 
5076 5076
 	// Ignore whitespace since that's what were verifying doesn't change stuff
5077 5077
 	if strings.Replace(e1, " ", "", -1) != strings.Replace(e2, " ", "", -1) {
... ...
@@ -244,7 +244,7 @@ func TestDaemonLoggingLevel(t *testing.T) {
244 244
 	}
245 245
 	d.Stop()
246 246
 	content, _ := ioutil.ReadFile(d.logFile.Name())
247
-	if !strings.Contains(string(content), `level="debug"`) {
247
+	if !strings.Contains(string(content), `level=debug`) {
248 248
 		t.Fatalf(`Missing level="debug" in log file:\n%s`, string(content))
249 249
 	}
250 250
 
... ...
@@ -254,7 +254,7 @@ func TestDaemonLoggingLevel(t *testing.T) {
254 254
 	}
255 255
 	d.Stop()
256 256
 	content, _ = ioutil.ReadFile(d.logFile.Name())
257
-	if strings.Contains(string(content), `level="debug"`) {
257
+	if strings.Contains(string(content), `level=debug`) {
258 258
 		t.Fatalf(`Should not have level="debug" in log file:\n%s`, string(content))
259 259
 	}
260 260
 
... ...
@@ -264,7 +264,7 @@ func TestDaemonLoggingLevel(t *testing.T) {
264 264
 	}
265 265
 	d.Stop()
266 266
 	content, _ = ioutil.ReadFile(d.logFile.Name())
267
-	if !strings.Contains(string(content), `level="debug"`) {
267
+	if !strings.Contains(string(content), `level=debug`) {
268 268
 		t.Fatalf(`Missing level="debug" in log file using -D:\n%s`, string(content))
269 269
 	}
270 270
 
... ...
@@ -274,7 +274,7 @@ func TestDaemonLoggingLevel(t *testing.T) {
274 274
 	}
275 275
 	d.Stop()
276 276
 	content, _ = ioutil.ReadFile(d.logFile.Name())
277
-	if !strings.Contains(string(content), `level="debug"`) {
277
+	if !strings.Contains(string(content), `level=debug`) {
278 278
 		t.Fatalf(`Missing level="debug" in log file using --debug:\n%s`, string(content))
279 279
 	}
280 280
 
... ...
@@ -284,7 +284,7 @@ func TestDaemonLoggingLevel(t *testing.T) {
284 284
 	}
285 285
 	d.Stop()
286 286
 	content, _ = ioutil.ReadFile(d.logFile.Name())
287
-	if !strings.Contains(string(content), `level="debug"`) {
287
+	if !strings.Contains(string(content), `level=debug`) {
288 288
 		t.Fatalf(`Missing level="debug" in log file when using both --debug and --log-level=fatal:\n%s`, string(content))
289 289
 	}
290 290
 
... ...
@@ -53,7 +53,7 @@ clone hg code.google.com/p/gosqlite 74691fb6f837
53 53
 
54 54
 clone git github.com/docker/libtrust 230dfd18c232
55 55
 
56
-clone git github.com/Sirupsen/logrus v0.6.0
56
+clone git github.com/Sirupsen/logrus v0.6.6
57 57
 
58 58
 clone git github.com/go-fsnotify/fsnotify v1.0.4
59 59
 
... ...
@@ -71,5 +71,5 @@ fi
71 71
 clone git github.com/docker/libcontainer aa10040b570386c1ae311c6245b9e21295b2b83a
72 72
 # see src/github.com/docker/libcontainer/update-vendor.sh which is the "source of truth" for libcontainer deps (just like this file)
73 73
 rm -rf src/github.com/docker/libcontainer/vendor
74
-eval "$(grep '^clone ' src/github.com/docker/libcontainer/update-vendor.sh | grep -v 'github.com/codegangsta/cli')"
74
+eval "$(grep '^clone ' src/github.com/docker/libcontainer/update-vendor.sh | grep -v 'github.com/codegangsta/cli' | grep -v 'github.com/Sirupsen/logrus')"
75 75
 # we exclude "github.com/codegangsta/cli" here because it's only needed for "nsinit", which Docker doesn't include
... ...
@@ -2,8 +2,7 @@ language: go
2 2
 go:
3 3
   - 1.2
4 4
   - 1.3
5
+  - 1.4
5 6
   - tip
6 7
 install:
7
-  - go get github.com/stretchr/testify
8
-  - go get github.com/stvp/go-udp-testing
9
-  - go get github.com/tobi/airbrake-go
8
+  - go get -t ./...
... ...
@@ -1,10 +1,11 @@
1
-# 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)
1
+# 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]
2 2
 
3 3
 Logrus is a structured logger for Go (golang), completely API compatible with
4 4
 the standard library logger. [Godoc][godoc]. **Please note the Logrus API is not
5
-yet stable (pre 1.0), the core API is unlikely change much but please version
6
-control your Logrus to make sure you aren't fetching latest `master` on every
7
-build.**
5
+yet stable (pre 1.0). Logrus itself is completely stable and has been used in
6
+many large deployments. The core API is unlikely to change much but please
7
+version control your Logrus to make sure you aren't fetching latest `master` on
8
+every build.**
8 9
 
9 10
 Nicely color-coded in development (when a TTY is attached, otherwise just
10 11
 plain text):
... ...
@@ -33,7 +34,7 @@ ocean","size":10,"time":"2014-03-10 19:57:38.562264131 -0400 EDT"}
33 33
 
34 34
 With the default `log.Formatter = new(logrus.TextFormatter)` when a TTY is not
35 35
 attached, the output is compatible with the
36
-[l2met](http://r.32k.io/l2met-introduction) format:
36
+[logfmt](http://godoc.org/github.com/kr/logfmt) format:
37 37
 
38 38
 ```text
39 39
 time="2014-04-20 15:36:23.830442383 -0400 EDT" level="info" msg="A group of walrus emerges from the ocean" animal="walrus" size=10
... ...
@@ -206,11 +207,18 @@ import (
206 206
   log "github.com/Sirupsen/logrus"
207 207
   "github.com/Sirupsen/logrus/hooks/airbrake"
208 208
   "github.com/Sirupsen/logrus/hooks/syslog"
209
+  "log/syslog"
209 210
 )
210 211
 
211 212
 func init() {
212 213
   log.AddHook(new(logrus_airbrake.AirbrakeHook))
213
-  log.AddHook(logrus_syslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, ""))
214
+
215
+  hook, err := logrus_syslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
216
+  if err != nil {
217
+    log.Error("Unable to connect to local syslog daemon")
218
+  } else {
219
+    log.AddHook(hook)
220
+  }
214 221
 }
215 222
 ```
216 223
 
... ...
@@ -228,6 +236,15 @@ func init() {
228 228
 * [`github.com/nubo/hiprus`](https://github.com/nubo/hiprus)
229 229
   Send errors to a channel in hipchat.
230 230
 
231
+* [`github.com/sebest/logrusly`](https://github.com/sebest/logrusly)
232
+  Send logs to Loggly (https://www.loggly.com/)
233
+
234
+* [`github.com/johntdyer/slackrus`](https://github.com/johntdyer/slackrus)
235
+  Hook for Slack chat.
236
+
237
+* [`github.com/wercker/journalhook`](https://github.com/wercker/journalhook).
238
+  Hook for logging to `systemd-journald`.
239
+
231 240
 #### Level logging
232 241
 
233 242
 Logrus has six logging levels: Debug, Info, Warning, Error, Fatal and Panic.
... ...
@@ -307,7 +324,7 @@ The built-in logging formatters are:
307 307
 
308 308
 Third party logging formatters:
309 309
 
310
-* [`zalgo`](https://github.com/aybabtme/logzalgo): invoking the P͉̫o̳̼̊w̖͈̰͎e̬͔̭͂r͚̼̹̲ ̫͓͉̳͈ō̠͕͖̚f̝͍̠ ͕̲̞͖͑Z̖̫̤̫ͪa͉̬͈̗l͖͎g̳̥o̰̥̅!̣͔̲̻͊̄ ̙̘̦̹̦.
310
+* [`zalgo`](https://github.com/aybabtme/logzalgo): invoking the P͉̫o̳̼̊w̖͈̰͎e̬͔̭͂r͚̼̹̲ ̫͓͉̳͈ō̠͕͖̚f̝͍̠ ͕̲̞͖͑Z̖̫̤̫ͪa͉̬͈̗l͖͎g̳̥o̰̥̅!̣͔̲̻͊̄ ̙̘̦̹̦.
311 311
 
312 312
 You can define your formatter by implementing the `Formatter` interface,
313 313
 requiring a `Format` method. `Format` takes an `*Entry`. `entry.Data` is a
... ...
@@ -332,10 +349,28 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
332 332
 }
333 333
 ```
334 334
 
335
+#### Logger as an `io.Writer`
336
+
337
+Logrus can be transormed into an `io.Writer`. That writer is the end of an `io.Pipe` and it is your responsibility to close it.
338
+
339
+```go
340
+w := logger.Writer()
341
+defer w.Close()
342
+
343
+srv := http.Server{
344
+    // create a stdlib log.Logger that writes to
345
+    // logrus.Logger.
346
+    ErrorLog: log.New(w, "", 0),
347
+}
348
+```
349
+
350
+Each line written to that writer will be printed the usual way, using formatters
351
+and hooks. The level for those entries is `info`.
352
+
335 353
 #### Rotation
336 354
 
337 355
 Log rotation is not provided with Logrus. Log rotation should be done by an
338
-external program (like `logrotated(8)`) that can compress and delete old log
356
+external program (like `logrotate(8)`) that can compress and delete old log
339 357
 entries. It should not be a feature of the application-level logger.
340 358
 
341 359
 
... ...
@@ -100,7 +100,7 @@ func (entry *Entry) log(level Level, msg string) {
100 100
 	// panic() to use in Entry#Panic(), we avoid the allocation by checking
101 101
 	// directly here.
102 102
 	if level <= PanicLevel {
103
-		panic(reader.String())
103
+		panic(entry)
104 104
 	}
105 105
 }
106 106
 
... ...
@@ -126,6 +126,10 @@ func (entry *Entry) Warn(args ...interface{}) {
126 126
 	}
127 127
 }
128 128
 
129
+func (entry *Entry) Warning(args ...interface{}) {
130
+	entry.Warn(args...)
131
+}
132
+
129 133
 func (entry *Entry) Error(args ...interface{}) {
130 134
 	if entry.Logger.Level >= ErrorLevel {
131 135
 		entry.log(ErrorLevel, fmt.Sprint(args...))
132 136
new file mode 100644
... ...
@@ -0,0 +1,53 @@
0
+package logrus
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"testing"
6
+
7
+	"github.com/stretchr/testify/assert"
8
+)
9
+
10
+func TestEntryPanicln(t *testing.T) {
11
+	errBoom := fmt.Errorf("boom time")
12
+
13
+	defer func() {
14
+		p := recover()
15
+		assert.NotNil(t, p)
16
+
17
+		switch pVal := p.(type) {
18
+		case *Entry:
19
+			assert.Equal(t, "kaboom", pVal.Message)
20
+			assert.Equal(t, errBoom, pVal.Data["err"])
21
+		default:
22
+			t.Fatalf("want type *Entry, got %T: %#v", pVal, pVal)
23
+		}
24
+	}()
25
+
26
+	logger := New()
27
+	logger.Out = &bytes.Buffer{}
28
+	entry := NewEntry(logger)
29
+	entry.WithField("err", errBoom).Panicln("kaboom")
30
+}
31
+
32
+func TestEntryPanicf(t *testing.T) {
33
+	errBoom := fmt.Errorf("boom again")
34
+
35
+	defer func() {
36
+		p := recover()
37
+		assert.NotNil(t, p)
38
+
39
+		switch pVal := p.(type) {
40
+		case *Entry:
41
+			assert.Equal(t, "kaboom true", pVal.Message)
42
+			assert.Equal(t, errBoom, pVal.Data["err"])
43
+		default:
44
+			t.Fatalf("want type *Entry, got %T: %#v", pVal, pVal)
45
+		}
46
+	}()
47
+
48
+	logger := New()
49
+	logger.Out = &bytes.Buffer{}
50
+	entry := NewEntry(logger)
51
+	entry.WithField("err", errBoom).Panicf("kaboom %v", true)
52
+}
... ...
@@ -9,9 +9,26 @@ var log = logrus.New()
9 9
 func init() {
10 10
 	log.Formatter = new(logrus.JSONFormatter)
11 11
 	log.Formatter = new(logrus.TextFormatter) // default
12
+	log.Level = logrus.DebugLevel
12 13
 }
13 14
 
14 15
 func main() {
16
+	defer func() {
17
+		err := recover()
18
+		if err != nil {
19
+			log.WithFields(logrus.Fields{
20
+				"omg":    true,
21
+				"err":    err,
22
+				"number": 100,
23
+			}).Fatal("The ice breaks!")
24
+		}
25
+	}()
26
+
27
+	log.WithFields(logrus.Fields{
28
+		"animal": "walrus",
29
+		"number": 8,
30
+	}).Debug("Started observing beach")
31
+
15 32
 	log.WithFields(logrus.Fields{
16 33
 		"animal": "walrus",
17 34
 		"size":   10,
... ...
@@ -23,7 +40,11 @@ func main() {
23 23
 	}).Warn("The group's number increased tremendously!")
24 24
 
25 25
 	log.WithFields(logrus.Fields{
26
-		"omg":    true,
27
-		"number": 100,
28
-	}).Fatal("The ice breaks!")
26
+		"temperature": -4,
27
+	}).Debug("Temperature changes")
28
+
29
+	log.WithFields(logrus.Fields{
30
+		"animal": "orca",
31
+		"size":   9009,
32
+	}).Panic("It's over 9000!")
29 33
 }
... ...
@@ -9,6 +9,10 @@ var (
9 9
 	std = New()
10 10
 )
11 11
 
12
+func StandardLogger() *Logger {
13
+	return std
14
+}
15
+
12 16
 // SetOutput sets the standard logger output.
13 17
 func SetOutput(out io.Writer) {
14 18
 	std.mu.Lock()
... ...
@@ -30,6 +34,13 @@ func SetLevel(level Level) {
30 30
 	std.Level = level
31 31
 }
32 32
 
33
+// GetLevel returns the standard logger level.
34
+func GetLevel() Level {
35
+	std.mu.Lock()
36
+	defer std.mu.Unlock()
37
+	return std.Level
38
+}
39
+
33 40
 // AddHook adds a hook to the standard logger hooks.
34 41
 func AddHook(hook Hook) {
35 42
 	std.mu.Lock()
... ...
@@ -26,19 +26,19 @@ type Formatter interface {
26 26
 //
27 27
 // It's not exported because it's still using Data in an opinionated way. It's to
28 28
 // avoid code duplication between the two default formatters.
29
-func prefixFieldClashes(entry *Entry) {
30
-	_, ok := entry.Data["time"]
29
+func prefixFieldClashes(data Fields) {
30
+	_, ok := data["time"]
31 31
 	if ok {
32
-		entry.Data["fields.time"] = entry.Data["time"]
32
+		data["fields.time"] = data["time"]
33 33
 	}
34 34
 
35
-	_, ok = entry.Data["msg"]
35
+	_, ok = data["msg"]
36 36
 	if ok {
37
-		entry.Data["fields.msg"] = entry.Data["msg"]
37
+		data["fields.msg"] = data["msg"]
38 38
 	}
39 39
 
40
-	_, ok = entry.Data["level"]
40
+	_, ok = data["level"]
41 41
 	if ok {
42
-		entry.Data["fields.level"] = entry.Data["level"]
42
+		data["fields.level"] = data["level"]
43 43
 	}
44 44
 }
... ...
@@ -9,7 +9,7 @@ import (
9 9
 // with the Airbrake API. You must set:
10 10
 // * airbrake.Endpoint
11 11
 // * airbrake.ApiKey
12
-// * airbrake.Environment (only sends exceptions when set to "production")
12
+// * airbrake.Environment
13 13
 //
14 14
 // Before using this hook, to send an error. Entries that trigger an Error,
15 15
 // Fatal or Panic should now include an "error" field to send to Airbrake.
... ...
@@ -30,7 +30,8 @@ func NewPapertrailHook(host string, port int, appName string) (*PapertrailHook,
30 30
 // Fire is called when a log event is fired.
31 31
 func (hook *PapertrailHook) Fire(entry *logrus.Entry) error {
32 32
 	date := time.Now().Format(format)
33
-	payload := fmt.Sprintf("<22> %s %s: [%s] %s", date, hook.AppName, entry.Data["level"], entry.Message)
33
+	msg, _ := entry.String()
34
+	payload := fmt.Sprintf("<22> %s %s: %s", date, hook.AppName, msg)
34 35
 
35 36
 	bytesWritten, err := hook.UDPConn.Write([]byte(payload))
36 37
 	if err != nil {
37 38
new file mode 100644
... ...
@@ -0,0 +1,61 @@
0
+# Sentry Hook for Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:" />
1
+
2
+[Sentry](https://getsentry.com) provides both self-hosted and hosted
3
+solutions for exception tracking.
4
+Both client and server are
5
+[open source](https://github.com/getsentry/sentry).
6
+
7
+## Usage
8
+
9
+Every sentry application defined on the server gets a different
10
+[DSN](https://www.getsentry.com/docs/). In the example below replace
11
+`YOUR_DSN` with the one created for your application.
12
+
13
+```go
14
+import (
15
+  "github.com/Sirupsen/logrus"
16
+  "github.com/Sirupsen/logrus/hooks/sentry"
17
+)
18
+
19
+func main() {
20
+  log       := logrus.New()
21
+  hook, err := logrus_sentry.NewSentryHook(YOUR_DSN, []logrus.Level{
22
+    logrus.PanicLevel,
23
+    logrus.FatalLevel,
24
+    logrus.ErrorLevel,
25
+  })
26
+
27
+  if err == nil {
28
+    log.Hooks.Add(hook)
29
+  }
30
+}
31
+```
32
+
33
+## Special fields
34
+
35
+Some logrus fields have a special meaning in this hook,
36
+these are server_name and logger.
37
+When logs are sent to sentry these fields are treated differently.
38
+- server_name (also known as hostname) is the name of the server which
39
+is logging the event (hostname.example.com)
40
+- logger is the part of the application which is logging the event.
41
+In go this usually means setting it to the name of the package.
42
+
43
+## Timeout
44
+
45
+`Timeout` is the time the sentry hook will wait for a response
46
+from the sentry server.
47
+
48
+If this time elapses with no response from
49
+the server an error will be returned.
50
+
51
+If `Timeout` is set to 0 the SentryHook will not wait for a reply
52
+and will assume a correct delivery.
53
+
54
+The SentryHook has a default timeout of `100 milliseconds` when created
55
+with a call to `NewSentryHook`. This can be changed by assigning a value to the `Timeout` field:
56
+
57
+```go
58
+hook, _ := logrus_sentry.NewSentryHook(...)
59
+hook.Timeout = 20*time.Second
60
+```
0 61
new file mode 100644
... ...
@@ -0,0 +1,100 @@
0
+package logrus_sentry
1
+
2
+import (
3
+	"fmt"
4
+	"time"
5
+
6
+	"github.com/Sirupsen/logrus"
7
+	"github.com/getsentry/raven-go"
8
+)
9
+
10
+var (
11
+	severityMap = map[logrus.Level]raven.Severity{
12
+		logrus.DebugLevel: raven.DEBUG,
13
+		logrus.InfoLevel:  raven.INFO,
14
+		logrus.WarnLevel:  raven.WARNING,
15
+		logrus.ErrorLevel: raven.ERROR,
16
+		logrus.FatalLevel: raven.FATAL,
17
+		logrus.PanicLevel: raven.FATAL,
18
+	}
19
+)
20
+
21
+func getAndDel(d logrus.Fields, key string) (string, bool) {
22
+	var (
23
+		ok  bool
24
+		v   interface{}
25
+		val string
26
+	)
27
+	if v, ok = d[key]; !ok {
28
+		return "", false
29
+	}
30
+
31
+	if val, ok = v.(string); !ok {
32
+		return "", false
33
+	}
34
+	delete(d, key)
35
+	return val, true
36
+}
37
+
38
+// SentryHook delivers logs to a sentry server.
39
+type SentryHook struct {
40
+	// Timeout sets the time to wait for a delivery error from the sentry server.
41
+	// If this is set to zero the server will not wait for any response and will
42
+	// consider the message correctly sent
43
+	Timeout time.Duration
44
+
45
+	client *raven.Client
46
+	levels []logrus.Level
47
+}
48
+
49
+// NewSentryHook creates a hook to be added to an instance of logger
50
+// and initializes the raven client.
51
+// This method sets the timeout to 100 milliseconds.
52
+func NewSentryHook(DSN string, levels []logrus.Level) (*SentryHook, error) {
53
+	client, err := raven.NewClient(DSN, nil)
54
+	if err != nil {
55
+		return nil, err
56
+	}
57
+	return &SentryHook{100 * time.Millisecond, client, levels}, nil
58
+}
59
+
60
+// Called when an event should be sent to sentry
61
+// Special fields that sentry uses to give more information to the server
62
+// are extracted from entry.Data (if they are found)
63
+// These fields are: logger and server_name
64
+func (hook *SentryHook) Fire(entry *logrus.Entry) error {
65
+	packet := &raven.Packet{
66
+		Message:   entry.Message,
67
+		Timestamp: raven.Timestamp(entry.Time),
68
+		Level:     severityMap[entry.Level],
69
+		Platform:  "go",
70
+	}
71
+
72
+	d := entry.Data
73
+
74
+	if logger, ok := getAndDel(d, "logger"); ok {
75
+		packet.Logger = logger
76
+	}
77
+	if serverName, ok := getAndDel(d, "server_name"); ok {
78
+		packet.ServerName = serverName
79
+	}
80
+	packet.Extra = map[string]interface{}(d)
81
+
82
+	_, errCh := hook.client.Capture(packet, nil)
83
+	timeout := hook.Timeout
84
+	if timeout != 0 {
85
+		timeoutCh := time.After(timeout)
86
+		select {
87
+		case err := <-errCh:
88
+			return err
89
+		case <-timeoutCh:
90
+			return fmt.Errorf("no response from sentry server in %s", timeout)
91
+		}
92
+	}
93
+	return nil
94
+}
95
+
96
+// Levels returns the available logging levels.
97
+func (hook *SentryHook) Levels() []logrus.Level {
98
+	return hook.levels
99
+}
0 100
new file mode 100644
... ...
@@ -0,0 +1,97 @@
0
+package logrus_sentry
1
+
2
+import (
3
+	"encoding/json"
4
+	"fmt"
5
+	"io/ioutil"
6
+	"net/http"
7
+	"net/http/httptest"
8
+	"strings"
9
+	"testing"
10
+
11
+	"github.com/Sirupsen/logrus"
12
+	"github.com/getsentry/raven-go"
13
+)
14
+
15
+const (
16
+	message     = "error message"
17
+	server_name = "testserver.internal"
18
+	logger_name = "test.logger"
19
+)
20
+
21
+func getTestLogger() *logrus.Logger {
22
+	l := logrus.New()
23
+	l.Out = ioutil.Discard
24
+	return l
25
+}
26
+
27
+func WithTestDSN(t *testing.T, tf func(string, <-chan *raven.Packet)) {
28
+	pch := make(chan *raven.Packet, 1)
29
+	s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
30
+		defer req.Body.Close()
31
+		d := json.NewDecoder(req.Body)
32
+		p := &raven.Packet{}
33
+		err := d.Decode(p)
34
+		if err != nil {
35
+			t.Fatal(err.Error())
36
+		}
37
+
38
+		pch <- p
39
+	}))
40
+	defer s.Close()
41
+
42
+	fragments := strings.SplitN(s.URL, "://", 2)
43
+	dsn := fmt.Sprintf(
44
+		"%s://public:secret@%s/sentry/project-id",
45
+		fragments[0],
46
+		fragments[1],
47
+	)
48
+	tf(dsn, pch)
49
+}
50
+
51
+func TestSpecialFields(t *testing.T) {
52
+	WithTestDSN(t, func(dsn string, pch <-chan *raven.Packet) {
53
+		logger := getTestLogger()
54
+
55
+		hook, err := NewSentryHook(dsn, []logrus.Level{
56
+			logrus.ErrorLevel,
57
+		})
58
+
59
+		if err != nil {
60
+			t.Fatal(err.Error())
61
+		}
62
+		logger.Hooks.Add(hook)
63
+		logger.WithFields(logrus.Fields{
64
+			"server_name": server_name,
65
+			"logger":      logger_name,
66
+		}).Error(message)
67
+
68
+		packet := <-pch
69
+		if packet.Logger != logger_name {
70
+			t.Errorf("logger should have been %s, was %s", logger_name, packet.Logger)
71
+		}
72
+
73
+		if packet.ServerName != server_name {
74
+			t.Errorf("server_name should have been %s, was %s", server_name, packet.ServerName)
75
+		}
76
+	})
77
+}
78
+
79
+func TestSentryHandler(t *testing.T) {
80
+	WithTestDSN(t, func(dsn string, pch <-chan *raven.Packet) {
81
+		logger := getTestLogger()
82
+		hook, err := NewSentryHook(dsn, []logrus.Level{
83
+			logrus.ErrorLevel,
84
+		})
85
+		if err != nil {
86
+			t.Fatal(err.Error())
87
+		}
88
+		logger.Hooks.Add(hook)
89
+
90
+		logger.Error(message)
91
+		packet := <-pch
92
+		if packet.Message != message {
93
+			t.Errorf("message should have been %s, was %s", message, packet.Message)
94
+		}
95
+	})
96
+}
... ...
@@ -6,7 +6,7 @@
6 6
 import (
7 7
   "log/syslog"
8 8
   "github.com/Sirupsen/logrus"
9
-  "github.com/Sirupsen/logrus/hooks/syslog"
9
+  logrus_syslog "github.com/Sirupsen/logrus/hooks/syslog"
10 10
 )
11 11
 
12 12
 func main() {
... ...
@@ -17,4 +17,4 @@ func main() {
17 17
     log.Hooks.Add(hook)
18 18
   }
19 19
 }
20
-```
21 20
\ No newline at end of file
21
+```
... ...
@@ -29,18 +29,18 @@ func (hook *SyslogHook) Fire(entry *logrus.Entry) error {
29 29
 		return err
30 30
 	}
31 31
 
32
-	switch entry.Data["level"] {
33
-	case "panic":
32
+	switch entry.Level {
33
+	case logrus.PanicLevel:
34 34
 		return hook.Writer.Crit(line)
35
-	case "fatal":
35
+	case logrus.FatalLevel:
36 36
 		return hook.Writer.Crit(line)
37
-	case "error":
37
+	case logrus.ErrorLevel:
38 38
 		return hook.Writer.Err(line)
39
-	case "warn":
39
+	case logrus.WarnLevel:
40 40
 		return hook.Writer.Warning(line)
41
-	case "info":
41
+	case logrus.InfoLevel:
42 42
 		return hook.Writer.Info(line)
43
-	case "debug":
43
+	case logrus.DebugLevel:
44 44
 		return hook.Writer.Debug(line)
45 45
 	default:
46 46
 		return nil
... ...
@@ -9,12 +9,22 @@ import (
9 9
 type JSONFormatter struct{}
10 10
 
11 11
 func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
12
-	prefixFieldClashes(entry)
13
-	entry.Data["time"] = entry.Time.Format(time.RFC3339)
14
-	entry.Data["msg"] = entry.Message
15
-	entry.Data["level"] = entry.Level.String()
12
+	data := make(Fields, len(entry.Data)+3)
13
+	for k, v := range entry.Data {
14
+		// Otherwise errors are ignored by `encoding/json`
15
+		// https://github.com/Sirupsen/logrus/issues/137
16
+		if err, ok := v.(error); ok {
17
+			data[k] = err.Error()
18
+		} else {
19
+			data[k] = v
20
+		}
21
+	}
22
+	prefixFieldClashes(data)
23
+	data["time"] = entry.Time.Format(time.RFC3339)
24
+	data["msg"] = entry.Message
25
+	data["level"] = entry.Level.String()
16 26
 
17
-	serialized, err := json.Marshal(entry.Data)
27
+	serialized, err := json.Marshal(data)
18 28
 	if err != nil {
19 29
 		return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
20 30
 	}
21 31
new file mode 100644
... ...
@@ -0,0 +1,120 @@
0
+package logrus
1
+
2
+import (
3
+	"encoding/json"
4
+	"errors"
5
+
6
+	"testing"
7
+)
8
+
9
+func TestErrorNotLost(t *testing.T) {
10
+	formatter := &JSONFormatter{}
11
+
12
+	b, err := formatter.Format(WithField("error", errors.New("wild walrus")))
13
+	if err != nil {
14
+		t.Fatal("Unable to format entry: ", err)
15
+	}
16
+
17
+	entry := make(map[string]interface{})
18
+	err = json.Unmarshal(b, &entry)
19
+	if err != nil {
20
+		t.Fatal("Unable to unmarshal formatted entry: ", err)
21
+	}
22
+
23
+	if entry["error"] != "wild walrus" {
24
+		t.Fatal("Error field not set")
25
+	}
26
+}
27
+
28
+func TestErrorNotLostOnFieldNotNamedError(t *testing.T) {
29
+	formatter := &JSONFormatter{}
30
+
31
+	b, err := formatter.Format(WithField("omg", errors.New("wild walrus")))
32
+	if err != nil {
33
+		t.Fatal("Unable to format entry: ", err)
34
+	}
35
+
36
+	entry := make(map[string]interface{})
37
+	err = json.Unmarshal(b, &entry)
38
+	if err != nil {
39
+		t.Fatal("Unable to unmarshal formatted entry: ", err)
40
+	}
41
+
42
+	if entry["omg"] != "wild walrus" {
43
+		t.Fatal("Error field not set")
44
+	}
45
+}
46
+
47
+func TestFieldClashWithTime(t *testing.T) {
48
+	formatter := &JSONFormatter{}
49
+
50
+	b, err := formatter.Format(WithField("time", "right now!"))
51
+	if err != nil {
52
+		t.Fatal("Unable to format entry: ", err)
53
+	}
54
+
55
+	entry := make(map[string]interface{})
56
+	err = json.Unmarshal(b, &entry)
57
+	if err != nil {
58
+		t.Fatal("Unable to unmarshal formatted entry: ", err)
59
+	}
60
+
61
+	if entry["fields.time"] != "right now!" {
62
+		t.Fatal("fields.time not set to original time field")
63
+	}
64
+
65
+	if entry["time"] != "0001-01-01T00:00:00Z" {
66
+		t.Fatal("time field not set to current time, was: ", entry["time"])
67
+	}
68
+}
69
+
70
+func TestFieldClashWithMsg(t *testing.T) {
71
+	formatter := &JSONFormatter{}
72
+
73
+	b, err := formatter.Format(WithField("msg", "something"))
74
+	if err != nil {
75
+		t.Fatal("Unable to format entry: ", err)
76
+	}
77
+
78
+	entry := make(map[string]interface{})
79
+	err = json.Unmarshal(b, &entry)
80
+	if err != nil {
81
+		t.Fatal("Unable to unmarshal formatted entry: ", err)
82
+	}
83
+
84
+	if entry["fields.msg"] != "something" {
85
+		t.Fatal("fields.msg not set to original msg field")
86
+	}
87
+}
88
+
89
+func TestFieldClashWithLevel(t *testing.T) {
90
+	formatter := &JSONFormatter{}
91
+
92
+	b, err := formatter.Format(WithField("level", "something"))
93
+	if err != nil {
94
+		t.Fatal("Unable to format entry: ", err)
95
+	}
96
+
97
+	entry := make(map[string]interface{})
98
+	err = json.Unmarshal(b, &entry)
99
+	if err != nil {
100
+		t.Fatal("Unable to unmarshal formatted entry: ", err)
101
+	}
102
+
103
+	if entry["fields.level"] != "something" {
104
+		t.Fatal("fields.level not set to original level field")
105
+	}
106
+}
107
+
108
+func TestJSONEntryEndsWithNewline(t *testing.T) {
109
+	formatter := &JSONFormatter{}
110
+
111
+	b, err := formatter.Format(WithField("level", "something"))
112
+	if err != nil {
113
+		t.Fatal("Unable to format entry: ", err)
114
+	}
115
+
116
+	if b[len(b)-1] != '\n' {
117
+		t.Fatal("Expected JSON log entry to end with a newline")
118
+	}
119
+}
... ...
@@ -38,7 +38,7 @@ type Logger struct {
38 38
 //      Out: os.Stderr,
39 39
 //      Formatter: new(JSONFormatter),
40 40
 //      Hooks: make(levelHooks),
41
-//      Level: logrus.Debug,
41
+//      Level: logrus.DebugLevel,
42 42
 //    }
43 43
 //
44 44
 // It's recommended to make this a global instance called `log`.
... ...
@@ -5,6 +5,7 @@ import (
5 5
 	"encoding/json"
6 6
 	"strconv"
7 7
 	"strings"
8
+	"sync"
8 9
 	"testing"
9 10
 
10 11
 	"github.com/stretchr/testify/assert"
... ...
@@ -44,8 +45,12 @@ func LogAndAssertText(t *testing.T, log func(*Logger), assertions func(fields ma
44 44
 		}
45 45
 		kvArr := strings.Split(kv, "=")
46 46
 		key := strings.TrimSpace(kvArr[0])
47
-		val, err := strconv.Unquote(kvArr[1])
48
-		assert.NoError(t, err)
47
+		val := kvArr[1]
48
+		if kvArr[1][0] == '"' {
49
+			var err error
50
+			val, err = strconv.Unquote(val)
51
+			assert.NoError(t, err)
52
+		}
49 53
 		fields[key] = val
50 54
 	}
51 55
 	assertions(fields)
... ...
@@ -204,6 +209,38 @@ func TestDefaultFieldsAreNotPrefixed(t *testing.T) {
204 204
 	})
205 205
 }
206 206
 
207
+func TestDoubleLoggingDoesntPrefixPreviousFields(t *testing.T) {
208
+
209
+	var buffer bytes.Buffer
210
+	var fields Fields
211
+
212
+	logger := New()
213
+	logger.Out = &buffer
214
+	logger.Formatter = new(JSONFormatter)
215
+
216
+	llog := logger.WithField("context", "eating raw fish")
217
+
218
+	llog.Info("looks delicious")
219
+
220
+	err := json.Unmarshal(buffer.Bytes(), &fields)
221
+	assert.NoError(t, err, "should have decoded first message")
222
+	assert.Equal(t, len(fields), 4, "should only have msg/time/level/context fields")
223
+	assert.Equal(t, fields["msg"], "looks delicious")
224
+	assert.Equal(t, fields["context"], "eating raw fish")
225
+
226
+	buffer.Reset()
227
+
228
+	llog.Warn("omg it is!")
229
+
230
+	err = json.Unmarshal(buffer.Bytes(), &fields)
231
+	assert.NoError(t, err, "should have decoded second message")
232
+	assert.Equal(t, len(fields), 4, "should only have msg/time/level/context fields")
233
+	assert.Equal(t, fields["msg"], "omg it is!")
234
+	assert.Equal(t, fields["context"], "eating raw fish")
235
+	assert.Nil(t, fields["fields.msg"], "should not have prefixed previous `msg` entry")
236
+
237
+}
238
+
207 239
 func TestConvertLevelToString(t *testing.T) {
208 240
 	assert.Equal(t, "debug", DebugLevel.String())
209 241
 	assert.Equal(t, "info", InfoLevel.String())
... ...
@@ -245,3 +282,20 @@ func TestParseLevel(t *testing.T) {
245 245
 	l, err = ParseLevel("invalid")
246 246
 	assert.Equal(t, "not a valid logrus Level: \"invalid\"", err.Error())
247 247
 }
248
+
249
+func TestGetSetLevelRace(t *testing.T) {
250
+	wg := sync.WaitGroup{}
251
+	for i := 0; i < 100; i++ {
252
+		wg.Add(1)
253
+		go func(i int) {
254
+			defer wg.Done()
255
+			if i%2 == 0 {
256
+				SetLevel(InfoLevel)
257
+			} else {
258
+				GetLevel()
259
+			}
260
+		}(i)
261
+
262
+	}
263
+	wg.Wait()
264
+}
... ...
@@ -3,7 +3,7 @@
3 3
 // Use of this source code is governed by a BSD-style
4 4
 // license that can be found in the LICENSE file.
5 5
 
6
-// +build linux,!appengine darwin freebsd
6
+// +build linux darwin freebsd openbsd
7 7
 
8 8
 package logrus
9 9
 
10 10
new file mode 100644
... ...
@@ -0,0 +1,8 @@
0
+
1
+package logrus
2
+
3
+import "syscall"
4
+
5
+const ioctlReadTermios = syscall.TIOCGETA
6
+
7
+type Termios syscall.Termios
... ...
@@ -3,6 +3,7 @@ package logrus
3 3
 import (
4 4
 	"bytes"
5 5
 	"fmt"
6
+	"regexp"
6 7
 	"sort"
7 8
 	"strings"
8 9
 	"time"
... ...
@@ -14,11 +15,13 @@ const (
14 14
 	green   = 32
15 15
 	yellow  = 33
16 16
 	blue    = 34
17
+	gray    = 37
17 18
 )
18 19
 
19 20
 var (
20 21
 	baseTimestamp time.Time
21 22
 	isTerminal    bool
23
+	noQuoteNeeded *regexp.Regexp
22 24
 )
23 25
 
24 26
 func init() {
... ...
@@ -32,28 +35,47 @@ func miniTS() int {
32 32
 
33 33
 type TextFormatter struct {
34 34
 	// Set to true to bypass checking for a TTY before outputting colors.
35
-	ForceColors   bool
35
+	ForceColors bool
36
+
37
+	// Force disabling colors.
36 38
 	DisableColors bool
39
+
40
+	// Disable timestamp logging. useful when output is redirected to logging
41
+	// system that already adds timestamps.
42
+	DisableTimestamp bool
43
+
44
+	// Enable logging the full timestamp when a TTY is attached instead of just
45
+	// the time passed since beginning of execution.
46
+	FullTimestamp bool
47
+
48
+	// The fields are sorted by default for a consistent output. For applications
49
+	// that log extremely frequently and don't use the JSON formatter this may not
50
+	// be desired.
51
+	DisableSorting bool
37 52
 }
38 53
 
39 54
 func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
40
-
41
-	var keys []string
55
+	var keys []string = make([]string, 0, len(entry.Data))
42 56
 	for k := range entry.Data {
43 57
 		keys = append(keys, k)
44 58
 	}
45
-	sort.Strings(keys)
59
+
60
+	if !f.DisableSorting {
61
+		sort.Strings(keys)
62
+	}
46 63
 
47 64
 	b := &bytes.Buffer{}
48 65
 
49
-	prefixFieldClashes(entry)
66
+	prefixFieldClashes(entry.Data)
50 67
 
51 68
 	isColored := (f.ForceColors || isTerminal) && !f.DisableColors
52 69
 
53 70
 	if isColored {
54
-		printColored(b, entry, keys)
71
+		f.printColored(b, entry, keys)
55 72
 	} else {
56
-		f.appendKeyValue(b, "time", entry.Time.Format(time.RFC3339))
73
+		if !f.DisableTimestamp {
74
+			f.appendKeyValue(b, "time", entry.Time.Format(time.RFC3339))
75
+		}
57 76
 		f.appendKeyValue(b, "level", entry.Level.String())
58 77
 		f.appendKeyValue(b, "msg", entry.Message)
59 78
 		for _, key := range keys {
... ...
@@ -65,9 +87,11 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
65 65
 	return b.Bytes(), nil
66 66
 }
67 67
 
68
-func printColored(b *bytes.Buffer, entry *Entry, keys []string) {
68
+func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string) {
69 69
 	var levelColor int
70 70
 	switch entry.Level {
71
+	case DebugLevel:
72
+		levelColor = gray
71 73
 	case WarnLevel:
72 74
 		levelColor = yellow
73 75
 	case ErrorLevel, FatalLevel, PanicLevel:
... ...
@@ -78,17 +102,43 @@ func printColored(b *bytes.Buffer, entry *Entry, keys []string) {
78 78
 
79 79
 	levelText := strings.ToUpper(entry.Level.String())[0:4]
80 80
 
81
-	fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, miniTS(), entry.Message)
81
+	if !f.FullTimestamp {
82
+		fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, miniTS(), entry.Message)
83
+	} else {
84
+		fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %-44s ", levelColor, levelText, entry.Time.Format(time.RFC3339), entry.Message)
85
+	}
82 86
 	for _, k := range keys {
83 87
 		v := entry.Data[k]
84 88
 		fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=%v", levelColor, k, v)
85 89
 	}
86 90
 }
87 91
 
92
+func needsQuoting(text string) bool {
93
+	for _, ch := range text {
94
+		if !((ch >= 'a' && ch <= 'z') ||
95
+			(ch >= 'A' && ch <= 'Z') ||
96
+			(ch >= '0' && ch <= '9') ||
97
+			ch == '-' || ch == '.') {
98
+			return false
99
+		}
100
+	}
101
+	return true
102
+}
103
+
88 104
 func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key, value interface{}) {
89 105
 	switch value.(type) {
90
-	case string, error:
91
-		fmt.Fprintf(b, "%v=%q ", key, value)
106
+	case string:
107
+		if needsQuoting(value.(string)) {
108
+			fmt.Fprintf(b, "%v=%s ", key, value)
109
+		} else {
110
+			fmt.Fprintf(b, "%v=%q ", key, value)
111
+		}
112
+	case error:
113
+		if needsQuoting(value.(error).Error()) {
114
+			fmt.Fprintf(b, "%v=%s ", key, value)
115
+		} else {
116
+			fmt.Fprintf(b, "%v=%q ", key, value)
117
+		}
92 118
 	default:
93 119
 		fmt.Fprintf(b, "%v=%v ", key, value)
94 120
 	}
95 121
new file mode 100644
... ...
@@ -0,0 +1,37 @@
0
+package logrus
1
+
2
+import (
3
+	"bytes"
4
+	"errors"
5
+
6
+	"testing"
7
+)
8
+
9
+func TestQuoting(t *testing.T) {
10
+	tf := &TextFormatter{DisableColors: true}
11
+
12
+	checkQuoting := func(q bool, value interface{}) {
13
+		b, _ := tf.Format(WithField("test", value))
14
+		idx := bytes.Index(b, ([]byte)("test="))
15
+		cont := bytes.Contains(b[idx+5:], []byte{'"'})
16
+		if cont != q {
17
+			if q {
18
+				t.Errorf("quoting expected for: %#v", value)
19
+			} else {
20
+				t.Errorf("quoting not expected for: %#v", value)
21
+			}
22
+		}
23
+	}
24
+
25
+	checkQuoting(false, "abcd")
26
+	checkQuoting(false, "v1.0")
27
+	checkQuoting(false, "1234567890")
28
+	checkQuoting(true, "/foobar")
29
+	checkQuoting(true, "x y")
30
+	checkQuoting(true, "x,y")
31
+	checkQuoting(false, errors.New("invalid"))
32
+	checkQuoting(true, errors.New("invalid argument"))
33
+}
34
+
35
+// TODO add tests for sorting etc., this requires a parser for the text
36
+// formatter output.
0 37
new file mode 100644
... ...
@@ -0,0 +1,31 @@
0
+package logrus
1
+
2
+import (
3
+	"bufio"
4
+	"io"
5
+	"runtime"
6
+)
7
+
8
+func (logger *Logger) Writer() (*io.PipeWriter) {
9
+	reader, writer := io.Pipe()
10
+
11
+	go logger.writerScanner(reader)
12
+	runtime.SetFinalizer(writer, writerFinalizer)
13
+
14
+	return writer
15
+}
16
+
17
+func (logger *Logger) writerScanner(reader *io.PipeReader) {
18
+	scanner := bufio.NewScanner(reader)
19
+	for scanner.Scan() {
20
+		logger.Print(scanner.Text())
21
+	}
22
+	if err := scanner.Err(); err != nil {
23
+		logger.Errorf("Error while reading from Writer: %s", err)
24
+	}
25
+	reader.Close()
26
+}
27
+
28
+func writerFinalizer(writer *io.PipeWriter) {
29
+	writer.Close()
30
+}