-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathslog_adapter_test.go
More file actions
200 lines (160 loc) · 5.32 KB
/
slog_adapter_test.go
File metadata and controls
200 lines (160 loc) · 5.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
// Copyright The ActForGood Authors.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://github.com/actforgood/xcache/blob/main/LICENSE.
package xcache_test
import (
"context"
"fmt"
"log/slog"
"sync"
"testing"
"github.com/actforgood/xcache"
)
// MockKeyNotFound is the type of value returned by [LogHandlerMock.ValueAt]
// in case the searched key is not found.
type MockKeyNotFound struct{}
// MockAny can be passed to [logHandlerMock.ValueAt] as call index,
// in case the order is not known/important/multiple logs happen concurrently.
const MockAny uint = 0
// LogHandlerMock is a mock for [slog.Handler],
// which allows us to intercept the log calls and assert on the log content.
type LogHandlerMock struct {
loggedKeyValues [][]slog.Attr
logCallsCnt map[slog.Level]uint32
mu sync.RWMutex
}
// NewLogHandlerMock instantiates a new LogHandlerMock object.
func NewLogHandlerMock() *LogHandlerMock {
return &LogHandlerMock{
logCallsCnt: make(map[slog.Level]uint32, 4),
}
}
func (mock *LogHandlerMock) Handle(_ context.Context, record slog.Record) error {
mock.mu.Lock()
defer mock.mu.Unlock()
lvl := record.Level
mock.logCallsCnt[lvl]++
// default behaviour is to store values in a slice.
// values can be retrieved later with ValueAt.
attrs := make([]slog.Attr, 0, record.NumAttrs()+1)
attrs = append(attrs, slog.Attr{Key: "msg", Value: slog.StringValue(record.Message)})
record.Attrs(func(attr slog.Attr) bool {
attrs = append(attrs, attr)
return true
})
mock.loggedKeyValues = append(mock.loggedKeyValues, attrs)
return nil
}
func (mock *LogHandlerMock) Enabled(_ context.Context, _ slog.Level) bool {
return true
}
func (mock *LogHandlerMock) WithAttrs(_ []slog.Attr) slog.Handler {
return mock
}
func (mock *LogHandlerMock) WithGroup(_ string) slog.Handler {
return mock
}
// ValueAt returns the value for a key at given call, in case no callback was set.
// Calls are positive numbers (starting with 1).
// If the order of the calls is not known/important/multiple logs happen concurrently,
// you can use [MockAtAnyCall].
// If the key is not found, [MockKeyNotFound] is returned.
func (mock *LogHandlerMock) ValueAt(callNo uint, forKey string) any {
mock.mu.RLock()
defer mock.mu.RUnlock()
if callNo == MockAny {
for call := range len(mock.loggedKeyValues) {
value := mock.valueAt(call+1, forKey)
if _, isNotFound := value.(MockKeyNotFound); !isNotFound {
return value
}
}
return MockKeyNotFound{}
}
return mock.valueAt(int(callNo), forKey)
}
func (mock *LogHandlerMock) valueAt(callNo int, forKey any) any {
if len(mock.loggedKeyValues) >= callNo {
for _, attr := range mock.loggedKeyValues[callNo-1] {
if attr.Key == forKey {
return attr.Value.Any()
}
}
}
return MockKeyNotFound{}
}
// LogCallsCount returns the no. of times Critical/Error/Warn/Info/Debug/Log was called.
// Differentiate methods calls count by passing appropriate level.
func (mock *LogHandlerMock) LogCallsCount(lvl slog.Level) int {
mock.mu.RLock()
defer mock.mu.RUnlock()
return int(mock.logCallsCnt[lvl])
}
// LogHandlerNop is a no-op implementation of [slog.Handler], which does nothing.
type LogHandlerNop struct{}
// NewLogHandlerNop instantiates a new LogHandlerNop object.
func NewLogHandlerNop() LogHandlerNop {
return LogHandlerNop{}
}
func (mock LogHandlerNop) Handle(_ context.Context, _ slog.Record) error {
return nil
}
func (mock LogHandlerNop) Enabled(_ context.Context, _ slog.Level) bool {
return false // disable loggin
}
func (mock LogHandlerNop) WithAttrs(_ []slog.Attr) slog.Handler {
return mock
}
func (mock LogHandlerNop) WithGroup(_ string) slog.Handler {
return mock
}
func TestRedisSLogger(t *testing.T) {
t.Parallel()
t.Run("error message", testRedisSLoggerByLevel(slog.LevelError))
t.Run("info message", testRedisSLoggerByLevel(slog.LevelInfo))
}
func testRedisSLoggerByLevel(lvl slog.Level) func(t *testing.T) {
return func(t *testing.T) {
t.Parallel()
// arrange
var (
logHandlerMock = NewLogHandlerMock()
logger = slog.New(logHandlerMock)
subject = xcache.NewRedisSLogger(logger)
ctx = context.Background()
expectedFormat = map[slog.Level]string{
slog.LevelInfo: "some redis message about master=%q",
slog.LevelError: "some redis message about master=%q failed due some err",
}
masterName = "testMaster"
expectedMsg = fmt.Sprintf(expectedFormat[lvl], masterName)
)
// act
subject.Printf(ctx, expectedFormat[lvl], masterName)
// assert
if assertEqual(t, 1, logHandlerMock.LogCallsCount(lvl)) {
assertEqual(t, expectedMsg, logHandlerMock.ValueAt(1, "msg"))
assertEqual(t, "redis", logHandlerMock.ValueAt(1, "pkg"))
}
}
}
func ExampleRedisSLogger() {
// somewhere in your bootstrap process...
// initialize an slog.Logger, here we use the default one...
logger := slog.Default()
// set the slog.Logger Redis adapter
xcache.SetRedis7Logger(xcache.NewRedisSLogger(logger))
}
func BenchmarkRedisSLogger(b *testing.B) {
logger := slog.New(LogHandlerNop{})
redisLogger := xcache.NewRedisSLogger(logger)
message := "some redis message about master=%q failed due some err"
masterName := "benchLoggerMaster"
ctx := context.Background()
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
redisLogger.Printf(ctx, message, masterName)
}
}