Merge branch 'master' into caller_feature

This commit is contained in:
David Bariod 2018-10-23 06:22:00 +02:00
commit 64d5b7e66c
35 changed files with 1414 additions and 249 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
logrus logrus
vendor

View File

@ -1,15 +1,51 @@
language: go language: go
go:
- 1.6.x
- 1.7.x
- 1.8.x
- tip
env: env:
- GOMAXPROCS=4 GORACE=halt_on_error=1 - GOMAXPROCS=4 GORACE=halt_on_error=1
install: matrix:
- go get github.com/stretchr/testify/assert include:
- go get gopkg.in/gemnasium/logrus-airbrake-hook.v2 - go: 1.10.x
- go get golang.org/x/sys/unix install:
- go get golang.org/x/sys/windows - go get github.com/stretchr/testify/assert
script: - go get golang.org/x/crypto/ssh/terminal
- go test -race -v ./... - go get golang.org/x/sys/unix
- go get golang.org/x/sys/windows
script:
- go test -race -v ./...
- go: 1.11.x
env: GO111MODULE=on
install:
- go mod download
script:
- go test -race -v ./...
- go: 1.11.x
env: GO111MODULE=off
install:
- go get github.com/stretchr/testify/assert
- go get golang.org/x/crypto/ssh/terminal
- go get golang.org/x/sys/unix
- go get golang.org/x/sys/windows
script:
- go test -race -v ./...
- go: 1.10.x
install:
- go get github.com/stretchr/testify/assert
- go get golang.org/x/crypto/ssh/terminal
- go get golang.org/x/sys/unix
- go get golang.org/x/sys/windows
script:
- go test -race -v -tags appengine ./...
- go: 1.11.x
env: GO111MODULE=on
install:
- go mod download
script:
- go test -race -v -tags appengine ./...
- go: 1.11.x
env: GO111MODULE=off
install:
- go get github.com/stretchr/testify/assert
- go get golang.org/x/crypto/ssh/terminal
- go get golang.org/x/sys/unix
- go get golang.org/x/sys/windows
script:
- go test -race -v -tags appengine ./...

View File

