forked from grailbio/base
/
logger.go
407 lines (364 loc) · 15.8 KB
/
logger.go
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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
package log
import (
"context"
"fmt"
"os"
"runtime"
"strings"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
v23 "v.io/v23"
vcontext "v.io/v23/context"
)
const (
// DebugLevel logs are typically voluminous.
DebugLevel = zapcore.DebugLevel
// InfoLevel is the default logging priority.
InfoLevel = zapcore.InfoLevel
// WarnLevel logs are more important than Info, but don't need individual human review.
WarnLevel = zapcore.WarnLevel
// ErrorLevel logs are high-priority.
// Applications running smoothly shouldn't generate any error-level logs.
ErrorLevel = zapcore.ErrorLevel
// FatalLevel logs a message, then calls os.Exit(1).
FatalLevel = zapcore.FatalLevel
// RFC3339TrailingNano is RFC3339 format with trailing nanoseconds precision.
RFC3339TrailingNano = "2006-01-02T15:04:05.000000000Z07:00"
// LOG_LEVEL_ENV_VAR is the environment variable name used to set logging level.
LOG_LEVEL_ENV_VAR = "LOG_LEVEL"
)
// contextFields is a list of context key-value pairs to be logged.
// Key is the name of the field.
// Value is the context key.
var contextFields = map[string]interface{}{
"requestID": RequestIDContextKey,
}
var logLvls = map[string]zapcore.Level{
"debug": DebugLevel,
"DEBUG": DebugLevel,
"info": InfoLevel,
"INFO": InfoLevel,
"warn": WarnLevel,
"WARN": WarnLevel,
"error": ErrorLevel,
"ERROR": ErrorLevel,
"fatal": FatalLevel,
"FATAL": FatalLevel,
}
type Logger struct {
coreLogger *zap.SugaredLogger
// Additional information that may be unique to each service (e.g. order UUID for Ensemble orders)
defaultFields []interface{}
levelToLogger map[zapcore.Level]func(msg string, keysAndValues ...interface{})
now func() time.Time
}
type Config struct {
OutputPaths []string
// note: setting the environment variable LOG_LEVEL will override Config.Level
Level zapcore.Level
}
func setDefaultLogLevelsMap(logger *Logger) *Logger {
logger.levelToLogger = map[zapcore.Level]func(msg string, keysAndValues ...interface{}){
DebugLevel: logger.coreLogger.Debugw,
InfoLevel: logger.coreLogger.Infow,
WarnLevel: logger.coreLogger.Warnw,
ErrorLevel: logger.coreLogger.Errorw,
FatalLevel: logger.coreLogger.Fatalw,
}
return logger
}
func NewLogger(config Config) *Logger {
return NewLoggerWithDefaultFields(config, []interface{}{})
}
// NewLogger creates a new logger instance.
// defaultFields is a list of key-value pairs to be included in every log message.
func NewLoggerWithDefaultFields(config Config, defaultFields []interface{}) *Logger {
if len(defaultFields)%2 != 0 {
danglingKey := defaultFields[len(defaultFields)-1]
defaultFields = defaultFields[:len(defaultFields)-1]
errLogger := NewLogger(config)
errLog := []interface{}{
"ignored", danglingKey,
}
logErr := errLogger.levelToLogger[ErrorLevel]
logErr("defaultFields contains a key without a value.", errLog...)
}
l := Logger{
coreLogger: mustBuildLogger(config, zap.AddCallerSkip(2)),
defaultFields: defaultFields,
now: time.Now,
}
return setDefaultLogLevelsMap(&l)
}
// NewLoggerFromCore allows the caller to pass in a zap.SugaredLogger into the logger.
// This allows one to make unit test assertions about logs.
func NewLoggerFromCore(lager *zap.SugaredLogger) *Logger {
l := Logger{
coreLogger: lager,
now: time.Now,
}
return setDefaultLogLevelsMap(&l)
}
func (l *Logger) log(ctx context.Context, level zapcore.Level, callerSkip int, msg string, keysAndValues []interface{}) {
t := l.now()
// Add default fields
keysAndValues = append(keysAndValues, l.defaultFields...)
// If there is a dangling key (i.e. odd length keysAndValues), log an error and then
// drop the dangling key and log original message.
if len(keysAndValues)%2 != 0 {
danglingKey := keysAndValues[len(keysAndValues)-1]
keysAndValues = keysAndValues[:len(keysAndValues)-1]
errLog := withDefaultFields(ctx, callerSkip, t, "ignored", danglingKey)
logErr := l.levelToLogger[ErrorLevel]
logErr("Ignored key without a value.", errLog...)
}
// Add caller and timestamp fields
prefix := withDefaultFields(ctx, callerSkip, t)
// Add context logged fields
if ctx != nil {
for k, v := range contextFields {
if ctxVal := ctx.Value(v); ctxVal != nil {
prefix = append(prefix, k, ctxVal)
}
}
}
keysAndValues = append(prefix, keysAndValues...)
// Log at the appropriate level
logLevel := l.levelToLogger[level]
logLevel(msg, keysAndValues...)
}
// Debug logs a message, the key-value pairs defined in contextFields from ctx, and variadic key-value pairs.
// If ctx is nil, all fields from contextFields will be omitted.
// If ctx does not contain a key in contextFields, that field will be omitted.
func (l *Logger) Debug(ctx context.Context, msg string, keysAndValues ...interface{}) {
l.Debugv(ctx, 1, msg, keysAndValues...)
}
// Debugf uses fmt.Sprintf to log a templated message and the key-value pairs defined in contextFields from ctx.
// If ctx is nil, all fields from contextFields will be omitted.
// If ctx does not contain a key in contextFields, that field will be omitted.
func (l *Logger) Debugf(ctx context.Context, fs string, args ...interface{}) {
l.Debugv(ctx, 1, fmt.Sprintf(fs, args...))
}
// Debugv logs a message, the key-value pairs defined in contextFields from ctx, and variadic key-value pairs.
// Caller stack field is skipped by skip levels.
// If ctx is nil, all fields from contextFields will be omitted.
// If ctx does not contain a key in contextFields, that field will be omitted.
func (l *Logger) Debugv(ctx context.Context, skip int, msg string, keysAndValues ...interface{}) {
l.log(ctx, DebugLevel, skip, msg, keysAndValues)
}
// DebugNoCtx logs a message and variadic key-value pairs.
func (l *Logger) DebugNoCtx(msg string, keysAndValues ...interface{}) {
l.Debugv(context.Background(), 1, msg, keysAndValues...)
}
// Info logs a message, the key-value pairs defined in contextFields from ctx, and variadic key-value pairs.
// If ctx is nil, all fields from contextFields will be omitted.
// If ctx does not contain a key in contextFields, that field will be omitted.
func (l *Logger) Info(ctx context.Context, msg string, keysAndValues ...interface{}) {
l.Infov(ctx, 1, msg, keysAndValues...)
}
// Infof uses fmt.Sprintf to log a templated message and the key-value pairs defined in contextFields from ctx.
// If ctx is nil, all fields from contextFields will be omitted.
// If ctx does not contain a key in contextFields, that field will be omitted.
func (l *Logger) Infof(ctx context.Context, fs string, args ...interface{}) {
l.Infov(ctx, 1, fmt.Sprintf(fs, args...))
}
// Infov logs a message, the key-value pairs defined in contextFields from ctx, and variadic key-value pairs.
// Caller stack field is skipped by skip levels.
// If ctx is nil, all fields from contextFields will be omitted.
// If ctx does not contain a key in contextFields, that field will be omitted.
func (l *Logger) Infov(ctx context.Context, skip int, msg string, keysAndValues ...interface{}) {
l.log(ctx, InfoLevel, skip, msg, keysAndValues)
}
// InfoNoCtx logs a message and variadic key-value pairs.
func (l *Logger) InfoNoCtx(msg string, keysAndValues ...interface{}) {
l.Infov(context.Background(), 1, msg, keysAndValues...)
}
// Warn logs a message, the key-value pairs defined in contextFields from ctx, and variadic key-value pairs.
// If ctx is nil, all fields from contextFields will be omitted.
// If ctx does not contain a key in contextFields, that field will be omitted.
func (l *Logger) Warn(ctx context.Context, msg string, keysAndValues ...interface{}) {
l.Warnv(ctx, 1, msg, keysAndValues...)
}
// Warnf uses fmt.Sprintf to log a templated message and the key-value pairs defined in contextFields from ctx.
// If ctx is nil, all fields from contextFields will be omitted.
// If ctx does not contain a key in contextFields, that field will be omitted.
func (l *Logger) Warnf(ctx context.Context, fs string, args ...interface{}) {
l.Warnv(ctx, 1, fmt.Sprintf(fs, args...))
}
// Warnv logs a message, the key-value pairs defined in contextFields from ctx, and variadic key-value pairs.
// Caller stack field is skipped by skip levels.
// If ctx is nil, all fields from contextFields will be omitted.
// If ctx does not contain a key in contextFields, that field will be omitted.
func (l *Logger) Warnv(ctx context.Context, skip int, msg string, keysAndValues ...interface{}) {
l.log(ctx, WarnLevel, skip, msg, keysAndValues)
}
// WarnNoCtx logs a message and variadic key-value pairs.
func (l *Logger) WarnNoCtx(msg string, keysAndValues ...interface{}) {
l.Warnv(context.Background(), 1, msg, keysAndValues...)
}
// Fatal logs a message, the key-value pairs defined in contextFields from ctx, and variadic key-value pairs.
// If ctx is nil, all fields from contextFields will be omitted.
// If ctx does not contain a key in contextFields, that field will be omitted.
func (l *Logger) Fatal(ctx context.Context, msg string, keysAndValues ...interface{}) {
l.Fatalv(ctx, 1, msg, keysAndValues...)
}
// Fatalf uses fmt.Sprintf to log a templated message and the key-value pairs defined in contextFields from ctx.
// If ctx is nil, all fields from contextFields will be omitted.
// If ctx does not contain a key in contextFields, that field will be omitted.
func (l *Logger) Fatalf(ctx context.Context, fs string, args ...interface{}) {
l.Fatalv(ctx, 1, fmt.Sprintf(fs, args...))
}
// Fatalv logs a message, the key-value pairs defined in contextFields from ctx, and variadic key-value pairs.
// Caller stack field is skipped by skip levels.
// If ctx is nil, all fields from contextFields will be omitted.
// If ctx does not contain a key in contextFields, that field will be omitted.
func (l *Logger) Fatalv(ctx context.Context, skip int, msg string, keysAndValues ...interface{}) {
l.log(ctx, FatalLevel, skip, msg, keysAndValues)
}
// FatalNoCtx logs a message and variadic key-value pairs.
func (l *Logger) FatalNoCtx(msg string, keysAndValues ...interface{}) {
l.Fatalv(context.Background(), 1, msg, keysAndValues...)
}
// Error logs a message, the key-value pairs defined in contextFields from ctx, and variadic key-value pairs.
// If ctx is nil, all fields from contextFields will be omitted.
// If ctx does not contain a key in contextFields, that field will be omitted.
func (l *Logger) Error(ctx context.Context, msg string, keysAndValues ...interface{}) {
l.Errorv(ctx, 1, msg, keysAndValues...)
}
// Errorf uses fmt.Sprintf to log a templated message and the key-value pairs defined in contextFields from ctx.
// If ctx is nil, all fields from contextFields will be omitted.
// If ctx does not contain a key in contextFields, that field will be omitted.
func (l *Logger) Errorf(ctx context.Context, fs string, args ...interface{}) {
l.Errorv(ctx, 1, fmt.Sprintf(fs, args...))
}
// Errorv logs a message, the key-value pairs defined in contextFields from ctx, and variadic key-value pairs.
// Caller stack field is skipped by skip levels.
// If ctx is nil, all fields from contextFields will be omitted.
// If ctx does not contain a key in contextFields, that field will be omitted.
func (l *Logger) Errorv(ctx context.Context, skip int, msg string, keysAndValues ...interface{}) {
l.log(ctx, ErrorLevel, skip, msg, keysAndValues)
}
// ErrorNoCtx logs a message and variadic key-value pairs.
func (l *Logger) ErrorNoCtx(msg string, keysAndValues ...interface{}) {
l.Errorv(context.Background(), 1, msg, keysAndValues...)
}
// ErrorAndReturn logs a message, the key-value pairs defined in contextFields from ctx, and variadic key-value pairs.
// If ctx is nil, all fields from contextFields will be omitted.
// If ctx does not contain a key in contextFields, that field will be omitted.
// Returns a new error constructed from the message.
func (l *Logger) ErrorAndReturn(ctx context.Context, msg string, keysAndValues ...interface{}) error {
return l.ErrorvAndReturn(ctx, 1, msg, keysAndValues...)
}
// ErrorfAndReturn uses fmt.Errorf to construct an error from the provided arguments.
// It then logs the error message, along with data from the context.
// If ctx is nil, all fields from contextFields will be omitted.
// If ctx does not contain a key in contextFields, that field will be omitted.
// Returns the error resulting from invoking fmt.Errorf with the provided arguments.
func (l *Logger) ErrorfAndReturn(ctx context.Context, fs string, args ...interface{}) error {
err := fmt.Errorf(fs, args...)
l.Errorv(ctx, 1, err.Error())
return err
}
// Errorv logs a message, the key-value pairs defined in contextFields from ctx, and variadic key-value pairs.
// Caller is skipped by skip.
// If ctx is nil, all fields from contextFields will be omitted.
// If ctx does not contain a key in contextFields, that field will be omitted.
// Returns a new error constructed from the message.
func (l *Logger) ErrorvAndReturn(ctx context.Context, skip int, msg string, keysAndValues ...interface{}) error {
l.Errorv(ctx, skip+1, msg, keysAndValues...)
return fmt.Errorf(msg)
}
// ErrorNoCtxAndReturn logs a message and variadic key-value pairs.
// Returns a new error constructed from the message.
func (l *Logger) ErrorNoCtxAndReturn(msg string, keysAndValues ...interface{}) error {
// context.Background() is a singleton and gets initialized once
return l.ErrorvAndReturn(context.Background(), 1, msg, keysAndValues...)
}
// rfc3339TrailingNanoTimeEncoder serializes a time.Time to an RFC3339-formatted string
// with trailing nanosecond precision.
func rfc3339TrailingNanoTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Format(RFC3339TrailingNano))
}
func mustBuildLogger(config Config, opts ...zap.Option) *zap.SugaredLogger {
zapLogger, err := newConfig(config).Build(opts...)
if err != nil {
panic(err)
}
return zapLogger.Sugar()
}
// newEncoderConfig is similar to Zap's NewProductionConfig with a few modifications
// to better fit our needs.
func newEncoderConfig() zapcore.EncoderConfig {
return zapcore.EncoderConfig{
LevelKey: "level",
NameKey: "logger",
MessageKey: "msg",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: rfc3339TrailingNanoTimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}
}
// newConfig is similar to Zap's NewProductionConfig with a few modifications
// to better fit our needs.
func newConfig(override Config) zap.Config {
// Default config
config := zap.Config{
Level: zap.NewAtomicLevelAt(zap.DebugLevel),
Development: false,
Sampling: &zap.SamplingConfig{
Initial: 100,
Thereafter: 100,
},
Encoding: "json",
EncoderConfig: newEncoderConfig(),
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}
// config overrides
if override.OutputPaths != nil {
config.OutputPaths = override.OutputPaths
}
if override.Level != zapcore.DebugLevel {
config.Level = zap.NewAtomicLevelAt(override.Level)
}
// LOG_LEVEL environment variable override
// Note: setting the environment variable LOG_LEVEL will override Config.Level
if logLvl, ok := logLvls[os.Getenv(LOG_LEVEL_ENV_VAR)]; ok {
config.Level = zap.NewAtomicLevelAt(logLvl)
}
return config
}
func withDefaultFields(ctx context.Context, callerSkip int, t time.Time,
keysAndValues ...interface{}) []interface{} {
defaultFields := []interface{}{
"caller", getCaller(callerSkip),
"ts", t,
}
if ctx != nil {
if vctx, ok := ctx.(*vcontext.T); ok {
if requestID := v23.GetRequestID(vctx); requestID != uuid.Nil {
defaultFields = append(defaultFields, "v23RequestID", requestID)
}
}
}
return append(defaultFields, keysAndValues...)
}
func getCaller(skip int) string {
skipOffset := 5
pc := make([]uintptr, 1)
numFrames := runtime.Callers(skip+skipOffset, pc)
if numFrames < 1 {
return ""
}
frame, _ := runtime.CallersFrames(pc).Next()
if frame.PC == 0 {
return ""
}
parts := strings.Split(frame.File, "/")
file := parts[len(parts)-1]
return fmt.Sprintf("%s:%d", file, frame.Line)
}