Skip to content

Commit 87c179b

Browse files
prashantvakshayjshah
authored andcommitted
Update zap.NewJSON to use functional options (uber-go#23)
Default options are: - Info - Write logs to stdout - Write logging errors to stderr - No fields Log levels implement the option interface so you can pass them directly. Added examples for NewJSON that show default vs passing options.
1 parent 6064240 commit 87c179b

6 files changed

+150
-47
lines changed

benchmarks/zap_bench_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ var _jane = user{
5151
}
5252

5353
func BenchmarkZapAddingFields(b *testing.B) {
54-
logger := zap.NewJSON(zap.All, ioutil.Discard, ioutil.Discard)
54+
logger := zap.NewJSON(zap.All, zap.Output(ioutil.Discard))
5555
b.ResetTimer()
5656
b.RunParallel(func(pb *testing.PB) {
5757
for pb.Next() {
@@ -72,7 +72,7 @@ func BenchmarkZapAddingFields(b *testing.B) {
7272
}
7373

7474
func BenchmarkZapWithAccumulatedContext(b *testing.B) {
75-
logger := zap.NewJSON(zap.All, ioutil.Discard, ioutil.Discard,
75+
logger := zap.NewJSON(zap.All, zap.Output(ioutil.Discard), zap.Fields(
7676
zap.Int("int", 1),
7777
zap.Int64("int64", 2),
7878
zap.Float64("float", 3.0),
@@ -83,7 +83,7 @@ func BenchmarkZapWithAccumulatedContext(b *testing.B) {
8383
zap.Duration("duration", time.Second),
8484
zap.Object("user-defined type", _jane),
8585
zap.String("another string", "done!"),
86-
)
86+
))
8787
b.ResetTimer()
8888
b.RunParallel(func(pb *testing.PB) {
8989
for pb.Next() {
@@ -93,7 +93,7 @@ func BenchmarkZapWithAccumulatedContext(b *testing.B) {
9393
}
9494

9595
func BenchmarkZapWithoutFields(b *testing.B) {
96-
logger := zap.NewJSON(zap.All, ioutil.Discard, ioutil.Discard)
96+
logger := zap.NewJSON(zap.All, zap.Output(ioutil.Discard))
9797
b.ResetTimer()
9898
b.RunParallel(func(pb *testing.PB) {
9999
for pb.Next() {

example_test.go

+36-9
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@
2121
package zap_test
2222

2323
import (
24-
"os"
2524
"time"
2625

2726
"github.com/uber-common/zap"
2827
)
2928

3029
func Example() {
3130
// Log in JSON, using zap's reflection-free JSON encoder.
32-
logger := zap.NewJSON(zap.Info, os.Stdout, os.Stderr)
31+
// The default options will log any Info or higher logs to standard out.
32+
logger := zap.NewJSON()
3333
// For repeatable tests, pretend that it's always 1970.
3434
logger.StubTime()
3535

@@ -53,20 +53,47 @@ func Example() {
5353
}
5454

5555
func ExampleNest() {
56-
logger := zap.NewJSON(zap.Info, os.Stdout, os.Stderr)
56+
logger := zap.NewJSON()
5757
// Stub the current time in tests.
5858
logger.StubTime()
5959

6060
// We'd like the logging context to be {"outer":{"inner":42}}
61-
logger.Debug("Nesting context.", zap.Nest("outer",
62-
zap.Int("inner", 42),
63-
))
64-
65-
// If we want to stop a field from being returned to a sync.Pool on use,
66-
// use Keep.
6761
nest := zap.Nest("outer", zap.Int("inner", 42))
6862
logger.Info("Logging a nested field.", nest)
6963

7064
// Output:
7165
// {"msg":"Logging a nested field.","level":"info","ts":0,"fields":{"outer":{"inner":42}}}
7266
}
67+
68+
func ExampleNewJSON() {
69+
// The default logger outputs to standard out and only writes logs that are
70+
// Info level or higher.
71+
logger := zap.NewJSON()
72+
// Stub the current time in tests.
73+
logger.StubTime()
74+
75+
// The default logger does not print Debug logs.
76+
logger.Debug("this won't be printed")
77+
logger.Info("This is an info log.")
78+
79+
// Output:
80+
// {"msg":"This is an info log.","level":"info","ts":0,"fields":{}}
81+
}
82+
83+
func ExampleNewJSON_options() {
84+
// We can pass multiple options to the NewJSON method to configure
85+
// the logging level, output location, or even the initial context.
86+
logger := zap.NewJSON(
87+
zap.Debug,
88+
zap.Fields(zap.Int("count", 1)),
89+
)
90+
// Stub the current time in tests.
91+
logger.StubTime()
92+
93+
logger.Debug("This is a debug log.")
94+
logger.Info("This is an info log.")
95+
96+
// Output:
97+
// {"msg":"This is a debug log.","level":"debug","ts":0,"fields":{"count":1}}
98+
// {"msg":"This is an info log.","level":"info","ts":0,"fields":{"count":1}}
99+
}

logger.go

+16-12
Original file line numberDiff line numberDiff line change
@@ -70,22 +70,26 @@ type jsonLogger struct {
7070
// NewJSON returns a logger that formats its output as JSON. Zap uses a
7171
// customized JSON encoder to avoid reflection and minimize allocations.
7272
//
73-
// Any passed fields are added to the logger as context.
74-
func NewJSON(lvl Level, sink, errSink io.Writer, fields ...Field) Logger {
75-
if errSink == nil {
76-
errSink = _defaultErrSink
77-
}
78-
79-
integerLevel := int32(lvl)
73+
// By default, the logger will write Info logs or higher to standard
74+
// out. Any errors during logging will be written to standard error.
75+
//
76+
// Options can change the log level, the output location, or the initial
77+
// fields that should be added as context.
78+
func NewJSON(options ...Option) Logger {
79+
defaultLevel := int32(Info)
8080
jl := &jsonLogger{
81-
level: &integerLevel,
8281
enc: newJSONEncoder(),
83-
errW: errSink,
84-
w: sink,
82+
level: &defaultLevel,
83+
errW: _defaultErrSink,
84+
w: os.Stdout,
8585
}
86-
if err := jl.enc.AddFields(fields); err != nil {
87-
jl.internalError(err.Error())
86+
87+
for _, opt := range options {
88+
if err := opt.apply(jl); err != nil {
89+
jl.internalError(err.Error())
90+
}
8891
}
92+
8993
return jl
9094
}
9195

logger_bench_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ var _jane = user{
4949
}
5050

5151
func withBenchedLogger(b *testing.B, f func(zap.Logger)) {
52-
logger := zap.NewJSON(zap.All, ioutil.Discard, ioutil.Discard)
52+
logger := zap.NewJSON(zap.All, zap.Output(ioutil.Discard))
5353
b.ResetTimer()
5454
b.RunParallel(func(pb *testing.PB) {
5555
for pb.Next() {
@@ -152,7 +152,7 @@ func Benchmark10Fields(b *testing.B) {
152152

153153
func Benchmark100Fields(b *testing.B) {
154154
const batchSize = 50
155-
logger := zap.NewJSON(zap.All, ioutil.Discard, ioutil.Discard)
155+
logger := zap.NewJSON(zap.All, zap.Output(ioutil.Discard))
156156

157157
// Don't include allocating these helper slices in the benchmark. Since
158158
// access to them isn't synchronized, we can't run the benchmark in

logger_test.go

+28-20
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,25 @@ package zap
2323
import (
2424
"bytes"
2525
"fmt"
26-
"io/ioutil"
2726
"os"
2827
"strings"
2928
"testing"
3029

3130
"github.com/stretchr/testify/assert"
3231
)
3332

34-
func withJSONLogger(t testing.TB, f func(*jsonLogger, func() []string), fields ...Field) {
33+
func opts(opts ...Option) []Option {
34+
return opts
35+
}
36+
37+
func withJSONLogger(t testing.TB, opts []Option, f func(*jsonLogger, func() []string)) {
3538
sink := &bytes.Buffer{}
3639
errSink := &bytes.Buffer{}
37-
jl := NewJSON(All, sink, errSink, fields...)
40+
41+
allOpts := make([]Option, 0, 3+len(opts))
42+
allOpts = append(allOpts, All, Output(sink), ErrorOutput(errSink))
43+
allOpts = append(allOpts, opts...)
44+
jl := NewJSON(allOpts...)
3845
jl.StubTime()
3946

4047
f(jl.(*jsonLogger), func() []string { return strings.Split(sink.String(), "\n") })
@@ -56,16 +63,15 @@ func assertFields(t testing.TB, jl Logger, getOutput func() []string, expectedFi
5663
}
5764

5865
func TestJSONLoggerSetLevel(t *testing.T) {
59-
withJSONLogger(t, func(jl *jsonLogger, _ func() []string) {
66+
withJSONLogger(t, nil, func(jl *jsonLogger, _ func() []string) {
6067
assert.Equal(t, All, jl.Level(), "Unexpected initial level.")
6168
jl.SetLevel(Debug)
6269
assert.Equal(t, Debug, jl.Level(), "Unexpected level after SetLevel.")
6370
})
6471
}
6572

6673
func TestJSONLoggerEnabled(t *testing.T) {
67-
withJSONLogger(t, func(jl *jsonLogger, _ func() []string) {
68-
jl.SetLevel(Info)
74+
withJSONLogger(t, opts(Info), func(jl *jsonLogger, _ func() []string) {
6975
assert.False(t, jl.Enabled(Debug), "Debug logs shouldn't be enabled at Info level.")
7076
assert.True(t, jl.Enabled(Info), "Info logs should be enabled at Info level.")
7177
assert.True(t, jl.Enabled(Warn), "Warn logs should be enabled at Info level.")
@@ -85,7 +91,7 @@ func TestJSONLoggerEnabled(t *testing.T) {
8591
func TestJSONLoggerConcurrentLevelMutation(t *testing.T) {
8692
// Trigger races for non-atomic level mutations.
8793
proceed := make(chan struct{})
88-
jl := NewJSON(Info, ioutil.Discard, ioutil.Discard)
94+
jl := NewJSON()
8995

9096
for i := 0; i < 50; i++ {
9197
go func(l Logger) {
@@ -101,51 +107,53 @@ func TestJSONLoggerConcurrentLevelMutation(t *testing.T) {
101107
}
102108

103109
func TestJSONLoggerInitialFields(t *testing.T) {
104-
withJSONLogger(t, func(jl *jsonLogger, output func() []string) {
110+
fieldOpts := opts(Fields(Int("foo", 42), String("bar", "baz")))
111+
withJSONLogger(t, fieldOpts, func(jl *jsonLogger, output func() []string) {
105112
assertFields(t, jl, output, `{"foo":42,"bar":"baz"}`)
106-
}, Int("foo", 42), String("bar", "baz"))
113+
})
107114
}
108115

109116
func TestJSONLoggerWith(t *testing.T) {
110-
withJSONLogger(t, func(jl *jsonLogger, output func() []string) {
117+
fieldOpts := opts(Fields(Int("foo", 42)))
118+
withJSONLogger(t, fieldOpts, func(jl *jsonLogger, output func() []string) {
111119
// Child loggers should have copy-on-write semantics, so two children
112120
// shouldn't stomp on each other's fields or affect the parent's fields.
113121
jl.With(String("one", "two")).Debug("")
114122
jl.With(String("three", "four")).Debug("")
115123
assertFields(t, jl, output, `{"foo":42,"one":"two"}`, `{"foo":42,"three":"four"}`, `{"foo":42}`)
116-
}, Int("foo", 42))
124+
})
117125
}
118126

119127
func TestJSONLoggerDebug(t *testing.T) {
120-
withJSONLogger(t, func(jl *jsonLogger, output func() []string) {
128+
withJSONLogger(t, nil, func(jl *jsonLogger, output func() []string) {
121129
jl.Debug("foo")
122130
assertMessage(t, "debug", "foo", output()[0])
123131
})
124132
}
125133

126134
func TestJSONLoggerInfo(t *testing.T) {
127-
withJSONLogger(t, func(jl *jsonLogger, output func() []string) {
135+
withJSONLogger(t, nil, func(jl *jsonLogger, output func() []string) {
128136
jl.Info("foo")
129137
assertMessage(t, "info", "foo", output()[0])
130138
})
131139
}
132140

133141
func TestJSONLoggerWarn(t *testing.T) {
134-
withJSONLogger(t, func(jl *jsonLogger, output func() []string) {
142+
withJSONLogger(t, nil, func(jl *jsonLogger, output func() []string) {
135143
jl.Warn("foo")
136144
assertMessage(t, "warn", "foo", output()[0])
137145
})
138146
}
139147

140148
func TestJSONLoggerError(t *testing.T) {
141-
withJSONLogger(t, func(jl *jsonLogger, output func() []string) {
149+
withJSONLogger(t, nil, func(jl *jsonLogger, output func() []string) {
142150
jl.Error("foo")
143151
assertMessage(t, "error", "foo", output()[0])
144152
})
145153
}
146154

147155
func TestJSONLoggerPanic(t *testing.T) {
148-
withJSONLogger(t, func(jl *jsonLogger, output func() []string) {
156+
withJSONLogger(t, nil, func(jl *jsonLogger, output func() []string) {
149157
assert.Panics(t, func() {
150158
jl.Panic("foo")
151159
})
@@ -154,7 +162,7 @@ func TestJSONLoggerPanic(t *testing.T) {
154162
}
155163

156164
func TestJSONLoggerNoOpsDisabledLevels(t *testing.T) {
157-
withJSONLogger(t, func(jl *jsonLogger, output func() []string) {
165+
withJSONLogger(t, nil, func(jl *jsonLogger, output func() []string) {
158166
jl.SetLevel(Warn)
159167
jl.Info("silence!")
160168
assert.Equal(t, []string{""}, output(), "Expected logging at a disabled level to produce no output.")
@@ -168,7 +176,7 @@ func TestJSONLoggerInternalErrorHandlingNoSink(t *testing.T) {
168176

169177
buf := &bytes.Buffer{}
170178

171-
jl := NewJSON(All, buf, nil, Object("user", fakeUser{"fail"}))
179+
jl := NewJSON(All, Output(buf), Fields(Object("user", fakeUser{"fail"})))
172180
jl.StubTime()
173181
output := func() []string { return strings.Split(buf.String(), "\n") }
174182

@@ -183,7 +191,7 @@ func TestJSONLoggerInternalErrorHandlingWithSink(t *testing.T) {
183191
buf := &bytes.Buffer{}
184192
errBuf := &bytes.Buffer{}
185193

186-
jl := NewJSON(All, buf, errBuf, Object("user", fakeUser{"fail"}))
194+
jl := NewJSON(All, Output(buf), ErrorOutput(errBuf), Fields(Object("user", fakeUser{"fail"})))
187195
jl.StubTime()
188196
output := func() []string { return strings.Split(buf.String(), "\n") }
189197

@@ -197,7 +205,7 @@ func TestJSONLoggerInternalErrorHandlingWithSink(t *testing.T) {
197205
func TestJSONLoggerRuntimeLevelChange(t *testing.T) {
198206
// Test that changing a logger's level also changes the level of all
199207
// ancestors and descendants.
200-
grandparent := NewJSON(Info, ioutil.Discard, ioutil.Discard, Int("generation", 1))
208+
grandparent := NewJSON(Fields(Int("generation", 1)))
201209
parent := grandparent.With(Int("generation", 2))
202210
child := parent.With(Int("generation", 3))
203211

options.go

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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 "io"
24+
25+
// Option is used to set options for the logger.
26+
type Option interface {
27+
apply(*jsonLogger) error
28+
}
29+
30+
// optionFunc wraps a func so it satisfies the Option interface.
31+
type optionFunc func(*jsonLogger) error
32+
33+
func (f optionFunc) apply(jl *jsonLogger) error {
34+
return f(jl)
35+
}
36+
37+
// This allows any Level to be used as an option.
38+
func (l Level) apply(jl *jsonLogger) error {
39+
jl.SetLevel(l)
40+
return nil
41+
}
42+
43+
// Fields sets the initial fields for the logger.
44+
func Fields(fields ...Field) Option {
45+
return optionFunc(func(jl *jsonLogger) error {
46+
return jl.enc.AddFields(fields)
47+
})
48+
}
49+
50+
// Output sets the destination for the logger's output.
51+
func Output(w io.Writer) Option {
52+
return optionFunc(func(jl *jsonLogger) error {
53+
jl.w = w
54+
return nil
55+
})
56+
}
57+
58+
// ErrorOutput sets the destination for errors generated by the logger.
59+
func ErrorOutput(w io.Writer) Option {
60+
return optionFunc(func(jl *jsonLogger) error {
61+
jl.errW = w
62+
return nil
63+
})
64+
}

0 commit comments

Comments
 (0)