Skip to content

Commit 3703ee6

Browse files
author
Akshay Shah
committed
Add an MVP of loggers and fields
As a preamble, fold the `encoder` sub-package into the main `zap` package. Then, * Tweak the encoder slightly, and define a typed key-value store interface over it. * Add a Field type to represent a deferred marshaling operation. * Add a JSON-formatted Logger.
1 parent e66c509 commit 3703ee6

18 files changed

+984
-85
lines changed

encoder/doc.go doc.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,5 @@
1818
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
1919
// THE SOFTWARE.
2020

21-
// Package encoder provides a JSON encoder specifically intended for logging
22-
// with zap. It's not suitable for general use, but it makes zap much faster
23-
// and lower-allocation.
24-
package encoder
21+
// Package zap provides efficient, structured, leveled logging in Go.
22+
package zap

encoder.go

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
"io"
25+
"time"
26+
)
27+
28+
// Encoder is a format-agnostic interface for all log field encoders. It's not
29+
// safe for concurrent use.
30+
type encoder interface {
31+
KeyValue
32+
Clone() encoder
33+
Free()
34+
WriteMessage(io.Writer, string, string, time.Time) error
35+
}

example_test.go

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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_test
22+
23+
import (
24+
"os"
25+
"time"
26+
27+
"github.com/uber-common/zap"
28+
)
29+
30+
func Example() {
31+
// Log in JSON, using zap's reflection-free JSON encoder.
32+
logger := zap.NewJSON(zap.Info, os.Stdout)
33+
34+
logger.Warn("Log without structured data...")
35+
logger.Warn(
36+
"Or use strongly-typed wrappers to add structured context.",
37+
zap.String("library", "zap"),
38+
zap.Duration("latency", time.Nanosecond),
39+
)
40+
41+
// Avoid re-serializing the same data repeatedly by creating a child logger
42+
// with some attached context. That context is added to all the child's
43+
// log output, but doesn't affect the parent.
44+
child := logger.With(zap.String("user", "[email protected]"), zap.Int("visits", 42))
45+
child.Error("Oh no!")
46+
}
47+
48+
func ExampleNest() {
49+
logger := zap.NewJSON(zap.Info, os.Stdout)
50+
// We'd like the logging context to be {"outer":{"inner":42}}
51+
logger.Debug("Nesting context.", zap.Nest("outer",
52+
zap.Int("inner", 42),
53+
))
54+
}

field.go

+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
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 "time"
24+
25+
// A Field is a deferred marshaling operation used to add a key-value pair to
26+
// a logger's context. Keys and values are appropriately escaped for the current
27+
// encoding scheme (e.g., JSON).
28+
type Field interface {
29+
addTo(encoder) error
30+
}
31+
32+
// Bool constructs a Field with the given key and value.
33+
func Bool(key string, val bool) Field {
34+
return boolField{key, val}
35+
}
36+
37+
// Float64 constructs a Field with the given key and value. The floating-point
38+
// value is encoded using strconv.FormatFloat's 'g' option (exponential notation
39+
// for large exponents, grade-school notation otherwise).
40+
func Float64(key string, val float64) Field {
41+
return float64Field{key, val}
42+
}
43+
44+
// Int constructs a Field with the given key and value.
45+
func Int(key string, val int) Field {
46+
return intField{key, val}
47+
}
48+
49+
// Int64 constructs a Field with the given key and value.
50+
func Int64(key string, val int64) Field {
51+
return int64Field{key, val}
52+
}
53+
54+
// String constructs a Field with the given key and value.
55+
func String(key string, val string) Field {
56+
return stringField{key, val}
57+
}
58+
59+
// Time constructs a Field with the given key and value. It represents a
60+
// time.Time as nanoseconds since epoch.
61+
func Time(key string, val time.Time) Field {
62+
return timeField{key, val}
63+
}
64+
65+
// Err constructs a Field that stores err.Error() under the key "error".
66+
func Err(err error) Field {
67+
return stringField{"error", err.Error()}
68+
}
69+
70+
// Duration constructs a Field with the given key and value. It represents
71+
// durations as an integer number of nanoseconds.
72+
func Duration(key string, val time.Duration) Field {
73+
return int64Field{key, int64(val)}
74+
}
75+
76+
// Object constructs a field with the given key and zap.Marshaler. It provides a
77+
// flexible, but still type-safe and efficient, way to add user-defined types to
78+
// the logging context.
79+
func Object(key string, val Marshaler) Field {
80+
return marshalerField{key, val}
81+
}
82+
83+
// Nest takes a key and a variadic number of Fields and creates a nested
84+
// namespace.
85+
func Nest(key string, fields ...Field) Field {
86+
return nestedField{key, fields}
87+
}
88+
89+
type boolField struct {
90+
key string
91+
val bool
92+
}
93+
94+
func (b boolField) addTo(enc encoder) error {
95+
enc.AddBool(b.key, b.val)
96+
return nil
97+
}
98+
99+
type float64Field struct {
100+
key string
101+
val float64
102+
}
103+
104+
func (f float64Field) addTo(enc encoder) error {
105+
enc.AddFloat64(f.key, f.val)
106+
return nil
107+
}
108+
109+
type intField struct {
110+
key string
111+
val int
112+
}
113+
114+
func (i intField) addTo(enc encoder) error {
115+
enc.AddInt(i.key, i.val)
116+
return nil
117+
}
118+
119+
type int64Field struct {
120+
key string
121+
val int64
122+
}
123+
124+
func (i int64Field) addTo(enc encoder) error {
125+
enc.AddInt64(i.key, i.val)
126+
return nil
127+
}
128+
129+
type stringField struct {
130+
key string
131+
val string
132+
}
133+
134+
func (s stringField) addTo(enc encoder) error {
135+
enc.AddString(s.key, s.val)
136+
return nil
137+
}
138+
139+
type timeField struct {
140+
key string
141+
val time.Time
142+
}
143+
144+
func (t timeField) addTo(enc encoder) error {
145+
enc.AddTime(t.key, t.val)
146+
return nil
147+
}
148+
149+
type marshalerField struct {
150+
key string
151+
val Marshaler
152+
}
153+
154+
func (m marshalerField) addTo(enc encoder) error {
155+
finish := enc.Nest(m.key)
156+
err := m.val.MarshalLog(enc)
157+
finish()
158+
return err
159+
}
160+
161+
type nestedField struct {
162+
key string
163+
vals []Field
164+
}
165+
166+
func (n nestedField) addTo(enc encoder) error {
167+
finish := enc.Nest(n.key)
168+
var errs multiError
169+
for _, f := range n.vals {
170+
if err := f.addTo(enc); err != nil {
171+
errs = append(errs, err)
172+
}
173+
}
174+
finish()
175+
if len(errs) > 0 {
176+
return errs
177+
}
178+
return nil
179+
}

