diff --git a/config.go b/config.go index a8e5c12c2..89c355d19 100644 --- a/config.go +++ b/config.go @@ -86,6 +86,7 @@ func NewProductionEncoderConfig() zapcore.EncoderConfig { CallerKey: "caller", MessageKey: "msg", StacktraceKey: "stacktrace", + LineEnding: zapcore.DefaultLineEnding, EncodeLevel: zapcore.LowercaseLevelEncoder, EncodeTime: zapcore.EpochTimeEncoder, EncodeDuration: zapcore.SecondsDurationEncoder, @@ -124,6 +125,7 @@ func NewDevelopmentEncoderConfig() zapcore.EncoderConfig { CallerKey: "C", MessageKey: "M", StacktraceKey: "S", + LineEnding: zapcore.DefaultLineEnding, EncodeLevel: zapcore.CapitalLevelEncoder, EncodeTime: zapcore.ISO8601TimeEncoder, EncodeDuration: zapcore.StringDurationEncoder, diff --git a/zapcore/console_encoder.go b/zapcore/console_encoder.go index 05a359a30..9fc89e924 100644 --- a/zapcore/console_encoder.go +++ b/zapcore/console_encoder.go @@ -109,7 +109,11 @@ func (c consoleEncoder) EncodeEntry(ent Entry, fields []Field) (*buffer.Buffer, line.AppendString(ent.Stack) } - line.AppendByte('\n') + if c.LineEnding != "" { + line.AppendString(c.LineEnding) + } else { + line.AppendString(DefaultLineEnding) + } return line, nil } diff --git a/zapcore/encoder.go b/zapcore/encoder.go index 6be047a5c..a026f4679 100644 --- a/zapcore/encoder.go +++ b/zapcore/encoder.go @@ -26,6 +26,11 @@ import ( "go.uber.org/zap/buffer" ) +// DefaultLineEnding defines the default line ending when writing logs. +// Alternate line endings specified in EncoderConfig can override this +// behavior. +const DefaultLineEnding = "\n" + // A LevelEncoder serializes a Level to a primitive type. type LevelEncoder func(Level, PrimitiveArrayEncoder) @@ -202,6 +207,7 @@ type EncoderConfig struct { NameKey string `json:"nameKey" yaml:"nameKey"` CallerKey string `json:"callerKey" yaml:"callerKey"` StacktraceKey string `json:"stacktraceKey" yaml:"stacktraceKey"` + LineEnding string `json:"lineEnding" yaml:"lineEnding"` // 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. diff --git a/zapcore/encoder_test.go b/zapcore/encoder_test.go index c9b57d8c8..0708902fa 100644 --- a/zapcore/encoder_test.go +++ b/zapcore/encoder_test.go @@ -50,6 +50,7 @@ func testEncoderConfig() EncoderConfig { TimeKey: "ts", CallerKey: "caller", StacktraceKey: "stacktrace", + LineEnding: "\n", EncodeTime: EpochTimeEncoder, EncodeLevel: LowercaseLevelEncoder, EncodeDuration: SecondsDurationEncoder, @@ -91,8 +92,8 @@ func TestEncoderConfiguration(t *testing.T) { ent.Message = `hello\` return ent }, - expectedJSON: `{"level":"info","ts":0,"name":"main","caller":"foo.go:42","msg":"hello\\","stacktrace":"fake-stack"}`, - expectedConsole: "0\tinfo\tmain\tfoo.go:42\thello\\\nfake-stack", + expectedJSON: `{"level":"info","ts":0,"name":"main","caller":"foo.go:42","msg":"hello\\","stacktrace":"fake-stack"}` + "\n", + expectedConsole: "0\tinfo\tmain\tfoo.go:42\thello\\\nfake-stack\n", }, { desc: "use custom entry keys in JSON output and ignore them in console output", @@ -103,13 +104,14 @@ func TestEncoderConfiguration(t *testing.T) { NameKey: "N", CallerKey: "C", StacktraceKey: "S", + LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, EncodeDuration: base.EncodeDuration, EncodeLevel: base.EncodeLevel, EncodeCaller: base.EncodeCaller, }, - expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}`, - expectedConsole: "0\tinfo\tmain\tfoo.go:42\thello\nfake-stack", + 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: "skip level if LevelKey is omitted", @@ -120,13 +122,14 @@ func TestEncoderConfiguration(t *testing.T) { NameKey: "N", CallerKey: "C", StacktraceKey: "S", + LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, EncodeDuration: base.EncodeDuration, EncodeLevel: base.EncodeLevel, EncodeCaller: base.EncodeCaller, }, - expectedJSON: `{"T":0,"N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}`, - expectedConsole: "0\tmain\tfoo.go:42\thello\nfake-stack", + expectedJSON: `{"T":0,"N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}` + "\n", + expectedConsole: "0\tmain\tfoo.go:42\thello\nfake-stack\n", }, { desc: "skip timestamp if TimeKey is omitted", @@ -137,13 +140,14 @@ func TestEncoderConfiguration(t *testing.T) { NameKey: "N", CallerKey: "C", StacktraceKey: "S", + LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, EncodeDuration: base.EncodeDuration, EncodeLevel: base.EncodeLevel, EncodeCaller: base.EncodeCaller, }, - expectedJSON: `{"L":"info","N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}`, - expectedConsole: "info\tmain\tfoo.go:42\thello\nfake-stack", + expectedJSON: `{"L":"info","N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}` + "\n", + expectedConsole: "info\tmain\tfoo.go:42\thello\nfake-stack\n", }, { desc: "skip message if MessageKey is omitted", @@ -154,13 +158,14 @@ func TestEncoderConfiguration(t *testing.T) { NameKey: "N", CallerKey: "C", StacktraceKey: "S", + LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, EncodeDuration: base.EncodeDuration, EncodeLevel: base.EncodeLevel, EncodeCaller: base.EncodeCaller, }, - expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","S":"fake-stack"}`, - expectedConsole: "0\tinfo\tmain\tfoo.go:42\nfake-stack", + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","S":"fake-stack"}` + "\n", + expectedConsole: "0\tinfo\tmain\tfoo.go:42\nfake-stack\n", }, { desc: "skip name if NameKey is omitted", @@ -171,13 +176,14 @@ func TestEncoderConfiguration(t *testing.T) { NameKey: "", CallerKey: "C", StacktraceKey: "S", + LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, EncodeDuration: base.EncodeDuration, EncodeLevel: base.EncodeLevel, EncodeCaller: base.EncodeCaller, }, - expectedJSON: `{"L":"info","T":0,"C":"foo.go:42","M":"hello","S":"fake-stack"}`, - expectedConsole: "0\tinfo\tfoo.go:42\thello\nfake-stack", + expectedJSON: `{"L":"info","T":0,"C":"foo.go:42","M":"hello","S":"fake-stack"}` + "\n", + expectedConsole: "0\tinfo\tfoo.go:42\thello\nfake-stack\n", }, { desc: "skip caller if CallerKey is omitted", @@ -188,13 +194,14 @@ func TestEncoderConfiguration(t *testing.T) { NameKey: "N", CallerKey: "", StacktraceKey: "S", + LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, EncodeDuration: base.EncodeDuration, EncodeLevel: base.EncodeLevel, EncodeCaller: base.EncodeCaller, }, - expectedJSON: `{"L":"info","T":0,"N":"main","M":"hello","S":"fake-stack"}`, - expectedConsole: "0\tinfo\tmain\thello\nfake-stack", + expectedJSON: `{"L":"info","T":0,"N":"main","M":"hello","S":"fake-stack"}` + "\n", + expectedConsole: "0\tinfo\tmain\thello\nfake-stack\n", }, { desc: "skip stacktrace if StacktraceKey is omitted", @@ -205,13 +212,14 @@ func TestEncoderConfiguration(t *testing.T) { NameKey: "N", CallerKey: "C", StacktraceKey: "", + LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, EncodeDuration: base.EncodeDuration, EncodeLevel: base.EncodeLevel, EncodeCaller: base.EncodeCaller, }, - expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello"}`, - expectedConsole: "0\tinfo\tmain\tfoo.go:42\thello", + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello"}` + "\n", + expectedConsole: "0\tinfo\tmain\tfoo.go:42\thello\n", }, { desc: "use the supplied EncodeTime, for both the entry and any times added", @@ -222,6 +230,7 @@ func TestEncoderConfiguration(t *testing.T) { NameKey: "N", CallerKey: "C", StacktraceKey: "S", + LineEnding: base.LineEnding, EncodeTime: func(t time.Time, enc PrimitiveArrayEncoder) { enc.AppendString(t.String()) }, EncodeDuration: base.EncodeDuration, EncodeLevel: base.EncodeLevel, @@ -234,10 +243,10 @@ func TestEncoderConfiguration(t *testing.T) { return nil })) }, - expectedJSON: `{"L":"info","T":"1970-01-01 00:00:00 +0000 UTC","N":"main","C":"foo.go:42","M":"hello","extra":"1970-01-01 00:00:00 +0000 UTC","extras":["1970-01-01 00:00:00 +0000 UTC"],"S":"fake-stack"}`, + expectedJSON: `{"L":"info","T":"1970-01-01 00:00:00 +0000 UTC","N":"main","C":"foo.go:42","M":"hello","extra":"1970-01-01 00:00:00 +0000 UTC","extras":["1970-01-01 00:00:00 +0000 UTC"],"S":"fake-stack"}` + "\n", expectedConsole: "1970-01-01 00:00:00 +0000 UTC\tinfo\tmain\tfoo.go:42\thello\t" + // plain-text preamble `{"extra": "1970-01-01 00:00:00 +0000 UTC", "extras": ["1970-01-01 00:00:00 +0000 UTC"]}` + // JSON context - "\nfake-stack", // stacktrace after newline + "\nfake-stack\n", // stacktrace after newline }, { desc: "use the supplied EncodeDuration for any durations added", @@ -248,6 +257,7 @@ func TestEncoderConfiguration(t *testing.T) { NameKey: "N", CallerKey: "C", StacktraceKey: "S", + LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, EncodeDuration: StringDurationEncoder, EncodeLevel: base.EncodeLevel, @@ -260,10 +270,10 @@ func TestEncoderConfiguration(t *testing.T) { return nil })) }, - expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","extra":"1s","extras":["1m0s"],"S":"fake-stack"}`, + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","extra":"1s","extras":["1m0s"],"S":"fake-stack"}` + "\n", expectedConsole: "0\tinfo\tmain\tfoo.go:42\thello\t" + // preamble `{"extra": "1s", "extras": ["1m0s"]}` + // context - "\nfake-stack", // stacktrace + "\nfake-stack\n", // stacktrace }, { desc: "use the supplied EncodeLevel", @@ -274,13 +284,14 @@ func TestEncoderConfiguration(t *testing.T) { NameKey: "N", CallerKey: "C", StacktraceKey: "S", + LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, EncodeDuration: base.EncodeDuration, EncodeLevel: CapitalLevelEncoder, EncodeCaller: base.EncodeCaller, }, - expectedJSON: `{"L":"INFO","T":0,"N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}`, - expectedConsole: "0\tINFO\tmain\tfoo.go:42\thello\nfake-stack", + 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", @@ -291,6 +302,7 @@ func TestEncoderConfiguration(t *testing.T) { NameKey: "N", CallerKey: "C", StacktraceKey: "S", + LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, EncodeDuration: base.EncodeDuration, EncodeLevel: base.EncodeLevel, @@ -302,10 +314,10 @@ func TestEncoderConfiguration(t *testing.T) { enc.AddString("foo", "bar") enc.OpenNamespace("innermost") }, - expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","outer":{"inner":{"foo":"bar","innermost":{}}},"S":"fake-stack"}`, + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","outer":{"inner":{"foo":"bar","innermost":{}}},"S":"fake-stack"}` + "\n", expectedConsole: "0\tinfo\tmain\tfoo.go:42\thello\t" + `{"outer": {"inner": {"foo": "bar", "innermost": {}}}}` + - "\nfake-stack", + "\nfake-stack\n", }, { desc: "handle no-op EncodeTime", @@ -316,14 +328,15 @@ func TestEncoderConfiguration(t *testing.T) { NameKey: "N", CallerKey: "C", StacktraceKey: "S", + LineEnding: base.LineEnding, EncodeTime: func(time.Time, PrimitiveArrayEncoder) {}, EncodeDuration: base.EncodeDuration, EncodeLevel: base.EncodeLevel, EncodeCaller: base.EncodeCaller, }, extra: func(enc Encoder) { enc.AddTime("sometime", time.Unix(0, 100)) }, - expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","sometime":100,"S":"fake-stack"}`, - expectedConsole: "info\tmain\tfoo.go:42\thello\t" + `{"sometime": 100}` + "\nfake-stack", + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","sometime":100,"S":"fake-stack"}` + "\n", + expectedConsole: "info\tmain\tfoo.go:42\thello\t" + `{"sometime": 100}` + "\nfake-stack\n", }, { desc: "handle no-op EncodeDuration", @@ -334,14 +347,15 @@ func TestEncoderConfiguration(t *testing.T) { NameKey: "N", CallerKey: "C", StacktraceKey: "S", + LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, EncodeDuration: func(time.Duration, PrimitiveArrayEncoder) {}, EncodeLevel: base.EncodeLevel, EncodeCaller: base.EncodeCaller, }, extra: func(enc Encoder) { enc.AddDuration("someduration", time.Microsecond) }, - expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","someduration":1000,"S":"fake-stack"}`, - expectedConsole: "0\tinfo\tmain\tfoo.go:42\thello\t" + `{"someduration": 1000}` + "\nfake-stack", + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","someduration":1000,"S":"fake-stack"}` + "\n", + expectedConsole: "0\tinfo\tmain\tfoo.go:42\thello\t" + `{"someduration": 1000}` + "\nfake-stack\n", }, { desc: "handle no-op EncodeLevel", @@ -352,13 +366,14 @@ func TestEncoderConfiguration(t *testing.T) { NameKey: "N", CallerKey: "C", StacktraceKey: "S", + LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, EncodeDuration: base.EncodeDuration, EncodeLevel: func(Level, PrimitiveArrayEncoder) {}, EncodeCaller: base.EncodeCaller, }, - expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}`, - expectedConsole: "0\tmain\tfoo.go:42\thello\nfake-stack", + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}` + "\n", + expectedConsole: "0\tmain\tfoo.go:42\thello\nfake-stack\n", }, { desc: "handle no-op EncodeCaller", @@ -369,13 +384,49 @@ func TestEncoderConfiguration(t *testing.T) { NameKey: "N", CallerKey: "C", StacktraceKey: "S", + LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, EncodeDuration: base.EncodeDuration, EncodeLevel: base.EncodeLevel, EncodeCaller: func(EntryCaller, PrimitiveArrayEncoder) {}, }, - expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}`, - expectedConsole: "0\tinfo\tmain\thello\nfake-stack", + 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: "use custom line separator", + cfg: EncoderConfig{ + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + StacktraceKey: "S", + LineEnding: "\r\n", + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + }, + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}` + "\r\n", + expectedConsole: "0\tinfo\tmain\tfoo.go:42\thello\nfake-stack\r\n", + }, + { + desc: "omit line separator definition - fall back to default", + cfg: EncoderConfig{ + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + StacktraceKey: "S", + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + }, + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}` + DefaultLineEnding, + expectedConsole: "0\tinfo\tmain\tfoo.go:42\thello\nfake-stack" + DefaultLineEnding, }, } @@ -394,7 +445,7 @@ func TestEncoderConfiguration(t *testing.T) { if assert.NoError(t, jsonErr, "Unexpected error JSON-encoding entry in case #%d.", i) { assert.Equal( t, - tt.expectedJSON+"\n", + tt.expectedJSON, jsonOut.String(), "Unexpected JSON output: expected to %v.", tt.desc, ) @@ -403,7 +454,7 @@ func TestEncoderConfiguration(t *testing.T) { if assert.NoError(t, consoleErr, "Unexpected error console-encoding entry in case #%d.", i) { assert.Equal( t, - tt.expectedConsole+"\n", + tt.expectedConsole, consoleOut.String(), "Unexpected console output: expected to %v.", tt.desc, ) diff --git a/zapcore/json_encoder.go b/zapcore/json_encoder.go index 45558f2a8..7d4a22c8d 100644 --- a/zapcore/json_encoder.go +++ b/zapcore/json_encoder.go @@ -328,7 +328,11 @@ func (enc *jsonEncoder) EncodeEntry(ent Entry, fields []Field) (*buffer.Buffer, final.AddString(final.StacktraceKey, ent.Stack) } final.buf.AppendByte('}') - final.buf.AppendByte('\n') + if final.LineEnding != "" { + final.buf.AppendString(final.LineEnding) + } else { + final.buf.AppendString(DefaultLineEnding) + } ret := final.buf putJSONEncoder(final)