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
21package observer_test
22
23import (
24	"testing"
25	"time"
26
27	"github.com/stretchr/testify/assert"
28	"github.com/stretchr/testify/require"
29
30	"go.uber.org/zap"
31	"go.uber.org/zap/zapcore"
32	. "go.uber.org/zap/zaptest/observer"
33)
34
35func assertEmpty(t testing.TB, logs *ObservedLogs) {
36	assert.Equal(t, 0, logs.Len(), "Expected empty ObservedLogs to have zero length.")
37	assert.Equal(t, []LoggedEntry{}, logs.All(), "Unexpected LoggedEntries in empty ObservedLogs.")
38}
39
40func TestObserver(t *testing.T) {
41	observer, logs := New(zap.InfoLevel)
42	assertEmpty(t, logs)
43
44	assert.NoError(t, observer.Sync(), "Unexpected failure in no-op Sync")
45
46	obs := zap.New(observer).With(zap.Int("i", 1))
47	obs.Info("foo")
48	obs.Debug("bar")
49	want := []LoggedEntry{{
50		Entry:   zapcore.Entry{Level: zap.InfoLevel, Message: "foo"},
51		Context: []zapcore.Field{zap.Int("i", 1)},
52	}}
53
54	assert.Equal(t, 1, logs.Len(), "Unexpected observed logs Len.")
55	assert.Equal(t, want, logs.AllUntimed(), "Unexpected contents from AllUntimed.")
56
57	all := logs.All()
58	require.Equal(t, 1, len(all), "Unexpected numbed of LoggedEntries returned from All.")
59	assert.NotEqual(t, time.Time{}, all[0].Time, "Expected non-zero time on LoggedEntry.")
60
61	// copy & zero time for stable assertions
62	untimed := append([]LoggedEntry{}, all...)
63	untimed[0].Time = time.Time{}
64	assert.Equal(t, want, untimed, "Unexpected LoggedEntries from All.")
65
66	assert.Equal(t, all, logs.TakeAll(), "Expected All and TakeAll to return identical results.")
67	assertEmpty(t, logs)
68}
69
70func TestObserverWith(t *testing.T) {
71	sf1, logs := New(zap.InfoLevel)
72
73	// need to pad out enough initial fields so that the underlying slice cap()
74	// gets ahead of its len() so that the sf3/4 With append's could choose
75	// not to copy (if the implementation doesn't force them)
76	sf1 = sf1.With([]zapcore.Field{zap.Int("a", 1), zap.Int("b", 2)})
77
78	sf2 := sf1.With([]zapcore.Field{zap.Int("c", 3)})
79	sf3 := sf2.With([]zapcore.Field{zap.Int("d", 4)})
80	sf4 := sf2.With([]zapcore.Field{zap.Int("e", 5)})
81	ent := zapcore.Entry{Level: zap.InfoLevel, Message: "hello"}
82
83	for i, core := range []zapcore.Core{sf2, sf3, sf4} {
84		if ce := core.Check(ent, nil); ce != nil {
85			ce.Write(zap.Int("i", i))
86		}
87	}
88
89	assert.Equal(t, []LoggedEntry{
90		{
91			Entry: ent,
92			Context: []zapcore.Field{
93				zap.Int("a", 1),
94				zap.Int("b", 2),
95				zap.Int("c", 3),
96				zap.Int("i", 0),
97			},
98		},
99		{
100			Entry: ent,
101			Context: []zapcore.Field{
102				zap.Int("a", 1),
103				zap.Int("b", 2),
104				zap.Int("c", 3),
105				zap.Int("d", 4),
106				zap.Int("i", 1),
107			},
108		},
109		{
110			Entry: ent,
111			Context: []zapcore.Field{
112				zap.Int("a", 1),
113				zap.Int("b", 2),
114				zap.Int("c", 3),
115				zap.Int("e", 5),
116				zap.Int("i", 2),
117			},
118		},
119	}, logs.All(), "expected no field sharing between With siblings")
120}
121
122func TestFilters(t *testing.T) {
123	logs := []LoggedEntry{
124		{
125			Entry:   zapcore.Entry{Level: zap.InfoLevel, Message: "log a"},
126			Context: []zapcore.Field{zap.String("fStr", "1"), zap.Int("a", 1)},
127		},
128		{
129			Entry:   zapcore.Entry{Level: zap.InfoLevel, Message: "log a"},
130			Context: []zapcore.Field{zap.String("fStr", "2"), zap.Int("b", 2)},
131		},
132		{
133			Entry:   zapcore.Entry{Level: zap.InfoLevel, Message: "log b"},
134			Context: []zapcore.Field{zap.Int("a", 1), zap.Int("b", 2)},
135		},
136		{
137			Entry:   zapcore.Entry{Level: zap.InfoLevel, Message: "log c"},
138			Context: []zapcore.Field{zap.Int("a", 1), zap.Namespace("ns"), zap.Int("a", 2)},
139		},
140		{
141			Entry:   zapcore.Entry{Level: zap.InfoLevel, Message: "msg 1"},
142			Context: []zapcore.Field{zap.Int("a", 1), zap.Namespace("ns")},
143		},
144		{
145			Entry:   zapcore.Entry{Level: zap.InfoLevel, Message: "any map"},
146			Context: []zapcore.Field{zap.Any("map", map[string]string{"a": "b"})},
147		},
148		{
149			Entry:   zapcore.Entry{Level: zap.InfoLevel, Message: "any slice"},
150			Context: []zapcore.Field{zap.Any("slice", []string{"a"})},
151		},
152	}
153
154	logger, sink := New(zap.InfoLevel)
155	for _, log := range logs {
156		logger.Write(log.Entry, log.Context)
157	}
158
159	tests := []struct {
160		msg      string
161		filtered *ObservedLogs
162		want     []LoggedEntry
163	}{
164		{
165			msg:      "filter by message",
166			filtered: sink.FilterMessage("log a"),
167			want:     logs[0:2],
168		},
169		{
170			msg:      "filter by field",
171			filtered: sink.FilterField(zap.String("fStr", "1")),
172			want:     logs[0:1],
173		},
174		{
175			msg:      "filter by message and field",
176			filtered: sink.FilterMessage("log a").FilterField(zap.Int("b", 2)),
177			want:     logs[1:2],
178		},
179		{
180			msg:      "filter by field with duplicate fields",
181			filtered: sink.FilterField(zap.Int("a", 2)),
182			want:     logs[3:4],
183		},
184		{
185			msg:      "filter doesn't match any messages",
186			filtered: sink.FilterMessage("no match"),
187			want:     []LoggedEntry{},
188		},
189		{
190			msg:      "filter by snippet",
191			filtered: sink.FilterMessageSnippet("log"),
192			want:     logs[0:4],
193		},
194		{
195			msg:      "filter by snippet and field",
196			filtered: sink.FilterMessageSnippet("a").FilterField(zap.Int("b", 2)),
197			want:     logs[1:2],
198		},
199		{
200			msg:      "filter for map",
201			filtered: sink.FilterField(zap.Any("map", map[string]string{"a": "b"})),
202			want:     logs[5:6],
203		},
204		{
205			msg:      "filter for slice",
206			filtered: sink.FilterField(zap.Any("slice", []string{"a"})),
207			want:     logs[6:7],
208		},
209	}
210
211	for _, tt := range tests {
212		got := tt.filtered.AllUntimed()
213		assert.Equal(t, tt.want, got, tt.msg)
214	}
215}
216