diff --git a/zapcore/console_encoder.go b/zapcore/console_encoder.go index 9fc89e924..b7875966f 100644 --- a/zapcore/console_encoder.go +++ b/zapcore/console_encoder.go @@ -80,7 +80,14 @@ func (c consoleEncoder) EncodeEntry(ent Entry, fields []Field) (*buffer.Buffer, c.EncodeLevel(ent.Level, arr) } if ent.LoggerName != "" && c.NameKey != "" { - arr.AppendString(ent.LoggerName) + nameEncoder := c.EncodeName + + if nameEncoder == nil { + // Fall back to FullNameEncoder for backward compatibility. + nameEncoder = FullNameEncoder + } + + nameEncoder(ent.LoggerName, arr) } if ent.Caller.Defined && c.CallerKey != "" && c.EncodeCaller != nil { c.EncodeCaller(ent.Caller, arr) diff --git a/zapcore/encoder.go b/zapcore/encoder.go index a026f4679..f0509522b 100644 --- a/zapcore/encoder.go +++ b/zapcore/encoder.go @@ -196,6 +196,27 @@ func (e *CallerEncoder) UnmarshalText(text []byte) error { return nil } +// A NameEncoder serializes a period-separated logger name to a primitive +// type. +type NameEncoder func(string, PrimitiveArrayEncoder) + +// FullNameEncoder serializes the logger name as-is. +func FullNameEncoder(loggerName string, enc PrimitiveArrayEncoder) { + enc.AppendString(loggerName) +} + +// UnmarshalText unmarshals text to a NameEncoder. Currently, everything is +// unmarshaled to FullNameEncoder. +func (e *NameEncoder) UnmarshalText(text []byte) error { + switch string(text) { + case "full": + *e = FullNameEncoder + default: + *e = FullNameEncoder + } + return nil +} + // An EncoderConfig allows users to configure the concrete encoders supplied by // zapcore. type EncoderConfig struct { @@ -215,6 +236,9 @@ type EncoderConfig struct { EncodeTime TimeEncoder `json:"timeEncoder" yaml:"timeEncoder"` EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"` EncodeCaller CallerEncoder `json:"callerEncoder" yaml:"callerEncoder"` + // Unlike the other primitive type encoders, EncodeName is optional. The + // zero value falls back to FullNameEncoder. + EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"` } // ObjectEncoder is a strongly-typed, encoding-agnostic interface for adding a diff --git a/zapcore/encoder_test.go b/zapcore/encoder_test.go index 0708902fa..04641678c 100644 --- a/zapcore/encoder_test.go +++ b/zapcore/encoder_test.go @@ -21,6 +21,7 @@ package zapcore_test import ( + "strings" "testing" "time" @@ -74,6 +75,10 @@ func withConsoleEncoder(f func(Encoder)) { f(NewConsoleEncoder(humanEncoderConfig())) } +func capitalNameEncoder(loggerName string, enc PrimitiveArrayEncoder) { + enc.AppendString(strings.ToUpper(loggerName)) +} + func TestEncoderConfiguration(t *testing.T) { base := testEncoderConfig() @@ -293,6 +298,25 @@ func TestEncoderConfiguration(t *testing.T) { expectedJSON: `{"L":"INFO","T":0,"N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}` + "\n", expectedConsole: "0\tINFO\tmain\tfoo.go:42\thello\nfake-stack\n", }, + { + desc: "use the supplied EncodeName", + cfg: EncoderConfig{ + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeName: capitalNameEncoder, + }, + expectedJSON: `{"L":"info","T":0,"N":"MAIN","C":"foo.go:42","M":"hello","S":"fake-stack"}` + "\n", + expectedConsole: "0\tinfo\tMAIN\tfoo.go:42\thello\nfake-stack\n", + }, { desc: "close all open namespaces", cfg: EncoderConfig{ @@ -393,6 +417,25 @@ func TestEncoderConfiguration(t *testing.T) { expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}` + "\n", expectedConsole: "0\tinfo\tmain\thello\nfake-stack\n", }, + { + desc: "handle no-op EncodeName", + cfg: EncoderConfig{ + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeName: func(string, PrimitiveArrayEncoder) {}, + }, + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}` + "\n", + expectedConsole: "0\tinfo\tfoo.go:42\thello\nfake-stack\n", + }, { desc: "use custom line separator", cfg: EncoderConfig{ @@ -559,6 +602,28 @@ func TestCallerEncoders(t *testing.T) { } } +func TestNameEncoders(t *testing.T) { + tests := []struct { + name string + expected interface{} // output of encoding InfoLevel + }{ + {"", "main"}, + {"full", "main"}, + {"something-random", "main"}, + } + + for _, tt := range tests { + var ne NameEncoder + require.NoError(t, ne.UnmarshalText([]byte(tt.name)), "Unexpected error unmarshaling %q.", tt.name) + assertAppended( + t, + tt.expected, + func(arr ArrayEncoder) { ne("main", arr) }, + "Unexpected output serializing logger name with %q.", tt.name, + ) + } +} + func assertAppended(t testing.TB, expected interface{}, f func(ArrayEncoder), msgAndArgs ...interface{}) { mem := NewMapObjectEncoder() mem.AddArray("k", ArrayMarshalerFunc(func(arr ArrayEncoder) error { diff --git a/zapcore/json_encoder.go b/zapcore/json_encoder.go index 7d4a22c8d..1006ba2b1 100644 --- a/zapcore/json_encoder.go +++ b/zapcore/json_encoder.go @@ -302,7 +302,21 @@ func (enc *jsonEncoder) EncodeEntry(ent Entry, fields []Field) (*buffer.Buffer, } if ent.LoggerName != "" && final.NameKey != "" { final.addKey(final.NameKey) - final.AppendString(ent.LoggerName) + cur := final.buf.Len() + nameEncoder := final.EncodeName + + // if no name encoder provided, fall back to FullNameEncoder for backwards + // compatibility + if nameEncoder == nil { + nameEncoder = FullNameEncoder + } + + nameEncoder(ent.LoggerName, final) + if cur == final.buf.Len() { + // User-supplied EncodeName was a no-op. Fall back to strings to + // keep output JSON valid. + final.AppendString(ent.LoggerName) + } } if ent.Caller.Defined && final.CallerKey != "" { final.addKey(final.CallerKey)