entry: break out time, level and message from data

This commit is contained in:
Simon Eskildsen 2014-07-26 21:26:04 -04:00
parent f0cb18fc85
commit 40069a98d6
6 changed files with 98 additions and 3 deletions

View File

@ -261,6 +261,9 @@ type MyJSONFormatter struct {
log.Formatter = new(MyJSONFormatter) log.Formatter = new(MyJSONFormatter)
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { 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) serialized, err := json.Marshal(entry.Data)
if err != nil { if 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)

View File

@ -11,6 +11,15 @@ import (
type Entry struct { type Entry struct {
Logger *Logger Logger *Logger
Data Fields 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 var baseTimestamp time.Time
@ -53,9 +62,9 @@ func (entry *Entry) WithFields(fields Fields) *Entry {
} }
func (entry *Entry) log(level Level, msg string) string { func (entry *Entry) log(level Level, msg string) string {
entry.Data["time"] = time.Now().String() entry.Time = time.Now()
entry.Data["level"] = level.String() entry.Level = level
entry.Data["msg"] = msg entry.Message = msg
if err := entry.Logger.Hooks.Fire(level, entry); err != nil { if err := entry.Logger.Hooks.Fire(level, entry); err != nil {
fmt.Fprintf(os.Stderr, "Failed to fire hook", err) fmt.Fprintf(os.Stderr, "Failed to fire hook", err)

View File

@ -1,5 +1,9 @@
package logrus package logrus
import (
"time"
)
// 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:
// //
@ -13,3 +17,41 @@ package logrus
type Formatter interface { type Formatter interface {
Format(*Entry) ([]byte, error) 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()
}

View File

@ -6,9 +6,12 @@ import (
) )
type JSONFormatter struct { type JSONFormatter struct {
*internalFormatter
} }
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
f.prefixFieldClashes(entry)
serialized, err := json.Marshal(entry.Data) serialized, err := json.Marshal(entry.Data)
if err != nil { if 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)

View File

@ -129,6 +129,40 @@ func TestWithFieldsShouldAllowAssignments(t *testing.T) {
assert.Equal(t, "value1", fields["key1"]) 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) { func TestConvertLevelToString(t *testing.T) {
assert.Equal(t, "debug", DebugLevel.String()) assert.Equal(t, "debug", DebugLevel.String())
assert.Equal(t, "info", InfoLevel.String()) assert.Equal(t, "info", InfoLevel.String())

View File

@ -27,11 +27,15 @@ func miniTS() int {
type TextFormatter struct { type TextFormatter struct {
// Set to true to bypass checking for a TTY before outputting colors. // Set to true to bypass checking for a TTY before outputting colors.
ForceColors bool ForceColors bool
*internalFormatter
} }
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
b := &bytes.Buffer{} b := &bytes.Buffer{}
f.prefixFieldClashes(entry)
if f.ForceColors || IsTerminal() { if f.ForceColors || IsTerminal() {
levelText := strings.ToUpper(entry.Data["level"].(string))[0:4] levelText := strings.ToUpper(entry.Data["level"].(string))[0:4]