entry: break out time, level and message from data
diff --git a/README.md b/README.md
index 318c2a9..108fb75 100644
--- a/README.md
+++ b/README.md
@@ -261,6 +261,9 @@
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 @@
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) 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 @@
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 @@
)
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 @@
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 @@
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]