@ -1,5 +1,42 @@
# 1.1.1
This is a bug fix release.
* fix the build break on Solaris
* don't drop a whole trace in JSONFormatter when a field param is a function pointer which can not be serialized
# 1.1.0
This new release introduces:
* several fixes:
* a fix for a race condition on entry formatting
* proper cleanup of previously used entries before putting them back in the pool
* the extra new line at the end of message in text formatter has been removed
* a new global public API to check if a level is activated: IsLevelEnabled
* the following methods have been added to the Logger object
* IsLevelEnabled
* SetFormatter
* SetOutput
* ReplaceHooks
* introduction of go module
* an indent configuration for the json formatter
* output colour support for windows
* the field sort function is now configurable for text formatter
* the CLICOLOR and CLICOLOR\_FORCE environment variable support in text formater
# 1.0.6
This new release introduces:
* a new api WithTime which allows to easily force the time of the log entry
which is mostly useful for logger wrapper
* a fix reverting the immutability of the entry given as parameter to the hooks
a new configuration field of the json formatter in order to put all the fields
in a nested dictionnary
* a new SetOutput method in the Logger
* a new configuration of the textformatter to configure the name of the default keys
* a new configuration of the text formatter to disable the level truncation
# 1.0.5 # 1.0.5
* Add optional logging of caller method
* Fix hooks race (#707)
* Fix panic deadlock (#695)
# 1.0.4 # 1.0.4

View File

@ -272,66 +272,15 @@ func init() {
``` ```
Note: Syslog hook also support connecting to local syslog (Ex. "/dev/log" or "/var/run/syslog" or "/var/run/log"). For the detail, please check the [syslog hook README](hooks/syslog/README.md). Note: Syslog hook also support connecting to local syslog (Ex. "/dev/log" or "/var/run/syslog" or "/var/run/log"). For the detail, please check the [syslog hook README](hooks/syslog/README.md).
| Hook | Description | A list of currently known of service hook can be found in this wiki [page](https://github.com/sirupsen/logrus/wiki/Hooks)
| ----- | ----------- |
| [Airbrake "legacy"](https://github.com/gemnasium/logrus-airbrake-legacy-hook) | Send errors to an exception tracking service compatible with the Airbrake API V2. Uses [`airbrake-go`](https://github.com/tobi/airbrake-go) behind the scenes. |
| [Airbrake](https://github.com/gemnasium/logrus-airbrake-hook) | Send errors to the Airbrake API V3. Uses the official [`gobrake`](https://github.com/airbrake/gobrake) behind the scenes. |
| [Amazon Kinesis](https://github.com/evalphobia/logrus_kinesis) | Hook for logging to [Amazon Kinesis](https://aws.amazon.com/kinesis/) |
| [Amqp-Hook](https://github.com/vladoatanasov/logrus_amqp) | Hook for logging to Amqp broker (Like RabbitMQ) |
| [Application Insights](https://github.com/jjcollinge/logrus-appinsights) | Hook for logging to [Application Insights](https://azure.microsoft.com/en-us/services/application-insights/)
| [AzureTableHook](https://github.com/kpfaulkner/azuretablehook/) | Hook for logging to Azure Table Storage|
| [Bugsnag](https://github.com/Shopify/logrus-bugsnag/blob/master/bugsnag.go) | Send errors to the Bugsnag exception tracking service. |
| [DeferPanic](https://github.com/deferpanic/dp-logrus) | Hook for logging to DeferPanic |
| [Discordrus](https://github.com/kz/discordrus) | Hook for logging to [Discord](https://discordapp.com/) |
| [ElasticSearch](https://github.com/sohlich/elogrus) | Hook for logging to ElasticSearch|
| [Firehose](https://github.com/beaubrewer/logrus_firehose) | Hook for logging to [Amazon Firehose](https://aws.amazon.com/kinesis/firehose/)
| [Fluentd](https://github.com/evalphobia/logrus_fluent) | Hook for logging to fluentd |
| [Go-Slack](https://github.com/multiplay/go-slack) | Hook for logging to [Slack](https://slack.com) |
| [Graylog](https://github.com/gemnasium/logrus-graylog-hook) | Hook for logging to [Graylog](http://graylog2.org/) |
| [Hiprus](https://github.com/nubo/hiprus) | Send errors to a channel in hipchat. |
| [Honeybadger](https://github.com/agonzalezro/logrus_honeybadger) | Hook for sending exceptions to Honeybadger |
| [InfluxDB](https://github.com/Abramovic/logrus_influxdb) | Hook for logging to influxdb |
| [Influxus](http://github.com/vlad-doru/influxus) | Hook for concurrently logging to [InfluxDB](http://influxdata.com/) |
| [Journalhook](https://github.com/wercker/journalhook) | Hook for logging to `systemd-journald` |
| [KafkaLogrus](https://github.com/tracer0tong/kafkalogrus) | Hook for logging to Kafka |
| [Kafka REST Proxy](https://github.com/Nordstrom/logrus-kafka-rest-proxy) | Hook for logging to [Kafka REST Proxy](https://docs.confluent.io/current/kafka-rest/docs) |
| [LFShook](https://github.com/rifflock/lfshook) | Hook for logging to the local filesystem |
| [Logbeat](https://github.com/macandmia/logbeat) | Hook for logging to [Opbeat](https://opbeat.com/) |
| [Logentries](https://github.com/jcftang/logentriesrus) | Hook for logging to [Logentries](https://logentries.com/) |
| [Logentrus](https://github.com/puddingfactory/logentrus) | Hook for logging to [Logentries](https://logentries.com/) |
| [Logmatic.io](https://github.com/logmatic/logmatic-go) | Hook for logging to [Logmatic.io](http://logmatic.io/) |
| [Logrusly](https://github.com/sebest/logrusly) | Send logs to [Loggly](https://www.loggly.com/) |
| [Logstash](https://github.com/bshuster-repo/logrus-logstash-hook) | Hook for logging to [Logstash](https://www.elastic.co/products/logstash) |
| [Mail](https://github.com/zbindenren/logrus_mail) | Hook for sending exceptions via mail |
| [Mattermost](https://github.com/shuLhan/mattermost-integration/tree/master/hooks/logrus) | Hook for logging to [Mattermost](https://mattermost.com/) |
| [Mongodb](https://github.com/weekface/mgorus) | Hook for logging to mongodb |
| [NATS-Hook](https://github.com/rybit/nats_logrus_hook) | Hook for logging to [NATS](https://nats.io) |
| [Octokit](https://github.com/dorajistyle/logrus-octokit-hook) | Hook for logging to github via octokit |
| [Papertrail](https://github.com/polds/logrus-papertrail-hook) | Send errors to the [Papertrail](https://papertrailapp.com) hosted logging service via UDP. |
| [PostgreSQL](https://github.com/gemnasium/logrus-postgresql-hook) | Send logs to [PostgreSQL](http://postgresql.org) |
| [Promrus](https://github.com/weaveworks/promrus) | Expose number of log messages as [Prometheus](https://prometheus.io/) metrics |
| [Pushover](https://github.com/toorop/logrus_pushover) | Send error via [Pushover](https://pushover.net) |
| [Raygun](https://github.com/squirkle/logrus-raygun-hook) | Hook for logging to [Raygun.io](http://raygun.io/) |
| [Redis-Hook](https://github.com/rogierlommers/logrus-redis-hook) | Hook for logging to a ELK stack (through Redis) |
| [Rollrus](https://github.com/heroku/rollrus) | Hook for sending errors to rollbar |
| [Scribe](https://github.com/sagar8192/logrus-scribe-hook) | Hook for logging to [Scribe](https://github.com/facebookarchive/scribe)|
| [Sentry](https://github.com/evalphobia/logrus_sentry) | Send errors to the Sentry error logging and aggregation service. |
| [Slackrus](https://github.com/johntdyer/slackrus) | Hook for Slack chat. |
| [Stackdriver](https://github.com/knq/sdhook) | Hook for logging to [Google Stackdriver](https://cloud.google.com/logging/) |
| [Sumorus](https://github.com/doublefree/sumorus) | Hook for logging to [SumoLogic](https://www.sumologic.com/)|
| [Syslog](https://github.com/sirupsen/logrus/blob/master/hooks/syslog/syslog.go) | Send errors to remote syslog server. Uses standard library `log/syslog` behind the scenes. |
| [Syslog TLS](https://github.com/shinji62/logrus-syslog-ng) | Send errors to remote syslog server with TLS support. |
| [Telegram](https://github.com/rossmcdonald/telegram_hook) | Hook for logging errors to [Telegram](https://telegram.org/) |
| [TraceView](https://github.com/evalphobia/logrus_appneta) | Hook for logging to [AppNeta TraceView](https://www.appneta.com/products/traceview/) |
| [Typetalk](https://github.com/dragon3/logrus-typetalk-hook) | Hook for logging to [Typetalk](https://www.typetalk.in/) |
| [logz.io](https://github.com/ripcurld00d/logrus-logzio-hook) | Hook for logging to [logz.io](https://logz.io), a Log as a Service using Logstash |
| [SQS-Hook](https://github.com/tsarpaul/logrus_sqs) | Hook for logging to [Amazon Simple Queue Service (SQS)](https://aws.amazon.com/sqs/) |
#### Level logging #### Level logging
Logrus has six logging levels: Debug, Info, Warning, Error, Fatal and Panic. Logrus has seven logging levels: Trace, Debug, Info, Warning, Error, Fatal and Panic.
```go ```go
log.Trace("Something very low level.")
log.Debug("Useful debugging information.") log.Debug("Useful debugging information.")
log.Info("Something noteworthy happened!") log.Info("Something noteworthy happened!")
log.Warn("You should probably take a look at this.") log.Warn("You should probably take a look at this.")
@ -403,6 +352,8 @@ The built-in logging formatters are:
field to `true`. To force no colored output even if there is a TTY set the field to `true`. To force no colored output even if there is a TTY set the
`DisableColors` field to `true`. For Windows, see `DisableColors` field to `true`. For Windows, see
[github.com/mattn/go-colorable](https://github.com/mattn/go-colorable). [github.com/mattn/go-colorable](https://github.com/mattn/go-colorable).
* When colors are enabled, levels are truncated to 4 characters by default. To disable
truncation set the `DisableLevelTruncation` field to `true`.
* All options are listed in the [generated docs](https://godoc.org/github.com/sirupsen/logrus#TextFormatter). * All options are listed in the [generated docs](https://godoc.org/github.com/sirupsen/logrus#TextFormatter).
* `logrus.JSONFormatter`. Logs fields as JSON. * `logrus.JSONFormatter`. Logs fields as JSON.
* All options are listed in the [generated docs](https://godoc.org/github.com/sirupsen/logrus#JSONFormatter). * All options are listed in the [generated docs](https://godoc.org/github.com/sirupsen/logrus#JSONFormatter).
@ -526,7 +477,7 @@ logrus.RegisterExitHandler(handler)
#### Thread safety #### Thread safety
By default Logger is protected by mutex for concurrent writes, this mutex is invoked when calling hooks and writing logs. By default, Logger is protected by a mutex for concurrent writes. The mutex is held when calling hooks and writing logs.
If you are sure such locking is not needed, you can call logger.SetNoLock() to disable the locking. If you are sure such locking is not needed, you can call logger.SetNoLock() to disable the locking.
Situation when locking is not needed includes: Situation when locking is not needed includes:

112
entry.go
View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"os" "os"
"reflect"
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
@ -36,9 +37,9 @@ func init() {
var ErrorKey = "error" var ErrorKey = "error"
// An entry is the final or intermediate Logrus logging entry. It contains all // An entry is the final or intermediate Logrus logging entry. It contains all
// the fields passed with WithField{,s}. It's finally logged when Debug, Info, // the fields passed with WithField{,s}. It's finally logged when Trace, Debug,
// Warn, Error, Fatal or Panic is called on it. These objects can be reused and // Info, Warn, Error, Fatal or Panic is called on it. These objects can be
// passed around as much as you wish to avoid field duplication. // reused and passed around as much as you wish to avoid field duplication.
type Entry struct { type Entry struct {
Logger *Logger Logger *Logger
@ -48,18 +49,21 @@ type Entry struct {
// Time at which the log entry was created // Time at which the log entry was created
Time time.Time Time time.Time
// Level the log entry was logged at: Debug, Info, Warn, Error, Fatal or Panic // Level the log entry was logged at: Trace, Debug, Info, Warn, Error, Fatal or Panic
// This field will be set on entry firing and the value will be equal to the one in Logger struct field. // This field will be set on entry firing and the value will be equal to the one in Logger struct field.
Level Level Level Level
// Calling method, with package name // Calling method, with package name
Caller string Caller string
// Message passed to Debug, Info, Warn, Error, Fatal or Panic // Message passed to Trace, Debug, Info, Warn, Error, Fatal or Panic
Message string Message string
// When formatter is called in entry.log(), an Buffer may be set to entry // When formatter is called in entry.log(), a Buffer may be set to entry
Buffer *bytes.Buffer Buffer *bytes.Buffer
// err may contain a field formatting error
err string
} }
func NewEntry(logger *Logger) *Entry { func NewEntry(logger *Logger) *Entry {
@ -97,10 +101,23 @@ func (entry *Entry) WithFields(fields Fields) *Entry {
for k, v := range entry.Data { for k, v := range entry.Data {
data[k] = v data[k] = v
} }
var field_err string
for k, v := range fields { for k, v := range fields {
data[k] = v if t := reflect.TypeOf(v); t != nil && t.Kind() == reflect.Func {
field_err = fmt.Sprintf("can not add field %q", k)
if entry.err != "" {
field_err = entry.err + ", " + field_err
}
} else {
data[k] = v
}
} }
return &Entry{Logger: entry.Logger, Data: data} return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time, err: field_err}
}
// Overrides the time of the Entry.
func (entry *Entry) WithTime(t time.Time) *Entry {
return &Entry{Logger: entry.Logger, Data: entry.Data, Time: t}
} }
// getPackageName reduces a fully qualified function name to the package name // getPackageName reduces a fully qualified function name to the package name
@ -157,7 +174,16 @@ func (entry Entry) HasCaller() (has bool) {
// race conditions will occur when using multiple goroutines // race conditions will occur when using multiple goroutines
func (entry Entry) log(level Level, msg string) { func (entry Entry) log(level Level, msg string) {
var buffer *bytes.Buffer var buffer *bytes.Buffer
entry.Time = time.Now()
// Default to now, but allow users to override if they want.
//
// We don't have to worry about polluting future calls to Entry#log()
// with this assignment because this function is declared with a
// non-pointer receiver.
if entry.Time.IsZero() {
entry.Time = time.Now()
}
entry.Level = level entry.Level = level
entry.Message = msg entry.Message = msg
if entry.Logger.ReportCaller { if entry.Logger.ReportCaller {
@ -183,21 +209,19 @@ func (entry Entry) log(level Level, msg string) {
} }
} }
// This function is not declared with a pointer value because otherwise func (entry *Entry) fireHooks() {
// race conditions will occur when using multiple goroutines
func (entry Entry) fireHooks() {
entry.Logger.mu.Lock() entry.Logger.mu.Lock()
defer entry.Logger.mu.Unlock() defer entry.Logger.mu.Unlock()
err := entry.Logger.Hooks.Fire(entry.Level, &entry) err := entry.Logger.Hooks.Fire(entry.Level, entry)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err) fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
} }
} }
func (entry *Entry) write() { func (entry *Entry) write() {
serialized, err := entry.Logger.Formatter.Format(entry)
entry.Logger.mu.Lock() entry.Logger.mu.Lock()
defer entry.Logger.mu.Unlock() defer entry.Logger.mu.Unlock()
serialized, err := entry.Logger.Formatter.Format(entry)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err) fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
} else { } else {
@ -208,8 +232,14 @@ func (entry *Entry) write() {
} }
} }
func (entry *Entry) Trace(args ...interface{}) {
if entry.Logger.IsLevelEnabled(TraceLevel) {
entry.log(TraceLevel, fmt.Sprint(args...))
}
}
func (entry *Entry) Debug(args ...interface{}) { func (entry *Entry) Debug(args ...interface{}) {
if entry.Logger.level() >= DebugLevel { if entry.Logger.IsLevelEnabled(DebugLevel) {
entry.log(DebugLevel, fmt.Sprint(args...)) entry.log(DebugLevel, fmt.Sprint(args...))
} }
} }
@ -219,13 +249,13 @@ func (entry *Entry) Print(args ...interface{}) {
} }
func (entry *Entry) Info(args ...interface{}) { func (entry *Entry) Info(args ...interface{}) {
if entry.Logger.level() >= InfoLevel { if entry.Logger.IsLevelEnabled(InfoLevel) {
entry.log(InfoLevel, fmt.Sprint(args...)) entry.log(InfoLevel, fmt.Sprint(args...))
} }
} }
func (entry *Entry) Warn(args ...interface{}) { func (entry *Entry) Warn(args ...interface{}) {
if entry.Logger.level() >= WarnLevel { if entry.Logger.IsLevelEnabled(WarnLevel) {
entry.log(WarnLevel, fmt.Sprint(args...)) entry.log(WarnLevel, fmt.Sprint(args...))
} }
} }
@ -235,20 +265,20 @@ func (entry *Entry) Warning(args ...interface{}) {
} }
func (entry *Entry) Error(args ...interface{}) { func (entry *Entry) Error(args ...interface{}) {
if entry.Logger.level() >= ErrorLevel { if entry.Logger.IsLevelEnabled(ErrorLevel) {
entry.log(ErrorLevel, fmt.Sprint(args...)) entry.log(ErrorLevel, fmt.Sprint(args...))
} }
} }
func (entry *Entry) Fatal(args ...interface{}) { func (entry *Entry) Fatal(args ...interface{}) {
if entry.Logger.level() >= FatalLevel { if entry.Logger.IsLevelEnabled(FatalLevel) {
entry.log(FatalLevel, fmt.Sprint(args...)) entry.log(FatalLevel, fmt.Sprint(args...))
} }
Exit(1) entry.Logger.Exit(1)
} }
func (entry *Entry) Panic(args ...interface{}) { func (entry *Entry) Panic(args ...interface{}) {
if entry.Logger.level() >= PanicLevel { if entry.Logger.IsLevelEnabled(PanicLevel) {
entry.log(PanicLevel, fmt.Sprint(args...)) entry.log(PanicLevel, fmt.Sprint(args...))
} }
panic(fmt.Sprint(args...)) panic(fmt.Sprint(args...))
@ -256,14 +286,20 @@ func (entry *Entry) Panic(args ...interface{}) {
// Entry Printf family functions // Entry Printf family functions
func (entry *Entry) Tracef(format string, args ...interface{}) {
if entry.Logger.IsLevelEnabled(TraceLevel) {
entry.Trace(fmt.Sprintf(format, args...))
}
}
func (entry *Entry) Debugf(format string, args ...interface{}) { func (entry *Entry) Debugf(format string, args ...interface{}) {
if entry.Logger.level() >= DebugLevel { if entry.Logger.IsLevelEnabled(DebugLevel) {
entry.Debug(fmt.Sprintf(format, args...)) entry.Debug(fmt.Sprintf(format, args...))
} }
} }
func (entry *Entry) Infof(format string, args ...interface{}) { func (entry *Entry) Infof(format string, args ...interface{}) {
if entry.Logger.level() >= InfoLevel { if entry.Logger.IsLevelEnabled(InfoLevel) {
entry.Info(fmt.Sprintf(format, args...)) entry.Info(fmt.Sprintf(format, args...))
} }
} }
@ -273,7 +309,7 @@ func (entry *Entry) Printf(format string, args ...interface{}) {
} }
func (entry *Entry) Warnf(format string, args ...interface{}) { func (entry *Entry) Warnf(format string, args ...interface{}) {
if entry.Logger.level() >= WarnLevel { if entry.Logger.IsLevelEnabled(WarnLevel) {
entry.Warn(fmt.Sprintf(format, args...)) entry.Warn(fmt.Sprintf(format, args...))
} }
} }
@ -283,34 +319,40 @@ func (entry *Entry) Warningf(format string, args ...interface{}) {
} }
func (entry *Entry) Errorf(format string, args ...interface{}) { func (entry *Entry) Errorf(format string, args ...interface{}) {
if entry.Logger.level() >= ErrorLevel { if entry.Logger.IsLevelEnabled(ErrorLevel) {
entry.Error(fmt.Sprintf(format, args...)) entry.Error(fmt.Sprintf(format, args...))
} }
} }
func (entry *Entry) Fatalf(format string, args ...interface{}) { func (entry *Entry) Fatalf(format string, args ...interface{}) {
if entry.Logger.level() >= FatalLevel { if entry.Logger.IsLevelEnabled(FatalLevel) {
entry.Fatal(fmt.Sprintf(format, args...)) entry.Fatal(fmt.Sprintf(format, args...))
} }
Exit(1) entry.Logger.Exit(1)
} }
func (entry *Entry) Panicf(format string, args ...interface{}) { func (entry *Entry) Panicf(format string, args ...interface{}) {
if entry.Logger.level() >= PanicLevel { if entry.Logger.IsLevelEnabled(PanicLevel) {
entry.Panic(fmt.Sprintf(format, args...)) entry.Panic(fmt.Sprintf(format, args...))
} }
} }
// Entry Println family functions // Entry Println family functions
func (entry *Entry) Traceln(args ...interface{}) {
if entry.Logger.IsLevelEnabled(TraceLevel) {
entry.Trace(entry.sprintlnn(args...))
}
}
func (entry *Entry) Debugln(args ...interface{}) { func (entry *Entry) Debugln(args ...interface{}) {
if entry.Logger.level() >= DebugLevel { if entry.Logger.IsLevelEnabled(DebugLevel) {
entry.Debug(entry.sprintlnn(args...)) entry.Debug(entry.sprintlnn(args...))
} }
} }
func (entry *Entry) Infoln(args ...interface{}) { func (entry *Entry) Infoln(args ...interface{}) {
if entry.Logger.level() >= InfoLevel { if entry.Logger.IsLevelEnabled(InfoLevel) {
entry.Info(entry.sprintlnn(args...)) entry.Info(entry.sprintlnn(args...))
} }
} }
@ -320,7 +362,7 @@ func (entry *Entry) Println(args ...interface{}) {
} }
func (entry *Entry) Warnln(args ...interface{}) { func (entry *Entry) Warnln(args ...interface{}) {
if entry.Logger.level() >= WarnLevel { if entry.Logger.IsLevelEnabled(WarnLevel) {
entry.Warn(entry.sprintlnn(args...)) entry.Warn(entry.sprintlnn(args...))
} }
} }
@ -330,20 +372,20 @@ func (entry *Entry) Warningln(args ...interface{}) {
} }
func (entry *Entry) Errorln(args ...interface{}) { func (entry *Entry) Errorln(args ...interface{}) {
if entry.Logger.level() >= ErrorLevel { if entry.Logger.IsLevelEnabled(ErrorLevel) {
entry.Error(entry.sprintlnn(args...)) entry.Error(entry.sprintlnn(args...))
} }
} }
func (entry *Entry) Fatalln(args ...interface{}) { func (entry *Entry) Fatalln(args ...interface{}) {
if entry.Logger.level() >= FatalLevel { if entry.Logger.IsLevelEnabled(FatalLevel) {
entry.Fatal(entry.sprintlnn(args...)) entry.Fatal(entry.sprintlnn(args...))
} }
Exit(1) entry.Logger.Exit(1)
} }
func (entry *Entry) Panicln(args ...interface{}) { func (entry *Entry) Panicln(args ...interface{}) {
if entry.Logger.level() >= PanicLevel { if entry.Logger.IsLevelEnabled(PanicLevel) {
entry.Panic(entry.sprintlnn(args...)) entry.Panic(entry.sprintlnn(args...))
} }
} }

View File

@ -1,16 +1,18 @@
package logrus_test package logrus_test
import ( import (
"github.com/sirupsen/logrus"
"os" "os"
"github.com/sirupsen/logrus"
) )
func Example_basic() { func Example_basic() {
var log = logrus.New() var log = logrus.New()
log.Formatter = new(logrus.JSONFormatter) log.Formatter = new(logrus.JSONFormatter)
log.Formatter = new(logrus.TextFormatter) //default log.Formatter = new(logrus.TextFormatter) //default
log.Formatter.(*logrus.TextFormatter).DisableColors = true // remove colors
log.Formatter.(*logrus.TextFormatter).DisableTimestamp = true // remove timestamp from test output log.Formatter.(*logrus.TextFormatter).DisableTimestamp = true // remove timestamp from test output
log.Level = logrus.DebugLevel log.Level = logrus.TraceLevel
log.Out = os.Stdout log.Out = os.Stdout
// file, err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY, 0666) // file, err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY, 0666)
@ -35,6 +37,11 @@ func Example_basic() {
} }
}() }()
log.WithFields(logrus.Fields{
"animal": "walrus",
"number": 0,
}).Trace("Went to the beach")
log.WithFields(logrus.Fields{ log.WithFields(logrus.Fields{
"animal": "walrus", "animal": "walrus",
"number": 8, "number": 8,
@ -60,6 +67,7 @@ func Example_basic() {
}).Panic("It's over 9000!") }).Panic("It's over 9000!")
// Output: // Output:
// level=trace msg="Went to the beach" animal=walrus number=0
// level=debug msg="Started observing beach" animal=walrus number=8 // level=debug msg="Started observing beach" animal=walrus number=8
// level=info msg="A group of walrus emerges from the ocean" animal=walrus size=10 // level=info msg="A group of walrus emerges from the ocean" animal=walrus size=10
// level=warning msg="The group's number increased tremendously!" number=122 omg=true // level=warning msg="The group's number increased tremendously!" number=122 omg=true

View File

@ -0,0 +1,36 @@
package logrus_test
import (
"github.com/sirupsen/logrus"
"os"
)
var (
mystring string
)
type GlobalHook struct {
}
func (h *GlobalHook) Levels() []logrus.Level {
return logrus.AllLevels
}
func (h *GlobalHook) Fire(e *logrus.Entry) error {
e.Data["mystring"] = mystring
return nil
}
func Example() {
l := logrus.New()
l.Out = os.Stdout
l.Formatter = &logrus.TextFormatter{DisableTimestamp: true, DisableColors: true}
l.AddHook(&GlobalHook{})
mystring = "first value"
l.Info("first log")
mystring = "another value"
l.Info("second log")
// Output:
// level=info msg="first log" mystring="first value"
// level=info msg="second log" mystring="another value"
}

View File

@ -1,16 +1,24 @@
// +build !windows
package logrus_test package logrus_test
import ( import (
"github.com/sirupsen/logrus" "log/syslog"
"gopkg.in/gemnasium/logrus-airbrake-hook.v2"
"os" "os"
"github.com/sirupsen/logrus"
slhooks "github.com/sirupsen/logrus/hooks/syslog"
) )
// An example on how to use a hook
func Example_hook() { func Example_hook() {
var log = logrus.New() var log = logrus.New()
log.Formatter = new(logrus.TextFormatter) // default log.Formatter = new(logrus.TextFormatter) // default
log.Formatter.(*logrus.TextFormatter).DisableColors = true // remove colors
log.Formatter.(*logrus.TextFormatter).DisableTimestamp = true // remove timestamp from test output log.Formatter.(*logrus.TextFormatter).DisableTimestamp = true // remove timestamp from test output
log.Hooks.Add(airbrake.NewHook(123, "xyz", "development")) if sl, err := slhooks.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, ""); err != nil {
log.Hooks.Add(sl)
}
log.Out = os.Stdout log.Out = os.Stdout
log.WithFields(logrus.Fields{ log.WithFields(logrus.Fields{

View File

@ -2,6 +2,7 @@ package logrus
import ( import (
"io" "io"
"time"
) )
var ( var (
@ -15,16 +16,12 @@ func StandardLogger() *Logger {
// 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.SetOutput(out)
defer std.mu.Unlock()
std.Out = out
} }
// SetFormatter sets the standard logger formatter. // SetFormatter sets the standard logger formatter.
func SetFormatter(formatter Formatter) { func SetFormatter(formatter Formatter) {
std.mu.Lock() std.SetFormatter(formatter)
defer std.mu.Unlock()
std.Formatter = formatter
} }
// SetReportCaller sets whether the standard logger will include the calling // SetReportCaller sets whether the standard logger will include the calling
@ -37,23 +34,22 @@ func SetReportCaller(include bool) {
// SetLevel sets the standard logger level. // SetLevel sets the standard logger level.
func SetLevel(level Level) { func SetLevel(level Level) {
std.mu.Lock()
defer std.mu.Unlock()
std.SetLevel(level) std.SetLevel(level)
} }
// GetLevel returns the standard logger level. // GetLevel returns the standard logger level.
func GetLevel() Level { func GetLevel() Level {
std.mu.Lock() return std.GetLevel()
defer std.mu.Unlock() }
return std.level()
// IsLevelEnabled checks if the log level of the standard logger is greater than the level param
func IsLevelEnabled(level Level) bool {
return std.IsLevelEnabled(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.AddHook(hook)
defer std.mu.Unlock()
std.Hooks.Add(hook)
} }
// WithError creates an entry from the standard logger and adds an error to it, using the value defined in ErrorKey as key. // WithError creates an entry from the standard logger and adds an error to it, using the value defined in ErrorKey as key.
@ -80,6 +76,20 @@ func WithFields(fields Fields) *Entry {
return std.WithFields(fields) return std.WithFields(fields)
} }
// WithTime creats an entry from the standard logger and overrides the time of
// logs generated with it.
//
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
// or Panic on the Entry it returns.
func WithTime(t time.Time) *Entry {
return std.WithTime(t)
}
// Trace logs a message at level Trace on the standard logger.
func Trace(args ...interface{}) {
std.Trace(args...)
}
// Debug logs a message at level Debug on the standard logger. // Debug logs a message at level Debug on the standard logger.
func Debug(args ...interface{}) { func Debug(args ...interface{}) {
std.Debug(args...) std.Debug(args...)
@ -115,11 +125,16 @@ func Panic(args ...interface{}) {
std.Panic(args...) std.Panic(args...)
} }
// Fatal logs a message at level Fatal on the standard logger. // Fatal logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
func Fatal(args ...interface{}) { func Fatal(args ...interface{}) {
std.Fatal(args...) std.Fatal(args...)
} }
// Tracef logs a message at level Trace on the standard logger.
func Tracef(format string, args ...interface{}) {
std.Tracef(format, args...)
}
// Debugf logs a message at level Debug on the standard logger. // Debugf logs a message at level Debug on the standard logger.
func Debugf(format string, args ...interface{}) { func Debugf(format string, args ...interface{}) {
std.Debugf(format, args...) std.Debugf(format, args...)
@ -155,11 +170,16 @@ func Panicf(format string, args ...interface{}) {
std.Panicf(format, args...) std.Panicf(format, args...)
} }
// Fatalf logs a message at level Fatal on the standard logger. // Fatalf logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
func Fatalf(format string, args ...interface{}) { func Fatalf(format string, args ...interface{}) {
std.Fatalf(format, args...) std.Fatalf(format, args...)
} }
// Traceln logs a message at level Trace on the standard logger.
func Traceln(args ...interface{}) {
std.Traceln(args...)
}
// Debugln logs a message at level Debug on the standard logger. // Debugln logs a message at level Debug on the standard logger.
func Debugln(args ...interface{}) { func Debugln(args ...interface{}) {
std.Debugln(args...) std.Debugln(args...)
@ -195,7 +215,7 @@ func Panicln(args ...interface{}) {
std.Panicln(args...) std.Panicln(args...)
} }
// Fatalln logs a message at level Fatal on the standard logger. // Fatalln logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
func Fatalln(args ...interface{}) { func Fatalln(args ...interface{}) {
std.Fatalln(args...) std.Fatalln(args...)
} }

View File

@ -2,8 +2,15 @@ package logrus
import "time" import "time"
// defaultTimestampFormat is YYYY-mm-DDTHH:MM:SS-TZ // Default key names for the default fields
const defaultTimestampFormat = time.RFC3339 const (
defaultTimestampFormat = time.RFC3339
FieldKeyMsg = "msg"
FieldKeyLevel = "level"
FieldKeyTime = "time"
FieldKeyLogrusError = "logrus_error"
FieldKeyFunc = "func"
)
// The Formatter interface is used to implement a custom Formatter. It takes an // The Formatter interface is used to implement a custom Formatter. It takes an
// `Entry`. It exposes all the fields, including the default ones: // `Entry`. It exposes all the fields, including the default ones:
@ -31,23 +38,36 @@ 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(data Fields, reportCaller bool) { func prefixFieldClashes(data Fields, fieldMap FieldMap, reportCaller bool) {
if t, ok := data["time"]; ok { timeKey := fieldMap.resolve(FieldKeyTime)
data["fields.time"] = t if t, ok := data[timeKey]; ok {
data["fields."+timeKey] = t
delete(data, timeKey)
} }
if m, ok := data["msg"]; ok { msgKey := fieldMap.resolve(FieldKeyMsg)
data["fields.msg"] = m if m, ok := data[msgKey]; ok {
data["fields."+msgKey] = m
delete(data, msgKey)
} }
if l, ok := data["level"]; ok { levelKey := fieldMap.resolve(FieldKeyLevel)
data["fields.level"] = l if l, ok := data[levelKey]; ok {
data["fields."+levelKey] = l
delete(data, levelKey)
}
logrusErrKey := fieldMap.resolve(FieldKeyLogrusError)
if l, ok := data[logrusErrKey]; ok {
data["fields."+logrusErrKey] = l
delete(data, logrusErrKey)
} }
// If reportCaller is not set, 'func' will not conflict. // If reportCaller is not set, 'func' will not conflict.
if reportCaller { if reportCaller {
if l, ok := data["func"]; ok { funcKey := fieldMap.resolve(FieldKeyFunc)
data["fields.func"] = l if l, ok := data[funcKey]; ok {
data["fields."+funcKey] = l
} }
} }
} }

11
go.mod Normal file
View File

@ -0,0 +1,11 @@
module github.com/sirupsen/logrus
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.1
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.1.1 // indirect
github.com/stretchr/testify v1.2.2
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33
)

15
go.sum Normal file
View File

@ -0,0 +1,15 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe h1:CHRGQ8V7OlCYtwaKPJi3iA7J+YdNKdo8j7nG5IgDhjs=
github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

View File

@ -1,10 +1,13 @@
package logrus package logrus
import ( import (
"bytes"
"encoding/json"
"sync" "sync"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
type TestHook struct { type TestHook struct {
@ -18,6 +21,7 @@ func (hook *TestHook) Fire(entry *Entry) error {
func (hook *TestHook) Levels() []Level { func (hook *TestHook) Levels() []Level {
return []Level{ return []Level{
TraceLevel,
DebugLevel, DebugLevel,
InfoLevel, InfoLevel,
WarnLevel, WarnLevel,
@ -50,6 +54,7 @@ func (hook *ModifyHook) Fire(entry *Entry) error {
func (hook *ModifyHook) Levels() []Level { func (hook *ModifyHook) Levels() []Level {
return []Level{ return []Level{
TraceLevel,
DebugLevel, DebugLevel,
InfoLevel, InfoLevel,
WarnLevel, WarnLevel,
@ -85,6 +90,46 @@ func TestCanFireMultipleHooks(t *testing.T) {
}) })
} }
type SingleLevelModifyHook struct {
ModifyHook
}
func (h *SingleLevelModifyHook) Levels() []Level {
return []Level{InfoLevel}
}
func TestHookEntryIsPristine(t *testing.T) {
l := New()
b := &bytes.Buffer{}
l.Formatter = &JSONFormatter{}
l.Out = b
l.AddHook(&SingleLevelModifyHook{})
l.Error("error message")
data := map[string]string{}
err := json.Unmarshal(b.Bytes(), &data)
require.NoError(t, err)
_, ok := data["wow"]
require.False(t, ok)
b.Reset()
l.Info("error message")
data = map[string]string{}
err = json.Unmarshal(b.Bytes(), &data)
require.NoError(t, err)
_, ok = data["wow"]
require.True(t, ok)
b.Reset()
l.Error("error message")
data = map[string]string{}
err = json.Unmarshal(b.Bytes(), &data)
require.NoError(t, err)
_, ok = data["wow"]
require.False(t, ok)
b.Reset()
}
type ErrorHook struct { type ErrorHook struct {
Fired bool Fired bool
} }

View File

@ -43,7 +43,7 @@ func (hook *SyslogHook) Fire(entry *logrus.Entry) error {
return hook.Writer.Warning(line) return hook.Writer.Warning(line)
case logrus.InfoLevel: case logrus.InfoLevel:
return hook.Writer.Info(line) return hook.Writer.Info(line)
case logrus.DebugLevel: case logrus.DebugLevel, logrus.TraceLevel:
return hook.Writer.Debug(line) return hook.Writer.Debug(line)
default: default:
return nil return nil

View File

@ -1,3 +1,5 @@
// +build !windows,!nacl,!plan9
package syslog package syslog
import ( import (

View File

@ -15,7 +15,7 @@ type Hook struct {
// Entries is an array of all entries that have been received by this hook. // Entries is an array of all entries that have been received by this hook.
// For safe access, use the AllEntries() method, rather than reading this // For safe access, use the AllEntries() method, rather than reading this
// value directly. // value directly.
Entries []*logrus.Entry Entries []logrus.Entry
mu sync.RWMutex mu sync.RWMutex
} }
@ -52,7 +52,7 @@ func NewNullLogger() (*logrus.Logger, *Hook) {
func (t *Hook) Fire(e *logrus.Entry) error { func (t *Hook) Fire(e *logrus.Entry) error {
t.mu.Lock() t.mu.Lock()
defer t.mu.Unlock() defer t.mu.Unlock()
t.Entries = append(t.Entries, e) t.Entries = append(t.Entries, *e)
return nil return nil
} }
@ -68,9 +68,7 @@ func (t *Hook) LastEntry() *logrus.Entry {
if i < 0 { if i < 0 {
return nil return nil
} }
// Make a copy, for safety return &t.Entries[i]
e := *t.Entries[i]
return &e
} }
// AllEntries returns all entries that were logged. // AllEntries returns all entries that were logged.
@ -79,10 +77,9 @@ func (t *Hook) AllEntries() []*logrus.Entry {
defer t.mu.RUnlock() defer t.mu.RUnlock()
// Make a copy so the returned value won't race with future log requests // Make a copy so the returned value won't race with future log requests
entries := make([]*logrus.Entry, len(t.Entries)) entries := make([]*logrus.Entry, len(t.Entries))
for i, entry := range t.Entries { for i := 0; i < len(t.Entries); i++ {
// Make a copy, for safety // Make a copy, for safety
e := *entry entries[i] = &t.Entries[i]
entries[i] = &e
} }
return entries return entries
} }
@ -91,5 +88,5 @@ func (t *Hook) AllEntries() []*logrus.Entry {
func (t *Hook) Reset() { func (t *Hook) Reset() {
t.mu.Lock() t.mu.Lock()
defer t.mu.Unlock() defer t.mu.Unlock()
t.Entries = make([]*logrus.Entry, 0) t.Entries = make([]logrus.Entry, 0)
} }

View File

@ -1,8 +1,10 @@
package test package test
import ( import (
"math/rand"
"sync" "sync"
"testing" "testing"
"time"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -38,24 +40,46 @@ func TestAllHooks(t *testing.T) {
} }
func TestLoggingWithHooksRace(t *testing.T) { func TestLoggingWithHooksRace(t *testing.T) {
rand.Seed(time.Now().Unix())
unlocker := rand.Int() % 100
assert := assert.New(t) assert := assert.New(t)
logger, hook := NewNullLogger() logger, hook := NewNullLogger()
var wg sync.WaitGroup var wgOne, wgAll sync.WaitGroup
wg.Add(100) wgOne.Add(1)
wgAll.Add(100)
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
go func() { go func(i int) {
logger.Info("info") logger.Info("info")
wg.Done() wgAll.Done()
}() if i == unlocker {
wgOne.Done()
}
}(i)
} }
wgOne.Wait()
assert.Equal(logrus.InfoLevel, hook.LastEntry().Level) assert.Equal(logrus.InfoLevel, hook.LastEntry().Level)
assert.Equal("info", hook.LastEntry().Message) assert.Equal("info", hook.LastEntry().Message)
wg.Wait() wgAll.Wait()
entries := hook.AllEntries() entries := hook.AllEntries()
assert.Equal(100, len(entries)) assert.Equal(100, len(entries))
} }
func TestFatalWithAlternateExit(t *testing.T) {
assert := assert.New(t)
logger, hook := NewNullLogger()
logger.ExitFunc = func(code int) {}
logger.Fatal("something went very wrong")
assert.Equal(logrus.FatalLevel, hook.LastEntry().Level)
assert.Equal("something went very wrong", hook.LastEntry().Message)
assert.Equal(1, len(hook.Entries))
}

View File

@ -1,6 +1,7 @@
package logrus package logrus
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
) )
@ -10,14 +11,6 @@ type fieldKey string
// FieldMap allows customization of the key names for default fields. // FieldMap allows customization of the key names for default fields.
type FieldMap map[fieldKey]string type FieldMap map[fieldKey]string
// Default key names for the default fields
const (
FieldKeyMsg = "msg"
FieldKeyLevel = "level"
FieldKeyTime = "time"
FieldKeyFunc = "func"
)
func (f FieldMap) resolve(key fieldKey) string { func (f FieldMap) resolve(key fieldKey) string {
if k, ok := f[key]; ok { if k, ok := f[key]; ok {
return k return k
@ -34,6 +27,9 @@ type JSONFormatter struct {
// DisableTimestamp allows disabling automatic timestamps in output // DisableTimestamp allows disabling automatic timestamps in output
DisableTimestamp bool DisableTimestamp bool
// DataKey allows users to put all the log entry parameters into a nested dictionary at a given key.
DataKey string
// FieldMap allows users to customize the names of keys for default fields. // FieldMap allows users to customize the names of keys for default fields.
// As an example: // As an example:
// formatter := &JSONFormatter{ // formatter := &JSONFormatter{
@ -42,11 +38,12 @@ type JSONFormatter struct {
// FieldKeyLevel: "@level", // FieldKeyLevel: "@level",
// FieldKeyMsg: "@message", // FieldKeyMsg: "@message",
// FieldKeyFunc: "@caller", // FieldKeyFunc: "@caller",
// FieldKeyMsg: "@message",
// FieldKeyFunc: "@caller",
// }, // },
// } // }
FieldMap FieldMap FieldMap FieldMap
// PrettyPrint will indent all json logs
PrettyPrint bool
} }
// Format renders a single log entry // Format renders a single log entry
@ -63,13 +60,22 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
} }
} }
prefixFieldClashes(data, entry.HasCaller()) if f.DataKey != "" {
newData := make(Fields, 4)
newData[f.DataKey] = data
data = newData
}
prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
timestampFormat := f.TimestampFormat timestampFormat := f.TimestampFormat
if timestampFormat == "" { if timestampFormat == "" {
timestampFormat = defaultTimestampFormat timestampFormat = defaultTimestampFormat
} }
if entry.err != "" {
data[f.FieldMap.resolve(FieldKeyLogrusError)] = entry.err
}
if !f.DisableTimestamp { if !f.DisableTimestamp {
data[f.FieldMap.resolve(FieldKeyTime)] = entry.Time.Format(timestampFormat) data[f.FieldMap.resolve(FieldKeyTime)] = entry.Time.Format(timestampFormat)
} }
@ -78,9 +84,21 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
if entry.HasCaller() { if entry.HasCaller() {
data[f.FieldMap.resolve(FieldKeyFunc)] = entry.Caller data[f.FieldMap.resolve(FieldKeyFunc)] = entry.Caller
} }
serialized, err := json.Marshal(data)
if err != nil { var b *bytes.Buffer
if entry.Buffer != nil {
b = entry.Buffer
} else {
b = &bytes.Buffer{}
}
encoder := json.NewEncoder(b)
if f.PrettyPrint {
encoder.SetIndent("", " ")
}
if err := encoder.Encode(data); 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)
} }
return append(serialized, '\n'), nil
return b.Bytes(), nil
} }

View File

@ -3,6 +3,7 @@ package logrus
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"strings" "strings"
"testing" "testing"
) )
@ -106,6 +107,102 @@ func TestFieldClashWithLevel(t *testing.T) {
} }
} }
func TestFieldClashWithRemappedFields(t *testing.T) {
formatter := &JSONFormatter{
FieldMap: FieldMap{
FieldKeyTime: "@timestamp",
FieldKeyLevel: "@level",
FieldKeyMsg: "@message",
},
}
b, err := formatter.Format(WithFields(Fields{
"@timestamp": "@timestamp",
"@level": "@level",
"@message": "@message",
"timestamp": "timestamp",
"level": "level",
"msg": "msg",
}))
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)
}
for _, field := range []string{"timestamp", "level", "msg"} {
if entry[field] != field {
t.Errorf("Expected field %v to be untouched; got %v", field, entry[field])
}
remappedKey := fmt.Sprintf("fields.%s", field)
if remapped, ok := entry[remappedKey]; ok {
t.Errorf("Expected %s to be empty; got %v", remappedKey, remapped)
}
}
for _, field := range []string{"@timestamp", "@level", "@message"} {
if entry[field] == field {
t.Errorf("Expected field %v to be mapped to an Entry value", field)
}
remappedKey := fmt.Sprintf("fields.%s", field)
if remapped, ok := entry[remappedKey]; ok {
if remapped != field {
t.Errorf("Expected field %v to be copied to %s; got %v", field, remappedKey, remapped)
}
} else {
t.Errorf("Expected field %v to be copied to %s; was absent", field, remappedKey)
}
}
}
func TestFieldsInNestedDictionary(t *testing.T) {
formatter := &JSONFormatter{
DataKey: "args",
}
logEntry := WithFields(Fields{
"level": "level",
"test": "test",
})
logEntry.Level = InfoLevel
b, err := formatter.Format(logEntry)
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)
}
args := entry["args"].(map[string]interface{})
for _, field := range []string{"test", "level"} {
if value, present := args[field]; !present || value != field {
t.Errorf("Expected field %v to be present under 'args'; untouched", field)
}
}
for _, field := range []string{"test", "fields.level"} {
if _, present := entry[field]; present {
t.Errorf("Expected field %v not to be present at top level", field)
}
}
// with nested object, "level" shouldn't clash
if entry["level"] != "info" {
t.Errorf("Expected 'level' field to contain 'info'")
}
}
func TestJSONEntryEndsWithNewline(t *testing.T) { func TestJSONEntryEndsWithNewline(t *testing.T) {
formatter := &JSONFormatter{} formatter := &JSONFormatter{}

135
logger.go
View File

@ -5,12 +5,13 @@ import (
"os" "os"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time"
) )
type Logger struct { type Logger struct {
// The logs are `io.Copy`'d to this in a mutex. It's common to set this to a // The logs are `io.Copy`'d to this in a mutex. It's common to set this to a
// file, or leave it default which is `os.Stderr`. You can also set this to // file, or leave it default which is `os.Stderr`. You can also set this to
// something more adventorous, such as logging to Kafka. // something more adventurous, such as logging to Kafka.
Out io.Writer Out io.Writer
// Hooks for the logger instance. These allow firing events based on logging // Hooks for the logger instance. These allow firing events based on logging
// levels and log entries. For example, to send errors to an error tracking // levels and log entries. For example, to send errors to an error tracking
@ -24,7 +25,7 @@ type Logger struct {
// formatters for examples. // formatters for examples.
Formatter Formatter Formatter Formatter
//Flag for whether to log caller info (off by default) // Flag for whether to log caller info (off by default)
ReportCaller bool ReportCaller bool
// The logging level the logger should log at. This is typically (and defaults // The logging level the logger should log at. This is typically (and defaults
@ -35,8 +36,12 @@ type Logger struct {
mu MutexWrap mu MutexWrap
// Reusable empty entry // Reusable empty entry
entryPool sync.Pool entryPool sync.Pool
// Function to exit the application, defaults to `os.Exit()`
ExitFunc exitFunc
} }
type exitFunc func(int)
type MutexWrap struct { type MutexWrap struct {
lock sync.Mutex lock sync.Mutex
disabled bool disabled bool
@ -76,6 +81,7 @@ func New() *Logger {
Formatter: new(TextFormatter), Formatter: new(TextFormatter),
Hooks: make(LevelHooks), Hooks: make(LevelHooks),
Level: InfoLevel, Level: InfoLevel,
ExitFunc: os.Exit,
ReportCaller: false, ReportCaller: false,
} }
} }
@ -89,11 +95,12 @@ func (logger *Logger) newEntry() *Entry {
} }
func (logger *Logger) releaseEntry(entry *Entry) { func (logger *Logger) releaseEntry(entry *Entry) {
entry.Data = map[string]interface{}{}
logger.entryPool.Put(entry) logger.entryPool.Put(entry)
} }
// Adds a field to the log entry, note that it doesn't log until you call // Adds a field to the log entry, note that it doesn't log until you call
// Debug, Print, Info, Warn, Fatal or Panic. It only creates a log entry. // Debug, Print, Info, Warn, Error, Fatal or Panic. It only creates a log entry.
// If you want multiple fields, use `WithFields`. // If you want multiple fields, use `WithFields`.
func (logger *Logger) WithField(key string, value interface{}) *Entry { func (logger *Logger) WithField(key string, value interface{}) *Entry {
entry := logger.newEntry() entry := logger.newEntry()
@ -117,8 +124,23 @@ func (logger *Logger) WithError(err error) *Entry {
return entry.WithError(err) return entry.WithError(err)
} }
// Overrides the time of the log entry.
func (logger *Logger) WithTime(t time.Time) *Entry {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
return entry.WithTime(t)
}
func (logger *Logger) Tracef(format string, args ...interface{}) {
if logger.IsLevelEnabled(TraceLevel) {
entry := logger.newEntry()
entry.Tracef(format, args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Debugf(format string, args ...interface{}) { func (logger *Logger) Debugf(format string, args ...interface{}) {
if logger.level() >= DebugLevel { if logger.IsLevelEnabled(DebugLevel) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Debugf(format, args...) entry.Debugf(format, args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
@ -126,7 +148,7 @@ func (logger *Logger) Debugf(format string, args ...interface{}) {
} }
func (logger *Logger) Infof(format string, args ...interface{}) { func (logger *Logger) Infof(format string, args ...interface{}) {
if logger.level() >= InfoLevel { if logger.IsLevelEnabled(InfoLevel) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Infof(format, args...) entry.Infof(format, args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
@ -140,7 +162,7 @@ func (logger *Logger) Printf(format string, args ...interface{}) {
} }
func (logger *Logger) Warnf(format string, args ...interface{}) { func (logger *Logger) Warnf(format string, args ...interface{}) {
if logger.level() >= WarnLevel { if logger.IsLevelEnabled(WarnLevel) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Warnf(format, args...) entry.Warnf(format, args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
@ -148,7 +170,7 @@ func (logger *Logger) Warnf(format string, args ...interface{}) {
} }
func (logger *Logger) Warningf(format string, args ...interface{}) { func (logger *Logger) Warningf(format string, args ...interface{}) {
if logger.level() >= WarnLevel { if logger.IsLevelEnabled(WarnLevel) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Warnf(format, args...) entry.Warnf(format, args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
@ -156,7 +178,7 @@ func (logger *Logger) Warningf(format string, args ...interface{}) {
} }
func (logger *Logger) Errorf(format string, args ...interface{}) { func (logger *Logger) Errorf(format string, args ...interface{}) {
if logger.level() >= ErrorLevel { if logger.IsLevelEnabled(ErrorLevel) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Errorf(format, args...) entry.Errorf(format, args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
@ -164,24 +186,32 @@ func (logger *Logger) Errorf(format string, args ...interface{}) {
} }
func (logger *Logger) Fatalf(format string, args ...interface{}) { func (logger *Logger) Fatalf(format string, args ...interface{}) {
if logger.level() >= FatalLevel { if logger.IsLevelEnabled(FatalLevel) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Fatalf(format, args...) entry.Fatalf(format, args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
} }
Exit(1) logger.Exit(1)
} }
func (logger *Logger) Panicf(format string, args ...interface{}) { func (logger *Logger) Panicf(format string, args ...interface{}) {
if logger.level() >= PanicLevel { if logger.IsLevelEnabled(PanicLevel) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Panicf(format, args...) entry.Panicf(format, args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Trace(args ...interface{}) {
if logger.IsLevelEnabled(TraceLevel) {
entry := logger.newEntry()
entry.Trace(args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Debug(args ...interface{}) { func (logger *Logger) Debug(args ...interface{}) {
if logger.level() >= DebugLevel { if logger.IsLevelEnabled(DebugLevel) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Debug(args...) entry.Debug(args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
@ -189,7 +219,7 @@ func (logger *Logger) Debug(args ...interface{}) {
} }
func (logger *Logger) Info(args ...interface{}) { func (logger *Logger) Info(args ...interface{}) {
if logger.level() >= InfoLevel { if logger.IsLevelEnabled(InfoLevel) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Info(args...) entry.Info(args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
@ -203,7 +233,7 @@ func (logger *Logger) Print(args ...interface{}) {
} }
func (logger *Logger) Warn(args ...interface{}) { func (logger *Logger) Warn(args ...interface{}) {
if logger.level() >= WarnLevel { if logger.IsLevelEnabled(WarnLevel) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Warn(args...) entry.Warn(args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
@ -211,7 +241,7 @@ func (logger *Logger) Warn(args ...interface{}) {
} }
func (logger *Logger) Warning(args ...interface{}) { func (logger *Logger) Warning(args ...interface{}) {
if logger.level() >= WarnLevel { if logger.IsLevelEnabled(WarnLevel) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Warn(args...) entry.Warn(args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
@ -219,7 +249,7 @@ func (logger *Logger) Warning(args ...interface{}) {
} }
func (logger *Logger) Error(args ...interface{}) { func (logger *Logger) Error(args ...interface{}) {
if logger.level() >= ErrorLevel { if logger.IsLevelEnabled(ErrorLevel) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Error(args...) entry.Error(args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
@ -227,24 +257,32 @@ func (logger *Logger) Error(args ...interface{}) {
} }
func (logger *Logger) Fatal(args ...interface{}) { func (logger *Logger) Fatal(args ...interface{}) {
if logger.level() >= FatalLevel { if logger.IsLevelEnabled(FatalLevel) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Fatal(args...) entry.Fatal(args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
} }
Exit(1) logger.Exit(1)
} }
func (logger *Logger) Panic(args ...interface{}) { func (logger *Logger) Panic(args ...interface{}) {
if logger.level() >= PanicLevel { if logger.IsLevelEnabled(PanicLevel) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Panic(args...) entry.Panic(args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Traceln(args ...interface{}) {
if logger.IsLevelEnabled(TraceLevel) {
entry := logger.newEntry()
entry.Traceln(args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Debugln(args ...interface{}) { func (logger *Logger) Debugln(args ...interface{}) {
if logger.level() >= DebugLevel { if logger.IsLevelEnabled(DebugLevel) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Debugln(args...) entry.Debugln(args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
@ -252,7 +290,7 @@ func (logger *Logger) Debugln(args ...interface{}) {
} }
func (logger *Logger) Infoln(args ...interface{}) { func (logger *Logger) Infoln(args ...interface{}) {
if logger.level() >= InfoLevel { if logger.IsLevelEnabled(InfoLevel) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Infoln(args...) entry.Infoln(args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
@ -266,7 +304,7 @@ func (logger *Logger) Println(args ...interface{}) {
} }
func (logger *Logger) Warnln(args ...interface{}) { func (logger *Logger) Warnln(args ...interface{}) {
if logger.level() >= WarnLevel { if logger.IsLevelEnabled(WarnLevel) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Warnln(args...) entry.Warnln(args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
@ -274,7 +312,7 @@ func (logger *Logger) Warnln(args ...interface{}) {
} }
func (logger *Logger) Warningln(args ...interface{}) { func (logger *Logger) Warningln(args ...interface{}) {
if logger.level() >= WarnLevel { if logger.IsLevelEnabled(WarnLevel) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Warnln(args...) entry.Warnln(args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
@ -282,7 +320,7 @@ func (logger *Logger) Warningln(args ...interface{}) {
} }
func (logger *Logger) Errorln(args ...interface{}) { func (logger *Logger) Errorln(args ...interface{}) {
if logger.level() >= ErrorLevel { if logger.IsLevelEnabled(ErrorLevel) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Errorln(args...) entry.Errorln(args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
@ -290,22 +328,30 @@ func (logger *Logger) Errorln(args ...interface{}) {
} }
func (logger *Logger) Fatalln(args ...interface{}) { func (logger *Logger) Fatalln(args ...interface{}) {
if logger.level() >= FatalLevel { if logger.IsLevelEnabled(FatalLevel) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Fatalln(args...) entry.Fatalln(args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
} }
Exit(1) logger.Exit(1)
} }
func (logger *Logger) Panicln(args ...interface{}) { func (logger *Logger) Panicln(args ...interface{}) {
if logger.level() >= PanicLevel { if logger.IsLevelEnabled(PanicLevel) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Panicln(args...) entry.Panicln(args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Exit(code int) {
runHandlers()
if logger.ExitFunc == nil {
logger.ExitFunc = os.Exit
}
logger.ExitFunc(code)
}
//When file is opened with appending mode, it's safe to //When file is opened with appending mode, it's safe to
//write concurrently to a file (within 4k message on Linux). //write concurrently to a file (within 4k message on Linux).
//In these cases user can choose to disable the lock. //In these cases user can choose to disable the lock.
@ -317,12 +363,47 @@ func (logger *Logger) level() Level {
return Level(atomic.LoadUint32((*uint32)(&logger.Level))) return Level(atomic.LoadUint32((*uint32)(&logger.Level)))
} }
// SetLevel sets the logger level.
func (logger *Logger) SetLevel(level Level) { func (logger *Logger) SetLevel(level Level) {
atomic.StoreUint32((*uint32)(&logger.Level), uint32(level)) atomic.StoreUint32((*uint32)(&logger.Level), uint32(level))
} }
// GetLevel returns the logger level.
func (logger *Logger) GetLevel() Level {
return logger.level()
}
// AddHook adds a hook to the logger hooks.
func (logger *Logger) AddHook(hook Hook) { func (logger *Logger) AddHook(hook Hook) {
logger.mu.Lock() logger.mu.Lock()
defer logger.mu.Unlock() defer logger.mu.Unlock()
logger.Hooks.Add(hook) logger.Hooks.Add(hook)
} }
// IsLevelEnabled checks if the log level of the logger is greater than the level param
func (logger *Logger) IsLevelEnabled(level Level) bool {
return logger.level() >= level
}
// SetFormatter sets the logger formatter.
func (logger *Logger) SetFormatter(formatter Formatter) {
logger.mu.Lock()
defer logger.mu.Unlock()
logger.Formatter = formatter
}
// SetOutput sets the logger output.
func (logger *Logger) SetOutput(output io.Writer) {
logger.mu.Lock()
defer logger.mu.Unlock()
logger.Out = output
}
// ReplaceHooks replaces the logger hooks and returns the old ones
func (logger *Logger) ReplaceHooks(hooks LevelHooks) LevelHooks {
logger.mu.Lock()
oldHooks := logger.Hooks
logger.Hooks = hooks
logger.mu.Unlock()
return oldHooks
}

View File

@ -1,6 +1,7 @@
package logrus package logrus
import ( import (
"io/ioutil"
"os" "os"
"testing" "testing"
) )
@ -59,3 +60,26 @@ func doLoggerBenchmarkNoLock(b *testing.B, out *os.File, formatter Formatter, fi
} }
}) })
} }
func BenchmarkLoggerJSONFormatter(b *testing.B) {
doLoggerBenchmarkWithFormatter(b, &JSONFormatter{})
}
func BenchmarkLoggerTextFormatter(b *testing.B) {
doLoggerBenchmarkWithFormatter(b, &TextFormatter{})
}
func doLoggerBenchmarkWithFormatter(b *testing.B, f Formatter) {
b.SetParallelism(100)
log := New()
log.Formatter = f
log.Out = ioutil.Discard
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
log.
WithField("foo1", "bar1").
WithField("foo2", "bar2").
Info("this is a dummy log")
}
})
}

42
logger_test.go Normal file
View File

@ -0,0 +1,42 @@
package logrus
import (
"bytes"
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestFieldValueError(t *testing.T) {
buf := &bytes.Buffer{}
l := &Logger{
Out: buf,
Formatter: new(JSONFormatter),
Hooks: make(LevelHooks),
Level: DebugLevel,
}
l.WithField("func", func() {}).Info("test")
fmt.Println(buf.String())
var data map[string]interface{}
json.Unmarshal(buf.Bytes(), &data)
_, ok := data[FieldKeyLogrusError]
require.True(t, ok)
}
func TestNoFieldValueError(t *testing.T) {
buf := &bytes.Buffer{}
l := &Logger{
Out: buf,
Formatter: new(JSONFormatter),
Hooks: make(LevelHooks),
Level: DebugLevel,
}
l.WithField("str", "str").Info("test")
fmt.Println(buf.String())
var data map[string]interface{}
json.Unmarshal(buf.Bytes(), &data)
_, ok := data[FieldKeyLogrusError]
require.False(t, ok)
}

View File

@ -15,6 +15,8 @@ type Level uint32
// Convert the Level to a string. E.g. PanicLevel becomes "panic". // Convert the Level to a string. E.g. PanicLevel becomes "panic".
func (level Level) String() string { func (level Level) String() string {
switch level { switch level {
case TraceLevel:
return "trace"
case DebugLevel: case DebugLevel:
return "debug" return "debug"
case InfoLevel: case InfoLevel:
@ -47,6 +49,8 @@ func ParseLevel(lvl string) (Level, error) {
return InfoLevel, nil return InfoLevel, nil
case "debug": case "debug":
return DebugLevel, nil return DebugLevel, nil
case "trace":
return TraceLevel, nil
} }
var l Level var l Level
@ -61,6 +65,7 @@ var AllLevels = []Level{
WarnLevel, WarnLevel,
InfoLevel, InfoLevel,
DebugLevel, DebugLevel,
TraceLevel,
} }
// These are the different logging levels. You can set the logging level to log // These are the different logging levels. You can set the logging level to log
@ -69,7 +74,7 @@ const (
// PanicLevel level, highest level of severity. Logs and then calls panic with the // PanicLevel level, highest level of severity. Logs and then calls panic with the
// message passed to Debug, Info, ... // message passed to Debug, Info, ...
PanicLevel Level = iota PanicLevel Level = iota
// FatalLevel level. Logs and then calls `os.Exit(1)`. It will exit even if the // FatalLevel level. Logs and then calls `logger.Exit(1)`. It will exit even if the
// logging level is set to Panic. // logging level is set to Panic.
FatalLevel FatalLevel
// ErrorLevel level. Logs. Used for errors that should definitely be noted. // ErrorLevel level. Logs. Used for errors that should definitely be noted.
@ -82,6 +87,8 @@ const (
InfoLevel InfoLevel
// DebugLevel level. Usually only enabled when debugging. Very verbose logging. // DebugLevel level. Usually only enabled when debugging. Very verbose logging.
DebugLevel DebugLevel
// TraceLevel level. Designates finer-grained informational events than the Debug.
TraceLevel
) )
// Won't compile if StdLogger can't be realized by a log.Logger // Won't compile if StdLogger can't be realized by a log.Logger
@ -140,4 +147,20 @@ type FieldLogger interface {
Errorln(args ...interface{}) Errorln(args ...interface{})
Fatalln(args ...interface{}) Fatalln(args ...interface{})
Panicln(args ...interface{}) Panicln(args ...interface{})
// IsDebugEnabled() bool
// IsInfoEnabled() bool
// IsWarnEnabled() bool
// IsErrorEnabled() bool
// IsFatalEnabled() bool
// IsPanicEnabled() bool
}
// Ext1FieldLogger (the first extension to FieldLogger) is superfluous, it is
// here for consistancy. Do not use. Use Logger or Entry instead.
type Ext1FieldLogger interface {
FieldLogger
Tracef(format string, args ...interface{})
Trace(args ...interface{})
Traceln(args ...interface{})
} }

View File

@ -3,6 +3,7 @@ package logrus
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"io/ioutil"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -279,6 +280,65 @@ func TestDefaultFieldsAreNotPrefixed(t *testing.T) {
}) })
} }
func TestWithTimeShouldOverrideTime(t *testing.T) {
now := time.Now().Add(24 * time.Hour)
LogAndAssertJSON(t, func(log *Logger) {
log.WithTime(now).Info("foobar")
}, func(fields Fields) {
assert.Equal(t, fields["time"], now.Format(defaultTimestampFormat))
})
}
func TestWithTimeShouldNotOverrideFields(t *testing.T) {
now := time.Now().Add(24 * time.Hour)
LogAndAssertJSON(t, func(log *Logger) {
log.WithField("herp", "derp").WithTime(now).Info("blah")
}, func(fields Fields) {
assert.Equal(t, fields["time"], now.Format(defaultTimestampFormat))
assert.Equal(t, fields["herp"], "derp")
})
}
func TestWithFieldShouldNotOverrideTime(t *testing.T) {
now := time.Now().Add(24 * time.Hour)
LogAndAssertJSON(t, func(log *Logger) {
log.WithTime(now).WithField("herp", "derp").Info("blah")
}, func(fields Fields) {
assert.Equal(t, fields["time"], now.Format(defaultTimestampFormat))
assert.Equal(t, fields["herp"], "derp")
})
}
func TestTimeOverrideMultipleLogs(t *testing.T) {
var buffer bytes.Buffer
var firstFields, secondFields Fields
logger := New()
logger.Out = &buffer
formatter := new(JSONFormatter)
formatter.TimestampFormat = time.StampMilli
logger.Formatter = formatter
llog := logger.WithField("herp", "derp")
llog.Info("foo")
err := json.Unmarshal(buffer.Bytes(), &firstFields)
assert.NoError(t, err, "should have decoded first message")
buffer.Reset()
time.Sleep(10 * time.Millisecond)
llog.Info("bar")
err = json.Unmarshal(buffer.Bytes(), &secondFields)
assert.NoError(t, err, "should have decoded second message")
assert.NotEqual(t, firstFields["time"], secondFields["time"], "timestamps should not be equal")
}
func TestDoubleLoggingDoesntPrefixPreviousFields(t *testing.T) { func TestDoubleLoggingDoesntPrefixPreviousFields(t *testing.T) {
var buffer bytes.Buffer var buffer bytes.Buffer
@ -408,6 +468,7 @@ func BenchmarkWithCallerTracing(b *testing.B) {
} }
func TestConvertLevelToString(t *testing.T) { func TestConvertLevelToString(t *testing.T) {
assert.Equal(t, "trace", TraceLevel.String())
assert.Equal(t, "debug", DebugLevel.String()) assert.Equal(t, "debug", DebugLevel.String())
assert.Equal(t, "info", InfoLevel.String()) assert.Equal(t, "info", InfoLevel.String())
assert.Equal(t, "warning", WarnLevel.String()) assert.Equal(t, "warning", WarnLevel.String())
@ -473,6 +534,14 @@ func TestParseLevel(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, DebugLevel, l) assert.Equal(t, DebugLevel, l)
l, err = ParseLevel("trace")
assert.Nil(t, err)
assert.Equal(t, TraceLevel, l)
l, err = ParseLevel("TRACE")
assert.Nil(t, err)
assert.Equal(t, TraceLevel, l)
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())
} }
@ -509,10 +578,52 @@ func TestLoggingRace(t *testing.T) {
wg.Wait() wg.Wait()
} }
func TestLoggingRaceWithHooksOnEntry(t *testing.T) {
logger := New()
hook := new(ModifyHook)
logger.AddHook(hook)
entry := logger.WithField("context", "clue")
var wg sync.WaitGroup
wg.Add(100)
for i := 0; i < 100; i++ {
go func() {
entry.Info("info")
wg.Done()
}()
}
wg.Wait()
}
func TestReplaceHooks(t *testing.T) {
old, cur := &TestHook{}, &TestHook{}
logger := New()
logger.SetOutput(ioutil.Discard)
logger.AddHook(old)
hooks := make(LevelHooks)
hooks.Add(cur)
replaced := logger.ReplaceHooks(hooks)
logger.Info("test")
assert.Equal(t, old.Fired, false)
assert.Equal(t, cur.Fired, true)
logger.ReplaceHooks(replaced)
logger.Info("test")
assert.Equal(t, old.Fired, true)
}
// Compile test // Compile test
func TestLogrusInterface(t *testing.T) { func TestLogrusInterfaces(t *testing.T) {
var buffer bytes.Buffer var buffer bytes.Buffer
fn := func(l FieldLogger) { // This verifies FieldLogger and Ext1FieldLogger work as designed.
// Please don't use them. Use Logger and Entry directly.
fn := func(xl Ext1FieldLogger) {
var l FieldLogger = xl
b := l.WithField("key", "value") b := l.WithField("key", "value")
b.Debug("Test") b.Debug("Test")
} }
@ -550,3 +661,69 @@ func TestEntryWriter(t *testing.T) {
assert.Equal(t, fields["foo"], "bar") assert.Equal(t, fields["foo"], "bar")
assert.Equal(t, fields["level"], "warning") assert.Equal(t, fields["level"], "warning")
} }
func TestLogLevelEnabled(t *testing.T) {
log := New()
log.SetLevel(PanicLevel)
assert.Equal(t, true, log.IsLevelEnabled(PanicLevel))
assert.Equal(t, false, log.IsLevelEnabled(FatalLevel))
assert.Equal(t, false, log.IsLevelEnabled(ErrorLevel))
assert.Equal(t, false, log.IsLevelEnabled(WarnLevel))
assert.Equal(t, false, log.IsLevelEnabled(InfoLevel))
assert.Equal(t, false, log.IsLevelEnabled(DebugLevel))
assert.Equal(t, false, log.IsLevelEnabled(TraceLevel))
log.SetLevel(FatalLevel)
assert.Equal(t, true, log.IsLevelEnabled(PanicLevel))
assert.Equal(t, true, log.IsLevelEnabled(FatalLevel))
assert.Equal(t, false, log.IsLevelEnabled(ErrorLevel))
assert.Equal(t, false, log.IsLevelEnabled(WarnLevel))
assert.Equal(t, false, log.IsLevelEnabled(InfoLevel))
assert.Equal(t, false, log.IsLevelEnabled(DebugLevel))
assert.Equal(t, false, log.IsLevelEnabled(TraceLevel))
log.SetLevel(ErrorLevel)
assert.Equal(t, true, log.IsLevelEnabled(PanicLevel))
assert.Equal(t, true, log.IsLevelEnabled(FatalLevel))
assert.Equal(t, true, log.IsLevelEnabled(ErrorLevel))
assert.Equal(t, false, log.IsLevelEnabled(WarnLevel))
assert.Equal(t, false, log.IsLevelEnabled(InfoLevel))
assert.Equal(t, false, log.IsLevelEnabled(DebugLevel))
assert.Equal(t, false, log.IsLevelEnabled(TraceLevel))
log.SetLevel(WarnLevel)
assert.Equal(t, true, log.IsLevelEnabled(PanicLevel))
assert.Equal(t, true, log.IsLevelEnabled(FatalLevel))
assert.Equal(t, true, log.IsLevelEnabled(ErrorLevel))
assert.Equal(t, true, log.IsLevelEnabled(WarnLevel))
assert.Equal(t, false, log.IsLevelEnabled(InfoLevel))
assert.Equal(t, false, log.IsLevelEnabled(DebugLevel))
assert.Equal(t, false, log.IsLevelEnabled(TraceLevel))
log.SetLevel(InfoLevel)
assert.Equal(t, true, log.IsLevelEnabled(PanicLevel))
assert.Equal(t, true, log.IsLevelEnabled(FatalLevel))
assert.Equal(t, true, log.IsLevelEnabled(ErrorLevel))
assert.Equal(t, true, log.IsLevelEnabled(WarnLevel))
assert.Equal(t, true, log.IsLevelEnabled(InfoLevel))
assert.Equal(t, false, log.IsLevelEnabled(DebugLevel))
assert.Equal(t, false, log.IsLevelEnabled(TraceLevel))
log.SetLevel(DebugLevel)
assert.Equal(t, true, log.IsLevelEnabled(PanicLevel))
assert.Equal(t, true, log.IsLevelEnabled(FatalLevel))
assert.Equal(t, true, log.IsLevelEnabled(ErrorLevel))
assert.Equal(t, true, log.IsLevelEnabled(WarnLevel))
assert.Equal(t, true, log.IsLevelEnabled(InfoLevel))
assert.Equal(t, true, log.IsLevelEnabled(DebugLevel))
assert.Equal(t, false, log.IsLevelEnabled(TraceLevel))
log.SetLevel(TraceLevel)
assert.Equal(t, true, log.IsLevelEnabled(PanicLevel))
assert.Equal(t, true, log.IsLevelEnabled(FatalLevel))
assert.Equal(t, true, log.IsLevelEnabled(ErrorLevel))
assert.Equal(t, true, log.IsLevelEnabled(WarnLevel))
assert.Equal(t, true, log.IsLevelEnabled(InfoLevel))
assert.Equal(t, true, log.IsLevelEnabled(DebugLevel))
assert.Equal(t, true, log.IsLevelEnabled(TraceLevel))
}

View File

@ -1,10 +0,0 @@
// +build darwin freebsd openbsd netbsd dragonfly
// +build !appengine,!gopherjs
package logrus
import "golang.org/x/sys/unix"
const ioctlReadTermios = unix.TIOCGETA
type Termios unix.Termios

View File

@ -1,4 +1,4 @@
// +build appengine gopherjs // +build appengine
package logrus package logrus

11
terminal_check_js.go Normal file
View File

@ -0,0 +1,11 @@
// +build js
package logrus
import (
"io"
)
func checkIfTerminal(w io.Writer) bool {
return false
}

View File

@ -1,4 +1,4 @@
// +build !appengine,!gopherjs // +build !appengine,!js,!windows
package logrus package logrus

20
terminal_check_windows.go Normal file
View File

@ -0,0 +1,20 @@
// +build !appengine,!js,windows
package logrus
import (
"io"
"os"
"syscall"
)
func checkIfTerminal(w io.Writer) bool {
switch v := w.(type) {
case *os.File:
var mode uint32
err := syscall.GetConsoleMode(syscall.Handle(v.Fd()), &mode)
return err == nil
default:
return false
}
}

View File

@ -1,14 +0,0 @@
// Based on ssh/terminal:
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !appengine,!gopherjs
package logrus
import "golang.org/x/sys/unix"
const ioctlReadTermios = unix.TCGETS
type Termios unix.Termios

8
terminal_notwindows.go Normal file
View File

@ -0,0 +1,8 @@
// +build !windows
package logrus
import "io"
func initTerminal(w io.Writer) {
}

18
terminal_windows.go Normal file
View File

@ -0,0 +1,18 @@
// +build !appengine,!js,windows
package logrus
import (
"io"
"os"
"syscall"
sequences "github.com/konsorten/go-windows-terminal-sequences"
)
func initTerminal(w io.Writer) {
switch v := w.(type) {
case *os.File:
sequences.EnableVirtualTerminalProcessing(syscall.Handle(v.Fd()), true)
}
}

View File

@ -3,6 +3,7 @@ package logrus
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"os"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@ -20,6 +21,7 @@ const (
var ( var (
baseTimestamp time.Time baseTimestamp time.Time
emptyFieldMap FieldMap
) )
func init() { func init() {
@ -34,6 +36,9 @@ type TextFormatter struct {
// Force disabling colors. // Force disabling colors.
DisableColors bool DisableColors bool
// Override coloring based on CLICOLOR and CLICOLOR_FORCE. - https://bixense.com/clicolors/
EnvironmentOverrideColors bool
// Disable timestamp logging. useful when output is redirected to logging // Disable timestamp logging. useful when output is redirected to logging
// system that already adds timestamps. // system that already adds timestamps.
DisableTimestamp bool DisableTimestamp bool
@ -50,63 +55,127 @@ type TextFormatter struct {
// be desired. // be desired.
DisableSorting bool DisableSorting bool
// The keys sorting function, when uninitialized it uses sort.Strings.
SortingFunc func([]string)
// Disables the truncation of the level text to 4 characters.
DisableLevelTruncation bool
// QuoteEmptyFields will wrap empty fields in quotes if true // QuoteEmptyFields will wrap empty fields in quotes if true
QuoteEmptyFields bool QuoteEmptyFields bool
// Whether the logger's out is to a terminal // Whether the logger's out is to a terminal
isTerminal bool isTerminal bool
sync.Once // FieldMap allows users to customize the names of keys for default fields.
// As an example:
// formatter := &TextFormatter{
// FieldMap: FieldMap{
// FieldKeyTime: "@timestamp",
// FieldKeyLevel: "@level",
// FieldKeyMsg: "@message"}}
FieldMap FieldMap
terminalInitOnce sync.Once
} }
func (f *TextFormatter) init(entry *Entry) { func (f *TextFormatter) init(entry *Entry) {
if entry.Logger != nil { if entry.Logger != nil {
f.isTerminal = checkIfTerminal(entry.Logger.Out) f.isTerminal = checkIfTerminal(entry.Logger.Out)
if f.isTerminal {
initTerminal(entry.Logger.Out)
}
} }
} }
func (f *TextFormatter) isColored() bool {
isColored := f.ForceColors || f.isTerminal
if f.EnvironmentOverrideColors {
if force, ok := os.LookupEnv("CLICOLOR_FORCE"); ok && force != "0" {
isColored = true
} else if ok && force == "0" {
isColored = false
} else if os.Getenv("CLICOLOR") == "0" {
isColored = false
}
}
return isColored && !f.DisableColors
}
// Format renders a single log entry // Format renders a single log entry
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
var b *bytes.Buffer prefixFieldClashes(entry.Data, f.FieldMap, entry.HasCaller())
keys := make([]string, 0, len(entry.Data)) keys := make([]string, 0, len(entry.Data))
for k := range entry.Data { for k := range entry.Data {
keys = append(keys, k) keys = append(keys, k)
} }
if !f.DisableSorting { fixedKeys := make([]string, 0, 4+len(entry.Data))
sort.Strings(keys) if !f.DisableTimestamp {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyTime))
} }
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLevel))
if entry.Message != "" {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyMsg))
}
if entry.err != "" {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLogrusError))
}
if entry.HasCaller() {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFunc))
}
if !f.DisableSorting {
if f.SortingFunc == nil {
sort.Strings(keys)
fixedKeys = append(fixedKeys, keys...)
} else {
if !f.isColored() {
fixedKeys = append(fixedKeys, keys...)
f.SortingFunc(fixedKeys)
} else {
f.SortingFunc(keys)
}
}
} else {
fixedKeys = append(fixedKeys, keys...)
}
var b *bytes.Buffer
if entry.Buffer != nil { if entry.Buffer != nil {
b = entry.Buffer b = entry.Buffer
} else { } else {
b = &bytes.Buffer{} b = &bytes.Buffer{}
} }
prefixFieldClashes(entry.Data, entry.HasCaller()) f.terminalInitOnce.Do(func() { f.init(entry) })
f.Do(func() { f.init(entry) })
isColored := (f.ForceColors || f.isTerminal) && !f.DisableColors
timestampFormat := f.TimestampFormat timestampFormat := f.TimestampFormat
if timestampFormat == "" { if timestampFormat == "" {
timestampFormat = defaultTimestampFormat timestampFormat = defaultTimestampFormat
} }
if isColored { if f.isColored() {
f.printColored(b, entry, keys, timestampFormat) f.printColored(b, entry, keys, timestampFormat)
} else { } else {
if !f.DisableTimestamp { for _, key := range fixedKeys {
f.appendKeyValue(b, "time", entry.Time.Format(timestampFormat)) var value interface{}
} switch key {
f.appendKeyValue(b, "level", entry.Level.String()) case f.FieldMap.resolve(FieldKeyTime):
if entry.HasCaller() { value = entry.Time.Format(timestampFormat)
f.appendKeyValue(b, "func", entry.Caller) case f.FieldMap.resolve(FieldKeyLevel):
} value = entry.Level.String()
if entry.Message != "" { case f.FieldMap.resolve(FieldKeyMsg):
f.appendKeyValue(b, "msg", entry.Message) value = entry.Message
} case f.FieldMap.resolve(FieldKeyLogrusError):
for _, key := range keys { value = entry.err
f.appendKeyValue(b, key, entry.Data[key]) default:
value = entry.Data[key]
}
f.appendKeyValue(b, key, value)
} }
} }
@ -117,7 +186,7 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, timestampFormat string) { func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, timestampFormat string) {
var levelColor int var levelColor int
switch entry.Level { switch entry.Level {
case DebugLevel: case DebugLevel, TraceLevel:
levelColor = gray levelColor = gray
case WarnLevel: case WarnLevel:
levelColor = yellow levelColor = yellow
@ -127,7 +196,14 @@ func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []strin
levelColor = blue levelColor = blue
} }
levelText := strings.ToUpper(entry.Level.String())[0:4] levelText := strings.ToUpper(entry.Level.String())
if !f.DisableLevelTruncation {
levelText = levelText[0:4]
}
// Remove a single newline if it already exists in the message to keep
// the behavior of logrus text_formatter the same as the stdlib log package
entry.Message = strings.TrimSuffix(entry.Message, "\n")
caller := "" caller := ""

View File

@ -4,9 +4,14 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"os"
"sort"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestFormatting(t *testing.T) { func TestFormatting(t *testing.T) {
@ -128,6 +133,44 @@ func TestTimestampFormat(t *testing.T) {
checkTimeStr("") checkTimeStr("")
} }
func TestDisableLevelTruncation(t *testing.T) {
entry := &Entry{
Time: time.Now(),
Message: "testing",
}
keys := []string{}
timestampFormat := "Mon Jan 2 15:04:05 -0700 MST 2006"
checkDisableTruncation := func(disabled bool, level Level) {
tf := &TextFormatter{DisableLevelTruncation: disabled}
var b bytes.Buffer
entry.Level = level
tf.printColored(&b, entry, keys, timestampFormat)
logLine := (&b).String()
if disabled {
expected := strings.ToUpper(level.String())
if !strings.Contains(logLine, expected) {
t.Errorf("level string expected to be %s when truncation disabled", expected)
}
} else {
expected := strings.ToUpper(level.String())
if len(level.String()) > 4 {
if strings.Contains(logLine, expected) {
t.Errorf("level string %s expected to be truncated to %s when truncation is enabled", expected, expected[0:4])
}
} else {
if !strings.Contains(logLine, expected) {
t.Errorf("level string expected to be %s when truncation is enabled and level string is below truncation threshold", expected)
}
}
}
}
checkDisableTruncation(true, DebugLevel)
checkDisableTruncation(true, InfoLevel)
checkDisableTruncation(false, ErrorLevel)
checkDisableTruncation(false, InfoLevel)
}
func TestDisableTimestampWithColoredOutput(t *testing.T) { func TestDisableTimestampWithColoredOutput(t *testing.T) {
tf := &TextFormatter{DisableTimestamp: true, ForceColors: true} tf := &TextFormatter{DisableTimestamp: true, ForceColors: true}
@ -137,5 +180,301 @@ func TestDisableTimestampWithColoredOutput(t *testing.T) {
} }
} }
// TODO add tests for sorting etc., this requires a parser for the text func TestNewlineBehavior(t *testing.T) {
// formatter output. tf := &TextFormatter{ForceColors: true}
// Ensure a single new line is removed as per stdlib log
e := NewEntry(StandardLogger())
e.Message = "test message\n"
b, _ := tf.Format(e)
if bytes.Contains(b, []byte("test message\n")) {
t.Error("first newline at end of Entry.Message resulted in unexpected 2 newlines in output. Expected newline to be removed.")
}
// Ensure a double new line is reduced to a single new line
e = NewEntry(StandardLogger())
e.Message = "test message\n\n"
b, _ = tf.Format(e)
if bytes.Contains(b, []byte("test message\n\n")) {
t.Error("Double newline at end of Entry.Message resulted in unexpected 2 newlines in output. Expected single newline")
}
if !bytes.Contains(b, []byte("test message\n")) {
t.Error("Double newline at end of Entry.Message did not result in a single newline after formatting")
}
}
func TestTextFormatterFieldMap(t *testing.T) {
formatter := &TextFormatter{
DisableColors: true,
FieldMap: FieldMap{
FieldKeyMsg: "message",
FieldKeyLevel: "somelevel",
FieldKeyTime: "timeywimey",
},
}
entry := &Entry{
Message: "oh hi",
Level: WarnLevel,
Time: time.Date(1981, time.February, 24, 4, 28, 3, 100, time.UTC),
Data: Fields{
"field1": "f1",
"message": "messagefield",
"somelevel": "levelfield",
"timeywimey": "timeywimeyfield",
},
}
b, err := formatter.Format(entry)
if err != nil {
t.Fatal("Unable to format entry: ", err)
}
assert.Equal(t,
`timeywimey="1981-02-24T04:28:03Z" `+
`somelevel=warning `+
`message="oh hi" `+
`field1=f1 `+
`fields.message=messagefield `+
`fields.somelevel=levelfield `+
`fields.timeywimey=timeywimeyfield`+"\n",
string(b),
"Formatted output doesn't respect FieldMap")
}
func TestTextFormatterIsColored(t *testing.T) {
params := []struct {
name string
expectedResult bool
isTerminal bool
disableColor bool
forceColor bool
envColor bool
clicolorIsSet bool
clicolorForceIsSet bool
clicolorVal string
clicolorForceVal string
}{
// Default values
{
name: "testcase1",
expectedResult: false,
isTerminal: false,
disableColor: false,
forceColor: false,
envColor: false,
clicolorIsSet: false,
clicolorForceIsSet: false,
},
// Output on terminal
{
name: "testcase2",
expectedResult: true,
isTerminal: true,
disableColor: false,
forceColor: false,
envColor: false,
clicolorIsSet: false,
clicolorForceIsSet: false,
},
// Output on terminal with color disabled
{
name: "testcase3",
expectedResult: false,
isTerminal: true,
disableColor: true,
forceColor: false,
envColor: false,
clicolorIsSet: false,
clicolorForceIsSet: false,
},
// Output not on terminal with color disabled
{
name: "testcase4",
expectedResult: false,
isTerminal: false,
disableColor: true,
forceColor: false,
envColor: false,
clicolorIsSet: false,
clicolorForceIsSet: false,
},
// Output not on terminal with color forced
{
name: "testcase5",
expectedResult: true,
isTerminal: false,
disableColor: false,
forceColor: true,
envColor: false,
clicolorIsSet: false,
clicolorForceIsSet: false,
},
// Output on terminal with clicolor set to "0"
{
name: "testcase6",
expectedResult: false,
isTerminal: true,
disableColor: false,
forceColor: false,
envColor: true,
clicolorIsSet: true,
clicolorForceIsSet: false,
clicolorVal: "0",
},
// Output on terminal with clicolor set to "1"
{
name: "testcase7",
expectedResult: true,
isTerminal: true,
disableColor: false,
forceColor: false,
envColor: true,
clicolorIsSet: true,
clicolorForceIsSet: false,
clicolorVal: "1",
},
// Output not on terminal with clicolor set to "0"
{
name: "testcase8",
expectedResult: false,
isTerminal: false,
disableColor: false,
forceColor: false,
envColor: true,
clicolorIsSet: true,
clicolorForceIsSet: false,
clicolorVal: "0",
},
// Output not on terminal with clicolor set to "1"
{
name: "testcase9",
expectedResult: false,
isTerminal: false,
disableColor: false,
forceColor: false,
envColor: true,
clicolorIsSet: true,
clicolorForceIsSet: false,
clicolorVal: "1",
},
// Output not on terminal with clicolor set to "1" and force color
{
name: "testcase10",
expectedResult: true,
isTerminal: false,
disableColor: false,
forceColor: true,
envColor: true,
clicolorIsSet: true,
clicolorForceIsSet: false,
clicolorVal: "1",
},
// Output not on terminal with clicolor set to "0" and force color
{
name: "testcase11",
expectedResult: false,
isTerminal: false,
disableColor: false,
forceColor: true,
envColor: true,
clicolorIsSet: true,
clicolorForceIsSet: false,
clicolorVal: "0",
},
// Output not on terminal with clicolor_force set to "1"
{
name: "testcase12",
expectedResult: true,
isTerminal: false,
disableColor: false,
forceColor: false,
envColor: true,
clicolorIsSet: false,
clicolorForceIsSet: true,
clicolorForceVal: "1",
},
// Output not on terminal with clicolor_force set to "0"
{
name: "testcase13",
expectedResult: false,
isTerminal: false,
disableColor: false,
forceColor: false,
envColor: true,
clicolorIsSet: false,
clicolorForceIsSet: true,
clicolorForceVal: "0",
},
// Output on terminal with clicolor_force set to "0"
{
name: "testcase14",
expectedResult: false,
isTerminal: true,
disableColor: false,
forceColor: false,
envColor: true,
clicolorIsSet: false,
clicolorForceIsSet: true,
clicolorForceVal: "0",
},
}
cleanenv := func() {
os.Unsetenv("CLICOLOR")
os.Unsetenv("CLICOLOR_FORCE")
}
defer cleanenv()
for _, val := range params {
t.Run("textformatter_"+val.name, func(subT *testing.T) {
tf := TextFormatter{
isTerminal: val.isTerminal,
DisableColors: val.disableColor,
ForceColors: val.forceColor,
EnvironmentOverrideColors: val.envColor,
}
cleanenv()
if val.clicolorIsSet {
os.Setenv("CLICOLOR", val.clicolorVal)
}
if val.clicolorForceIsSet {
os.Setenv("CLICOLOR_FORCE", val.clicolorForceVal)
}
res := tf.isColored()
assert.Equal(subT, val.expectedResult, res)
})
}
}
func TestCustomSorting(t *testing.T) {
formatter := &TextFormatter{
DisableColors: true,
SortingFunc: func(keys []string) {
sort.Slice(keys, func(i, j int) bool {
if keys[j] == "prefix" {
return false
}
if keys[i] == "prefix" {
return true
}
return strings.Compare(keys[i], keys[j]) == -1
})
},
}
entry := &Entry{
Message: "Testing custom sort function",
Time: time.Now(),
Level: InfoLevel,
Data: Fields{
"test": "testvalue",
"prefix": "the application prefix",
"blablabla": "blablabla",
},
}
b, err := formatter.Format(entry)
require.NoError(t, err)
require.True(t, strings.HasPrefix(string(b), "prefix="), "format output is %q", string(b))
}

View File

@ -24,6 +24,8 @@ func (entry *Entry) WriterLevel(level Level) *io.PipeWriter {
var printFunc func(args ...interface{}) var printFunc func(args ...interface{})
switch level { switch level {
case TraceLevel:
printFunc = entry.Trace
case DebugLevel: case DebugLevel:
printFunc = entry.Debug printFunc = entry.Debug
case InfoLevel: case InfoLevel: