зеркало из https://github.com/microsoft/docker.git
Merge pull request #11331 from jfrazelle/update-logrus
Update logrus to 0.6.6
This commit is contained in:
Коммит
5494432f91
|
@ -5042,8 +5042,8 @@ func TestBuildSpaces(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip over the times
|
// Skip over the times
|
||||||
e1 := err1.Error()[strings.Index(err1.Error(), `level="`):]
|
e1 := err1.Error()[strings.Index(err1.Error(), `level=`):]
|
||||||
e2 := err2.Error()[strings.Index(err1.Error(), `level="`):]
|
e2 := err2.Error()[strings.Index(err1.Error(), `level=`):]
|
||||||
|
|
||||||
// Ignore whitespace since that's what were verifying doesn't change stuff
|
// Ignore whitespace since that's what were verifying doesn't change stuff
|
||||||
if strings.Replace(e1, " ", "", -1) != strings.Replace(e2, " ", "", -1) {
|
if strings.Replace(e1, " ", "", -1) != strings.Replace(e2, " ", "", -1) {
|
||||||
|
@ -5056,8 +5056,8 @@ func TestBuildSpaces(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip over the times
|
// Skip over the times
|
||||||
e1 = err1.Error()[strings.Index(err1.Error(), `level="`):]
|
e1 = err1.Error()[strings.Index(err1.Error(), `level=`):]
|
||||||
e2 = err2.Error()[strings.Index(err1.Error(), `level="`):]
|
e2 = err2.Error()[strings.Index(err1.Error(), `level=`):]
|
||||||
|
|
||||||
// Ignore whitespace since that's what were verifying doesn't change stuff
|
// Ignore whitespace since that's what were verifying doesn't change stuff
|
||||||
if strings.Replace(e1, " ", "", -1) != strings.Replace(e2, " ", "", -1) {
|
if strings.Replace(e1, " ", "", -1) != strings.Replace(e2, " ", "", -1) {
|
||||||
|
@ -5070,8 +5070,8 @@ func TestBuildSpaces(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip over the times
|
// Skip over the times
|
||||||
e1 = err1.Error()[strings.Index(err1.Error(), `level="`):]
|
e1 = err1.Error()[strings.Index(err1.Error(), `level=`):]
|
||||||
e2 = err2.Error()[strings.Index(err1.Error(), `level="`):]
|
e2 = err2.Error()[strings.Index(err1.Error(), `level=`):]
|
||||||
|
|
||||||
// Ignore whitespace since that's what were verifying doesn't change stuff
|
// Ignore whitespace since that's what were verifying doesn't change stuff
|
||||||
if strings.Replace(e1, " ", "", -1) != strings.Replace(e2, " ", "", -1) {
|
if strings.Replace(e1, " ", "", -1) != strings.Replace(e2, " ", "", -1) {
|
||||||
|
|
|
@ -244,7 +244,7 @@ func TestDaemonLoggingLevel(t *testing.T) {
|
||||||
}
|
}
|
||||||
d.Stop()
|
d.Stop()
|
||||||
content, _ := ioutil.ReadFile(d.logFile.Name())
|
content, _ := ioutil.ReadFile(d.logFile.Name())
|
||||||
if !strings.Contains(string(content), `level="debug"`) {
|
if !strings.Contains(string(content), `level=debug`) {
|
||||||
t.Fatalf(`Missing level="debug" in log file:\n%s`, string(content))
|
t.Fatalf(`Missing level="debug" in log file:\n%s`, string(content))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,7 +254,7 @@ func TestDaemonLoggingLevel(t *testing.T) {
|
||||||
}
|
}
|
||||||
d.Stop()
|
d.Stop()
|
||||||
content, _ = ioutil.ReadFile(d.logFile.Name())
|
content, _ = ioutil.ReadFile(d.logFile.Name())
|
||||||
if strings.Contains(string(content), `level="debug"`) {
|
if strings.Contains(string(content), `level=debug`) {
|
||||||
t.Fatalf(`Should not have level="debug" in log file:\n%s`, string(content))
|
t.Fatalf(`Should not have level="debug" in log file:\n%s`, string(content))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,7 +264,7 @@ func TestDaemonLoggingLevel(t *testing.T) {
|
||||||
}
|
}
|
||||||
d.Stop()
|
d.Stop()
|
||||||
content, _ = ioutil.ReadFile(d.logFile.Name())
|
content, _ = ioutil.ReadFile(d.logFile.Name())
|
||||||
if !strings.Contains(string(content), `level="debug"`) {
|
if !strings.Contains(string(content), `level=debug`) {
|
||||||
t.Fatalf(`Missing level="debug" in log file using -D:\n%s`, string(content))
|
t.Fatalf(`Missing level="debug" in log file using -D:\n%s`, string(content))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,7 +274,7 @@ func TestDaemonLoggingLevel(t *testing.T) {
|
||||||
}
|
}
|
||||||
d.Stop()
|
d.Stop()
|
||||||
content, _ = ioutil.ReadFile(d.logFile.Name())
|
content, _ = ioutil.ReadFile(d.logFile.Name())
|
||||||
if !strings.Contains(string(content), `level="debug"`) {
|
if !strings.Contains(string(content), `level=debug`) {
|
||||||
t.Fatalf(`Missing level="debug" in log file using --debug:\n%s`, string(content))
|
t.Fatalf(`Missing level="debug" in log file using --debug:\n%s`, string(content))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,7 +284,7 @@ func TestDaemonLoggingLevel(t *testing.T) {
|
||||||
}
|
}
|
||||||
d.Stop()
|
d.Stop()
|
||||||
content, _ = ioutil.ReadFile(d.logFile.Name())
|
content, _ = ioutil.ReadFile(d.logFile.Name())
|
||||||
if !strings.Contains(string(content), `level="debug"`) {
|
if !strings.Contains(string(content), `level=debug`) {
|
||||||
t.Fatalf(`Missing level="debug" in log file when using both --debug and --log-level=fatal:\n%s`, string(content))
|
t.Fatalf(`Missing level="debug" in log file when using both --debug and --log-level=fatal:\n%s`, string(content))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ clone hg code.google.com/p/gosqlite 74691fb6f837
|
||||||
|
|
||||||
clone git github.com/docker/libtrust 230dfd18c232
|
clone git github.com/docker/libtrust 230dfd18c232
|
||||||
|
|
||||||
clone git github.com/Sirupsen/logrus v0.6.0
|
clone git github.com/Sirupsen/logrus v0.6.6
|
||||||
|
|
||||||
clone git github.com/go-fsnotify/fsnotify v1.0.4
|
clone git github.com/go-fsnotify/fsnotify v1.0.4
|
||||||
|
|
||||||
|
@ -71,5 +71,5 @@ fi
|
||||||
clone git github.com/docker/libcontainer aa10040b570386c1ae311c6245b9e21295b2b83a
|
clone git github.com/docker/libcontainer aa10040b570386c1ae311c6245b9e21295b2b83a
|
||||||
# see src/github.com/docker/libcontainer/update-vendor.sh which is the "source of truth" for libcontainer deps (just like this file)
|
# see src/github.com/docker/libcontainer/update-vendor.sh which is the "source of truth" for libcontainer deps (just like this file)
|
||||||
rm -rf src/github.com/docker/libcontainer/vendor
|
rm -rf src/github.com/docker/libcontainer/vendor
|
||||||
eval "$(grep '^clone ' src/github.com/docker/libcontainer/update-vendor.sh | grep -v 'github.com/codegangsta/cli')"
|
eval "$(grep '^clone ' src/github.com/docker/libcontainer/update-vendor.sh | grep -v 'github.com/codegangsta/cli' | grep -v 'github.com/Sirupsen/logrus')"
|
||||||
# we exclude "github.com/codegangsta/cli" here because it's only needed for "nsinit", which Docker doesn't include
|
# 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
|
||||||
go:
|
go:
|
||||||
- 1.2
|
- 1.2
|
||||||
- 1.3
|
- 1.3
|
||||||
|
- 1.4
|
||||||
- tip
|
- tip
|
||||||
install:
|
install:
|
||||||
- go get github.com/stretchr/testify
|
- go get -t ./...
|
||||||
- go get github.com/stvp/go-udp-testing
|
|
||||||
- go get github.com/tobi/airbrake-go
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
# Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:"/> [![Build Status](https://travis-ci.org/Sirupsen/logrus.svg?branch=master)](https://travis-ci.org/Sirupsen/logrus)
|
# Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:"/> [![Build Status](https://travis-ci.org/Sirupsen/logrus.svg?branch=master)](https://travis-ci.org/Sirupsen/logrus) [![godoc reference](https://godoc.org/github.com/Sirupsen/logrus?status.png)][godoc]
|
||||||
|
|
||||||
Logrus is a structured logger for Go (golang), completely API compatible with
|
Logrus is a structured logger for Go (golang), completely API compatible with
|
||||||
the standard library logger. [Godoc][godoc]. **Please note the Logrus API is not
|
the standard library logger. [Godoc][godoc]. **Please note the Logrus API is not
|
||||||
yet stable (pre 1.0), the core API is unlikely change much but please version
|
yet stable (pre 1.0). Logrus itself is completely stable and has been used in
|
||||||
control your Logrus to make sure you aren't fetching latest `master` on every
|
many large deployments. The core API is unlikely to change much but please
|
||||||
build.**
|
version control your Logrus to make sure you aren't fetching latest `master` on
|
||||||
|
every build.**
|
||||||
|
|
||||||
Nicely color-coded in development (when a TTY is attached, otherwise just
|
Nicely color-coded in development (when a TTY is attached, otherwise just
|
||||||
plain text):
|
plain text):
|
||||||
|
@ -33,7 +34,7 @@ ocean","size":10,"time":"2014-03-10 19:57:38.562264131 -0400 EDT"}
|
||||||
|
|
||||||
With the default `log.Formatter = new(logrus.TextFormatter)` when a TTY is not
|
With the default `log.Formatter = new(logrus.TextFormatter)` when a TTY is not
|
||||||
attached, the output is compatible with the
|
attached, the output is compatible with the
|
||||||
[l2met](http://r.32k.io/l2met-introduction) format:
|
[logfmt](http://godoc.org/github.com/kr/logfmt) format:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
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
|
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 (
|
||||||
log "github.com/Sirupsen/logrus"
|
log "github.com/Sirupsen/logrus"
|
||||||
"github.com/Sirupsen/logrus/hooks/airbrake"
|
"github.com/Sirupsen/logrus/hooks/airbrake"
|
||||||
"github.com/Sirupsen/logrus/hooks/syslog"
|
"github.com/Sirupsen/logrus/hooks/syslog"
|
||||||
|
"log/syslog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
log.AddHook(new(logrus_airbrake.AirbrakeHook))
|
log.AddHook(new(logrus_airbrake.AirbrakeHook))
|
||||||
log.AddHook(logrus_syslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, ""))
|
|
||||||
|
hook, err := logrus_syslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to connect to local syslog daemon")
|
||||||
|
} else {
|
||||||
|
log.AddHook(hook)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -228,6 +236,15 @@ func init() {
|
||||||
* [`github.com/nubo/hiprus`](https://github.com/nubo/hiprus)
|
* [`github.com/nubo/hiprus`](https://github.com/nubo/hiprus)
|
||||||
Send errors to a channel in hipchat.
|
Send errors to a channel in hipchat.
|
||||||
|
|
||||||
|
* [`github.com/sebest/logrusly`](https://github.com/sebest/logrusly)
|
||||||
|
Send logs to Loggly (https://www.loggly.com/)
|
||||||
|
|
||||||
|
* [`github.com/johntdyer/slackrus`](https://github.com/johntdyer/slackrus)
|
||||||
|
Hook for Slack chat.
|
||||||
|
|
||||||
|
* [`github.com/wercker/journalhook`](https://github.com/wercker/journalhook).
|
||||||
|
Hook for logging to `systemd-journald`.
|
||||||
|
|
||||||
#### Level logging
|
#### Level logging
|
||||||
|
|
||||||
Logrus has six logging levels: Debug, Info, Warning, Error, Fatal and Panic.
|
Logrus has six logging levels: Debug, Info, Warning, Error, Fatal and Panic.
|
||||||
|
@ -307,7 +324,7 @@ The built-in logging formatters are:
|
||||||
|
|
||||||
Third party logging formatters:
|
Third party logging formatters:
|
||||||
|
|
||||||
* [`zalgo`](https://github.com/aybabtme/logzalgo): invoking the P͉̫o̳̼̊w̖͈̰͎e̬͔̭͂r͚̼̹̲ ̫͓͉̳͈ō̠͕͖̚f̝͍̠ ͕̲̞͖͑Z̖̫̤̫ͪa͉̬͈̗l͖͎g̳̥o̰̥̅!̣͔̲̻͊̄ ̙̘̦̹̦.
|
* [`zalgo`](https://github.com/aybabtme/logzalgo): invoking the P͉̫o̳̼̊w̖͈̰͎e̬͔̭͂r͚̼̹̲ ̫͓͉̳͈ō̠͕͖̚f̝͍̠ ͕̲̞͖͑Z̖̫̤̫ͪa͉̬͈̗l͖͎g̳̥o̰̥̅!̣͔̲̻͊̄ ̙̘̦̹̦.
|
||||||
|
|
||||||
You can define your formatter by implementing the `Formatter` interface,
|
You can define your formatter by implementing the `Formatter` interface,
|
||||||
requiring a `Format` method. `Format` takes an `*Entry`. `entry.Data` is a
|
requiring a `Format` method. `Format` takes an `*Entry`. `entry.Data` is a
|
||||||
|
@ -332,10 +349,28 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Logger as an `io.Writer`
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
```go
|
||||||
|
w := logger.Writer()
|
||||||
|
defer w.Close()
|
||||||
|
|
||||||
|
srv := http.Server{
|
||||||
|
// create a stdlib log.Logger that writes to
|
||||||
|
// logrus.Logger.
|
||||||
|
ErrorLog: log.New(w, "", 0),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each line written to that writer will be printed the usual way, using formatters
|
||||||
|
and hooks. The level for those entries is `info`.
|
||||||
|
|
||||||
#### Rotation
|
#### Rotation
|
||||||
|
|
||||||
Log rotation is not provided with Logrus. Log rotation should be done by an
|
Log rotation is not provided with Logrus. Log rotation should be done by an
|
||||||
external program (like `logrotated(8)`) that can compress and delete old log
|
external program (like `logrotate(8)`) that can compress and delete old log
|
||||||
entries. It should not be a feature of the application-level logger.
|
entries. It should not be a feature of the application-level logger.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -100,7 +100,7 @@ func (entry *Entry) log(level Level, msg string) {
|
||||||
// panic() to use in Entry#Panic(), we avoid the allocation by checking
|
// panic() to use in Entry#Panic(), we avoid the allocation by checking
|
||||||
// directly here.
|
// directly here.
|
||||||
if level <= PanicLevel {
|
if level <= PanicLevel {
|
||||||
panic(reader.String())
|
panic(entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,6 +126,10 @@ func (entry *Entry) Warn(args ...interface{}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Warning(args ...interface{}) {
|
||||||
|
entry.Warn(args...)
|
||||||
|
}
|
||||||
|
|
||||||
func (entry *Entry) Error(args ...interface{}) {
|
func (entry *Entry) Error(args ...interface{}) {
|
||||||
if entry.Logger.Level >= ErrorLevel {
|
if entry.Logger.Level >= ErrorLevel {
|
||||||
entry.log(ErrorLevel, fmt.Sprint(args...))
|
entry.log(ErrorLevel, fmt.Sprint(args...))
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEntryPanicln(t *testing.T) {
|
||||||
|
errBoom := fmt.Errorf("boom time")
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
p := recover()
|
||||||
|
assert.NotNil(t, p)
|
||||||
|
|
||||||
|
switch pVal := p.(type) {
|
||||||
|
case *Entry:
|
||||||
|
assert.Equal(t, "kaboom", pVal.Message)
|
||||||
|
assert.Equal(t, errBoom, pVal.Data["err"])
|
||||||
|
default:
|
||||||
|
t.Fatalf("want type *Entry, got %T: %#v", pVal, pVal)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
logger := New()
|
||||||
|
logger.Out = &bytes.Buffer{}
|
||||||
|
entry := NewEntry(logger)
|
||||||
|
entry.WithField("err", errBoom).Panicln("kaboom")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryPanicf(t *testing.T) {
|
||||||
|
errBoom := fmt.Errorf("boom again")
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
p := recover()
|
||||||
|
assert.NotNil(t, p)
|
||||||
|
|
||||||
|
switch pVal := p.(type) {
|
||||||
|
case *Entry:
|
||||||
|
assert.Equal(t, "kaboom true", pVal.Message)
|
||||||
|
assert.Equal(t, errBoom, pVal.Data["err"])
|
||||||
|
default:
|
||||||
|
t.Fatalf("want type *Entry, got %T: %#v", pVal, pVal)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
logger := New()
|
||||||
|
logger.Out = &bytes.Buffer{}
|
||||||
|
entry := NewEntry(logger)
|
||||||
|
entry.WithField("err", errBoom).Panicf("kaboom %v", true)
|
||||||
|
}
|
|
@ -9,9 +9,26 @@ var log = logrus.New()
|
||||||
func init() {
|
func init() {
|
||||||
log.Formatter = new(logrus.JSONFormatter)
|
log.Formatter = new(logrus.JSONFormatter)
|
||||||
log.Formatter = new(logrus.TextFormatter) // default
|
log.Formatter = new(logrus.TextFormatter) // default
|
||||||
|
log.Level = logrus.DebugLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
defer func() {
|
||||||
|
err := recover()
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"omg": true,
|
||||||
|
"err": err,
|
||||||
|
"number": 100,
|
||||||
|
}).Fatal("The ice breaks!")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"animal": "walrus",
|
||||||
|
"number": 8,
|
||||||
|
}).Debug("Started observing beach")
|
||||||
|
|
||||||
log.WithFields(logrus.Fields{
|
log.WithFields(logrus.Fields{
|
||||||
"animal": "walrus",
|
"animal": "walrus",
|
||||||
"size": 10,
|
"size": 10,
|
||||||
|
@ -23,7 +40,11 @@ func main() {
|
||||||
}).Warn("The group's number increased tremendously!")
|
}).Warn("The group's number increased tremendously!")
|
||||||
|
|
||||||
log.WithFields(logrus.Fields{
|
log.WithFields(logrus.Fields{
|
||||||
"omg": true,
|
"temperature": -4,
|
||||||
"number": 100,
|
}).Debug("Temperature changes")
|
||||||
}).Fatal("The ice breaks!")
|
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"animal": "orca",
|
||||||
|
"size": 9009,
|
||||||
|
}).Panic("It's over 9000!")
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,10 @@ var (
|
||||||
std = New()
|
std = New()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func StandardLogger() *Logger {
|
||||||
|
return std
|
||||||
|
}
|
||||||
|
|
||||||
// SetOutput sets the standard logger output.
|
// SetOutput sets the standard logger output.
|
||||||
func SetOutput(out io.Writer) {
|
func SetOutput(out io.Writer) {
|
||||||
std.mu.Lock()
|
std.mu.Lock()
|
||||||
|
@ -30,6 +34,13 @@ func SetLevel(level Level) {
|
||||||
std.Level = level
|
std.Level = level
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetLevel returns the standard logger level.
|
||||||
|
func GetLevel() Level {
|
||||||
|
std.mu.Lock()
|
||||||
|
defer std.mu.Unlock()
|
||||||
|
return std.Level
|
||||||
|
}
|
||||||
|
|
||||||
// AddHook adds a hook to the standard logger hooks.
|
// AddHook adds a hook to the standard logger hooks.
|
||||||
func AddHook(hook Hook) {
|
func AddHook(hook Hook) {
|
||||||
std.mu.Lock()
|
std.mu.Lock()
|
||||||
|
|
|
@ -26,19 +26,19 @@ type Formatter interface {
|
||||||
//
|
//
|
||||||
// It's not exported because it's still using Data in an opinionated way. It's to
|
// 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.
|
// avoid code duplication between the two default formatters.
|
||||||
func prefixFieldClashes(entry *Entry) {
|
func prefixFieldClashes(data Fields) {
|
||||||
_, ok := entry.Data["time"]
|
_, ok := data["time"]
|
||||||
if ok {
|
if ok {
|
||||||
entry.Data["fields.time"] = entry.Data["time"]
|
data["fields.time"] = data["time"]
|
||||||
}
|
}
|
||||||
|
|
||||||
_, ok = entry.Data["msg"]
|
_, ok = data["msg"]
|
||||||
if ok {
|
if ok {
|
||||||
entry.Data["fields.msg"] = entry.Data["msg"]
|
data["fields.msg"] = data["msg"]
|
||||||
}
|
}
|
||||||
|
|
||||||
_, ok = entry.Data["level"]
|
_, ok = data["level"]
|
||||||
if ok {
|
if ok {
|
||||||
entry.Data["fields.level"] = entry.Data["level"]
|
data["fields.level"] = data["level"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
// with the Airbrake API. You must set:
|
// with the Airbrake API. You must set:
|
||||||
// * airbrake.Endpoint
|
// * airbrake.Endpoint
|
||||||
// * airbrake.ApiKey
|
// * airbrake.ApiKey
|
||||||
// * airbrake.Environment (only sends exceptions when set to "production")
|
// * airbrake.Environment
|
||||||
//
|
//
|
||||||
// Before using this hook, to send an error. Entries that trigger an Error,
|
// Before using this hook, to send an error. Entries that trigger an Error,
|
||||||
// Fatal or Panic should now include an "error" field to send to Airbrake.
|
// 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,
|
||||||
// Fire is called when a log event is fired.
|
// Fire is called when a log event is fired.
|
||||||
func (hook *PapertrailHook) Fire(entry *logrus.Entry) error {
|
func (hook *PapertrailHook) Fire(entry *logrus.Entry) error {
|
||||||
date := time.Now().Format(format)
|
date := time.Now().Format(format)
|
||||||
payload := fmt.Sprintf("<22> %s %s: [%s] %s", date, hook.AppName, entry.Data["level"], entry.Message)
|
msg, _ := entry.String()
|
||||||
|
payload := fmt.Sprintf("<22> %s %s: %s", date, hook.AppName, msg)
|
||||||
|
|
||||||
bytesWritten, err := hook.UDPConn.Write([]byte(payload))
|
bytesWritten, err := hook.UDPConn.Write([]byte(payload))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -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.Second
|
||||||
|
```
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -6,7 +6,7 @@
|
||||||
import (
|
import (
|
||||||
"log/syslog"
|
"log/syslog"
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
"github.com/Sirupsen/logrus/hooks/syslog"
|
logrus_syslog "github.com/Sirupsen/logrus/hooks/syslog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -17,4 +17,4 @@ func main() {
|
||||||
log.Hooks.Add(hook)
|
log.Hooks.Add(hook)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
@ -29,18 +29,18 @@ func (hook *SyslogHook) Fire(entry *logrus.Entry) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
switch entry.Data["level"] {
|
switch entry.Level {
|
||||||
case "panic":
|
case logrus.PanicLevel:
|
||||||
return hook.Writer.Crit(line)
|
return hook.Writer.Crit(line)
|
||||||
case "fatal":
|
case logrus.FatalLevel:
|
||||||
return hook.Writer.Crit(line)
|
return hook.Writer.Crit(line)
|
||||||
case "error":
|
case logrus.ErrorLevel:
|
||||||
return hook.Writer.Err(line)
|
return hook.Writer.Err(line)
|
||||||
case "warn":
|
case logrus.WarnLevel:
|
||||||
return hook.Writer.Warning(line)
|
return hook.Writer.Warning(line)
|
||||||
case "info":
|
case logrus.InfoLevel:
|
||||||
return hook.Writer.Info(line)
|
return hook.Writer.Info(line)
|
||||||
case "debug":
|
case logrus.DebugLevel:
|
||||||
return hook.Writer.Debug(line)
|
return hook.Writer.Debug(line)
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -9,12 +9,22 @@ import (
|
||||||
type JSONFormatter struct{}
|
type JSONFormatter struct{}
|
||||||
|
|
||||||
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
|
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
|
||||||
prefixFieldClashes(entry)
|
data := make(Fields, len(entry.Data)+3)
|
||||||
entry.Data["time"] = entry.Time.Format(time.RFC3339)
|
for k, v := range entry.Data {
|
||||||
entry.Data["msg"] = entry.Message
|
// Otherwise errors are ignored by `encoding/json`
|
||||||
entry.Data["level"] = entry.Level.String()
|
// https://github.com/Sirupsen/logrus/issues/137
|
||||||
|
if err, ok := v.(error); ok {
|
||||||
|
data[k] = err.Error()
|
||||||
|
} else {
|
||||||
|
data[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prefixFieldClashes(data)
|
||||||
|
data["time"] = entry.Time.Format(time.RFC3339)
|
||||||
|
data["msg"] = entry.Message
|
||||||
|
data["level"] = entry.Level.String()
|
||||||
|
|
||||||
serialized, err := json.Marshal(entry.Data)
|
serialized, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
|
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestErrorNotLost(t *testing.T) {
|
||||||
|
formatter := &JSONFormatter{}
|
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("error", errors.New("wild walrus")))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := make(map[string]interface{})
|
||||||
|
err = json.Unmarshal(b, &entry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to unmarshal formatted entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry["error"] != "wild walrus" {
|
||||||
|
t.Fatal("Error field not set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrorNotLostOnFieldNotNamedError(t *testing.T) {
|
||||||
|
formatter := &JSONFormatter{}
|
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("omg", errors.New("wild walrus")))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := make(map[string]interface{})
|
||||||
|
err = json.Unmarshal(b, &entry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to unmarshal formatted entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry["omg"] != "wild walrus" {
|
||||||
|
t.Fatal("Error field not set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldClashWithTime(t *testing.T) {
|
||||||
|
formatter := &JSONFormatter{}
|
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("time", "right now!"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := make(map[string]interface{})
|
||||||
|
err = json.Unmarshal(b, &entry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to unmarshal formatted entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry["fields.time"] != "right now!" {
|
||||||
|
t.Fatal("fields.time not set to original time field")
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry["time"] != "0001-01-01T00:00:00Z" {
|
||||||
|
t.Fatal("time field not set to current time, was: ", entry["time"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldClashWithMsg(t *testing.T) {
|
||||||
|
formatter := &JSONFormatter{}
|
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("msg", "something"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := make(map[string]interface{})
|
||||||
|
err = json.Unmarshal(b, &entry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to unmarshal formatted entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry["fields.msg"] != "something" {
|
||||||
|
t.Fatal("fields.msg not set to original msg field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldClashWithLevel(t *testing.T) {
|
||||||
|
formatter := &JSONFormatter{}
|
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("level", "something"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := make(map[string]interface{})
|
||||||
|
err = json.Unmarshal(b, &entry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to unmarshal formatted entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry["fields.level"] != "something" {
|
||||||
|
t.Fatal("fields.level not set to original level field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONEntryEndsWithNewline(t *testing.T) {
|
||||||
|
formatter := &JSONFormatter{}
|
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("level", "something"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b[len(b)-1] != '\n' {
|
||||||
|
t.Fatal("Expected JSON log entry to end with a newline")
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,7 +38,7 @@ type Logger struct {
|
||||||
// Out: os.Stderr,
|
// Out: os.Stderr,
|
||||||
// Formatter: new(JSONFormatter),
|
// Formatter: new(JSONFormatter),
|
||||||
// Hooks: make(levelHooks),
|
// Hooks: make(levelHooks),
|
||||||
// Level: logrus.Debug,
|
// Level: logrus.DebugLevel,
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// It's recommended to make this a global instance called `log`.
|
// It's recommended to make this a global instance called `log`.
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -44,8 +45,12 @@ func LogAndAssertText(t *testing.T, log func(*Logger), assertions func(fields ma
|
||||||
}
|
}
|
||||||
kvArr := strings.Split(kv, "=")
|
kvArr := strings.Split(kv, "=")
|
||||||
key := strings.TrimSpace(kvArr[0])
|
key := strings.TrimSpace(kvArr[0])
|
||||||
val, err := strconv.Unquote(kvArr[1])
|
val := kvArr[1]
|
||||||
assert.NoError(t, err)
|
if kvArr[1][0] == '"' {
|
||||||
|
var err error
|
||||||
|
val, err = strconv.Unquote(val)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
fields[key] = val
|
fields[key] = val
|
||||||
}
|
}
|
||||||
assertions(fields)
|
assertions(fields)
|
||||||
|
@ -204,6 +209,38 @@ func TestDefaultFieldsAreNotPrefixed(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.Equal(t, len(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.Equal(t, len(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) {
|
func TestConvertLevelToString(t *testing.T) {
|
||||||
assert.Equal(t, "debug", DebugLevel.String())
|
assert.Equal(t, "debug", DebugLevel.String())
|
||||||
assert.Equal(t, "info", InfoLevel.String())
|
assert.Equal(t, "info", InfoLevel.String())
|
||||||
|
@ -245,3 +282,20 @@ func TestParseLevel(t *testing.T) {
|
||||||
l, err = ParseLevel("invalid")
|
l, err = ParseLevel("invalid")
|
||||||
assert.Equal(t, "not a valid logrus Level: \"invalid\"", err.Error())
|
assert.Equal(t, "not a valid logrus Level: \"invalid\"", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetSetLevelRace(t *testing.T) {
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(i int) {
|
||||||
|
defer wg.Done()
|
||||||
|
if i%2 == 0 {
|
||||||
|
SetLevel(InfoLevel)
|
||||||
|
} else {
|
||||||
|
GetLevel()
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// +build linux,!appengine darwin freebsd
|
// +build linux darwin freebsd openbsd
|
||||||
|
|
||||||
package logrus
|
package logrus
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
const ioctlReadTermios = syscall.TIOCGETA
|
||||||
|
|
||||||
|
type Termios syscall.Termios
|
|
@ -3,6 +3,7 @@ package logrus
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -14,11 +15,13 @@ const (
|
||||||
green = 32
|
green = 32
|
||||||
yellow = 33
|
yellow = 33
|
||||||
blue = 34
|
blue = 34
|
||||||
|
gray = 37
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
baseTimestamp time.Time
|
baseTimestamp time.Time
|
||||||
isTerminal bool
|
isTerminal bool
|
||||||
|
noQuoteNeeded *regexp.Regexp
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -32,28 +35,47 @@ func miniTS() int {
|
||||||
|
|
||||||
type TextFormatter struct {
|
type TextFormatter struct {
|
||||||
// Set to true to bypass checking for a TTY before outputting colors.
|
// Set to true to bypass checking for a TTY before outputting colors.
|
||||||
ForceColors bool
|
ForceColors bool
|
||||||
|
|
||||||
|
// Force disabling colors.
|
||||||
DisableColors bool
|
DisableColors bool
|
||||||
|
|
||||||
|
// Disable timestamp logging. useful when output is redirected to logging
|
||||||
|
// system that already adds timestamps.
|
||||||
|
DisableTimestamp bool
|
||||||
|
|
||||||
|
// Enable logging the full timestamp when a TTY is attached instead of just
|
||||||
|
// the time passed since beginning of execution.
|
||||||
|
FullTimestamp bool
|
||||||
|
|
||||||
|
// The fields are sorted by default for a consistent output. For applications
|
||||||
|
// that log extremely frequently and don't use the JSON formatter this may not
|
||||||
|
// be desired.
|
||||||
|
DisableSorting bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
|
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
|
||||||
|
var keys []string = make([]string, 0, len(entry.Data))
|
||||||
var keys []string
|
|
||||||
for k := range entry.Data {
|
for k := range entry.Data {
|
||||||
keys = append(keys, k)
|
keys = append(keys, k)
|
||||||
}
|
}
|
||||||
sort.Strings(keys)
|
|
||||||
|
if !f.DisableSorting {
|
||||||
|
sort.Strings(keys)
|
||||||
|
}
|
||||||
|
|
||||||
b := &bytes.Buffer{}
|
b := &bytes.Buffer{}
|
||||||
|
|
||||||
prefixFieldClashes(entry)
|
prefixFieldClashes(entry.Data)
|
||||||
|
|
||||||
isColored := (f.ForceColors || isTerminal) && !f.DisableColors
|
isColored := (f.ForceColors || isTerminal) && !f.DisableColors
|
||||||
|
|
||||||
if isColored {
|
if isColored {
|
||||||
printColored(b, entry, keys)
|
f.printColored(b, entry, keys)
|
||||||
} else {
|
} 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, "level", entry.Level.String())
|
||||||
f.appendKeyValue(b, "msg", entry.Message)
|
f.appendKeyValue(b, "msg", entry.Message)
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
|
@ -65,9 +87,11 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
|
||||||
return b.Bytes(), nil
|
return b.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func printColored(b *bytes.Buffer, entry *Entry, keys []string) {
|
func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string) {
|
||||||
var levelColor int
|
var levelColor int
|
||||||
switch entry.Level {
|
switch entry.Level {
|
||||||
|
case DebugLevel:
|
||||||
|
levelColor = gray
|
||||||
case WarnLevel:
|
case WarnLevel:
|
||||||
levelColor = yellow
|
levelColor = yellow
|
||||||
case ErrorLevel, FatalLevel, PanicLevel:
|
case ErrorLevel, FatalLevel, PanicLevel:
|
||||||
|
@ -78,17 +102,43 @@ func printColored(b *bytes.Buffer, entry *Entry, keys []string) {
|
||||||
|
|
||||||
levelText := strings.ToUpper(entry.Level.String())[0:4]
|
levelText := strings.ToUpper(entry.Level.String())[0:4]
|
||||||
|
|
||||||
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, miniTS(), entry.Message)
|
if !f.FullTimestamp {
|
||||||
|
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, miniTS(), entry.Message)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %-44s ", levelColor, levelText, entry.Time.Format(time.RFC3339), entry.Message)
|
||||||
|
}
|
||||||
for _, k := range keys {
|
for _, k := range keys {
|
||||||
v := entry.Data[k]
|
v := entry.Data[k]
|
||||||
fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=%v", levelColor, k, v)
|
fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=%v", levelColor, k, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func needsQuoting(text string) bool {
|
||||||
|
for _, ch := range text {
|
||||||
|
if !((ch >= 'a' && ch <= 'z') ||
|
||||||
|
(ch >= 'A' && ch <= 'Z') ||
|
||||||
|
(ch >= '0' && ch <= '9') ||
|
||||||
|
ch == '-' || ch == '.') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key, value interface{}) {
|
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key, value interface{}) {
|
||||||
switch value.(type) {
|
switch value.(type) {
|
||||||
case string, error:
|
case string:
|
||||||
fmt.Fprintf(b, "%v=%q ", key, value)
|
if needsQuoting(value.(string)) {
|
||||||
|
fmt.Fprintf(b, "%v=%s ", key, value)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(b, "%v=%q ", key, value)
|
||||||
|
}
|
||||||
|
case error:
|
||||||
|
if needsQuoting(value.(error).Error()) {
|
||||||
|
fmt.Fprintf(b, "%v=%s ", key, value)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(b, "%v=%q ", key, value)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(b, "%v=%v ", key, value)
|
fmt.Fprintf(b, "%v=%v ", key, value)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestQuoting(t *testing.T) {
|
||||||
|
tf := &TextFormatter{DisableColors: true}
|
||||||
|
|
||||||
|
checkQuoting := func(q bool, value interface{}) {
|
||||||
|
b, _ := tf.Format(WithField("test", value))
|
||||||
|
idx := bytes.Index(b, ([]byte)("test="))
|
||||||
|
cont := bytes.Contains(b[idx+5:], []byte{'"'})
|
||||||
|
if cont != q {
|
||||||
|
if q {
|
||||||
|
t.Errorf("quoting expected for: %#v", value)
|
||||||
|
} else {
|
||||||
|
t.Errorf("quoting not expected for: %#v", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkQuoting(false, "abcd")
|
||||||
|
checkQuoting(false, "v1.0")
|
||||||
|
checkQuoting(false, "1234567890")
|
||||||
|
checkQuoting(true, "/foobar")
|
||||||
|
checkQuoting(true, "x y")
|
||||||
|
checkQuoting(true, "x,y")
|
||||||
|
checkQuoting(false, errors.New("invalid"))
|
||||||
|
checkQuoting(true, errors.New("invalid argument"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO add tests for sorting etc., this requires a parser for the text
|
||||||
|
// formatter output.
|
|
@ -0,0 +1,31 @@
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (logger *Logger) Writer() (*io.PipeWriter) {
|
||||||
|
reader, writer := io.Pipe()
|
||||||
|
|
||||||
|
go logger.writerScanner(reader)
|
||||||
|
runtime.SetFinalizer(writer, writerFinalizer)
|
||||||
|
|
||||||
|
return writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) writerScanner(reader *io.PipeReader) {
|
||||||
|
scanner := bufio.NewScanner(reader)
|
||||||
|
for scanner.Scan() {
|
||||||
|
logger.Print(scanner.Text())
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
logger.Errorf("Error while reading from Writer: %s", err)
|
||||||
|
}
|
||||||
|
reader.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func writerFinalizer(writer *io.PipeWriter) {
|
||||||
|
writer.Close()
|
||||||
|
}
|
Загрузка…
Ссылка в новой задаче