From 40069a98d66cf6190ad97b6990d672cc2324dcac Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Sat, 26 Jul 2014 21:26:04 -0400 Subject: [PATCH] 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]