Skip to content

Commit 02f50f7

Browse files
committed
Add test for logger
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
1 parent 6838a26 commit 02f50f7

3 files changed

Lines changed: 689 additions & 0 deletions

File tree

log/log_test.go

Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
package log
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"errors"
8+
"fmt"
9+
"log/slog"
10+
"testing"
11+
"time"
12+
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
"go.opentelemetry.io/otel/trace"
16+
)
17+
18+
func parseJSONLog(t *testing.T, buf *bytes.Buffer) map[string]any {
19+
t.Helper()
20+
var m map[string]any
21+
require.NoError(t, json.Unmarshal(buf.Bytes(), &m))
22+
return m
23+
}
24+
25+
func TestNewLoggerDefaults(t *testing.T) {
26+
var buf bytes.Buffer
27+
l := NewLogger(WithOutput(&buf))
28+
29+
l.Info("hello")
30+
31+
m := parseJSONLog(t, &buf)
32+
assert.Equal(t, "INFO", m["level"])
33+
assert.Equal(t, "hello", m["msg"])
34+
assert.NotEmpty(t, m["time"])
35+
}
36+
37+
func TestNewLoggerWithLevel(t *testing.T) {
38+
var buf bytes.Buffer
39+
l := NewLogger(WithOutput(&buf), WithLevel(LevelError))
40+
41+
l.Info("should not appear")
42+
assert.Empty(t, buf.String())
43+
44+
l.Error("visible")
45+
m := parseJSONLog(t, &buf)
46+
assert.Equal(t, "ERROR", m["level"])
47+
assert.Equal(t, "visible", m["msg"])
48+
}
49+
50+
func TestNewLoggerWithName(t *testing.T) {
51+
var buf bytes.Buffer
52+
l := NewLogger(WithOutput(&buf), WithName("myapp"))
53+
54+
l.Info("test")
55+
56+
m := parseJSONLog(t, &buf)
57+
assert.Equal(t, "myapp", m["name"])
58+
}
59+
60+
func TestNewLoggerWithAttributes(t *testing.T) {
61+
var buf bytes.Buffer
62+
l := NewLogger(
63+
WithOutput(&buf),
64+
WithAttributes(String("service", "api"), Int("version", 2)),
65+
)
66+
67+
l.Info("test")
68+
69+
m := parseJSONLog(t, &buf)
70+
assert.Equal(t, "api", m["service"])
71+
assert.Equal(t, float64(2), m["version"])
72+
}
73+
74+
func TestNewLoggerWithFormatText(t *testing.T) {
75+
var buf bytes.Buffer
76+
l := NewLogger(WithOutput(&buf), WithFormat(FormatText))
77+
78+
l.Info("text-log")
79+
assert.Contains(t, buf.String(), "text-log")
80+
assert.Contains(t, buf.String(), "level=INFO")
81+
}
82+
83+
func TestNewLoggerWithFormatPretty(t *testing.T) {
84+
var buf bytes.Buffer
85+
l := NewLogger(WithOutput(&buf), WithFormat(FormatPretty))
86+
87+
l.Info("pretty-log")
88+
assert.Contains(t, buf.String(), "pretty-log")
89+
}
90+
91+
func TestNewLoggerPanicsOnUnsupportedFormat(t *testing.T) {
92+
assert.Panics(t, func() {
93+
NewLogger(WithFormat("xml"))
94+
})
95+
}
96+
97+
func TestSkipMatch(t *testing.T) {
98+
var buf bytes.Buffer
99+
l := NewLogger(
100+
WithOutput(&buf),
101+
SkipMatch(func(level Level, msg string, attrs []Attr) bool {
102+
return msg == "skip-me"
103+
}),
104+
)
105+
106+
l.Info("skip-me")
107+
assert.Empty(t, buf.String())
108+
109+
l.Info("keep-me")
110+
m := parseJSONLog(t, &buf)
111+
assert.Equal(t, "keep-me", m["msg"])
112+
}
113+
114+
func TestLoggerWith(t *testing.T) {
115+
var buf bytes.Buffer
116+
l := NewLogger(
117+
WithOutput(&buf),
118+
WithName("base"),
119+
WithAttributes(String("env", "test")),
120+
)
121+
122+
l2 := l.With(String("extra", "val"))
123+
124+
l2.Info("with-test")
125+
126+
m := parseJSONLog(t, &buf)
127+
assert.Equal(t, "test", m["env"])
128+
assert.Equal(t, "val", m["extra"])
129+
assert.Equal(t, "base", m["name"])
130+
}
131+
132+
func TestLoggerWithPreservesMatch(t *testing.T) {
133+
var buf bytes.Buffer
134+
l := NewLogger(
135+
WithOutput(&buf),
136+
SkipMatch(func(level Level, msg string, attrs []Attr) bool {
137+
return msg == "skip"
138+
}),
139+
)
140+
141+
l2 := l.With(String("k", "v"))
142+
l2.Info("skip")
143+
assert.Empty(t, buf.String())
144+
}
145+
146+
func TestLoggerNamed(t *testing.T) {
147+
var buf bytes.Buffer
148+
l := NewLogger(WithOutput(&buf), WithName("parent"))
149+
150+
child := l.Named("child")
151+
child.Info("named-test")
152+
153+
m := parseJSONLog(t, &buf)
154+
assert.Equal(t, "parent.child", m["name"])
155+
}
156+
157+
func TestLoggerNamedFromEmpty(t *testing.T) {
158+
var buf bytes.Buffer
159+
l := NewLogger(WithOutput(&buf))
160+
161+
child := l.Named("service")
162+
child.Info("test")
163+
164+
m := parseJSONLog(t, &buf)
165+
assert.Equal(t, "service", m["name"])
166+
}
167+
168+
func TestLoggerNamedPreservesMatch(t *testing.T) {
169+
var buf bytes.Buffer
170+
l := NewLogger(
171+
WithOutput(&buf),
172+
SkipMatch(func(level Level, msg string, attrs []Attr) bool {
173+
return msg == "skip"
174+
}),
175+
)
176+
177+
child := l.Named("child")
178+
child.Info("skip")
179+
assert.Empty(t, buf.String())
180+
}
181+
182+
func TestLoggerNamedOptionOverride(t *testing.T) {
183+
var buf bytes.Buffer
184+
l := NewLogger(WithOutput(&buf), WithLevel(LevelInfo))
185+
186+
child := l.Named("child", WithLevel(LevelDebug))
187+
child.Debug("debug-visible")
188+
189+
m := parseJSONLog(t, &buf)
190+
assert.Equal(t, "DEBUG", m["level"])
191+
}
192+
193+
func TestLogLevelFiltering(t *testing.T) {
194+
tests := []struct {
195+
name string
196+
logLevel Level
197+
logFunc string
198+
shouldLog bool
199+
}{
200+
{"info at info level", LevelInfo, "info", true},
201+
{"debug at info level", LevelInfo, "debug", false},
202+
{"warn at info level", LevelInfo, "warn", true},
203+
{"error at info level", LevelInfo, "error", true},
204+
{"debug at debug level", LevelDebug, "debug", true},
205+
{"info at error level", LevelError, "info", false},
206+
{"warn at error level", LevelError, "warn", false},
207+
}
208+
209+
for _, tt := range tests {
210+
t.Run(tt.name, func(t *testing.T) {
211+
var buf bytes.Buffer
212+
l := NewLogger(WithOutput(&buf), WithLevel(tt.logLevel))
213+
214+
switch tt.logFunc {
215+
case "info":
216+
l.Info("msg")
217+
case "debug":
218+
l.Debug("msg")
219+
case "warn":
220+
l.Warn("msg")
221+
case "error":
222+
l.Error("msg")
223+
}
224+
225+
if tt.shouldLog {
226+
assert.NotEmpty(t, buf.String())
227+
} else {
228+
assert.Empty(t, buf.String())
229+
}
230+
})
231+
}
232+
}
233+
234+
func TestLogWithTraceContext(t *testing.T) {
235+
var buf bytes.Buffer
236+
l := NewLogger(WithOutput(&buf))
237+
238+
traceID, _ := trace.TraceIDFromHex("0102030405060708090a0b0c0d0e0f10")
239+
spanID, _ := trace.SpanIDFromHex("0102030405060708")
240+
sc := trace.NewSpanContext(trace.SpanContextConfig{
241+
TraceID: traceID,
242+
SpanID: spanID,
243+
TraceFlags: trace.FlagsSampled,
244+
})
245+
ctx := trace.ContextWithRemoteSpanContext(context.Background(), sc)
246+
247+
l.InfoCtx(ctx, "traced")
248+
249+
m := parseJSONLog(t, &buf)
250+
assert.Equal(t, "traced", m["msg"])
251+
}
252+
253+
func TestConvenienceMethods(t *testing.T) {
254+
methods := []struct {
255+
name string
256+
level string
257+
call func(l *Logger)
258+
}{
259+
{"Info", "INFO", func(l *Logger) { l.Info("msg") }},
260+
{"Warn", "WARN", func(l *Logger) { l.Warn("msg") }},
261+
{"Error", "ERROR", func(l *Logger) { l.Error("msg") }},
262+
{"Debug", "DEBUG", func(l *Logger) { l.Debug("msg") }},
263+
}
264+
265+
for _, tt := range methods {
266+
t.Run(tt.name, func(t *testing.T) {
267+
var buf bytes.Buffer
268+
l := NewLogger(WithOutput(&buf), WithLevel(LevelDebug))
269+
tt.call(l)
270+
271+
m := parseJSONLog(t, &buf)
272+
assert.Equal(t, tt.level, m["level"])
273+
assert.Equal(t, "msg", m["msg"])
274+
})
275+
}
276+
}
277+
278+
func TestCtxConvenienceMethods(t *testing.T) {
279+
methods := []struct {
280+
name string
281+
level string
282+
call func(l *Logger, ctx context.Context)
283+
}{
284+
{"InfoCtx", "INFO", func(l *Logger, ctx context.Context) { l.InfoCtx(ctx, "msg") }},
285+
{"WarnCtx", "WARN", func(l *Logger, ctx context.Context) { l.WarnCtx(ctx, "msg") }},
286+
{"ErrorCtx", "ERROR", func(l *Logger, ctx context.Context) { l.ErrorCtx(ctx, "msg") }},
287+
{"DebugCtx", "DEBUG", func(l *Logger, ctx context.Context) { l.DebugCtx(ctx, "msg") }},
288+
}
289+
290+
for _, tt := range methods {
291+
t.Run(tt.name, func(t *testing.T) {
292+
var buf bytes.Buffer
293+
l := NewLogger(WithOutput(&buf), WithLevel(LevelDebug))
294+
tt.call(l, context.Background())
295+
296+
m := parseJSONLog(t, &buf)
297+
assert.Equal(t, tt.level, m["level"])
298+
})
299+
}
300+
}
301+
302+
func TestConvenienceMethodsWithAttrs(t *testing.T) {
303+
var buf bytes.Buffer
304+
l := NewLogger(WithOutput(&buf))
305+
306+
l.Info("msg", String("key", "value"), Int("count", 42))
307+
308+
m := parseJSONLog(t, &buf)
309+
assert.Equal(t, "value", m["key"])
310+
assert.Equal(t, float64(42), m["count"])
311+
}
312+
313+
func TestAttrHelpers(t *testing.T) {
314+
now := time.Now()
315+
dur := 5 * time.Second
316+
317+
tests := []struct {
318+
name string
319+
attr Attr
320+
wantKey string
321+
wantKind slog.Kind
322+
}{
323+
{"Any", Any("k", []int{1}), "k", slog.KindAny},
324+
{"Bool", Bool("k", true), "k", slog.KindBool},
325+
{"Duration", Duration("k", dur), "k", slog.KindDuration},
326+
{"Float64", Float64("k", 3.14), "k", slog.KindFloat64},
327+
{"Int", Int("k", 42), "k", slog.KindInt64},
328+
{"Int64", Int64("k", 99), "k", slog.KindInt64},
329+
{"String", String("k", "v"), "k", slog.KindString},
330+
{"Time", Time("k", now), "k", slog.KindTime},
331+
{"Uint64", Uint64("k", 7), "k", slog.KindUint64},
332+
}
333+
334+
for _, tt := range tests {
335+
t.Run(tt.name, func(t *testing.T) {
336+
assert.Equal(t, tt.wantKey, tt.attr.Key)
337+
assert.Equal(t, tt.wantKind, tt.attr.Value.Kind())
338+
})
339+
}
340+
}
341+
342+
func TestErrorAttr(t *testing.T) {
343+
err := errors.New("something broke")
344+
attr := Error(err)
345+
346+
assert.Equal(t, "error", attr.Key)
347+
assert.Equal(t, "something broke", attr.Value.String())
348+
}
349+
350+
func TestLogNameAlwaysPresent(t *testing.T) {
351+
var buf bytes.Buffer
352+
l := NewLogger(WithOutput(&buf))
353+
354+
l.Info("test")
355+
356+
m := parseJSONLog(t, &buf)
357+
_, ok := m["name"]
358+
assert.True(t, ok, "name attribute should always be present")
359+
}
360+
361+
func TestLogWithMultipleAttributes(t *testing.T) {
362+
var buf bytes.Buffer
363+
l := NewLogger(WithOutput(&buf))
364+
365+
l.Log(context.Background(), LevelInfo, "multi",
366+
String("a", "1"),
367+
Int("b", 2),
368+
Bool("c", true),
369+
)
370+
371+
m := parseJSONLog(t, &buf)
372+
assert.Equal(t, "1", m["a"])
373+
assert.Equal(t, float64(2), m["b"])
374+
assert.Equal(t, true, m["c"])
375+
}
376+
377+
func TestErrorAttrFormat(t *testing.T) {
378+
attr := Error(fmt.Errorf("wrapped: %w", errors.New("inner")))
379+
assert.Equal(t, "error", attr.Key)
380+
assert.Contains(t, attr.Value.String(), "wrapped")
381+
assert.Contains(t, attr.Value.String(), "inner")
382+
}

0 commit comments

Comments
 (0)