Skip to content

Commit a26b259

Browse files
akshayjshahAiden Scandella
authored and
Aiden Scandella
committedSep 26, 2016
Add a human-optimized text encoder (uber-go#123)
* Add a TextEncoder for human-readable output When logging to console, JSON isn't the most friendly output - it's nice for machines but not optimal for humans. This PR adds a text encoder that prioritizes human readability, along with a few time-handling options for that encoder. In advance of uber-go#116, this PR also includes support for unsigned ints.
1 parent 47323e2 commit a26b259

6 files changed

+530
-8
lines changed
 

‎example_test.go

+26-3
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,9 @@ func ExampleNest() {
105105
func ExampleNew() {
106106
// The default logger outputs to standard out and only writes logs that are
107107
// Info level or higher.
108-
logger := zap.New(
109-
zap.NewJSONEncoder(zap.NoTime()), // drop timestamps in tests
110-
)
108+
logger := zap.New(zap.NewJSONEncoder(
109+
zap.NoTime(), // drop timestamps in tests
110+
))
111111

112112
// The default logger does not print Debug logs.
113113
logger.Debug("This won't be printed.")
@@ -117,6 +117,18 @@ func ExampleNew() {
117117
// {"level":"info","msg":"This is an info log."}
118118
}
119119

120+
func ExampleNew_textEncoder() {
121+
// For more human-readable output in the console, use a TextEncoder.
122+
textLogger := zap.New(zap.NewTextEncoder(
123+
zap.TextNoTime(), // drop timestamps in tests.
124+
))
125+
126+
textLogger.Info("This is a text log.", zap.Int("foo", 42))
127+
128+
// Output:
129+
// [I] This is a text log. foo=42
130+
}
131+
120132
func ExampleNew_options() {
121133
// We can pass multiple options to the NewJSON method to configure
122134
// the logging level, output location, or even the initial context.
@@ -199,3 +211,14 @@ func ExampleNewJSONEncoder() {
199211
zap.LevelString("@level"), // stringify the log level
200212
)
201213
}
214+
215+
func ExampleNewTextEncoder() {
216+
// A text encoder with the default settings.
217+
zap.NewTextEncoder()
218+
219+
// Dropping timestamps is often useful in tests.
220+
zap.NewTextEncoder(zap.TextNoTime())
221+
222+
// If you don't like the default timestamp formatting, choose another.
223+
zap.NewTextEncoder(zap.TextTimeFormat(time.RFC822))
224+
}

‎json_encoder_test.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import (
3636
"github.com/stretchr/testify/require"
3737
)
3838

39+
var epoch = time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC)
40+
3941
func newJSONEncoder(opts ...JSONOption) *jsonEncoder {
4042
return NewJSONEncoder(opts...).(*jsonEncoder)
4143
}
@@ -280,7 +282,7 @@ func TestJSONOptions(t *testing.T) {
280282

281283
for _, enc := range []Encoder{root, root.Clone()} {
282284
buf := &bytes.Buffer{}
283-
enc.WriteEntry(buf, "fake msg", DebugLevel, time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC))
285+
enc.WriteEntry(buf, "fake msg", DebugLevel, epoch)
284286
assert.Equal(
285287
t,
286288
`{"the-level":"debug","the-timestamp":"1970-01-01T00:00:00Z","the-message":"fake msg"}`+"\n",

‎json_options_test.go

+1-4
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ package zap
2222

2323
import (
2424
"testing"
25-
"time"
2625

2726
"github.com/stretchr/testify/assert"
2827
)
@@ -45,8 +44,6 @@ func TestMessageFormatters(t *testing.T) {
4544
}
4645

4746
func TestTimeFormatters(t *testing.T) {
48-
ts := time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC)
49-
5047
tests := []struct {
5148
name string
5249
formatter TimeFormatter
@@ -59,7 +56,7 @@ func TestTimeFormatters(t *testing.T) {
5956
}
6057

6158
for _, tt := range tests {
62-
assert.Equal(t, tt.expected, tt.formatter(ts), "Unexpected output from TimeFormatter %s.", tt.name)
59+
assert.Equal(t, tt.expected, tt.formatter(epoch), "Unexpected output from TimeFormatter %s.", tt.name)
6360
}
6461
}
6562

‎text_encoder.go

+217
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// Copyright (c) 2016 Uber Technologies, Inc.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
// THE SOFTWARE.
20+
21+
package zap
22+
23+
import (
24+
"fmt"
25+
"io"
26+
"strconv"
27+
"sync"
28+
"time"
29+
)
30+
31+
var textPool = sync.Pool{New: func() interface{} {
32+
return &textEncoder{
33+
bytes: make([]byte, 0, _initialBufSize),
34+
}
35+
}}
36+
37+
type textEncoder struct {
38+
bytes []byte
39+
timeFmt string
40+
firstNested bool
41+
}
42+
43+
// NewTextEncoder creates a line-oriented text encoder whose output is optimized
44+
// for human, rather than machine, consumption. By default, the encoder uses
45+
// RFC3339-formatted timestamps.
46+
func NewTextEncoder(options ...TextOption) Encoder {
47+
enc := textPool.Get().(*textEncoder)
48+
enc.truncate()
49+
enc.timeFmt = time.RFC3339
50+
for _, opt := range options {
51+
opt.apply(enc)
52+
}
53+
return enc
54+
}
55+
56+
func (enc *textEncoder) Free() {
57+
textPool.Put(enc)
58+
}
59+
60+
func (enc *textEncoder) AddString(key, val string) {
61+
enc.addKey(key)
62+
enc.bytes = append(enc.bytes, val...)
63+
}
64+
65+
func (enc *textEncoder) AddBool(key string, val bool) {
66+
enc.addKey(key)
67+
enc.bytes = strconv.AppendBool(enc.bytes, val)
68+
}
69+
70+
func (enc *textEncoder) AddInt(key string, val int) {
71+
enc.AddInt64(key, int64(val))
72+
}
73+
74+
func (enc *textEncoder) AddInt64(key string, val int64) {
75+
enc.addKey(key)
76+
enc.bytes = strconv.AppendInt(enc.bytes, val, 10)
77+
}
78+
79+
func (enc *textEncoder) AddUint(key string, val uint) {
80+
enc.AddUint64(key, uint64(val))
81+
}
82+
83+
func (enc *textEncoder) AddUint64(key string, val uint64) {
84+
enc.addKey(key)
85+
enc.bytes = strconv.AppendUint(enc.bytes, val, 10)
86+
}
87+
88+
func (enc *textEncoder) AddFloat64(key string, val float64) {
89+
enc.addKey(key)
90+
enc.bytes = strconv.AppendFloat(enc.bytes, val, 'f', -1, 64)
91+
}
92+
93+
func (enc *textEncoder) AddMarshaler(key string, obj LogMarshaler) error {
94+
enc.addKey(key)
95+
enc.firstNested = true
96+
enc.bytes = append(enc.bytes, '{')
97+
err := obj.MarshalLog(enc)
98+
enc.bytes = append(enc.bytes, '}')
99+
enc.firstNested = false
100+
return err
101+
}
102+
103+
func (enc *textEncoder) AddObject(key string, obj interface{}) error {
104+
enc.AddString(key, fmt.Sprintf("%+v", obj))
105+
return nil
106+
}
107+
108+
func (enc *textEncoder) Clone() Encoder {
109+
clone := textPool.Get().(*textEncoder)
110+
clone.truncate()
111+
clone.bytes = append(clone.bytes, enc.bytes...)
112+
clone.timeFmt = enc.timeFmt
113+
clone.firstNested = enc.firstNested
114+
return clone
115+
}
116+
117+
func (enc *textEncoder) WriteEntry(sink io.Writer, msg string, lvl Level, t time.Time) error {
118+
if sink == nil {
119+
return errNilSink
120+
}
121+
122+
final := textPool.Get().(*textEncoder)
123+
final.truncate()
124+
enc.addLevel(final, lvl)
125+
enc.addTime(final, t)
126+
enc.addMessage(final, msg)
127+
128+
if len(enc.bytes) > 0 {
129+
final.bytes = append(final.bytes, ' ')
130+
final.bytes = append(final.bytes, enc.bytes...)
131+
}
132+
final.bytes = append(final.bytes, '\n')
133+
134+
expectedBytes := len(final.bytes)
135+
n, err := sink.Write(final.bytes)
136+
final.Free()
137+
if err != nil {
138+
return err
139+
}
140+
if n != expectedBytes {
141+
return fmt.Errorf("incomplete write: only wrote %v of %v bytes", n, expectedBytes)
142+
}
143+
return nil
144+
}
145+
146+
func (enc *textEncoder) truncate() {
147+
enc.bytes = enc.bytes[:0]
148+
}
149+
150+
func (enc *textEncoder) addKey(key string) {
151+
lastIdx := len(enc.bytes) - 1
152+
if lastIdx >= 0 && !enc.firstNested {
153+
enc.bytes = append(enc.bytes, ' ')
154+
} else {
155+
enc.firstNested = false
156+
}
157+
enc.bytes = append(enc.bytes, key...)
158+
enc.bytes = append(enc.bytes, '=')
159+
}
160+
161+
func (enc *textEncoder) addLevel(final *textEncoder, lvl Level) {
162+
final.bytes = append(final.bytes, '[')
163+
switch lvl {
164+
case DebugLevel:
165+
final.bytes = append(final.bytes, 'D')
166+
case InfoLevel:
167+
final.bytes = append(final.bytes, 'I')
168+
case WarnLevel:
169+
final.bytes = append(final.bytes, 'W')
170+
case ErrorLevel:
171+
final.bytes = append(final.bytes, 'E')
172+
case PanicLevel:
173+
final.bytes = append(final.bytes, 'P')
174+
case FatalLevel:
175+
final.bytes = append(final.bytes, 'F')
176+
default:
177+
final.bytes = strconv.AppendInt(final.bytes, int64(lvl), 10)
178+
}
179+
final.bytes = append(final.bytes, ']')
180+
}
181+
182+
func (enc *textEncoder) addTime(final *textEncoder, t time.Time) {
183+
if enc.timeFmt == "" {
184+
return
185+
}
186+
final.bytes = append(final.bytes, ' ')
187+
final.bytes = t.AppendFormat(final.bytes, enc.timeFmt)
188+
}
189+
190+
func (enc *textEncoder) addMessage(final *textEncoder, msg string) {
191+
final.bytes = append(final.bytes, ' ')
192+
final.bytes = append(final.bytes, msg...)
193+
}
194+
195+
// A TextOption is used to set options for a text encoder.
196+
type TextOption interface {
197+
apply(*textEncoder)
198+
}
199+
200+
type textOptionFunc func(*textEncoder)
201+
202+
func (opt textOptionFunc) apply(enc *textEncoder) {
203+
opt(enc)
204+
}
205+
206+
// TextTimeFormat sets the format for log timestamps, using the same layout
207+
// strings supported by time.Parse.
208+
func TextTimeFormat(layout string) TextOption {
209+
return textOptionFunc(func(enc *textEncoder) {
210+
enc.timeFmt = layout
211+
})
212+
}
213+
214+
// TextNoTime omits timestamps from the serialized log entries.
215+
func TextNoTime() TextOption {
216+
return TextTimeFormat("")
217+
}

‎text_encoder_test.go

+215
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
// Copyright (c) 2016 Uber Technologies, Inc.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
// THE SOFTWARE.
20+
21+
package zap
22+
23+
import (
24+
"fmt"
25+
"io"
26+
"math"
27+
"testing"
28+
"time"
29+
30+
"github.com/stretchr/testify/assert"
31+
"github.com/uber-go/zap/spywrite"
32+
)
33+
34+
func newTextEncoder(opts ...TextOption) *textEncoder {
35+
return NewTextEncoder(opts...).(*textEncoder)
36+
}
37+
38+
func withTextEncoder(f func(*textEncoder)) {
39+
enc := newTextEncoder()
40+
f(enc)
41+
enc.Free()
42+
}
43+
44+
func assertTextOutput(t testing.TB, desc string, expected string, f func(Encoder)) {
45+
withTextEncoder(func(enc *textEncoder) {
46+
f(enc)
47+
assert.Equal(t, expected, string(enc.bytes), "Unexpected encoder output after adding a %s.", desc)
48+
})
49+
withTextEncoder(func(enc *textEncoder) {
50+
enc.AddString("foo", "bar")
51+
f(enc)
52+
expectedPrefix := "foo=bar"
53+
if expected != "" {
54+
// If we expect output, it should be space-separated from the previous
55+
// field.
56+
expectedPrefix += " "
57+
}
58+
assert.Equal(t, expectedPrefix+expected, string(enc.bytes), "Unexpected encoder output after adding a %s as a second field.", desc)
59+
})
60+
}
61+
62+
func TestTextEncoderFields(t *testing.T) {
63+
tests := []struct {
64+
desc string
65+
expected string
66+
f func(Encoder)
67+
}{
68+
{"string", "k=v", func(e Encoder) { e.AddString("k", "v") }},
69+
{"string", "k=", func(e Encoder) { e.AddString("k", "") }},
70+
{"bool", "k=true", func(e Encoder) { e.AddBool("k", true) }},
71+
{"bool", "k=false", func(e Encoder) { e.AddBool("k", false) }},
72+
{"int", "k=42", func(e Encoder) { e.AddInt("k", 42) }},
73+
{"int64", "k=42", func(e Encoder) { e.AddInt64("k", 42) }},
74+
{"int64", fmt.Sprintf("k=%d", math.MaxInt64), func(e Encoder) { e.AddInt64("k", math.MaxInt64) }},
75+
{"uint", "k=42", func(e Encoder) { e.AddUint("k", 42) }},
76+
{"uint64", "k=42", func(e Encoder) { e.AddUint64("k", 42) }},
77+
{"uint64", fmt.Sprintf("k=%d", uint64(math.MaxUint64)), func(e Encoder) { e.AddUint64("k", math.MaxUint64) }},
78+
{"float64", "k=1", func(e Encoder) { e.AddFloat64("k", 1.0) }},
79+
{"float64", "k=10000000000", func(e Encoder) { e.AddFloat64("k", 1e10) }},
80+
{"float64", "k=NaN", func(e Encoder) { e.AddFloat64("k", math.NaN()) }},
81+
{"float64", "k=+Inf", func(e Encoder) { e.AddFloat64("k", math.Inf(1)) }},
82+
{"float64", "k=-Inf", func(e Encoder) { e.AddFloat64("k", math.Inf(-1)) }},
83+
{"marshaler", "k={loggable=yes}", func(e Encoder) {
84+
assert.NoError(t, e.AddMarshaler("k", loggable{true}), "Unexpected error calling MarshalLog.")
85+
}},
86+
{"marshaler", "k={}", func(e Encoder) {
87+
assert.Error(t, e.AddMarshaler("k", loggable{false}), "Expected an error calling MarshalLog.")
88+
}},
89+
{"map[string]string", "k=map[loggable:yes]", func(e Encoder) {
90+
assert.NoError(t, e.AddObject("k", map[string]string{"loggable": "yes"}), "Unexpected error serializing a map.")
91+
}},
92+
{"arbitrary object", "k={Name:jane}", func(e Encoder) {
93+
assert.NoError(t, e.AddObject("k", struct{ Name string }{"jane"}), "Unexpected error serializing a struct.")
94+
}},
95+
}
96+
97+
for _, tt := range tests {
98+
assertTextOutput(t, tt.desc, tt.expected, tt.f)
99+
}
100+
}
101+
102+
func TestTextWriteEntry(t *testing.T) {
103+
entry := &Entry{Level: InfoLevel, Message: "Something happened.", Time: epoch}
104+
tests := []struct {
105+
enc Encoder
106+
expected string
107+
name string
108+
}{
109+
{
110+
enc: NewTextEncoder(),
111+
expected: "[I] 1970-01-01T00:00:00Z Something happened.",
112+
name: "RFC822",
113+
},
114+
{
115+
enc: NewTextEncoder(TextTimeFormat(time.RFC822)),
116+
expected: "[I] 01 Jan 70 00:00 UTC Something happened.",
117+
name: "RFC822",
118+
},
119+
{
120+
enc: NewTextEncoder(TextTimeFormat("")),
121+
expected: "[I] Something happened.",
122+
name: "empty layout",
123+
},
124+
{
125+
enc: NewTextEncoder(TextNoTime()),
126+
expected: "[I] Something happened.",
127+
name: "NoTime",
128+
},
129+
}
130+
131+
sink := &testBuffer{}
132+
for _, tt := range tests {
133+
assert.NoError(
134+
t,
135+
tt.enc.WriteEntry(sink, entry.Message, entry.Level, entry.Time),
136+
"Unexpected failure writing entry with text time formatter %s.", tt.name,
137+
)
138+
assert.Equal(t, tt.expected, sink.Stripped(), "Unexpected output from text time formatter %s.", tt.name)
139+
sink.Reset()
140+
}
141+
}
142+
143+
func TestTextWriteEntryLevels(t *testing.T) {
144+
tests := []struct {
145+
level Level
146+
expected string
147+
}{
148+
{DebugLevel, "D"},
149+
{InfoLevel, "I"},
150+
{WarnLevel, "W"},
151+
{ErrorLevel, "E"},
152+
{PanicLevel, "P"},
153+
{FatalLevel, "F"},
154+
{Level(42), "42"},
155+
}
156+
157+
sink := &testBuffer{}
158+
enc := NewTextEncoder(TextNoTime())
159+
for _, tt := range tests {
160+
assert.NoError(
161+
t,
162+
enc.WriteEntry(sink, "Fake message.", tt.level, epoch),
163+
"Unexpected failure writing entry with level %s.", tt.level,
164+
)
165+
expected := fmt.Sprintf("[%s] Fake message.", tt.expected)
166+
assert.Equal(t, expected, sink.Stripped(), "Unexpected text output for level %s.", tt.level)
167+
sink.Reset()
168+
}
169+
}
170+
171+
func TestTextClone(t *testing.T) {
172+
parent := &textEncoder{bytes: make([]byte, 0, 128)}
173+
clone := parent.Clone()
174+
175+
// Adding to the parent shouldn't affect the clone, and vice versa.
176+
parent.AddString("foo", "bar")
177+
clone.AddString("baz", "bing")
178+
179+
assert.Equal(t, "foo=bar", string(parent.bytes), "Unexpected serialized fields in parent encoder.")
180+
assert.Equal(t, "baz=bing", string(clone.(*textEncoder).bytes), "Unexpected serialized fields in cloned encoder.")
181+
}
182+
183+
func TestTextWriteEntryFailure(t *testing.T) {
184+
withTextEncoder(func(enc *textEncoder) {
185+
tests := []struct {
186+
sink io.Writer
187+
msg string
188+
}{
189+
{nil, "Expected an error when writing to a nil sink."},
190+
{spywrite.FailWriter{}, "Expected an error when writing to sink fails."},
191+
{spywrite.ShortWriter{}, "Expected an error on partial writes to sink."},
192+
}
193+
for _, tt := range tests {
194+
err := enc.WriteEntry(tt.sink, "hello", InfoLevel, time.Unix(0, 0))
195+
assert.Error(t, err, tt.msg)
196+
}
197+
})
198+
}
199+
200+
func TestTextTimeOptions(t *testing.T) {
201+
epoch := time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC)
202+
entry := &Entry{Level: InfoLevel, Message: "Something happened.", Time: epoch}
203+
204+
enc := NewTextEncoder()
205+
206+
sink := &testBuffer{}
207+
enc.AddString("foo", "bar")
208+
err := enc.WriteEntry(sink, entry.Message, entry.Level, entry.Time)
209+
assert.NoError(t, err, "WriteEntry returned an unexpected error.")
210+
assert.Equal(
211+
t,
212+
"[I] 1970-01-01T00:00:00Z Something happened. foo=bar",
213+
sink.Stripped(),
214+
)
215+
}

‎text_logger_test.go

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright (c) 2016 Uber Technologies, Inc.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
// THE SOFTWARE.
20+
21+
package zap
22+
23+
import (
24+
"testing"
25+
26+
"github.com/stretchr/testify/assert"
27+
)
28+
29+
func withTextLogger(t testing.TB, opts []Option, f func(Logger, *testBuffer)) {
30+
sink := &testBuffer{}
31+
errSink := &testBuffer{}
32+
33+
allOpts := make([]Option, 0, 3+len(opts))
34+
allOpts = append(allOpts, DebugLevel, Output(sink), ErrorOutput(errSink))
35+
allOpts = append(allOpts, opts...)
36+
logger := New(newTextEncoder(TextNoTime()), allOpts...)
37+
38+
f(logger, sink)
39+
assert.Empty(t, errSink.String(), "Expected error sink to be empty.")
40+
}
41+
42+
func TestTextLoggerDebugLevel(t *testing.T) {
43+
withTextLogger(t, nil, func(logger Logger, buf *testBuffer) {
44+
logger.Log(DebugLevel, "foo")
45+
assert.Equal(t, "[D] foo", buf.Stripped(), "Unexpected output from logger")
46+
})
47+
}
48+
49+
func TestTextLoggerNestedMarshal(t *testing.T) {
50+
m := LogMarshalerFunc(func(kv KeyValue) error {
51+
kv.AddString("loggable", "yes")
52+
kv.AddInt("number", 1)
53+
return nil
54+
})
55+
56+
withTextLogger(t, nil, func(logger Logger, buf *testBuffer) {
57+
logger.Info("Fields", String("f1", "{"), Marshaler("m", m))
58+
assert.Equal(t, "[I] Fields f1={ m={loggable=yes number=1}", buf.Stripped(), "Unexpected output from logger")
59+
})
60+
}
61+
62+
func TestTextLoggerAddMarshalEmpty(t *testing.T) {
63+
empty := LogMarshalerFunc(func(_ KeyValue) error { return nil })
64+
withTextLogger(t, nil, func(logger Logger, buf *testBuffer) {
65+
logger.Info("Empty", Marshaler("m", empty), String("something", "val"))
66+
assert.Equal(t, "[I] Empty m={} something=val", buf.Stripped(), "Unexpected output from logger")
67+
})
68+
}

0 commit comments

Comments
 (0)
Please sign in to comment.