From c44d5246287d92a71847107c7907dd52ce55142a Mon Sep 17 00:00:00 2001 From: Aditya Mukerjee Date: Mon, 9 Oct 2017 11:18:43 -0400 Subject: [PATCH 01/34] Fix typo in docstring --- logger.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logger.go b/logger.go index fdaf8a6..03a5de2 100644 --- a/logger.go +++ b/logger.go @@ -10,7 +10,7 @@ import ( type Logger struct { // 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 - // something more adventorous, such as logging to Kafka. + // something more adventurous, such as logging to Kafka. Out io.Writer // 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 From bf1fb70b2b4308a0aed02f31c5f327be46620269 Mon Sep 17 00:00:00 2001 From: Neil Isaac Date: Tue, 21 Nov 2017 22:43:47 -0500 Subject: [PATCH 02/34] Add FieldMap support to TestFormatter --- text_formatter.go | 15 ++++++++++++--- text_formatter_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/text_formatter.go b/text_formatter.go index be412aa..8592825 100644 --- a/text_formatter.go +++ b/text_formatter.go @@ -60,6 +60,15 @@ type TextFormatter struct { // Whether the logger's out is to a terminal isTerminal bool + // FieldMap allows users to customize the names of keys for default fields. + // As an example: + // formatter := &JSONFormatter{ + // FieldMap: FieldMap{ + // FieldKeyTime: "@timestamp", + // FieldKeyLevel: "@level", + // FieldKeyMsg: "@message"}} + FieldMap FieldMap + sync.Once } @@ -109,11 +118,11 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { f.printColored(b, entry, keys, timestampFormat) } else { if !f.DisableTimestamp { - f.appendKeyValue(b, "time", entry.Time.Format(timestampFormat)) + f.appendKeyValue(b, f.FieldMap.resolve(FieldKeyTime), entry.Time.Format(timestampFormat)) } - f.appendKeyValue(b, "level", entry.Level.String()) + f.appendKeyValue(b, f.FieldMap.resolve(FieldKeyLevel), entry.Level.String()) if entry.Message != "" { - f.appendKeyValue(b, "msg", entry.Message) + f.appendKeyValue(b, f.FieldMap.resolve(FieldKeyMsg), entry.Message) } for _, key := range keys { f.appendKeyValue(b, key, entry.Data[key]) diff --git a/text_formatter_test.go b/text_formatter_test.go index d93b931..789d52d 100644 --- a/text_formatter_test.go +++ b/text_formatter_test.go @@ -7,6 +7,8 @@ import ( "strings" "testing" "time" + + "github.com/stretchr/testify/assert" ) func TestFormatting(t *testing.T) { @@ -137,5 +139,32 @@ func TestDisableTimestampWithColoredOutput(t *testing.T) { } } +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), + } + + 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"`+"\n", + string(b), + "Formatted doesn't respect correct FieldMap") +} + // TODO add tests for sorting etc., this requires a parser for the text // formatter output. From b9eceae8f663facb1b89f517fa03e4ae43a5c517 Mon Sep 17 00:00:00 2001 From: Neil Isaac Date: Tue, 21 Nov 2017 22:56:37 -0500 Subject: [PATCH 03/34] fix example --- text_formatter.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/text_formatter.go b/text_formatter.go index 8592825..aa0694c 100644 --- a/text_formatter.go +++ b/text_formatter.go @@ -62,11 +62,11 @@ type TextFormatter struct { // FieldMap allows users to customize the names of keys for default fields. // As an example: - // formatter := &JSONFormatter{ + // formatter := &TextFormatter{ // FieldMap: FieldMap{ - // FieldKeyTime: "@timestamp", + // FieldKeyTime: "@timestamp", // FieldKeyLevel: "@level", - // FieldKeyMsg: "@message"}} + // FieldKeyMsg: "@message"}} FieldMap FieldMap sync.Once From 0c03a05a0e896c23dcd631acafd2f35add6a3fec Mon Sep 17 00:00:00 2001 From: conor Date: Thu, 21 Dec 2017 14:04:49 -0500 Subject: [PATCH 04/34] mirror and wrap Logger instance methods in exported.go --- .gitignore | 1 + exported.go | 16 +++------------- logger.go | 21 +++++++++++++++++++++ 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 66be63a..c722487 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ logrus +.idea \ No newline at end of file diff --git a/exported.go b/exported.go index 013183e..141e951 100644 --- a/exported.go +++ b/exported.go @@ -15,36 +15,26 @@ func StandardLogger() *Logger { // SetOutput sets the standard logger output. func SetOutput(out io.Writer) { - std.mu.Lock() - defer std.mu.Unlock() - std.Out = out + std.SetOutput(out) } // SetFormatter sets the standard logger formatter. func SetFormatter(formatter Formatter) { - std.mu.Lock() - defer std.mu.Unlock() - std.Formatter = formatter + std.SetFormatter(formatter) } // SetLevel sets the standard logger level. func SetLevel(level Level) { - std.mu.Lock() - defer std.mu.Unlock() std.SetLevel(level) } // GetLevel returns the standard logger level. func GetLevel() Level { - std.mu.Lock() - defer std.mu.Unlock() - return std.level() + return std.GetLevel() } // AddHook adds a hook to the standard logger hooks. func AddHook(hook Hook) { - std.mu.Lock() - defer std.mu.Unlock() std.Hooks.Add(hook) } diff --git a/logger.go b/logger.go index fdaf8a6..91617fc 100644 --- a/logger.go +++ b/logger.go @@ -312,12 +312,33 @@ func (logger *Logger) level() Level { return Level(atomic.LoadUint32((*uint32)(&logger.Level))) } +// SetLevel sets the logger level. func (logger *Logger) SetLevel(level 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) { logger.mu.Lock() defer logger.mu.Unlock() logger.Hooks.Add(hook) } + +// 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 +} From 20cc8e2bc33863de7191b9bfb55d27804e05e90e Mon Sep 17 00:00:00 2001 From: conor Date: Thu, 21 Dec 2017 14:10:48 -0500 Subject: [PATCH 05/34] remove .gitignore changes --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index c722487..bb7e111 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -logrus -.idea \ No newline at end of file +logrus \ No newline at end of file From eb156905d7c48f0f2191a3f805e59849c9c018ed Mon Sep 17 00:00:00 2001 From: conor Date: Thu, 21 Dec 2017 14:16:49 -0500 Subject: [PATCH 06/34] remove .gitignore changes and update AddHook --- .gitignore | 2 +- exported.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index bb7e111..66be63a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -logrus \ No newline at end of file +logrus diff --git a/exported.go b/exported.go index 141e951..e5e484f 100644 --- a/exported.go +++ b/exported.go @@ -35,7 +35,7 @@ func GetLevel() Level { // AddHook adds a hook to the standard logger hooks. func AddHook(hook Hook) { - std.Hooks.Add(hook) + std.AddHook(hook) } // WithError creates an entry from the standard logger and adds an error to it, using the value defined in ErrorKey as key. From 92aece568bf7ac7d2008928efa09da14b63395fe Mon Sep 17 00:00:00 2001 From: Dennis de Reus Date: Fri, 29 Dec 2017 20:26:35 +0100 Subject: [PATCH 07/34] TextFormatter behaviour aligned with stdlib log (fixes #167) stdlib `log` adds a newline at the end of a message if none is present, otherwise does not. Before this change logrus would always add a newline, resulting in inconsistent behaviour if stdlib log was replaced with logrus, and a user would e.g. use 'log.printf("test\n")' --- text_formatter.go | 4 ++++ text_formatter_test.go | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/text_formatter.go b/text_formatter.go index 61b21ca..9f7dc35 100644 --- a/text_formatter.go +++ b/text_formatter.go @@ -126,6 +126,10 @@ func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []strin levelText := strings.ToUpper(entry.Level.String())[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") + if f.DisableTimestamp { fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m %-44s ", levelColor, levelText, entry.Message) } else if !f.FullTimestamp { diff --git a/text_formatter_test.go b/text_formatter_test.go index d93b931..aea8727 100644 --- a/text_formatter_test.go +++ b/text_formatter_test.go @@ -137,5 +137,28 @@ func TestDisableTimestampWithColoredOutput(t *testing.T) { } } +func TestNewlineBehavior(t *testing.T) { + 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") + } +} + // TODO add tests for sorting etc., this requires a parser for the text // formatter output. From efbfdb5f09fe8f6efee1efd21ae690156304cf36 Mon Sep 17 00:00:00 2001 From: Michael Haines Date: Mon, 5 Feb 2018 12:42:00 -0700 Subject: [PATCH 08/34] Add failing test for using a FieldLogger with hooks inside goroutines --- logrus_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/logrus_test.go b/logrus_test.go index 78cbc28..1585709 100644 --- a/logrus_test.go +++ b/logrus_test.go @@ -343,6 +343,24 @@ func TestLoggingRace(t *testing.T) { wg.Wait() } +func TestLoggingRaceWithHooksOnFieldLogger(t *testing.T) { + logger := New() + hook := new(ModifyHook) + logger.AddHook(hook) + fieldLogger := logger.WithField("context", "clue") + + var wg sync.WaitGroup + wg.Add(100) + + for i := 0; i < 100; i++ { + go func() { + fieldLogger.Info("info") + wg.Done() + }() + } + wg.Wait() +} + // Compile test func TestLogrusInterface(t *testing.T) { var buffer bytes.Buffer From eeb653535cb49f0aee7aefce8583b2593d4466fd Mon Sep 17 00:00:00 2001 From: Michael Haines Date: Mon, 5 Feb 2018 12:44:11 -0700 Subject: [PATCH 09/34] Lock mutex before formatting to avoid race --- entry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entry.go b/entry.go index df6f92d..24ded45 100644 --- a/entry.go +++ b/entry.go @@ -123,9 +123,9 @@ func (entry *Entry) fireHooks() { } func (entry *Entry) write() { - serialized, err := entry.Logger.Formatter.Format(entry) entry.Logger.mu.Lock() defer entry.Logger.mu.Unlock() + serialized, err := entry.Logger.Formatter.Format(entry) if err != nil { fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err) } else { From 828a649ef2d3660936a7182dadbf5573de38433d Mon Sep 17 00:00:00 2001 From: Michael Haines Date: Mon, 5 Feb 2018 12:52:11 -0700 Subject: [PATCH 10/34] rename fieldLogger to entry --- logrus_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/logrus_test.go b/logrus_test.go index 1585709..cd17602 100644 --- a/logrus_test.go +++ b/logrus_test.go @@ -343,18 +343,18 @@ func TestLoggingRace(t *testing.T) { wg.Wait() } -func TestLoggingRaceWithHooksOnFieldLogger(t *testing.T) { +func TestLoggingRaceWithHooksOnEntry(t *testing.T) { logger := New() hook := new(ModifyHook) logger.AddHook(hook) - fieldLogger := logger.WithField("context", "clue") + entry := logger.WithField("context", "clue") var wg sync.WaitGroup wg.Add(100) for i := 0; i < 100; i++ { go func() { - fieldLogger.Info("info") + entry.Info("info") wg.Done() }() } From 5513c600346d78234f1addc60f4953908cdd79c5 Mon Sep 17 00:00:00 2001 From: David Bariod Date: Mon, 16 Apr 2018 14:16:44 +0200 Subject: [PATCH 11/34] Improve documentation for Fatal* class functions --- exported.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/exported.go b/exported.go index 013183e..dfd982a 100644 --- a/exported.go +++ b/exported.go @@ -107,7 +107,7 @@ func Panic(args ...interface{}) { 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{}) { std.Fatal(args...) } @@ -147,7 +147,7 @@ func Panicf(format string, args ...interface{}) { 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{}) { std.Fatalf(format, args...) } @@ -187,7 +187,7 @@ func Panicln(args ...interface{}) { 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{}) { std.Fatalln(args...) } From aa6766adfe97f5f5d05f9bd694986d171db9b4ff Mon Sep 17 00:00:00 2001 From: taylorchu Date: Tue, 15 May 2018 10:07:01 -0700 Subject: [PATCH 12/34] PERF: use buffer pool in json formatter benchmark old ns/op new ns/op delta BenchmarkLogrus-8 4163 4369 +4.95% benchmark old allocs new allocs delta BenchmarkLogrus-8 36 31 -13.89% benchmark old bytes new bytes delta BenchmarkLogrus-8 3027 2163 -28.54% --- json_formatter.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/json_formatter.go b/json_formatter.go index 7064947..82a1da8 100644 --- a/json_formatter.go +++ b/json_formatter.go @@ -1,6 +1,7 @@ package logrus import ( + "bytes" "encoding/json" "fmt" ) @@ -71,9 +72,15 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { data[f.FieldMap.resolve(FieldKeyMsg)] = entry.Message data[f.FieldMap.resolve(FieldKeyLevel)] = entry.Level.String() - serialized, err := json.Marshal(data) + var b *bytes.Buffer + if entry.Buffer != nil { + b = entry.Buffer + } else { + b = &bytes.Buffer{} + } + err := json.NewEncoder(b).Encode(data) if err != nil { return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err) } - return append(serialized, '\n'), nil + return b.Bytes(), nil } From caed59ec68033ac87a3ef67eccf1d52282efc1bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=87o?= Date: Thu, 17 May 2018 11:02:39 +0200 Subject: [PATCH 13/34] Fix Logger.WithField doscription I was puzzled by function documentation not mentioning it works with Error level, so I had to check it out by creating example before I add logrus as a dependency on the company project. Example confirmed what logic was telling me that Logger.WithFields works with Error level of logs. This is is a fix of this small documentation oversight. --- logger.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logger.go b/logger.go index fdaf8a6..0ac8ce2 100644 --- a/logger.go +++ b/logger.go @@ -88,7 +88,7 @@ func (logger *Logger) releaseEntry(entry *Entry) { } // 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`. func (logger *Logger) WithField(key string, value interface{}) *Entry { entry := logger.newEntry() From 070c81def33f6362a8267b6a4e56fb7bf23fc6b5 Mon Sep 17 00:00:00 2001 From: Moriyoshi Koizumi Date: Wed, 30 May 2018 09:50:59 +0000 Subject: [PATCH 14/34] Revert the change introduced in #707 and do the proper fix. Fixes #729 --- entry.go | 6 ++---- hooks/test/test.go | 15 ++++++--------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/entry.go b/entry.go index d075d72..1be48ab 100644 --- a/entry.go +++ b/entry.go @@ -113,12 +113,10 @@ func (entry Entry) log(level Level, msg string) { } } -// This function is not declared with a pointer value because otherwise -// race conditions will occur when using multiple goroutines -func (entry Entry) fireHooks() { +func (entry *Entry) fireHooks() { entry.Logger.mu.Lock() defer entry.Logger.mu.Unlock() - err := entry.Logger.Hooks.Fire(entry.Level, &entry) + err := entry.Logger.Hooks.Fire(entry.Level, entry) if err != nil { fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err) } diff --git a/hooks/test/test.go b/hooks/test/test.go index 62c4845..234a17d 100644 --- a/hooks/test/test.go +++ b/hooks/test/test.go @@ -15,7 +15,7 @@ type Hook struct { // 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 // value directly. - Entries []*logrus.Entry + Entries []logrus.Entry mu sync.RWMutex } @@ -52,7 +52,7 @@ func NewNullLogger() (*logrus.Logger, *Hook) { func (t *Hook) Fire(e *logrus.Entry) error { t.mu.Lock() defer t.mu.Unlock() - t.Entries = append(t.Entries, e) + t.Entries = append(t.Entries, *e) return nil } @@ -68,9 +68,7 @@ func (t *Hook) LastEntry() *logrus.Entry { if i < 0 { return nil } - // Make a copy, for safety - e := *t.Entries[i] - return &e + return &t.Entries[i] } // AllEntries returns all entries that were logged. @@ -79,10 +77,9 @@ func (t *Hook) AllEntries() []*logrus.Entry { defer t.mu.RUnlock() // Make a copy so the returned value won't race with future log requests 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 - e := *entry - entries[i] = &e + entries[i] = &t.Entries[i] } return entries } @@ -91,5 +88,5 @@ func (t *Hook) AllEntries() []*logrus.Entry { func (t *Hook) Reset() { t.mu.Lock() defer t.mu.Unlock() - t.Entries = make([]*logrus.Entry, 0) + t.Entries = make([]logrus.Entry, 0) } From 5d60369ef3a5c165e66ece9cdebb2d4177729d84 Mon Sep 17 00:00:00 2001 From: Neil Isaac Date: Mon, 18 Jun 2018 21:32:35 -0400 Subject: [PATCH 15/34] Fixed prefixFieldClashes for TextFormatter and added coverage --- formatter.go | 3 +++ text_formatter.go | 8 ++++---- text_formatter_test.go | 14 +++++++++++++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/formatter.go b/formatter.go index 849dc8b..83c7494 100644 --- a/formatter.go +++ b/formatter.go @@ -34,15 +34,18 @@ func prefixFieldClashes(data Fields, fieldMap FieldMap) { timeKey := fieldMap.resolve(FieldKeyTime) if t, ok := data[timeKey]; ok { data["fields."+timeKey] = t + delete(data, timeKey) } msgKey := fieldMap.resolve(FieldKeyMsg) if m, ok := data[msgKey]; ok { data["fields."+msgKey] = m + delete(data, msgKey) } levelKey := fieldMap.resolve(FieldKeyLevel) if l, ok := data[levelKey]; ok { data["fields."+levelKey] = l + delete(data, levelKey) } } diff --git a/text_formatter.go b/text_formatter.go index 5af4e56..3e55040 100644 --- a/text_formatter.go +++ b/text_formatter.go @@ -51,7 +51,6 @@ type TextFormatter struct { // be desired. DisableSorting bool - // Disables the truncation of the level text to 4 characters. DisableLevelTruncation bool @@ -81,7 +80,8 @@ func (f *TextFormatter) init(entry *Entry) { // Format renders a single log entry func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { - var b *bytes.Buffer + prefixFieldClashes(entry.Data, f.FieldMap) + keys := make([]string, 0, len(entry.Data)) for k := range entry.Data { keys = append(keys, k) @@ -90,14 +90,14 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { if !f.DisableSorting { sort.Strings(keys) } + + var b *bytes.Buffer if entry.Buffer != nil { b = entry.Buffer } else { b = &bytes.Buffer{} } - prefixFieldClashes(entry.Data, emptyFieldMap) - f.Do(func() { f.init(entry) }) isColored := (f.ForceColors || f.isTerminal) && !f.DisableColors diff --git a/text_formatter_test.go b/text_formatter_test.go index 4f21861..7245f94 100644 --- a/text_formatter_test.go +++ b/text_formatter_test.go @@ -191,6 +191,12 @@ func TestTextFormatterFieldMap(t *testing.T) { 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) @@ -199,7 +205,13 @@ func TestTextFormatterFieldMap(t *testing.T) { } assert.Equal(t, - `timeywimey="1981-02-24T04:28:03Z" somelevel=warning message="oh hi"`+"\n", + `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 doesn't respect correct FieldMap") } From 6b28c2c7d76fb49e829b83ab6dc2f25327db3189 Mon Sep 17 00:00:00 2001 From: Neil Isaac Date: Mon, 18 Jun 2018 21:39:53 -0400 Subject: [PATCH 16/34] error message --- text_formatter_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text_formatter_test.go b/text_formatter_test.go index 7245f94..921d052 100644 --- a/text_formatter_test.go +++ b/text_formatter_test.go @@ -213,7 +213,7 @@ func TestTextFormatterFieldMap(t *testing.T) { `fields.somelevel=levelfield `+ `fields.timeywimey=timeywimeyfield`+"\n", string(b), - "Formatted doesn't respect correct FieldMap") + "Formatted output doesn't respect FieldMap") } // TODO add tests for sorting etc., this requires a parser for the text From 2ce6c0cb44b88b3cf9b97e7513269d00c7eee11c Mon Sep 17 00:00:00 2001 From: Przemyslaw Wegrzyn Date: Tue, 19 Jun 2018 14:31:57 +0200 Subject: [PATCH 17/34] Support for Entry data under nested JSON dictionary. --- json_formatter.go | 10 ++++++++++ json_formatter_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/json_formatter.go b/json_formatter.go index 7064947..dab1761 100644 --- a/json_formatter.go +++ b/json_formatter.go @@ -33,6 +33,9 @@ type JSONFormatter struct { // DisableTimestamp allows disabling automatic timestamps in output 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. // As an example: // formatter := &JSONFormatter{ @@ -58,6 +61,13 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { data[k] = v } } + + if f.DataKey != "" { + newData := make(Fields, 4) + newData[f.DataKey] = data + data = newData + } + prefixFieldClashes(data, f.FieldMap) timestampFormat := f.TimestampFormat diff --git a/json_formatter_test.go b/json_formatter_test.go index 1c140d0..0dde300 100644 --- a/json_formatter_test.go +++ b/json_formatter_test.go @@ -161,6 +161,48 @@ func TestFieldClashWithRemappedFields(t *testing.T) { } } +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) { formatter := &JSONFormatter{} From eed1c0f832603c63deae9d5ffa2c4a6e611ed31f Mon Sep 17 00:00:00 2001 From: Christian Stewart Date: Wed, 20 Jun 2018 21:39:23 -0700 Subject: [PATCH 18/34] Fix GopherJS build tags The GopherJS build tag is "js" not "gopherjs" Signed-off-by: Christian Stewart --- terminal_bsd.go | 2 +- terminal_check_appengine.go | 2 +- terminal_check_notappengine.go | 2 +- terminal_linux.go | 2 +- text_formatter_js.go | 11 +++++++++++ text_formatter_other.go | 3 +++ 6 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 text_formatter_js.go create mode 100644 text_formatter_other.go diff --git a/terminal_bsd.go b/terminal_bsd.go index 4880d13..5b6212d 100644 --- a/terminal_bsd.go +++ b/terminal_bsd.go @@ -1,5 +1,5 @@ // +build darwin freebsd openbsd netbsd dragonfly -// +build !appengine,!gopherjs +// +build !appengine,!js package logrus diff --git a/terminal_check_appengine.go b/terminal_check_appengine.go index 3de08e8..26a2867 100644 --- a/terminal_check_appengine.go +++ b/terminal_check_appengine.go @@ -1,4 +1,4 @@ -// +build appengine gopherjs +// +build appengine js package logrus diff --git a/terminal_check_notappengine.go b/terminal_check_notappengine.go index 067047a..87f0b80 100644 --- a/terminal_check_notappengine.go +++ b/terminal_check_notappengine.go @@ -1,4 +1,4 @@ -// +build !appengine,!gopherjs +// +build !appengine,!js package logrus diff --git a/terminal_linux.go b/terminal_linux.go index f29a009..634e39b 100644 --- a/terminal_linux.go +++ b/terminal_linux.go @@ -3,7 +3,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build !appengine,!gopherjs +// +build !appengine,!js package logrus diff --git a/text_formatter_js.go b/text_formatter_js.go new file mode 100644 index 0000000..d52803a --- /dev/null +++ b/text_formatter_js.go @@ -0,0 +1,11 @@ +// +build js + +package logrus + +import ( + "io" +) + +func (f *TextFormatter) checkIfTerminal(w io.Writer) bool { + return false +} diff --git a/text_formatter_other.go b/text_formatter_other.go new file mode 100644 index 0000000..0d9704f --- /dev/null +++ b/text_formatter_other.go @@ -0,0 +1,3 @@ +// +build !js + +package logrus From fc9bbf2f57995271c5cd6911ede7a2ebc5ea7c6f Mon Sep 17 00:00:00 2001 From: Daniel Bershatsky Date: Wed, 27 Jun 2018 20:29:28 +0300 Subject: [PATCH 19/34] [#241] Allow to set writer during logger usage. --- exported.go | 4 +--- logger.go | 6 ++++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/exported.go b/exported.go index 013183e..6c98afd 100644 --- a/exported.go +++ b/exported.go @@ -15,9 +15,7 @@ func StandardLogger() *Logger { // SetOutput sets the standard logger output. func SetOutput(out io.Writer) { - std.mu.Lock() - defer std.mu.Unlock() - std.Out = out + std.SetOutput(out) } // SetFormatter sets the standard logger formatter. diff --git a/logger.go b/logger.go index 0ac8ce2..0c1b05e 100644 --- a/logger.go +++ b/logger.go @@ -316,6 +316,12 @@ func (logger *Logger) SetLevel(level Level) { atomic.StoreUint32((*uint32)(&logger.Level), uint32(level)) } +func (logger *Logger) SetOutput(out io.Writer) { + logger.mu.Lock() + defer logger.mu.Unlock() + logger.Out = out +} + func (logger *Logger) AddHook(hook Hook) { logger.mu.Lock() defer logger.mu.Unlock() From 52b92f5b89ba5a81f021ad845bcd3a0d3ba1b2ac Mon Sep 17 00:00:00 2001 From: Simon Brisson Date: Thu, 28 Jun 2018 16:33:52 -0400 Subject: [PATCH 20/34] Allows overriding Entry.Time. --- entry.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/entry.go b/entry.go index d075d72..c3adf01 100644 --- a/entry.go +++ b/entry.go @@ -90,7 +90,16 @@ func (entry *Entry) WithFields(fields Fields) *Entry { // race conditions will occur when using multiple goroutines func (entry Entry) log(level Level, msg string) { 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.Message = msg From 725f3be1995f9bb46c7aa065ff200bef3a346cee Mon Sep 17 00:00:00 2001 From: Simon Brisson Date: Fri, 29 Jun 2018 10:53:51 -0400 Subject: [PATCH 21/34] Adds WithTime to Logger and Entry types, as well as a pure module-level function. --- entry.go | 7 +++++- exported.go | 10 +++++++++ logger.go | 8 +++++++ logrus_test.go | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 1 deletion(-) diff --git a/entry.go b/entry.go index c3adf01..14f0a26 100644 --- a/entry.go +++ b/entry.go @@ -83,7 +83,12 @@ func (entry *Entry) WithFields(fields Fields) *Entry { for k, v := range fields { data[k] = v } - return &Entry{Logger: entry.Logger, Data: data} + return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time} +} + +// Overrides the time of the Entry. +func (entry *Entry) WithTime(t time.Time) *Entry { + return &Entry{Logger: entry.Logger, Data: entry.Data, Time: t} } // This function is not declared with a pointer value because otherwise diff --git a/exported.go b/exported.go index 013183e..ec1a417 100644 --- a/exported.go +++ b/exported.go @@ -2,6 +2,7 @@ package logrus import ( "io" + "time" ) var ( @@ -72,6 +73,15 @@ func WithFields(fields Fields) *Entry { 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) +} + // Debug logs a message at level Debug on the standard logger. func Debug(args ...interface{}) { std.Debug(args...) diff --git a/logger.go b/logger.go index 0ac8ce2..52b942d 100644 --- a/logger.go +++ b/logger.go @@ -5,6 +5,7 @@ import ( "os" "sync" "sync/atomic" + "time" ) type Logger struct { @@ -112,6 +113,13 @@ func (logger *Logger) WithError(err error) *Entry { 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) Debugf(format string, args ...interface{}) { if logger.level() >= DebugLevel { entry := logger.newEntry() diff --git a/logrus_test.go b/logrus_test.go index 78cbc28..78e1301 100644 --- a/logrus_test.go +++ b/logrus_test.go @@ -7,6 +7,7 @@ import ( "strings" "sync" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -209,6 +210,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) { var buffer bytes.Buffer From 6999e59e73b0b94716d9cef60c51e5adb5e5b4c3 Mon Sep 17 00:00:00 2001 From: David Bariod Date: Fri, 20 Jul 2018 13:16:19 +0200 Subject: [PATCH 22/34] properly fix the hooks race test --- hooks/test/test_test.go | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/hooks/test/test_test.go b/hooks/test/test_test.go index 742be55..d6f6d30 100644 --- a/hooks/test/test_test.go +++ b/hooks/test/test_test.go @@ -1,8 +1,10 @@ package test import ( + "math/rand" "sync" "testing" + "time" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" @@ -38,24 +40,34 @@ func TestAllHooks(t *testing.T) { } func TestLoggingWithHooksRace(t *testing.T) { + + rand.Seed(time.Now().Unix()) + unlocker := rand.Int() % 100 + assert := assert.New(t) logger, hook := NewNullLogger() - var wg sync.WaitGroup - wg.Add(100) + var wgOne, wgAll sync.WaitGroup + wgOne.Add(1) + wgAll.Add(100) for i := 0; i < 100; i++ { - go func() { + go func(i int) { logger.Info("info") - wg.Done() - }() + wgAll.Done() + if i == unlocker { + wgOne.Done() + } + }(i) } - wg.Wait() + wgOne.Wait() assert.Equal(logrus.InfoLevel, hook.LastEntry().Level) assert.Equal("info", hook.LastEntry().Message) + wgAll.Wait() + entries := hook.AllEntries() assert.Equal(100, len(entries)) } From 54db2bb29af499574a7b8f7f86dcf1dc11297823 Mon Sep 17 00:00:00 2001 From: David Bariod Date: Fri, 20 Jul 2018 13:34:26 +0200 Subject: [PATCH 23/34] limit the build/test matrix to the two latest stable version --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index aebdc35..2f19b4a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,7 @@ language: go go: - - 1.8.x - 1.9.x - 1.10.x - - tip env: - GOMAXPROCS=4 GORACE=halt_on_error=1 install: From d3162770a8b8e496c83f63327ede96304f57ddee Mon Sep 17 00:00:00 2001 From: David Bariod Date: Sat, 28 Jul 2018 17:21:06 +0200 Subject: [PATCH 24/34] Add logger benchmark --- logger_bench_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/logger_bench_test.go b/logger_bench_test.go index dd23a35..f0a7684 100644 --- a/logger_bench_test.go +++ b/logger_bench_test.go @@ -1,6 +1,7 @@ package logrus import ( + "io/ioutil" "os" "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") + } + }) +} From 179037fcd41cd279507e65aeebb32b0af35958fc Mon Sep 17 00:00:00 2001 From: David Bariod Date: Tue, 31 Jul 2018 18:08:27 +0200 Subject: [PATCH 25/34] Ensure a new entry data fields are empty Fixes #795 --- hook_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++ logger.go | 1 + 2 files changed, 44 insertions(+) diff --git a/hook_test.go b/hook_test.go index 4fea751..80b93b8 100644 --- a/hook_test.go +++ b/hook_test.go @@ -1,10 +1,13 @@ package logrus import ( + "bytes" + "encoding/json" "sync" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type TestHook struct { @@ -85,6 +88,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 { Fired bool } diff --git a/logger.go b/logger.go index 342f797..7fa8d7d 100644 --- a/logger.go +++ b/logger.go @@ -85,6 +85,7 @@ func (logger *Logger) newEntry() *Entry { } func (logger *Logger) releaseEntry(entry *Entry) { + entry.Data = map[string]interface{}{} logger.entryPool.Put(entry) } From 37d651c1f2847d8514b79d1dd7389be06ec60447 Mon Sep 17 00:00:00 2001 From: Alessio Caiazza Date: Fri, 13 Jul 2018 17:33:25 +0200 Subject: [PATCH 26/34] Add CLICOLOR support This implement CLICOLOR and CLICOLOR_FORCE check on terminal coloring as defined in https://bixense.com/clicolors/ --- text_formatter.go | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/text_formatter.go b/text_formatter.go index 3e55040..cdf3185 100644 --- a/text_formatter.go +++ b/text_formatter.go @@ -3,6 +3,7 @@ package logrus import ( "bytes" "fmt" + "os" "sort" "strings" "sync" @@ -35,6 +36,9 @@ type TextFormatter struct { // Force disabling colors. DisableColors bool + // Override coloring based on CLICOLOR and CLICOLOR_FORCE. - https://bixense.com/clicolors/ + OverrideColors bool + // Disable timestamp logging. useful when output is redirected to logging // system that already adds timestamps. DisableTimestamp bool @@ -78,6 +82,22 @@ func (f *TextFormatter) init(entry *Entry) { } } +func (f *TextFormatter) isColored() bool { + isColored := f.ForceColors || f.isTerminal + + if f.OverrideColors { + if force, ok := os.LookupEnv("CLICOLOR_FORCE"); ok && force != "0" { + isColored = true + } + + if os.Getenv("CLICOLOR") == "0" { + isColored = false + } + } + + return isColored && !f.DisableColors +} + // Format renders a single log entry func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { prefixFieldClashes(entry.Data, f.FieldMap) @@ -100,13 +120,11 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { f.Do(func() { f.init(entry) }) - isColored := (f.ForceColors || f.isTerminal) && !f.DisableColors - timestampFormat := f.TimestampFormat if timestampFormat == "" { timestampFormat = defaultTimestampFormat } - if isColored { + if f.isColored() { f.printColored(b, entry, keys, timestampFormat) } else { if !f.DisableTimestamp { From da39da23485a153e5c9b7902e4b5cefbd924ef78 Mon Sep 17 00:00:00 2001 From: Kwok-kuen Cheung Date: Mon, 6 Aug 2018 00:43:49 +0800 Subject: [PATCH 27/34] Keep terminal check naming convention --- terminal_check_appengine.go | 2 +- text_formatter_js.go => terminal_check_js.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename text_formatter_js.go => terminal_check_js.go (51%) diff --git a/terminal_check_appengine.go b/terminal_check_appengine.go index 26a2867..2403de9 100644 --- a/terminal_check_appengine.go +++ b/terminal_check_appengine.go @@ -1,4 +1,4 @@ -// +build appengine js +// +build appengine package logrus diff --git a/text_formatter_js.go b/terminal_check_js.go similarity index 51% rename from text_formatter_js.go rename to terminal_check_js.go index d52803a..0c20975 100644 --- a/text_formatter_js.go +++ b/terminal_check_js.go @@ -6,6 +6,6 @@ import ( "io" ) -func (f *TextFormatter) checkIfTerminal(w io.Writer) bool { +func checkIfTerminal(w io.Writer) bool { return false } From d950ecd55bc2e1ddaf9af20cb8c6910a5633b031 Mon Sep 17 00:00:00 2001 From: Kwok-kuen Cheung Date: Mon, 6 Aug 2018 00:43:58 +0800 Subject: [PATCH 28/34] Remove unnecessary text_formatter file --- text_formatter_other.go | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 text_formatter_other.go diff --git a/text_formatter_other.go b/text_formatter_other.go deleted file mode 100644 index 0d9704f..0000000 --- a/text_formatter_other.go +++ /dev/null @@ -1,3 +0,0 @@ -// +build !js - -package logrus From 8a6a17c00343eaa23f978b454d878ae304992ef2 Mon Sep 17 00:00:00 2001 From: Dennis Date: Sun, 5 Aug 2018 22:40:58 +0200 Subject: [PATCH 29/34] Fixed missing brace after wrong merge --- text_formatter_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/text_formatter_test.go b/text_formatter_test.go index 72adda1..092b19d 100644 --- a/text_formatter_test.go +++ b/text_formatter_test.go @@ -198,6 +198,7 @@ func TestNewlineBehavior(t *testing.T) { 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{ From eb968b65069b7b415d83ba5a6451718d3f93763e Mon Sep 17 00:00:00 2001 From: David Bariod Date: Thu, 9 Aug 2018 15:00:46 +0200 Subject: [PATCH 30/34] Fix for CLICOLOR_FORCE handling --- text_formatter.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/text_formatter.go b/text_formatter.go index cdf3185..beb628f 100644 --- a/text_formatter.go +++ b/text_formatter.go @@ -37,7 +37,7 @@ type TextFormatter struct { DisableColors bool // Override coloring based on CLICOLOR and CLICOLOR_FORCE. - https://bixense.com/clicolors/ - OverrideColors bool + EnvironmentOverrideColors bool // Disable timestamp logging. useful when output is redirected to logging // system that already adds timestamps. @@ -85,12 +85,12 @@ func (f *TextFormatter) init(entry *Entry) { func (f *TextFormatter) isColored() bool { isColored := f.ForceColors || f.isTerminal - if f.OverrideColors { + if f.EnvironmentOverrideColors { if force, ok := os.LookupEnv("CLICOLOR_FORCE"); ok && force != "0" { isColored = true - } - - if os.Getenv("CLICOLOR") == "0" { + } else if ok && force == "0" { + isColored = false + } else if os.Getenv("CLICOLOR") == "0" { isColored = false } } From cadf2ceaf8580dddc21b34895f5fed8d2c3b2e60 Mon Sep 17 00:00:00 2001 From: David Bariod Date: Thu, 9 Aug 2018 15:01:49 +0200 Subject: [PATCH 31/34] Add unit test for TextFormatter.isColored --- text_formatter_test.go | 214 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) diff --git a/text_formatter_test.go b/text_formatter_test.go index 921d052..023f346 100644 --- a/text_formatter_test.go +++ b/text_formatter_test.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "fmt" + "os" "strings" "testing" "time" @@ -216,5 +217,218 @@ func TestTextFormatterFieldMap(t *testing.T) { "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 set to "1" + { + name: "testcase12", + expectedResult: false, + isTerminal: false, + disableColor: false, + forceColor: false, + envColor: true, + clicolorIsSet: true, + clicolorForceIsSet: false, + clicolorVal: "1", + }, + // Output not on terminal with clicolor_force set to "1" + { + name: "testcase13", + 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: "testcase14", + 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", + }, + } + + defer os.Clearenv() + + 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, + } + os.Clearenv() + 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) + }) + } +} + // TODO add tests for sorting etc., this requires a parser for the text // formatter output. From b5e6fae4fba49f90e609d6379cc6409fa8f85e2e Mon Sep 17 00:00:00 2001 From: David Bariod Date: Mon, 13 Aug 2018 17:27:32 +0200 Subject: [PATCH 32/34] Cleanup on unit test on isColored --- text_formatter_test.go | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/text_formatter_test.go b/text_formatter_test.go index 023f346..652102d 100644 --- a/text_formatter_test.go +++ b/text_formatter_test.go @@ -357,21 +357,9 @@ func TestTextFormatterIsColored(t *testing.T) { clicolorForceIsSet: false, clicolorVal: "0", }, - // Output not on terminal with clicolor set to "1" - { - name: "testcase12", - expectedResult: false, - isTerminal: false, - disableColor: false, - forceColor: false, - envColor: true, - clicolorIsSet: true, - clicolorForceIsSet: false, - clicolorVal: "1", - }, // Output not on terminal with clicolor_force set to "1" { - name: "testcase13", + name: "testcase12", expectedResult: true, isTerminal: false, disableColor: false, @@ -383,7 +371,7 @@ func TestTextFormatterIsColored(t *testing.T) { }, // Output not on terminal with clicolor_force set to "0" { - name: "testcase14", + name: "testcase13", expectedResult: false, isTerminal: false, disableColor: false, @@ -407,7 +395,12 @@ func TestTextFormatterIsColored(t *testing.T) { }, } - defer os.Clearenv() + cleanenv := func() { + os.Unsetenv("CLICOLOR") + os.Unsetenv("CLICOLOR_FORCE") + } + + defer cleanenv() for _, val := range params { t.Run("textformatter_"+val.name, func(subT *testing.T) { @@ -417,7 +410,7 @@ func TestTextFormatterIsColored(t *testing.T) { ForceColors: val.forceColor, EnvironmentOverrideColors: val.envColor, } - os.Clearenv() + cleanenv() if val.clicolorIsSet { os.Setenv("CLICOLOR", val.clicolorVal) } From 7a0120e2c67ac3a748674a84fbb6ca4fe6231897 Mon Sep 17 00:00:00 2001 From: betrok Date: Wed, 22 Aug 2018 12:10:05 +0300 Subject: [PATCH 33/34] logger.ReplaceHooks --- logger.go | 6 ++++++ logrus_test.go | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/logger.go b/logger.go index 7fa8d7d..53bbf95 100644 --- a/logger.go +++ b/logger.go @@ -336,3 +336,9 @@ func (logger *Logger) AddHook(hook Hook) { defer logger.mu.Unlock() logger.Hooks.Add(hook) } + +func (logger *Logger) ReplaceHooks(hooks LevelHooks) { + logger.mu.Lock() + logger.Hooks = hooks + logger.mu.Unlock() +} diff --git a/logrus_test.go b/logrus_test.go index 57fb8d1..7a96686 100644 --- a/logrus_test.go +++ b/logrus_test.go @@ -421,6 +421,22 @@ func TestLoggingRaceWithHooksOnEntry(t *testing.T) { wg.Wait() } +func TestHooksReplace(t *testing.T) { + old, cur := &TestHook{}, &TestHook{} + + logger := New() + logger.AddHook(old) + + hooks := make(LevelHooks) + hooks.Add(cur) + logger.ReplaceHooks(hooks) + + logger.Info("test") + + assert.Equal(t, old.Fired, false) + assert.Equal(t, cur.Fired, true) +} + // Compile test func TestLogrusInterface(t *testing.T) { var buffer bytes.Buffer From 13d10d8d89db071ade54fb0b1667817dd48dc53e Mon Sep 17 00:00:00 2001 From: betrok Date: Sun, 26 Aug 2018 14:40:51 +0300 Subject: [PATCH 34/34] return old hooks from RelplaceHooks --- logger.go | 5 ++++- logrus_test.go | 10 ++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/logger.go b/logger.go index 53bbf95..885f150 100644 --- a/logger.go +++ b/logger.go @@ -337,8 +337,11 @@ func (logger *Logger) AddHook(hook Hook) { logger.Hooks.Add(hook) } -func (logger *Logger) ReplaceHooks(hooks LevelHooks) { +// 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 } diff --git a/logrus_test.go b/logrus_test.go index 7a96686..f6db6e9 100644 --- a/logrus_test.go +++ b/logrus_test.go @@ -3,6 +3,7 @@ package logrus import ( "bytes" "encoding/json" + "io/ioutil" "strconv" "strings" "sync" @@ -421,20 +422,25 @@ func TestLoggingRaceWithHooksOnEntry(t *testing.T) { wg.Wait() } -func TestHooksReplace(t *testing.T) { +func TestReplaceHooks(t *testing.T) { old, cur := &TestHook{}, &TestHook{} logger := New() + logger.SetOutput(ioutil.Discard) logger.AddHook(old) hooks := make(LevelHooks) hooks.Add(cur) - logger.ReplaceHooks(hooks) + 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