Repository navigation
Expand file tree
/
Copy pathtestlogger.go
More file actions
166 lines (138 loc) · 5 KB
/
Copy pathtestlogger.go
File metadata and controls
166 lines (138 loc) · 5 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
package logging
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest/observer"
)
// Record is a log record captured by TestLogger.
type Record struct {
// Level is the logging level of the record.
Level Level
// Message is the record message.
Message string
// Logger is the name of the named logger, empty for the unnamed one.
Logger string
// Caller is the call site ("file.go:42"), empty if the caller is not detected.
Caller string
// Fields are the fields added with WithKey, WithKeys, WithError and context extractors.
// Values are as zap stores them: integers are int64, unsigned integers uint64, floats float64,
// strings and bools as is. An error is stored under "error" as its text.
Fields map[string]any
}
// TestLogger is a Logger recording everything it logs into memory. It is meant for tests of the code
// which takes a Logger: pass it as a dependency and check Records. It does not touch the active backend
// of the package and writes nothing anywhere. It is safe for concurrent use.
//
// Fatal records the message and panics with it instead of exiting the process.
type TestLogger struct {
Logger
zl *zapLogger // the same as Logger, to reach optional interfaces
backend *Backend
logs *observer.ObservedLogs
}
// NewTestLogger returns a TestLogger recording records of all levels up from LevelTrace.
// Options work as in Init: WithLevel restricts the recorded levels, WithContextExtractors makes
// WithContext work, WithConfig brings CustomLevels. Output, format and endpoint settings are ignored.
func NewTestLogger(opts ...Option) *TestLogger {
config := NewConfig()
config.Level = LevelTrace
config.Apply(opts...)
core, logs := observer.New(zapTraceLevel)
backend := &Backend{st: newState(config.clone(), core, zap.WithFatalHook(zapcore.WriteThenPanic))}
logger := backend.newLogger()
return &TestLogger{Logger: logger, zl: logger, backend: backend, logs: logs}
}
var (
_ Panicer = (*TestLogger)(nil)
_ Namer = (*TestLogger)(nil)
)
// Panic records the message and panics with it, see Panicer.
func (tl *TestLogger) Panic(args ...interface{}) { tl.zl.Panic(args...) }
// Panicf records the formatted message and panics with it, see Panicer.
func (tl *TestLogger) Panicf(format string, args ...interface{}) {
tl.zl.Panicf(format, args...)
}
// WithName returns a named child logger whose records are captured too, see Namer.
func (tl *TestLogger) WithName(name string) Logger { return tl.zl.WithName(name) }
// Backend returns the backend of the test logger. Use it to make named and context loggers
// whose records are captured too: tl.Backend().NewNamedLogger("db").
func (tl *TestLogger) Backend() *Backend {
return tl.backend
}
// Records returns a copy of the records captured so far, oldest first.
func (tl *TestLogger) Records() []Record {
return tl.Match(func(Record) bool { return true })
}
// ByLevel returns the records of the level.
func (tl *TestLogger) ByLevel(level Level) []Record {
return tl.Match(func(rec Record) bool { return rec.Level == level })
}
// Match returns the records for which the function reports true.
func (tl *TestLogger) Match(match func(Record) bool) []Record {
var res []Record
for _, entry := range tl.logs.All() {
rec := newRecord(entry)
if match(rec) {
res = append(res, rec)
}
}
return res
}
// Reset forgets the captured records.
func (tl *TestLogger) Reset() {
tl.logs.TakeAll()
}
func newRecord(entry observer.LoggedEntry) Record {
rec := Record{
Level: fromZapLevel(entry.Level),
Message: entry.Message,
Logger: entry.LoggerName,
Fields: entry.ContextMap(),
}
if entry.Caller.Defined {
rec.Caller = entry.Caller.TrimmedPath()
}
return rec
}
// fromZapLevel is the reverse of the level mapping done by zapLogger.
func fromZapLevel(level zapcore.Level) Level {
switch {
case level < zapcore.DebugLevel:
return LevelTrace
case level == zapcore.DebugLevel:
return LevelDebug
case level == zapcore.InfoLevel:
return LevelInfo
case level == zapcore.WarnLevel:
return LevelWarn
case level == zapcore.ErrorLevel:
return LevelError
case level == zapcore.PanicLevel:
return LevelPanic
default:
return LevelFatal
}
}
// Cleaner is implemented by *testing.T, *testing.B and testing.TB.
type Cleaner interface {
Cleanup(func())
}
// UseTestLogger makes a TestLogger and installs its backend as the active one, so loggers made with
// package functions (NewLogger, NewNamedLogger, Default and others) are captured too.
// The previous backend and configuration are restored when the test is done.
//
// NOTE: the active backend is global, so tests using UseTestLogger must not run in parallel.
// Loggers made before the call keep using the previous backend.
func UseTestLogger(t Cleaner, opts ...Option) *TestLogger {
tl := NewTestLogger(opts...)
mu.Lock()
previous, previousConfig := usingBackend, useConfig
usingBackend, useConfig = tl.backend, &tl.backend.st.config
mu.Unlock()
t.Cleanup(func() {
mu.Lock()
usingBackend, useConfig = previous, previousConfig
mu.Unlock()
})
return tl
}