/
config.go
444 lines (369 loc) · 15.5 KB
/
config.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
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
package sallust
import (
"io/fs"
"net/url"
"os"
"path/filepath"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
const (
// DefaultMessageKey is the default value for EncoderConfig.MessageKey. This value
// is not used if EncoderConfig.DisableDefaultKeys is true.
DefaultMessageKey = "msg"
// DefaultLevelKey is the default value for EncoderConfig.LevelKey. This value
// is not used if EncoderConfig.DisableDefaultKeys is true.
DefaultLevelKey = "level"
// DefaultTimeKey is the default value for EncoderConfig.TimeKey. This value
// is not used if EncoderConfig.DisableDefaultKeys is true.
DefaultTimeKey = "ts"
// DefaultNameKey is the default value for EncoderConfig.NameKey. This value
// is not used if EncoderConfig.DisableDefaultKeys is true.
DefaultNameKey = "name"
// Stdout is the reserved zap output path name that corresponds to stdout.
Stdout = "stdout"
// Stderr is the reserved zap output path name that corresponds to stderr.
Stderr = "stderr"
)
// EncoderConfig is an analog to zap.EncoderConfig. This type is friendlier
// to unmarshaling from maps, as encoding.TextUnmarshaler is not honored in those cases.
//
// See: https://pkg.go.dev/go.uber.org/zap/zapcore#EncoderConfig
type EncoderConfig struct {
// DisableDefaultKeys disables the convenience defaulting of certain log keys.
// Useful when you want to turn off one of those keys, but explicitly set the others.
DisableDefaultKeys bool `json:"disableDefaultKeys" yaml:"disableDefaultKeys"`
// MessageKey is the logging key for the log message. If unset and if DisableDefaultKeys is true,
// messages are not inserted into log output.
MessageKey string `json:"messageKey" yaml:"messageKey"`
// LevelKey is the logging key for the log level. If unset and if DisableDefaultKeys is true,
// log levels are not inserted into log output.
LevelKey string `json:"levelKey" yaml:"levelKey"`
// TimeKey is the logging key for the log timestamp. If unset and if DisableDefaultKeys is true,
// timestamps are not inserted into log output.
TimeKey string `json:"timeKey" yaml:"timeKey"`
// NameKey is the logging key for the logger name. If unset and if DisableDefaultKeys is true,
// logger names are not inserted into log output.
NameKey string `json:"nameKey" yaml:"nameKey"`
// CallerKey is the logging key for the caller of the logging method. If unset, callers are not
// inserted into log output.
//
// Note that Config.DisableCaller, if set, will also prevent callers in each log record.
// This difference is that Config.DisableCaller shuts off the code that determines the caller,
// while this field simply doesn't output the caller even though it may have been computed.
CallerKey string `json:"callerKey" yaml:"callerKey"`
// FunctionKey is the logging key for the function which called the logging method. If unset, functions are not
// inserted into log output.
//
// As with CallerKey, Config.DisableCaller also affects whether functions are output.
FunctionKey string `json:"functionKey" yaml:"functionKey"`
// StacktraceKey is the logging key for stacktraces for warn, error, and panics. If unset,
// stacktraces are never produced.
StacktraceKey string `json:"stacktraceKey" yaml:"stacktraceKey"`
// LineEnding is the US-ASCII string that terminates each log record. By default,
// a single '\n' is used.
LineEnding string `json:"lineEnding" yaml:"lineEnding"`
// EncodeLevel determines how levels are represented. If unset, LowercaseLevelEncoder is used.
//
// See: https://pkg.go.dev/go.uber.org/zap/zapcore#LowercaseLevelEncoder
EncodeLevel string `json:"levelEncoder" yaml:"levelEncoder" mapstructure:"levelEncoder"`
// EncodeTime determines how timestamps are represented. If unset, RFC3339TimeEncoder is used.
//
// See: https://pkg.go.dev/go.uber.org/zap/zapcore#RFC3339TimeEncoder
EncodeTime string `json:"timeEncoder" yaml:"timeEncoder" mapstructure:"timeEncoder"`
// EncodeDuration determines how time durations are represented. If unset,
// StringDurationEncoder is used.
//
// See: https://pkg.go.dev/go.uber.org/zap/zapcore#StringDurationEncoder
EncodeDuration string `json:"durationEncoder" yaml:"durationEncoder" mapstructure:"durationEncoder"`
// EncodeCaller determines how callers are represented. If unset,
// FullCallerEncoder is used.
//
// See: https://pkg.go.dev/go.uber.org/zap/zapcore#FullCallerEncoder
EncodeCaller string `json:"callerEncoder" yaml:"callerEncoder" mapstructure:"callerEncoder"`
// EncodeName determines how logger names are represented. If unset,
// FullNameEncoder is used.
//
// See: https://pkg.go.dev/go.uber.org/zap/zapcore#FullNameEncoder
EncodeName string `json:"nameEncoder" yaml:"nameEncoder" mapstructure:"nameEncoder"`
// Configures the field separator used by the console encoder. Defaults
// to tab.
ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}
func applyEncoderConfigDefaults(zec *zapcore.EncoderConfig) {
if len(zec.MessageKey) == 0 {
zec.MessageKey = "msg"
}
if len(zec.LevelKey) == 0 {
zec.LevelKey = "level"
}
if len(zec.TimeKey) == 0 {
zec.TimeKey = "ts"
}
if len(zec.NameKey) == 0 {
zec.NameKey = "name"
}
}
// NewZapcoreEncoderConfig converts this instance into a zapcore.EncoderConfig.
//
// In order to ease configuration of zap, this method implements a few conveniences
// on the returned zapcore.EncoderConfig:
//
// (1) Each of the EncodeXXX fields is defaulted to a sane value. Leaving them unset
// does not raise an error.
//
// (2) Several logging key fields are defaulted. This defaulting can be turned off by
// setting DisableDefaultKeys to true. The fields that are defaulted are: MessageKey,
// LevelKey, TimeKey, and NameKey. The other logging key fields, such as CallerKey,
// are not defaulted. It's common to leave them turned off for performance or preference.
//
// This method returns an error, as the various UnmarshalText methods it uses return errors.
// However, in actual practice this method returns a nil error since zapcore falls back to
// known defaults for any unrecognized text. This method still checks errors internally,
// in case zapcore changes in the future.
func (ec EncoderConfig) NewZapcoreEncoderConfig() (zec zapcore.EncoderConfig, err error) {
zec = zapcore.EncoderConfig{
MessageKey: ec.MessageKey,
LevelKey: ec.LevelKey,
TimeKey: ec.TimeKey,
NameKey: ec.NameKey,
CallerKey: ec.CallerKey,
FunctionKey: ec.FunctionKey,
StacktraceKey: ec.StacktraceKey,
LineEnding: ec.LineEnding,
ConsoleSeparator: ec.ConsoleSeparator,
}
if !ec.DisableDefaultKeys {
applyEncoderConfigDefaults(&zec)
}
if len(ec.EncodeLevel) > 0 {
err = zec.EncodeLevel.UnmarshalText([]byte(ec.EncodeLevel))
} else {
zec.EncodeLevel = zapcore.LowercaseLevelEncoder
}
if err == nil {
if len(ec.EncodeTime) > 0 {
err = zec.EncodeTime.UnmarshalText([]byte(ec.EncodeTime))
} else {
zec.EncodeTime = zapcore.RFC3339TimeEncoder
}
}
if err == nil {
if len(ec.EncodeDuration) > 0 {
err = zec.EncodeDuration.UnmarshalText([]byte(ec.EncodeDuration))
} else {
zec.EncodeDuration = zapcore.StringDurationEncoder
}
}
if err == nil {
if len(ec.EncodeCaller) > 0 {
err = zec.EncodeCaller.UnmarshalText([]byte(ec.EncodeCaller))
} else {
zec.EncodeCaller = zapcore.FullCallerEncoder
}
}
if err == nil {
if len(ec.EncodeName) > 0 {
err = zec.EncodeName.UnmarshalText([]byte(ec.EncodeName))
} else {
zec.EncodeName = zapcore.FullNameEncoder
}
}
return
}
// Config describes the set of options for building a single zap.Logger. Most of these
// fields correspond with zap.Config. Use of this type is optional. It simply provides
// easier configuration for certain features like log rotation. This type is also easier
// to use with libraries like spf13/viper, which unmarshal from a map[string]interface{}
// instead of directly from a file.
//
// A Config instance is converted to a zap.Config by applying certain features,
// such as log rotation. Ultimately, zap.Config.Build is used to actually construct
// the logger.
//
// See: https://pkg.go.dev/go.uber.org/zap?tab=doc#Config.Build
type Config struct {
// Level is the log level, which is converted to a zap.AtomicLevel. If unset,
// info level is assumed.
Level string `json:"level" yaml:"level"`
// Development corresponds to zap.Config.Development
Development bool `json:"development" yaml:"development"`
// DisableCaller corresponds to zap.Config.DisableCaller
DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
// DisableStacktrace corresponds to zap.Config.DisableStacktrace
DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
// Sampling corresponds to zap.Config.Sampling. No custom type is necessary
// here because zap.SamplingConfig uses primitive types.
Sampling *zap.SamplingConfig `json:"samplingConfig" yaml:"samplingConfig"`
// Encoding corresponds to zap.Config.Encoding. If this is unset, and if Development
// is false, "json" is used. "console" is the other built-in value for this field,
// and other encodings can be registered via the zap package.
//
// See: https://pkg.go.dev/go.uber.org/zap#RegisterEncoder
Encoding string `json:"encoding" yaml:"encoding"`
// EncoderConfig corresponds to zap.Config.EncoderConfig. A custom type is used
// here to make integration with libraries like spf13/viper much easier.
EncoderConfig EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
// OutputPaths are the set of sinks for log output. This field corresponds to
// zap.Config.OutputPaths. If unset, all logging output is discarded. There is
// no default for this field.
//
// Each output path will have environment variable references expanded unless
// DisablePathExpansion is true.
//
// If Rotation is set, then each output path that is a system file will undergo
// log file rotation.
OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
// ErrorOutputPaths are the set of sinks for zap's internal messages. This field
// corresponds to zap.Config.ErrorOutputPaths. If unset, Stderr is assumed.
//
// As with OutputPaths, environment variable references in each path are expanded
// unless DisablePathExpansion is true.
//
// If Rotation is set, then each output path that is a system file will undergo
// log file rota
ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
// InitialFields corresponds to zap.Config.InitialFields. Note that when unmarshaling
// from spf13/viper, all keys in this map will be lowercased.
//
// Any fields set here will be set on all loggers derived from this configuration.
InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
// DisablePathExpansion controls whether the paths in OutputPaths and ErrorOutputPaths
// are expanded. If this field is set to true, Mapping is ignored and no
// expansion, even with environment variables, is performed.
DisablePathExpansion bool `json:"disablePathExpansion" yaml:"disablePathExpansion"`
// Permissions is the optional nix-style file permissions to use when creating log files.
// If supplied, this value must be parseable via ParsePermissions. If this field is unset,
// zap and lumberjack will control what permissions new log files have.
Permissions string `json:"permissions" yaml:"permissions"`
// Mapping is an optional strategy for expanding variables in output paths.
// If not supplied, os.Getenv is used.
Mapping func(string) string `json:"-" yaml:"-"`
// Rotation describes the set of log file rotation options. This field is optional,
// and if unset log files are not rotated.
Rotation *Rotation `json:"rotation,omitempty" yaml:"rotation,omitempty"`
}
func applyConfigDefaults(zc *zap.Config) {
if len(zc.Encoding) == 0 {
zc.Encoding = "json"
}
if zc.Development && len(zc.OutputPaths) == 0 {
// NOTE: difference from zap ... in development they send output to stderr
zc.OutputPaths = []string{Stdout}
}
if len(zc.ErrorOutputPaths) == 0 {
zc.ErrorOutputPaths = []string{Stderr}
}
// NOTE: can't compare the Level with nil very easily, so just
// unconditionally set this default. It will be overwritten
// by decoding code if appropriate.
zc.Level = zap.NewAtomicLevelAt(zapcore.InfoLevel)
}
// ensureExists makes sure the given path exists with the specified permissions.
// If the path has already been created or if perms is 0, this function won't do anything.
//
// The path is treated as a URI in a similar fashion to zap.Open.
func ensureExists(path string, perms fs.FileMode) (err error) {
if perms == 0 {
return
}
var f *os.File
defer func() {
if f != nil {
f.Close()
}
}()
switch {
case path == Stdout:
fallthrough
case path == Stderr:
break
// Windows hack: filepath.Abs will return false outside of Windows
// for many paths. This just makes sure we don't have to do a bunch
// of platform-specific nonsense.
case filepath.IsAbs(path):
f, err = os.OpenFile(path, os.O_CREATE|os.O_WRONLY, perms)
default:
var url *url.URL
url, err = url.Parse(path)
if err == nil {
f, err = os.OpenFile(url.Path, os.O_CREATE|os.O_WRONLY, perms)
}
}
return
}
// NewZapConfig creates a zap.Config enriched with features from these Options.
// Primarily, this involves creating lumberjack URLs so that the registered sink
// will create the appropriate infrastructure to do log file rotation.
//
// This method also enforces the Permissions field. Any output or error path
// will be created initially with the configured file permissions. This allows
// both zap's file sink and the custom lumberjack sink in this package to honor
// custom permissions.
func (c Config) NewZapConfig() (zc zap.Config, err error) {
zc = zap.Config{
Development: c.Development,
DisableCaller: c.DisableCaller,
DisableStacktrace: c.DisableStacktrace,
Encoding: c.Encoding,
OutputPaths: append([]string{}, c.OutputPaths...),
ErrorOutputPaths: append([]string{}, c.ErrorOutputPaths...),
}
if c.Sampling != nil {
zc.Sampling = new(zap.SamplingConfig)
*zc.Sampling = *c.Sampling
}
if len(c.InitialFields) > 0 {
zc.InitialFields = make(map[string]interface{}, len(c.InitialFields))
for k, v := range c.InitialFields {
zc.InitialFields[k] = v
}
}
applyConfigDefaults(&zc)
if len(c.Level) > 0 {
var l zapcore.Level
err = l.UnmarshalText([]byte(c.Level))
if err == nil {
zc.Level = zap.NewAtomicLevelAt(l)
}
}
var perms fs.FileMode
perms, err = ParsePermissions(c.Permissions)
if err == nil {
pt := PathTransformer{
Rotation: c.Rotation,
}
if !c.DisablePathExpansion {
pt.Mapping = c.Mapping
if pt.Mapping == nil {
pt.Mapping = os.Getenv
}
}
zc.OutputPaths, err = ApplyTransform(pt.Transform, zc.OutputPaths...)
if err == nil {
zc.ErrorOutputPaths, err = ApplyTransform(pt.Transform, zc.ErrorOutputPaths...)
}
}
// Iterate over the transformed paths and ensure that any URIs that refer to
// files are created with relevant permissions.
for i := 0; err == nil && i < len(zc.OutputPaths); i++ {
err = ensureExists(zc.OutputPaths[i], perms)
}
for i := 0; err == nil && i < len(zc.ErrorOutputPaths); i++ {
err = ensureExists(zc.ErrorOutputPaths[i], perms)
}
if err == nil {
zc.EncoderConfig, err = c.EncoderConfig.NewZapcoreEncoderConfig()
}
return
}
// Build behaves similarly to zap.Config.Build. It uses the configuration created
// by NewZapConfig to build the root logger.
func (c Config) Build(opts ...zap.Option) (l *zap.Logger, err error) {
var zc zap.Config
zc, err = c.NewZapConfig()
if err == nil {
l, err = zc.Build(opts...)
}
return
}