github.com/jxskiss/gopkg/v2@v2.14.9-0.20240514120614-899f3e7952b4/zlog/config.go (about) 1 package zlog 2 3 import ( 4 "fmt" 5 "os" 6 "time" 7 8 "go.uber.org/zap" 9 "go.uber.org/zap/zapcore" 10 11 "github.com/jxskiss/gopkg/v2/zlog/internal/terminal" 12 ) 13 14 const ( 15 defaultMethodNameKey = "methodName" 16 consoleTimeLayout = "2006/01/02 15:04:05.000000" 17 ) 18 19 // FileConfig serializes file log related config in json/yaml. 20 type FileConfig struct { 21 // Filename is the file to write logs to, leave empty to disable file log. 22 Filename string `json:"filename" yaml:"filename"` 23 24 // MaxSize is the maximum size in MB of the log file before it gets 25 // rotated. It defaults to 100 MB. 26 MaxSize int `json:"maxSize" yaml:"maxSize"` 27 28 // MaxDays is the maximum days to retain old log files based on the 29 // timestamp encoded in their filenames. 30 // Note that a day is defined as 24 hours and may not exactly correspond 31 // to calendar days due to daylight savings, leap seconds, etc. 32 // The default is not to remove old log files. 33 MaxDays int `json:"maxDays" yaml:"maxDays"` 34 35 // MaxBackups is the maximum number of old log files to retain. 36 // The default is to retain all old log files (though MaxAge may still 37 // cause them to get deleted.) 38 MaxBackups int `json:"maxBackups" yaml:"maxBackups"` 39 40 // Compress determines if the rotated log files should be compressed. 41 // The default is not to perform compression. 42 Compress bool `json:"compress" yaml:"compress"` 43 } 44 45 // FileWriterFactory opens a file to write log, FileConfig specifies 46 // filename and optional settings to rotate the log files. 47 // The returned WriteSyncer should be safe for concurrent use, 48 // you may use zap.Lock to wrap a WriteSyncer to be concurrent safe. 49 // It also returns any error encountered and a function to close 50 // the opened file. 51 // 52 // User may check github.com/jxskiss/gopkg/_examples/zlog/lumberjack_writer 53 // for an example to use "lumberjack.v2" as a rolling logger. 54 type FileWriterFactory func(fc *FileConfig) (zapcore.WriteSyncer, func(), error) 55 56 // GlobalConfig configures some global behavior of this package. 57 type GlobalConfig struct { 58 // RedirectStdLog redirects output from the standard log library's 59 // package-global logger to the global logger in this package at 60 // InfoLevel. 61 RedirectStdLog bool `json:"redirectStdLog" yaml:"redirectStdLog"` 62 63 // MethodNameKey specifies the key to use when adding caller's method 64 // name to logging messages. It defaults to "methodName". 65 MethodNameKey string `json:"methodNameKey" yaml:"methodNameKey"` 66 67 // TraceFilterRule optionally configures filter rule to allow or deny 68 // trace logging in some packages or files. 69 // 70 // It uses glob to match filename, the syntax is "allow=glob1,glob2;deny=glob3,glob4". 71 // For example: 72 // 73 // - "", empty rule means allow all messages 74 // - "allow=all", allow all messages 75 // - "deny=all", deny all messages 76 // - "allow=pkg1/*,pkg2/*.go", 77 // allow messages from files in `pkg1` and `pkg2`, 78 // deny messages from all other packages 79 // - "allow=pkg1/sub1/abc.go,pkg1/sub2/def.go", 80 // allow messages from file `pkg1/sub1/abc.go` and `pkg1/sub2/def.go`, 81 // deny messages from all other files 82 // - "allow=pkg1/**", 83 // allow messages from files and sub-packages in `pkg1`, 84 // deny messages from all other packages 85 // - "deny=pkg1/**.go,pkg2/**.go", 86 // deny messages from files and sub-packages in `pkg1` and `pkg2`, 87 // allow messages from all other packages 88 // - "allow=all;deny=pkg/**", same as "deny=pkg/**" 89 // 90 // If both "allow" and "deny" directives are configured, the "allow" directive 91 // takes effect, the "deny" directive is ignored. 92 // 93 // The default value is empty, which means all messages are allowed. 94 // 95 // User can also set the environment variable "ZLOG_TRACE_FILTER_RULE" 96 // to configure it at runtime, if available, the environment variable 97 // is used when this value is empty. 98 TraceFilterRule string `json:"traceFilterRule" yaml:"traceFilterRule"` 99 100 // CtxHandler customizes a logger's behavior at runtime dynamically. 101 CtxHandler CtxHandler `json:"-" yaml:"-"` 102 } 103 104 // Config serializes log related config in json/yaml. 105 type Config struct { 106 // Level sets the default logging level for the logger. 107 Level string `json:"level" yaml:"level"` 108 109 // PerLoggerLevels optionally configures logging level by logger names. 110 // The format is "loggerName.subLogger=level". 111 // If a level is configured for a parent logger, but not configured for 112 // a child logger, the child logger derives from its parent. 113 PerLoggerLevels []string `json:"perLoggerLevels" yaml:"perLoggerLevels"` 114 115 // Format sets the logger's encoding format. 116 // Valid values are "json", "console", and "logfmt". 117 Format string `json:"format" yaml:"format"` 118 119 // File specifies file log config. 120 File FileConfig `json:"file" yaml:"file"` 121 122 // PerLoggerFiles optionally set different file destination for different 123 // loggers specified by logger name. 124 // If a destination is configured for a parent logger, but not configured 125 // for a child logger, the child logger derives from its parent. 126 PerLoggerFiles map[string]FileConfig `json:"perLoggerFiles" yaml:"perLoggerFiles"` 127 128 // FileWriterFactory optionally specifies a custom factory function, 129 // when File is configured, to open a file to write log. 130 // By default, [zap.Open] is used, which does not support file rotation. 131 FileWriterFactory FileWriterFactory `json:"-" yaml:"-"` 132 133 // FunctionKey enables logging the function name. 134 // By default, function name is not logged. 135 FunctionKey string `json:"functionKey" yaml:"functionKey"` 136 137 // Development puts the logger in development mode, which changes the 138 // behavior of DPanicLevel and takes stacktrace more liberally. 139 Development bool `json:"development" yaml:"development"` 140 141 // DisableTimestamp disables automatic timestamps in output. 142 DisableTimestamp bool `json:"disableTimestamp" yaml:"disableTimestamp"` 143 144 // DisableCaller stops annotating logs with the calling function's file 145 // name and line number. By default, all logs are annotated. 146 DisableCaller bool `json:"disableCaller" yaml:"disableCaller"` 147 148 // DisableStacktrace disables automatic stacktrace capturing. 149 DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"` 150 151 // StacktraceLevel sets the level that stacktrace will be captured. 152 // By default, stacktraces are captured for ErrorLevel and above. 153 StacktraceLevel string `json:"stacktraceLeve" yaml:"stacktraceLevel"` 154 155 // Sampling sets a sampling strategy for the logger. Sampling caps the 156 // global CPU and I/O load that logging puts on your process while 157 // attempting to preserve a representative subset of your logs. 158 // 159 // Values configured here are per-second. See zapcore.NewSampler for details. 160 Sampling *zap.SamplingConfig `json:"sampling" yaml:"sampling"` 161 162 // Hooks registers functions which will be called each time the logger 163 // writes out an Entry. Repeated use of Hooks is additive. 164 // 165 // This offers users an easy way to register simple callbacks (e.g., 166 // metrics collection) without implementing the full Core interface. 167 // 168 // See zap.Hooks and zapcore.RegisterHooks for details. 169 Hooks []func(zapcore.Entry) error `json:"-" yaml:"-"` 170 171 // GlobalConfig configures some global behavior of this package. 172 // It works with SetupGlobals and ReplaceGlobals, it has no effect for 173 // individual non-global loggers. 174 GlobalConfig `yaml:",inline"` 175 } 176 177 func checkAndFillDefaults(cfg *Config) *Config { 178 if cfg == nil { 179 cfg = &Config{} 180 } 181 if cfg.FileWriterFactory == nil { 182 cfg.FileWriterFactory = func(fc *FileConfig) (zapcore.WriteSyncer, func(), error) { 183 return zap.Open(fc.Filename) 184 } 185 } 186 if cfg.Development { 187 setIfZero(&cfg.Level, "trace") 188 setIfZero(&cfg.Format, "console") 189 } else { 190 setIfZero(&cfg.Level, "info") 191 setIfZero(&cfg.Format, "json") 192 } 193 setIfZero(&cfg.StacktraceLevel, "error") 194 return cfg 195 } 196 197 func (cfg *Config) buildEncoder(isStderr bool) (zapcore.Encoder, error) { 198 encConfig := zap.NewProductionEncoderConfig() 199 encConfig.EncodeLevel = encodeLevelLowercase 200 if cfg.Development { 201 encConfig = zap.NewDevelopmentEncoderConfig() 202 encConfig.EncodeLevel = encodeLevelCapital 203 } 204 encConfig.FunctionKey = cfg.FunctionKey 205 if cfg.DisableTimestamp { 206 encConfig.TimeKey = zapcore.OmitKey 207 } 208 switch cfg.Format { 209 case "json": 210 return zapcore.NewJSONEncoder(encConfig), nil 211 case "console": 212 encConfig.EncodeLevel = encodeLevelCapital 213 encConfig.EncodeTime = zapcore.TimeEncoderOfLayout(consoleTimeLayout) 214 encConfig.ConsoleSeparator = " " 215 if isStderr && terminal.CheckIsTerminal(os.Stderr) { 216 encConfig.EncodeLevel = encodeLevelColorCapital 217 } 218 return zapcore.NewConsoleEncoder(encConfig), nil 219 case "logfmt": 220 return NewLogfmtEncoder(encConfig), nil 221 default: 222 return nil, fmt.Errorf("unknown format: %s", cfg.Format) 223 } 224 } 225 226 func (cfg *Config) buildOptions() ([]zap.Option, error) { 227 var opts []zap.Option 228 if cfg.Development { 229 opts = append(opts, zap.Development()) 230 } 231 if !cfg.DisableCaller { 232 opts = append(opts, zap.AddCaller()) 233 } 234 if !cfg.DisableStacktrace { 235 var stackLevel Level 236 if !unmarshalLevel(&stackLevel, cfg.StacktraceLevel) { 237 return nil, fmt.Errorf("unrecognized stacktrace level: %s", cfg.StacktraceLevel) 238 } 239 opts = append(opts, zap.AddStacktrace(stackLevel)) 240 } 241 if cfg.Sampling != nil { 242 opts = append(opts, zap.WrapCore(func(core zapcore.Core) zapcore.Core { 243 tick := time.Second 244 first, thereafter := cfg.Sampling.Initial, cfg.Sampling.Thereafter 245 return zapcore.NewSamplerWithOptions(core, tick, first, thereafter) 246 })) 247 } 248 return opts, nil 249 } 250 251 // New initializes a zap logger. 252 // 253 // If Config.File is configured, logs will be written to the specified file, 254 // and Config.PerLoggerFiles can be used to write logs to different files 255 // specified by logger name. 256 // By default, logs are written to stderr. 257 // 258 // The returned zap.Logger supports dynamic level, see Config.PerLoggerLevels 259 // and GlobalConfig.CtxHandler for details about dynamic level. 260 // The returned zap.Logger and Properties may be passed to ReplaceGlobals 261 // to change the global logger and customize some global behavior of this 262 // package. 263 func New(cfg *Config, opts ...zap.Option) (*zap.Logger, *Properties, error) { 264 cfg = checkAndFillDefaults(cfg) 265 var err error 266 var output zapcore.WriteSyncer 267 var closer func() 268 if len(cfg.File.Filename) > 0 { 269 if len(cfg.PerLoggerFiles) > 0 { 270 return newWithMultiFilesOutput(cfg, opts...) 271 } 272 output, closer, err = cfg.FileWriterFactory(&cfg.File) 273 if err != nil { 274 return nil, nil, err 275 } 276 } else { 277 output, closer, err = zap.Open("stderr") 278 if err != nil { 279 return nil, nil, err 280 } 281 output = &wrapStderr{output} 282 } 283 l, p, err := NewWithOutput(cfg, output, opts...) 284 if err != nil { 285 closer() 286 return nil, nil, err 287 } 288 p.closers = append(p.closers, closer) 289 return l, p, nil 290 } 291 292 func newWithMultiFilesOutput(cfg *Config, opts ...zap.Option) (*zap.Logger, *Properties, error) { 293 enc, err := cfg.buildEncoder(false) 294 if err != nil { 295 return nil, nil, err 296 } 297 298 var level Level 299 if !unmarshalLevel(&level, cfg.Level) { 300 return nil, nil, fmt.Errorf("unrecognized level: %s", cfg.Level) 301 } 302 303 cfgOpts, err := cfg.buildOptions() 304 if err != nil { 305 return nil, nil, err 306 } 307 opts = append(cfgOpts, opts...) 308 309 // base multi-files core at trace level 310 core, closers, err := newMultiFilesCore(cfg, enc, TraceLevel) 311 if err != nil { 312 return nil, nil, err 313 } 314 315 wcc := &WrapCoreConfig{ 316 Level: level, 317 PerLoggerLevels: cfg.PerLoggerLevels, 318 Hooks: cfg.Hooks, 319 GlobalConfig: cfg.GlobalConfig, 320 } 321 l, p, err := newWithWrapCoreConfig(wcc, core, opts...) 322 if err != nil { 323 runClosers(closers) 324 return nil, nil, err 325 } 326 p.disableCaller = cfg.DisableCaller 327 p.closers = closers 328 return l, p, nil 329 } 330 331 // NewWithOutput initializes a zap logger with given write syncer as output. 332 // 333 // The returned zap.Logger supports dynamic level, see Config.PerLoggerLevels 334 // and GlobalConfig.CtxHandler for details about dynamic level. 335 // The returned zap.Logger and Properties may be passed to ReplaceGlobals 336 // to change the global logger and customize some global behavior of this 337 // package. 338 func NewWithOutput(cfg *Config, output zapcore.WriteSyncer, opts ...zap.Option) (*zap.Logger, *Properties, error) { 339 cfg = checkAndFillDefaults(cfg) 340 isStderr := false 341 if wrapper, ok := output.(*wrapStderr); ok { 342 isStderr = true 343 output = wrapper.WriteSyncer 344 } 345 encoder, err := cfg.buildEncoder(isStderr) 346 if err != nil { 347 return nil, nil, err 348 } 349 350 // base core logging any level messages 351 core := zapcore.NewCore(encoder, output, Level(-127)) 352 353 var level Level 354 if !unmarshalLevel(&level, cfg.Level) { 355 return nil, nil, fmt.Errorf("unrecognized level: %s", cfg.Level) 356 } 357 358 cfgOpts, err := cfg.buildOptions() 359 if err != nil { 360 return nil, nil, err 361 } 362 opts = append(cfgOpts, opts...) 363 364 wcc := &WrapCoreConfig{ 365 Level: level, 366 PerLoggerLevels: cfg.PerLoggerLevels, 367 Hooks: cfg.Hooks, 368 GlobalConfig: cfg.GlobalConfig, 369 } 370 l, p, err := newWithWrapCoreConfig(wcc, core, opts...) 371 if err == nil { 372 p.disableCaller = cfg.DisableCaller 373 } 374 return l, p, err 375 } 376 377 type WrapCoreConfig struct { 378 // Level sets the default logging level for the logger. 379 Level Level `json:"level" yaml:"level"` 380 381 // PerLoggerLevels optionally configures logging level by logger names. 382 // The format is "loggerName.subLogger=level". 383 // If a level is configured for a parent logger, but not configured for 384 // a child logger, the child logger will derive the level from its parent. 385 PerLoggerLevels []string `json:"perLoggerLevels" yaml:"perLoggerLevels"` 386 387 // Hooks registers functions which will be called each time the logger 388 // writes out an Entry. Repeated use of Hooks is additive. 389 // 390 // This offers users an easy way to register simple callbacks (e.g., 391 // metrics collection) without implementing the full Core interface. 392 // 393 // See zap.Hooks and zapcore.RegisterHooks for details. 394 Hooks []func(zapcore.Entry) error `json:"-" yaml:"-"` 395 396 // GlobalConfig configures some global behavior of this package. 397 // It works with SetupGlobals and ReplaceGlobals, it has no effect for 398 // individual non-global loggers. 399 GlobalConfig `yaml:",inline"` 400 } 401 402 // NewWithCore initializes a zap logger with given core. 403 // 404 // You may use this function to integrate with custom cores (e.g. to 405 // integrate with Sentry or Graylog, or output to multiple sinks). 406 // 407 // The returned zap.Logger supports dynamic level, see 408 // WrapCoreConfig.PerLoggerLevels and GlobalConfig.CtxHandler for details 409 // about dynamic level. Note that if you want to use the dynamic level 410 // feature, the provided core must be configured to log low level messages 411 // (e.g. debug). 412 // 413 // The returned zap.Logger and Properties may be passed to ReplaceGlobals 414 // to change the global logger and customize some global behavior of this 415 // package. 416 func NewWithCore(cfg *WrapCoreConfig, core zapcore.Core, opts ...zap.Option) (*zap.Logger, *Properties, error) { 417 if cfg == nil { 418 cfg = &WrapCoreConfig{Level: InfoLevel} 419 } 420 return newWithWrapCoreConfig(cfg, core, opts...) 421 } 422 423 func newWithWrapCoreConfig( 424 cfg *WrapCoreConfig, 425 core zapcore.Core, 426 opts ...zap.Option, 427 ) (*zap.Logger, *Properties, error) { 428 if len(cfg.Hooks) > 0 { 429 core = zapcore.RegisterHooks(core, cfg.Hooks...) 430 } 431 432 // build per logger level rules 433 perLoggerLevelFn, err := buildPerLoggerLevelFunc(cfg.PerLoggerLevels) 434 if err != nil { 435 return nil, nil, err 436 } 437 438 // wrap the base core with dynamic level 439 aLevel := zap.NewAtomicLevelAt(cfg.Level) 440 opts = append(opts, zap.WrapCore(func(core zapcore.Core) zapcore.Core { 441 return &dynamicLevelCore{ 442 Core: core, 443 baseLevel: aLevel, 444 levelFunc: perLoggerLevelFn, 445 } 446 })) 447 448 lg := zap.New(core, opts...) 449 prop := &Properties{ 450 cfg: cfg.GlobalConfig, 451 level: aLevel, 452 } 453 return lg, prop, nil 454 } 455 456 func mergeFileConfig(fc, defaultConfig FileConfig) FileConfig { 457 setIfZero(&fc.MaxSize, defaultConfig.MaxSize) 458 setIfZero(&fc.MaxDays, defaultConfig.MaxDays) 459 setIfZero(&fc.MaxBackups, defaultConfig.MaxBackups) 460 setIfZero(&fc.Compress, defaultConfig.Compress) 461 return fc 462 } 463 464 func setIfZero[T comparable](dst *T, value T) { 465 var zero T 466 if *dst == zero { 467 *dst = value 468 } 469 } 470 471 func runClosers(closers []func()) { 472 for _, closeFunc := range closers { 473 closeFunc() 474 } 475 } 476 477 type wrapStderr struct { 478 zapcore.WriteSyncer 479 }