| ... | ... |
@@ -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 |
| ... | ... |
@@ -1,10 +1,11 @@ |
| 1 |
-# Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:"/> [](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:"/> [](https://travis-ci.org/Sirupsen/logrus) [][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 |
+} |
| ... | ... |
@@ -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,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 |
+} |