From 40069a98d66cf6190ad97b6990d672cc2324dcac Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Sat, 26 Jul 2014 21:26:04 -0400 Subject: [PATCH 1/4] entry: break out time, level and message from data --- README.md | 3 +++ entry.go | 15 ++++++++++++--- formatter.go | 42 ++++++++++++++++++++++++++++++++++++++++++ json_formatter.go | 3 +++ logrus_test.go | 34 ++++++++++++++++++++++++++++++++++ text_formatter.go | 4 ++++ 6 files changed, 98 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 318c2a9..108fb75 100644 --- a/README.md +++ b/README.md @@ -261,6 +261,9 @@ type MyJSONFormatter struct { log.Formatter = new(MyJSONFormatter) func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { + // Note this doesn't include Time, Level and Message which are available on + // the Entry. Consult `godoc` on information about those fields or read the + // source of the official loggers. serialized, err := json.Marshal(entry.Data) if err != nil { return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err) diff --git a/entry.go b/entry.go index 8292ded..1c8e041 100644 --- a/entry.go +++ b/entry.go @@ -11,6 +11,15 @@ import ( type Entry struct { Logger *Logger Data Fields + + // Time at which the log entry was created + Time time.Time + + // Level the log entry was logged at: Debug, Info, Warn, Error, Fatal or Panic + Level Level + + // Message passed to Debug, Info, Warn, Error, Fatal or Panic + Message string } var baseTimestamp time.Time @@ -53,9 +62,9 @@ func (entry *Entry) WithFields(fields Fields) *Entry { } func (entry *Entry) log(level Level, msg string) string { - entry.Data["time"] = time.Now().String() - entry.Data["level"] = level.String() - entry.Data["msg"] = msg + entry.Time = time.Now() + entry.Level = level + entry.Message = msg if err := entry.Logger.Hooks.Fire(level, entry); err != nil { fmt.Fprintf(os.Stderr, "Failed to fire hook", err) diff --git a/formatter.go b/formatter.go index 3a2eff5..6c9560d 100644 --- a/formatter.go +++ b/formatter.go @@ -1,5 +1,9 @@ package logrus +import ( + "time" +) + // The Formatter interface is used to implement a custom Formatter. It takes an // `Entry`. It exposes all the fields, including the default ones: // @@ -13,3 +17,41 @@ package logrus type Formatter interface { Format(*Entry) ([]byte, error) } + +type internalFormatter struct { +} + +// This is to not silently overwrite `time`, `msg` and `level` fields when +// dumping it. If this code wasn't there doing: +// +// logrus.WithField("level", 1).Info("hello") +// +// Would just silently drop the user provided level. Instead with this code +// it'll logged as: +// +// {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."} +// +// It's not exported because it's still using Data in an opionated way. It's to +// avoid code duplication between the two default formatters. +func (f *internalFormatter) prefixFieldClashes(entry *Entry) { + _, ok := entry.Data["time"] + if ok { + entry.Data["fields.time"] = entry.Data["time"] + } + + entry.Data["time"] = entry.Time.Format(time.RFC3339) + + _, ok = entry.Data["msg"] + if ok { + entry.Data["fields.msg"] = entry.Data["msg"] + } + + entry.Data["msg"] = entry.Message + + _, ok = entry.Data["level"] + if ok { + entry.Data["fields.level"] = entry.Data["level"] + } + + entry.Data["level"] = entry.Level.String() +} diff --git a/json_formatter.go b/json_formatter.go index cb3489e..c3fb9b8 100644 --- a/json_formatter.go +++ b/json_formatter.go @@ -6,9 +6,12 @@ import ( ) type JSONFormatter struct { + *internalFormatter } func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { + f.prefixFieldClashes(entry) + serialized, err := json.Marshal(entry.Data) if err != nil { return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err) diff --git a/logrus_test.go b/logrus_test.go index f14445c..6202300 100644 --- a/logrus_test.go +++ b/logrus_test.go @@ -129,6 +129,40 @@ func TestWithFieldsShouldAllowAssignments(t *testing.T) { assert.Equal(t, "value1", fields["key1"]) } +func TestUserSuppliedFieldDoesNotOverwriteDefaults(t *testing.T) { + LogAndAssertJSON(t, func(log *Logger) { + log.WithField("msg", "hello").Info("test") + }, func(fields Fields) { + assert.Equal(t, fields["msg"], "test") + }) +} + +func TestUserSuppliedMsgFieldHasPrefix(t *testing.T) { + LogAndAssertJSON(t, func(log *Logger) { + log.WithField("msg", "hello").Info("test") + }, func(fields Fields) { + assert.Equal(t, fields["msg"], "test") + assert.Equal(t, fields["fields.msg"], "hello") + }) +} + +func TestUserSuppliedTimeFieldHasPrefix(t *testing.T) { + LogAndAssertJSON(t, func(log *Logger) { + log.WithField("time", "hello").Info("test") + }, func(fields Fields) { + assert.Equal(t, fields["fields.time"], "hello") + }) +} + +func TestUserSuppliedLevelFieldHasPrefix(t *testing.T) { + LogAndAssertJSON(t, func(log *Logger) { + log.WithField("level", 1).Info("test") + }, func(fields Fields) { + assert.Equal(t, fields["level"], "info") + assert.Equal(t, fields["fields.level"], 1) + }) +} + func TestConvertLevelToString(t *testing.T) { assert.Equal(t, "debug", DebugLevel.String()) assert.Equal(t, "info", InfoLevel.String()) diff --git a/text_formatter.go b/text_formatter.go index 06b4970..09fafde 100644 --- a/text_formatter.go +++ b/text_formatter.go @@ -27,11 +27,15 @@ func miniTS() int { type TextFormatter struct { // Set to true to bypass checking for a TTY before outputting colors. ForceColors bool + + *internalFormatter } func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { b := &bytes.Buffer{} + f.prefixFieldClashes(entry) + if f.ForceColors || IsTerminal() { levelText := strings.ToUpper(entry.Data["level"].(string))[0:4] From cfddc663250ab4a9dd995175f15afded1df99b99 Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Sat, 26 Jul 2014 21:37:06 -0400 Subject: [PATCH 2/4] entry: document entry and methods --- entry.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/entry.go b/entry.go index 1c8e041..44ff056 100644 --- a/entry.go +++ b/entry.go @@ -8,9 +8,15 @@ import ( "time" ) +// An entry is the final or intermediate Logrus logging entry. It containts all +// the fields passed with WithField{,s}. It's finally logged when Debug, Info, +// Warn, Error, Fatal or Panic is called on it. These objects can be reused and +// passed around as much as you wish to avoid field duplication. type Entry struct { Logger *Logger - Data Fields + + // Contains all the fields set by the user. + Data Fields // Time at which the log entry was created Time time.Time @@ -32,11 +38,14 @@ func NewEntry(logger *Logger) *Entry { } } +// Returns a reader for the entry, which is a proxy to the formatter. func (entry *Entry) Reader() (*bytes.Buffer, error) { serialized, err := entry.Logger.Formatter.Format(entry) return bytes.NewBuffer(serialized), err } +// Returns the string representation from the reader and ultimately the +// formatter. func (entry *Entry) String() (string, error) { reader, err := entry.Reader() if err != nil { @@ -46,10 +55,12 @@ func (entry *Entry) String() (string, error) { return reader.String(), err } +// Add a single field to the Entry. func (entry *Entry) WithField(key string, value interface{}) *Entry { return entry.WithFields(Fields{key: value}) } +// Add a map of fields to the Entry. func (entry *Entry) WithFields(fields Fields) *Entry { data := Fields{} for k, v := range entry.Data { From 2c0db7c868ca1bb005627e6816fa5f51cb9708c1 Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Sat, 26 Jul 2014 22:22:39 -0400 Subject: [PATCH 3/4] formatter: drop internalFormatter --- formatter.go | 5 +---- json_formatter.go | 3 +-- text_formatter.go | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/formatter.go b/formatter.go index 6c9560d..fc0ebd7 100644 --- a/formatter.go +++ b/formatter.go @@ -18,9 +18,6 @@ type Formatter interface { Format(*Entry) ([]byte, error) } -type internalFormatter struct { -} - // This is to not silently overwrite `time`, `msg` and `level` fields when // dumping it. If this code wasn't there doing: // @@ -33,7 +30,7 @@ type internalFormatter struct { // // It's not exported because it's still using Data in an opionated way. It's to // avoid code duplication between the two default formatters. -func (f *internalFormatter) prefixFieldClashes(entry *Entry) { +func prefixFieldClashes(entry *Entry) { _, ok := entry.Data["time"] if ok { entry.Data["fields.time"] = entry.Data["time"] diff --git a/json_formatter.go b/json_formatter.go index c3fb9b8..c0e2d18 100644 --- a/json_formatter.go +++ b/json_formatter.go @@ -6,11 +6,10 @@ import ( ) type JSONFormatter struct { - *internalFormatter } func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { - f.prefixFieldClashes(entry) + prefixFieldClashes(entry) serialized, err := json.Marshal(entry.Data) if err != nil { diff --git a/text_formatter.go b/text_formatter.go index 09fafde..d71eba1 100644 --- a/text_formatter.go +++ b/text_formatter.go @@ -27,14 +27,12 @@ func miniTS() int { type TextFormatter struct { // Set to true to bypass checking for a TTY before outputting colors. ForceColors bool - - *internalFormatter } func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { b := &bytes.Buffer{} - f.prefixFieldClashes(entry) + prefixFieldClashes(entry) if f.ForceColors || IsTerminal() { levelText := strings.ToUpper(entry.Data["level"].(string))[0:4] From 33c9d5aebc1b5419db852072f07e14de7705132e Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Sat, 26 Jul 2014 22:23:41 -0400 Subject: [PATCH 4/4] readme: fix levels --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 108fb75..e1ff807 100644 --- a/README.md +++ b/README.md @@ -191,10 +191,10 @@ that severity or anything above it: ```go // Will log anything that is info or above (warn, error, fatal, panic). Default. -log.Level = logrus.Info +log.Level = logrus.InfoLevel ``` -It may be useful to set `log.Level = logrus.Debug` in a debug or verbose +It may be useful to set `log.Level = logrus.DebugLevel` in a debug or verbose environment if your application has that. #### Entries