field_test.go

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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+
"errors"
25+
"testing"
26+
"time"
27+
28+
"github.com/stretchr/testify/assert"
29+
)
30+
31+
type fakeUser struct{ name string }
32+
33+
func (f fakeUser) MarshalLog(kv KeyValue) error {
34+
if f.name == "fail" {
35+
return errors.New("fail")
36+
}
37+
kv.AddString("name", f.name)
38+
return nil
39+
}
40+
41+
func assertFieldJSON(t testing.TB, expected string, field Field) {
42+
enc := newJSONEncoder()
43+
defer enc.Free()
44+
45+
field.addTo(enc)
46+
assert.Equal(
47+
t,
48+
expected,
49+
string(enc.bytes),
50+
"Unexpected JSON output after applying field %+v.",
51+
field,
52+
)
53+
}
54+
55+
func TestBoolField(t *testing.T) {
56+
assertFieldJSON(t, `"foo":true`, Bool("foo", true))
57+
}
58+
59+
func TestFloat64Field(t *testing.T) {
60+
assertFieldJSON(t, `"foo":1.314`, Float64("foo", 1.314))
61+
}
62+
63+
func TestIntField(t *testing.T) {
64+
assertFieldJSON(t, `"foo":1`, Int("foo", 1))
65+
}
66+
67+
func TestInt64Field(t *testing.T) {
68+
assertFieldJSON(t, `"foo":1`, Int64("foo", int64(1)))
69+
}
70+
71+
func TestStringField(t *testing.T) {
72+
assertFieldJSON(t, `"foo":"bar"`, String("foo", "bar"))
73+
}
74+
75+
func TestTimeField(t *testing.T) {
76+
assertFieldJSON(t, `"foo":0`, Time("foo", time.Unix(0, 0)))
77+
}
78+
79+
func TestErrField(t *testing.T) {
80+
assertFieldJSON(t, `"error":"fail"`, Err(errors.New("fail")))
81+
}
82+
83+
func TestDurationField(t *testing.T) {
84+
assertFieldJSON(t, `"foo":1`, Duration("foo", time.Nanosecond))
85+
}
86+
87+
func TestObjectField(t *testing.T) {
88+
assertFieldJSON(t, `"foo":{"name":"phil"}`, Object("foo", fakeUser{"phil"}))
89+
// Marshaling the user failed, so we expect an empty object.
90+
assertFieldJSON(t, `"foo":{}`, Object("foo", fakeUser{"fail"}))
91+
}
92+
93+
func TestNestField(t *testing.T) {
94+
assertFieldJSON(
95+
t,
96+
`"foo":{"name":"phil","age":42}`,
97+
Nest("foo",
98+
String("name", "phil"),
99+
Int("age", 42),
100+
),
101+
)
102+
assertFieldJSON(
103+
t,
104+
// Marshaling the user failed, so we expect an empty object.
105+
`"foo":{"user":{}}`,
106+
Nest("foo", Object("user", fakeUser{"fail"})),
107+
)
108+
}

0 commit comments

Comments
 (0)