diff --git a/config.go b/config.go index e76e4e64f..edc8d5edd 100644 --- a/config.go +++ b/config.go @@ -123,18 +123,19 @@ type Config struct { // cfg.EncodeTime = zapcore.ISO8601TimeEncoder func NewProductionEncoderConfig() zapcore.EncoderConfig { return zapcore.EncoderConfig{ - TimeKey: "ts", - LevelKey: "level", - NameKey: "logger", - CallerKey: "caller", - FunctionKey: zapcore.OmitKey, - MessageKey: "msg", - StacktraceKey: "stacktrace", - LineEnding: zapcore.DefaultLineEnding, - EncodeLevel: zapcore.LowercaseLevelEncoder, - EncodeTime: zapcore.EpochTimeEncoder, - EncodeDuration: zapcore.SecondsDurationEncoder, - EncodeCaller: zapcore.ShortCallerEncoder, + TimeKey: "ts", + LevelKey: "level", + NameKey: "logger", + CallerKey: "caller", + FunctionKey: zapcore.OmitKey, + MessageKey: "msg", + StacktraceKey: "stacktrace", + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.LowercaseLevelEncoder, + EncodeTime: zapcore.EpochTimeEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + EncodeStacktrace: zapcore.FullStacktraceEncoder, } } @@ -200,18 +201,19 @@ func NewProductionConfig() Config { func NewDevelopmentEncoderConfig() zapcore.EncoderConfig { return zapcore.EncoderConfig{ // Keys can be anything except the empty string. - TimeKey: "T", - LevelKey: "L", - NameKey: "N", - CallerKey: "C", - FunctionKey: zapcore.OmitKey, - MessageKey: "M", - StacktraceKey: "S", - LineEnding: zapcore.DefaultLineEnding, - EncodeLevel: zapcore.CapitalLevelEncoder, - EncodeTime: zapcore.ISO8601TimeEncoder, - EncodeDuration: zapcore.StringDurationEncoder, - EncodeCaller: zapcore.ShortCallerEncoder, + TimeKey: "T", + LevelKey: "L", + NameKey: "N", + CallerKey: "C", + FunctionKey: zapcore.OmitKey, + MessageKey: "M", + StacktraceKey: "S", + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.CapitalLevelEncoder, + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.StringDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + EncodeStacktrace: zapcore.FullStacktraceEncoder, } } diff --git a/zapcore/console_encoder.go b/zapcore/console_encoder.go index cc2b4e07b..9d119355b 100644 --- a/zapcore/console_encoder.go +++ b/zapcore/console_encoder.go @@ -101,6 +101,7 @@ func (c consoleEncoder) EncodeEntry(ent Entry, fields []Field) (*buffer.Buffer, arr.AppendString(ent.Caller.Function) } } + for i := range arr.elems { if i > 0 { line.AppendString(c.ConsoleSeparator) @@ -119,10 +120,23 @@ func (c consoleEncoder) EncodeEntry(ent Entry, fields []Field) (*buffer.Buffer, c.writeContext(line, fields) // If there's no stacktrace key, honor that; this allows users to force - // single-line output. + // single-line output by avoiding printing the stacktrace. if ent.Stack != "" && c.StacktraceKey != "" { line.AppendByte('\n') - line.AppendString(ent.Stack) + + arr = getSliceEncoder() + stacktraceEncoder := c.EncodeStacktrace + if stacktraceEncoder == nil { + stacktraceEncoder = FullStacktraceEncoder + } + stacktraceEncoder(ent.Stack, arr) + for i := range arr.elems { + if i > 0 { + line.AppendString(c.ConsoleSeparator) + } + fmt.Fprint(line, arr.elems[i]) + } + putSliceEncoder(arr) } line.AppendString(c.LineEnding) diff --git a/zapcore/encoder.go b/zapcore/encoder.go index 044625415..5ade814e8 100644 --- a/zapcore/encoder.go +++ b/zapcore/encoder.go @@ -270,6 +270,30 @@ func (e *DurationEncoder) UnmarshalText(text []byte) error { return nil } +// A StacktraceEncoder serializes a stacktrace +// +// This function must make exactly one call +// to a PrimitiveArrayEncoder's Append* method. +type StacktraceEncoder func(string, PrimitiveArrayEncoder) + +// FullStacktraceEncoder passes down the full stacktrace as a string to the encoder. +func FullStacktraceEncoder(stacktrace string, enc PrimitiveArrayEncoder) { + enc.AppendString(stacktrace) +} + +// UnmarshalText unmarshals a StacktraceEncoder from its name. +// The following names are supported: "full" +// Defaults to "full" for unknown names. +func (e *StacktraceEncoder) UnmarshalText(text []byte) error { + switch string(text) { + case "full": + *e = FullStacktraceEncoder + default: + *e = FullStacktraceEncoder + } + return nil +} + // A CallerEncoder serializes an EntryCaller to a primitive type. // // This function must make exactly one call @@ -343,10 +367,11 @@ type EncoderConfig struct { // Configure the primitive representations of common complex types. For // example, some users may want all time.Times serialized as floating-point // seconds since epoch, while others may prefer ISO8601 strings. - EncodeLevel LevelEncoder `json:"levelEncoder" yaml:"levelEncoder"` - EncodeTime TimeEncoder `json:"timeEncoder" yaml:"timeEncoder"` - EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"` - EncodeCaller CallerEncoder `json:"callerEncoder" yaml:"callerEncoder"` + EncodeLevel LevelEncoder `json:"levelEncoder" yaml:"levelEncoder"` + EncodeTime TimeEncoder `json:"timeEncoder" yaml:"timeEncoder"` + EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"` + EncodeCaller CallerEncoder `json:"callerEncoder" yaml:"callerEncoder"` + EncodeStacktrace StacktraceEncoder `json:"stacktraceEncoder" yaml:"stacktraceEncoder"` // Unlike the other primitive type encoders, EncodeName is optional. The // zero value falls back to FullNameEncoder. EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"` diff --git a/zapcore/encoder_test.go b/zapcore/encoder_test.go index 9b8142f5d..be5086d9a 100644 --- a/zapcore/encoder_test.go +++ b/zapcore/encoder_test.go @@ -47,18 +47,19 @@ var ( func testEncoderConfig() EncoderConfig { return EncoderConfig{ - MessageKey: "msg", - LevelKey: "level", - NameKey: "name", - TimeKey: "ts", - CallerKey: "caller", - FunctionKey: "func", - StacktraceKey: "stacktrace", - LineEnding: "\n", - EncodeTime: EpochTimeEncoder, - EncodeLevel: LowercaseLevelEncoder, - EncodeDuration: SecondsDurationEncoder, - EncodeCaller: ShortCallerEncoder, + MessageKey: "msg", + LevelKey: "level", + NameKey: "name", + TimeKey: "ts", + CallerKey: "caller", + FunctionKey: "func", + StacktraceKey: "stacktrace", + LineEnding: "\n", + EncodeTime: EpochTimeEncoder, + EncodeLevel: LowercaseLevelEncoder, + EncodeDuration: SecondsDurationEncoder, + EncodeCaller: ShortCallerEncoder, + EncodeStacktrace: FullStacktraceEncoder, } } @@ -74,6 +75,10 @@ func capitalNameEncoder(loggerName string, enc PrimitiveArrayEncoder) { enc.AppendString(strings.ToUpper(loggerName)) } +func capitalStacktraceEncoder(stacktrace string, enc PrimitiveArrayEncoder) { + enc.AppendString(strings.ToUpper(stacktrace)) +} + func TestEncoderConfiguration(t *testing.T) { base := testEncoderConfig() @@ -98,18 +103,19 @@ func TestEncoderConfiguration(t *testing.T) { { desc: "use custom entry keys in JSON output and ignore them in console output", cfg: EncoderConfig{ - LevelKey: "L", - TimeKey: "T", - MessageKey: "M", - NameKey: "N", - CallerKey: "C", - FunctionKey: "F", - StacktraceKey: "S", - LineEnding: base.LineEnding, - EncodeTime: base.EncodeTime, - EncodeDuration: base.EncodeDuration, - EncodeLevel: base.EncodeLevel, - EncodeCaller: base.EncodeCaller, + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: base.EncodeStacktrace, }, expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n", expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n", @@ -117,19 +123,20 @@ func TestEncoderConfiguration(t *testing.T) { { desc: "skip line ending if SkipLineEnding is 'true'", cfg: EncoderConfig{ - LevelKey: "L", - TimeKey: "T", - MessageKey: "M", - NameKey: "N", - CallerKey: "C", - FunctionKey: "F", - StacktraceKey: "S", - LineEnding: base.LineEnding, - SkipLineEnding: true, - EncodeTime: base.EncodeTime, - EncodeDuration: base.EncodeDuration, - EncodeLevel: base.EncodeLevel, - EncodeCaller: base.EncodeCaller, + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + LineEnding: base.LineEnding, + SkipLineEnding: true, + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: base.EncodeStacktrace, }, expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}`, expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack", @@ -137,18 +144,19 @@ func TestEncoderConfiguration(t *testing.T) { { desc: "skip level if LevelKey is omitted", cfg: EncoderConfig{ - LevelKey: OmitKey, - TimeKey: "T", - MessageKey: "M", - NameKey: "N", - CallerKey: "C", - FunctionKey: "F", - StacktraceKey: "S", - LineEnding: base.LineEnding, - EncodeTime: base.EncodeTime, - EncodeDuration: base.EncodeDuration, - EncodeLevel: base.EncodeLevel, - EncodeCaller: base.EncodeCaller, + LevelKey: OmitKey, + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: base.EncodeStacktrace, }, expectedJSON: `{"T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n", expectedConsole: "0\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n", @@ -156,18 +164,19 @@ func TestEncoderConfiguration(t *testing.T) { { desc: "skip timestamp if TimeKey is omitted", cfg: EncoderConfig{ - LevelKey: "L", - TimeKey: OmitKey, - MessageKey: "M", - NameKey: "N", - CallerKey: "C", - FunctionKey: "F", - StacktraceKey: "S", - LineEnding: base.LineEnding, - EncodeTime: base.EncodeTime, - EncodeDuration: base.EncodeDuration, - EncodeLevel: base.EncodeLevel, - EncodeCaller: base.EncodeCaller, + LevelKey: "L", + TimeKey: OmitKey, + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: base.EncodeStacktrace, }, expectedJSON: `{"L":"info","N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n", expectedConsole: "info\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n", @@ -175,18 +184,19 @@ func TestEncoderConfiguration(t *testing.T) { { desc: "skip message if MessageKey is omitted", cfg: EncoderConfig{ - LevelKey: "L", - TimeKey: "T", - MessageKey: OmitKey, - NameKey: "N", - CallerKey: "C", - FunctionKey: "F", - StacktraceKey: "S", - LineEnding: base.LineEnding, - EncodeTime: base.EncodeTime, - EncodeDuration: base.EncodeDuration, - EncodeLevel: base.EncodeLevel, - EncodeCaller: base.EncodeCaller, + LevelKey: "L", + TimeKey: "T", + MessageKey: OmitKey, + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: base.EncodeStacktrace, }, expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","S":"fake-stack"}` + "\n", expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\nfake-stack\n", @@ -194,18 +204,19 @@ func TestEncoderConfiguration(t *testing.T) { { desc: "skip name if NameKey is omitted", cfg: EncoderConfig{ - LevelKey: "L", - TimeKey: "T", - MessageKey: "M", - NameKey: OmitKey, - CallerKey: "C", - FunctionKey: "F", - StacktraceKey: "S", - LineEnding: base.LineEnding, - EncodeTime: base.EncodeTime, - EncodeDuration: base.EncodeDuration, - EncodeLevel: base.EncodeLevel, - EncodeCaller: base.EncodeCaller, + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: OmitKey, + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: base.EncodeStacktrace, }, expectedJSON: `{"L":"info","T":0,"C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n", expectedConsole: "0\tinfo\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n", @@ -213,18 +224,19 @@ func TestEncoderConfiguration(t *testing.T) { { desc: "skip caller if CallerKey is omitted", cfg: EncoderConfig{ - LevelKey: "L", - TimeKey: "T", - MessageKey: "M", - NameKey: "N", - CallerKey: OmitKey, - FunctionKey: "F", - StacktraceKey: "S", - LineEnding: base.LineEnding, - EncodeTime: base.EncodeTime, - EncodeDuration: base.EncodeDuration, - EncodeLevel: base.EncodeLevel, - EncodeCaller: base.EncodeCaller, + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: OmitKey, + FunctionKey: "F", + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: base.EncodeStacktrace, }, expectedJSON: `{"L":"info","T":0,"N":"main","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n", expectedConsole: "0\tinfo\tmain\tfoo.Foo\thello\nfake-stack\n", @@ -232,18 +244,19 @@ func TestEncoderConfiguration(t *testing.T) { { desc: "skip function if FunctionKey is omitted", cfg: EncoderConfig{ - LevelKey: "L", - TimeKey: "T", - MessageKey: "M", - NameKey: "N", - CallerKey: "C", - FunctionKey: OmitKey, - StacktraceKey: "S", - LineEnding: base.LineEnding, - EncodeTime: base.EncodeTime, - EncodeDuration: base.EncodeDuration, - EncodeLevel: base.EncodeLevel, - EncodeCaller: base.EncodeCaller, + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + FunctionKey: OmitKey, + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: base.EncodeStacktrace, }, 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", @@ -251,18 +264,19 @@ func TestEncoderConfiguration(t *testing.T) { { desc: "skip stacktrace if StacktraceKey is omitted", cfg: EncoderConfig{ - LevelKey: "L", - TimeKey: "T", - MessageKey: "M", - NameKey: "N", - CallerKey: "C", - FunctionKey: "F", - StacktraceKey: OmitKey, - LineEnding: base.LineEnding, - EncodeTime: base.EncodeTime, - EncodeDuration: base.EncodeDuration, - EncodeLevel: base.EncodeLevel, - EncodeCaller: base.EncodeCaller, + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: OmitKey, + LineEnding: base.LineEnding, + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: base.EncodeStacktrace, }, expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello"}` + "\n", expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\n", @@ -270,18 +284,19 @@ func TestEncoderConfiguration(t *testing.T) { { desc: "use the supplied EncodeTime, for both the entry and any times added", cfg: EncoderConfig{ - LevelKey: "L", - TimeKey: "T", - MessageKey: "M", - NameKey: "N", - CallerKey: "C", - FunctionKey: "F", - StacktraceKey: "S", - LineEnding: base.LineEnding, - EncodeTime: func(t time.Time, enc PrimitiveArrayEncoder) { enc.AppendString(t.String()) }, - EncodeDuration: base.EncodeDuration, - EncodeLevel: base.EncodeLevel, - EncodeCaller: base.EncodeCaller, + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: func(t time.Time, enc PrimitiveArrayEncoder) { enc.AppendString(t.String()) }, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: base.EncodeStacktrace, }, extra: func(enc Encoder) { enc.AddTime("extra", _epoch) @@ -299,18 +314,19 @@ func TestEncoderConfiguration(t *testing.T) { { desc: "use the supplied EncodeDuration for any durations added", cfg: EncoderConfig{ - LevelKey: "L", - TimeKey: "T", - MessageKey: "M", - NameKey: "N", - CallerKey: "C", - FunctionKey: "F", - StacktraceKey: "S", - LineEnding: base.LineEnding, - EncodeTime: base.EncodeTime, - EncodeDuration: StringDurationEncoder, - EncodeLevel: base.EncodeLevel, - EncodeCaller: base.EncodeCaller, + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: base.EncodeTime, + EncodeDuration: StringDurationEncoder, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: base.EncodeStacktrace, }, extra: func(enc Encoder) { enc.AddDuration("extra", time.Second) @@ -328,18 +344,19 @@ func TestEncoderConfiguration(t *testing.T) { { desc: "use the supplied EncodeLevel", cfg: EncoderConfig{ - LevelKey: "L", - TimeKey: "T", - MessageKey: "M", - NameKey: "N", - CallerKey: "C", - FunctionKey: "F", - StacktraceKey: "S", - LineEnding: base.LineEnding, - EncodeTime: base.EncodeTime, - EncodeDuration: base.EncodeDuration, - EncodeLevel: CapitalLevelEncoder, - EncodeCaller: base.EncodeCaller, + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: CapitalLevelEncoder, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: base.EncodeStacktrace, }, expectedJSON: `{"L":"INFO","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n", expectedConsole: "0\tINFO\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n", @@ -347,38 +364,60 @@ func TestEncoderConfiguration(t *testing.T) { { desc: "use the supplied EncodeName", cfg: EncoderConfig{ - LevelKey: "L", - TimeKey: "T", - MessageKey: "M", - NameKey: "N", - CallerKey: "C", - FunctionKey: "F", - StacktraceKey: "S", - LineEnding: base.LineEnding, - EncodeTime: base.EncodeTime, - EncodeDuration: base.EncodeDuration, - EncodeLevel: base.EncodeLevel, - EncodeCaller: base.EncodeCaller, - EncodeName: capitalNameEncoder, + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: base.EncodeStacktrace, + EncodeName: capitalNameEncoder, }, expectedJSON: `{"L":"info","T":0,"N":"MAIN","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n", expectedConsole: "0\tinfo\tMAIN\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n", }, + { + desc: "use the supplied EncodeStacktrace", + cfg: EncoderConfig{ + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: capitalStacktraceEncoder, + }, + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"FAKE-STACK"}` + "\n", + expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\nFAKE-STACK\n", + }, { desc: "close all open namespaces", cfg: EncoderConfig{ - LevelKey: "L", - TimeKey: "T", - MessageKey: "M", - NameKey: "N", - CallerKey: "C", - FunctionKey: "F", - StacktraceKey: "S", - LineEnding: base.LineEnding, - EncodeTime: base.EncodeTime, - EncodeDuration: base.EncodeDuration, - EncodeLevel: base.EncodeLevel, - EncodeCaller: base.EncodeCaller, + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: base.EncodeStacktrace, }, extra: func(enc Encoder) { enc.OpenNamespace("outer") @@ -394,18 +433,19 @@ func TestEncoderConfiguration(t *testing.T) { { desc: "handle no-op EncodeTime", cfg: EncoderConfig{ - LevelKey: "L", - TimeKey: "T", - MessageKey: "M", - NameKey: "N", - CallerKey: "C", - FunctionKey: "F", - StacktraceKey: "S", - LineEnding: base.LineEnding, - EncodeTime: func(time.Time, PrimitiveArrayEncoder) {}, - EncodeDuration: base.EncodeDuration, - EncodeLevel: base.EncodeLevel, - EncodeCaller: base.EncodeCaller, + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: func(time.Time, PrimitiveArrayEncoder) {}, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: base.EncodeStacktrace, }, extra: func(enc Encoder) { enc.AddTime("sometime", time.Unix(0, 100)) }, expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","sometime":100,"S":"fake-stack"}` + "\n", @@ -414,18 +454,19 @@ func TestEncoderConfiguration(t *testing.T) { { desc: "handle no-op EncodeDuration", cfg: EncoderConfig{ - LevelKey: "L", - TimeKey: "T", - MessageKey: "M", - NameKey: "N", - CallerKey: "C", - FunctionKey: "F", - StacktraceKey: "S", - LineEnding: base.LineEnding, - EncodeTime: base.EncodeTime, - EncodeDuration: func(time.Duration, PrimitiveArrayEncoder) {}, - EncodeLevel: base.EncodeLevel, - EncodeCaller: base.EncodeCaller, + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: base.EncodeTime, + EncodeDuration: func(time.Duration, PrimitiveArrayEncoder) {}, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: base.EncodeStacktrace, }, extra: func(enc Encoder) { enc.AddDuration("someduration", time.Microsecond) }, expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","someduration":1000,"S":"fake-stack"}` + "\n", @@ -434,18 +475,19 @@ func TestEncoderConfiguration(t *testing.T) { { desc: "handle no-op EncodeLevel", cfg: EncoderConfig{ - LevelKey: "L", - TimeKey: "T", - MessageKey: "M", - NameKey: "N", - CallerKey: "C", - FunctionKey: "F", - StacktraceKey: "S", - LineEnding: base.LineEnding, - EncodeTime: base.EncodeTime, - EncodeDuration: base.EncodeDuration, - EncodeLevel: func(Level, PrimitiveArrayEncoder) {}, - EncodeCaller: base.EncodeCaller, + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: func(Level, PrimitiveArrayEncoder) {}, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: base.EncodeStacktrace, }, expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n", expectedConsole: "0\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n", @@ -453,18 +495,19 @@ func TestEncoderConfiguration(t *testing.T) { { desc: "handle no-op EncodeCaller", cfg: EncoderConfig{ - LevelKey: "L", - TimeKey: "T", - MessageKey: "M", - NameKey: "N", - CallerKey: "C", - FunctionKey: "F", - StacktraceKey: "S", - LineEnding: base.LineEnding, - EncodeTime: base.EncodeTime, - EncodeDuration: base.EncodeDuration, - EncodeLevel: base.EncodeLevel, - EncodeCaller: func(EntryCaller, PrimitiveArrayEncoder) {}, + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: func(EntryCaller, PrimitiveArrayEncoder) {}, + EncodeStacktrace: base.EncodeStacktrace, }, expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n", expectedConsole: "0\tinfo\tmain\tfoo.Foo\thello\nfake-stack\n", @@ -472,19 +515,20 @@ func TestEncoderConfiguration(t *testing.T) { { desc: "handle no-op EncodeName", cfg: EncoderConfig{ - LevelKey: "L", - TimeKey: "T", - MessageKey: "M", - NameKey: "N", - CallerKey: "C", - FunctionKey: "F", - StacktraceKey: "S", - LineEnding: base.LineEnding, - EncodeTime: base.EncodeTime, - EncodeDuration: base.EncodeDuration, - EncodeLevel: base.EncodeLevel, - EncodeCaller: base.EncodeCaller, - EncodeName: func(string, PrimitiveArrayEncoder) {}, + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: base.EncodeStacktrace, + EncodeName: func(string, PrimitiveArrayEncoder) {}, }, expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n", expectedConsole: "0\tinfo\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n", @@ -492,18 +536,19 @@ func TestEncoderConfiguration(t *testing.T) { { desc: "use custom line separator", cfg: EncoderConfig{ - LevelKey: "L", - TimeKey: "T", - MessageKey: "M", - NameKey: "N", - CallerKey: "C", - FunctionKey: "F", - StacktraceKey: "S", - LineEnding: "\r\n", - EncodeTime: base.EncodeTime, - EncodeDuration: base.EncodeDuration, - EncodeLevel: base.EncodeLevel, - EncodeCaller: base.EncodeCaller, + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + LineEnding: "\r\n", + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: base.EncodeStacktrace, }, expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\r\n", expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack\r\n", @@ -511,21 +556,64 @@ func TestEncoderConfiguration(t *testing.T) { { desc: "omit line separator definition - fall back to default", cfg: EncoderConfig{ - LevelKey: "L", - TimeKey: "T", - MessageKey: "M", - NameKey: "N", - CallerKey: "C", - FunctionKey: "F", - StacktraceKey: "S", - EncodeTime: base.EncodeTime, - EncodeDuration: base.EncodeDuration, - EncodeLevel: base.EncodeLevel, - EncodeCaller: base.EncodeCaller, + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: base.EncodeStacktrace, }, expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + DefaultLineEnding, expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack" + DefaultLineEnding, }, + { + desc: "ensure no error with noop EncodeStacktrace", + cfg: EncoderConfig{ + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: func(string, PrimitiveArrayEncoder) {}, + }, + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":""}` + "\n", + + expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\n" + "\n", + }, + { + desc: "ensure no panic with nil EncodeStacktrace", + cfg: EncoderConfig{ + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + EncodeStacktrace: nil, + }, + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n", + + expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack" + "\n", + }, } for i, tt := range tests { @@ -698,6 +786,10 @@ func TestCallerEncoders(t *testing.T) { } } +func TestStacktraceEncoders(t *testing.T) { + +} + func TestNameEncoders(t *testing.T) { tests := []struct { name string diff --git a/zapcore/json_encoder.go b/zapcore/json_encoder.go index 9685169b2..dc9083a76 100644 --- a/zapcore/json_encoder.go +++ b/zapcore/json_encoder.go @@ -420,7 +420,21 @@ func (enc *jsonEncoder) EncodeEntry(ent Entry, fields []Field) (*buffer.Buffer, addFields(final, fields) final.closeOpenNamespaces() if ent.Stack != "" && final.StacktraceKey != "" { - final.AddString(final.StacktraceKey, ent.Stack) + final.addKey(final.StacktraceKey) + cur := final.buf.Len() + + // if no stacktrace encoder is provided, fall back to FullStacktraceEncoder to protect backwards compatibility + stacktraceEncoder := final.EncodeStacktrace + if stacktraceEncoder == nil { + stacktraceEncoder = FullStacktraceEncoder + } + + stacktraceEncoder(ent.Stack, final) + if cur == final.buf.Len() { + // User-supplied EncodeStacktrace was a no-op. Fall back to strings to + // keep output JSON valid. + final.AppendString("") + } } final.buf.AppendByte('}') final.buf.AppendString(final.LineEnding) diff --git a/zapcore/json_encoder_test.go b/zapcore/json_encoder_test.go index b2150256e..667df866f 100644 --- a/zapcore/json_encoder_test.go +++ b/zapcore/json_encoder_test.go @@ -126,17 +126,18 @@ func TestJSONEncodeEntry(t *testing.T) { } enc := zapcore.NewJSONEncoder(zapcore.EncoderConfig{ - MessageKey: "M", - LevelKey: "L", - TimeKey: "T", - NameKey: "N", - CallerKey: "C", - FunctionKey: "F", - StacktraceKey: "S", - EncodeLevel: zapcore.LowercaseLevelEncoder, - EncodeTime: zapcore.ISO8601TimeEncoder, - EncodeDuration: zapcore.SecondsDurationEncoder, - EncodeCaller: zapcore.ShortCallerEncoder, + MessageKey: "M", + LevelKey: "L", + TimeKey: "T", + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + EncodeLevel: zapcore.LowercaseLevelEncoder, + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + EncodeStacktrace: zapcore.FullStacktraceEncoder, }) for _, tt := range tests { @@ -152,16 +153,17 @@ func TestJSONEncodeEntry(t *testing.T) { func TestNoEncodeLevelSupplied(t *testing.T) { enc := zapcore.NewJSONEncoder(zapcore.EncoderConfig{ - MessageKey: "M", - LevelKey: "L", - TimeKey: "T", - NameKey: "N", - CallerKey: "C", - FunctionKey: "F", - StacktraceKey: "S", - EncodeTime: zapcore.ISO8601TimeEncoder, - EncodeDuration: zapcore.SecondsDurationEncoder, - EncodeCaller: zapcore.ShortCallerEncoder, + MessageKey: "M", + LevelKey: "L", + TimeKey: "T", + NameKey: "N", + CallerKey: "C", + FunctionKey: "F", + StacktraceKey: "S", + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + EncodeStacktrace: zapcore.FullStacktraceEncoder, }) ent := zapcore.Entry{