github.com/jxskiss/gopkg@v0.17.3/zlog/config.go (about) 1 package zlog 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "os" 8 "time" 9 10 "go.uber.org/zap" 11 "go.uber.org/zap/zapcore" 12 "gopkg.in/natefinch/lumberjack.v2" 13 ) 14 15 const defaultLogMaxSize = 300 // MB 16 17 const defaultMethodNameKey = "methodName" 18 19 // FileLogConfig serializes file log related config in json/yaml. 20 type FileLogConfig 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 300 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. The default is not to remove 30 // old log files. 31 MaxDays int `json:"maxDays" yaml:"maxDays"` 32 33 // MaxBackups is the maximum number of old log files to retain. 34 MaxBackups int `json:"maxBackups" yaml:"maxBackups"` 35 } 36 37 // GlobalConfig configures some global behavior of this package. 38 type GlobalConfig struct { 39 // RedirectStdLog redirects output from the standard log library's 40 // package-global logger to the global logger in this package at 41 // InfoLevel. 42 RedirectStdLog bool `json:"redirectStdLog" yaml:"redirectStdLog"` 43 44 // DisableTrace disables trace level messages. 45 // 46 // Disabling trace level messages makes the trace logging functions no-op, 47 // it gives better performance when you definitely don't need TraceLevel 48 // messages (e.g. in production deployment). 49 DisableTrace bool `json:"disableTrace" yaml:"disableTrace"` 50 51 // MethodNameKey specifies the key to use when adding caller's method 52 // name to logging messages. It defaults to "methodName". 53 MethodNameKey string `json:"methodNameKey" yaml:"methodNameKey"` 54 55 // CtxFunc gets additional logging information from ctx, it's optional. 56 // 57 // See also CtxArgs, CtxResult, WithCtx and Builder.Ctx. 58 CtxFunc CtxFunc `json:"-" yaml:"-"` 59 } 60 61 // Config serializes log related config in json/yaml. 62 type Config struct { 63 // Level sets the default logging level for the logger. 64 Level string `json:"level" yaml:"level"` 65 66 // PerLoggerLevels optionally configures logging level by logger names. 67 // The format is "loggerName.subLogger=level". 68 // If a level is configured for a parent logger, but not configured for 69 // a child logger, the child logger will derive the level from its parent. 70 PerLoggerLevels []string `json:"perLoggerLevels" yaml:"perLoggerLevels"` 71 72 // Format sets the logger's encoding format. 73 // Valid values are "json", "console", and "logfmt". 74 Format string `json:"format" yaml:"format"` 75 76 // File specifies file log config. 77 File FileLogConfig `json:"file" yaml:"file"` 78 79 // FunctionKey enables logging the function name. By default, function 80 // name is not logged. 81 FunctionKey string `json:"functionKey" yaml:"functionKey"` 82 83 // Development puts the logger in development mode, which changes the 84 // behavior of DPanicLevel and takes stacktraces more liberally. 85 Development bool `json:"development" yaml:"development"` 86 87 // DisableTimestamp disables automatic timestamps in output. 88 DisableTimestamp bool `json:"disableTimestamp" yaml:"disableTimestamp"` 89 90 // DisableCaller stops annotating logs with the calling function's file 91 // name and line number. By default, all logs are annotated. 92 DisableCaller bool `json:"disableCaller" yaml:"disableCaller"` 93 94 // DisableStacktrace disables automatic stacktrace capturing. 95 DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"` 96 97 // StacktraceLevel sets the level that stacktrace will be captured. 98 // By default, stacktraces are captured for WarnLevel and above logs in 99 // development and ErrorLevel and above in production. 100 StacktraceLevel string `json:"stacktraceLeve" yaml:"stacktraceLevel"` 101 102 // Sampling sets a sampling strategy for the logger. Sampling caps the 103 // global CPU and I/O load that logging puts on your process while 104 // attempting to preserve a representative subset of your logs. 105 // 106 // Values configured here are per-second. See zapcore.NewSampler for details. 107 Sampling *zap.SamplingConfig `json:"sampling" yaml:"sampling"` 108 109 // Hooks registers functions which will be called each time the Logger 110 // writes out an Entry. Repeated use of Hooks is additive. 111 // 112 // This offers users an easy way to register simple callbacks (e.g., 113 // metrics collection) without implementing the full Core interface. 114 // 115 // See zap.Hooks and zapcore.RegisterHooks for details. 116 Hooks []func(zapcore.Entry) error `json:"-" yaml:"-"` 117 118 // GlobalConfig configures some global behavior of this package. 119 // It works with SetupGlobals and ReplaceGlobals, it has no effect for 120 // non-global individual loggers. 121 GlobalConfig `yaml:",inline"` 122 } 123 124 // CtxArgs holds arguments passed to Config.CtxFunc. 125 type CtxArgs struct{} 126 127 // CtxResult holds values returned by Config.CtxFunc, which will be used 128 // to customize a logger's behavior. 129 type CtxResult struct { 130 // Fields will be added to the logger as additional fields. 131 Fields []zap.Field 132 133 // An optional Level can be used to dynamically change the logging level. 134 Level *Level 135 } 136 137 // CtxFunc gets additional logging data from ctx, it may return extra fields 138 // to attach to the logging entry, or change the logging level dynamically. 139 type CtxFunc func(ctx context.Context, args CtxArgs) CtxResult 140 141 func (cfg *Config) fillDefaults() *Config { 142 if cfg == nil { 143 cfg = &Config{} 144 } 145 if cfg.Level == "" { 146 if cfg.Development { 147 cfg.Level = "trace" 148 } else { 149 cfg.Level = "info" 150 } 151 } 152 if cfg.Format == "" { 153 if cfg.Development { 154 cfg.Format = "console" 155 } else { 156 cfg.Format = "json" 157 } 158 } 159 if cfg.StacktraceLevel == "" { 160 if cfg.Development { 161 cfg.StacktraceLevel = "warn" 162 } else { 163 cfg.StacktraceLevel = "error" 164 } 165 } 166 if cfg.File.Filename != "" && cfg.File.MaxSize == 0 { 167 cfg.File.MaxSize = defaultLogMaxSize 168 } 169 return cfg 170 } 171 172 func (cfg *Config) buildEncoder() (zapcore.Encoder, error) { 173 encConfig := zap.NewProductionEncoderConfig() 174 encConfig.EncodeLevel = func(lv zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { 175 enc.AppendString(fromZapLevel(lv).String()) 176 } 177 if cfg.Development { 178 encConfig = zap.NewDevelopmentEncoderConfig() 179 } 180 encConfig.FunctionKey = cfg.FunctionKey 181 if cfg.DisableTimestamp { 182 encConfig.TimeKey = zapcore.OmitKey 183 } 184 switch cfg.Format { 185 case "json": 186 return zapcore.NewJSONEncoder(encConfig), nil 187 case "console": 188 encConfig.EncodeLevel = func(lv zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { 189 enc.AppendString(fromZapLevel(lv).CapitalString()) 190 } 191 return zapcore.NewConsoleEncoder(encConfig), nil 192 case "logfmt": 193 return NewLogfmtEncoder(encConfig), nil 194 default: 195 return nil, fmt.Errorf("unknown format: %s", cfg.Format) 196 } 197 } 198 199 func (cfg *Config) buildFileLogger() (*lumberjack.Logger, error) { 200 fc := cfg.File 201 if st, err := os.Stat(fc.Filename); err == nil { 202 if st.IsDir() { 203 return nil, errors.New("can't use directory as log filename") 204 } 205 } 206 return &lumberjack.Logger{ 207 Filename: fc.Filename, 208 MaxSize: fc.MaxSize, 209 MaxAge: fc.MaxDays, 210 MaxBackups: fc.MaxBackups, 211 LocalTime: true, 212 Compress: true, 213 }, nil 214 } 215 216 func (cfg *Config) buildOptions() ([]zap.Option, error) { 217 var opts []zap.Option 218 if cfg.Development { 219 opts = append(opts, zap.Development()) 220 } 221 if !cfg.DisableCaller { 222 opts = append(opts, zap.AddCaller()) 223 } 224 if !cfg.DisableStacktrace { 225 var stackLevel Level 226 if !stackLevel.unmarshalText([]byte(cfg.StacktraceLevel)) { 227 return nil, fmt.Errorf("unrecognized stacktrace level: %s", cfg.StacktraceLevel) 228 } 229 opts = append(opts, zap.AddStacktrace(stackLevel)) 230 } 231 if cfg.Sampling != nil { 232 opts = append(opts, zap.WrapCore(func(core zapcore.Core) zapcore.Core { 233 tick := time.Second 234 first, thereafter := cfg.Sampling.Initial, cfg.Sampling.Thereafter 235 return zapcore.NewSamplerWithOptions(core, tick, first, thereafter) 236 })) 237 } 238 return opts, nil 239 } 240 241 // New initializes a zap logger. 242 // 243 // If Config.File is configured, the log messages will be written to the 244 // specified file with rotation, else they will be written to stderr. 245 // 246 // The returned zap.Logger supports dynamic level, see Config.PerLoggerLevels 247 // and GlobalConfig.CtxFunc for details about dynamic level. 248 // The returned zap.Logger and Properties may be passed to ReplaceGlobals 249 // to change the global logger and customize some global behavior of this 250 // package. 251 func New(cfg *Config, opts ...zap.Option) (*zap.Logger, *Properties, error) { 252 cfg = cfg.fillDefaults() 253 var output zapcore.WriteSyncer 254 if len(cfg.File.Filename) > 0 { 255 out, err := cfg.buildFileLogger() 256 if err != nil { 257 return nil, nil, err 258 } 259 output = zapcore.AddSync(out) 260 } else { 261 stderr, _, err := zap.Open("stderr") 262 if err != nil { 263 return nil, nil, err 264 } 265 output = stderr 266 } 267 return NewWithOutput(cfg, output, opts...) 268 } 269 270 // NewWithOutput initializes a zap logger with given write syncer as output 271 // destination. 272 // 273 // The returned zap.Logger supports dynamic level, see Config.PerLoggerLevels 274 // and GlobalConfig.CtxFunc for details about dynamic level. 275 // The returned zap.Logger and Properties may be passed to ReplaceGlobals 276 // to change the global logger and customize some global behavior of this 277 // package. 278 func NewWithOutput(cfg *Config, output zapcore.WriteSyncer, opts ...zap.Option) (*zap.Logger, *Properties, error) { 279 cfg = cfg.fillDefaults() 280 encoder, err := cfg.buildEncoder() 281 if err != nil { 282 return nil, nil, err 283 } 284 285 level := newAtomicLevel() 286 err = level.UnmarshalText([]byte(cfg.Level)) 287 if err != nil { 288 return nil, nil, err 289 } 290 291 cfgOpts, err := cfg.buildOptions() 292 if err != nil { 293 return nil, nil, err 294 } 295 opts = append(cfgOpts, opts...) 296 297 // base core at trace level 298 core := zapcore.NewCore(encoder, output, TraceLevel) 299 if len(cfg.Hooks) > 0 { 300 core = zapcore.RegisterHooks(core, cfg.Hooks...) 301 } 302 303 // build per logger level rules 304 _, perLoggerLevelFn, err := buildPerLoggerLevelFunc(cfg.PerLoggerLevels) 305 if err != nil { 306 return nil, nil, err 307 } 308 309 // wrap the base core with dynamic level 310 opts = append(opts, zap.WrapCore(func(core zapcore.Core) zapcore.Core { 311 return &dynamicLevelCore{ 312 Core: core, 313 baseLevel: level.zl, 314 levelFunc: perLoggerLevelFn, 315 } 316 })) 317 lg := zap.New(core, opts...) 318 prop := &Properties{ 319 cfg: cfg.GlobalConfig, 320 level: level, 321 } 322 return lg, prop, nil 323 } 324 325 type WrapCoreConfig struct { 326 // Level sets the default logging level for the logger. 327 Level Level 328 329 // PerLoggerLevels optionally configures logging level by logger names. 330 // The format is "loggerName.subLogger=level". 331 // If a level is configured for a parent logger, but not configured for 332 // a child logger, the child logger will derive the level from its parent. 333 PerLoggerLevels []string 334 335 // Hooks registers functions which will be called each time the Logger 336 // writes out an Entry. Repeated use of Hooks is additive. 337 // 338 // This offers users an easy way to register simple callbacks (e.g., 339 // metrics collection) without implementing the full Core interface. 340 // 341 // See zap.Hooks and zapcore.RegisterHooks for details. 342 Hooks []func(zapcore.Entry) error 343 344 // GlobalConfig configures some global behavior of this package. 345 // It works with SetupGlobals and ReplaceGlobals, it has no effect for 346 // non-global individual loggers. 347 GlobalConfig `yaml:",inline"` 348 } 349 350 // NewWithCore initializes a zap logger with given core. 351 // 352 // You may use this function to integrate with custom cores (e.g. to 353 // integrate with Sentry or Graylog, or output to multiple sinks). 354 // 355 // The returned zap.Logger supports dynamic level, see 356 // WrapCoreConfig.PerLoggerLevels and GlobalConfig.CtxFunc for details 357 // about dynamic level. Note that if you want to use the dynamic level 358 // feature, the provided core must be configured to log low level messages 359 // (e.g. debug). 360 // 361 // The returned zap.Logger and Properties may be passed to ReplaceGlobals 362 // to change the global logger and customize some global behavior of this 363 // package. 364 func NewWithCore(cfg *WrapCoreConfig, core zapcore.Core, opts ...zap.Option) (*zap.Logger, *Properties, error) { 365 if cfg == nil { 366 cfg = &WrapCoreConfig{Level: InfoLevel} 367 } 368 369 atomLevel := newAtomicLevel() 370 atomLevel.SetLevel(cfg.Level) 371 372 if len(cfg.Hooks) > 0 { 373 core = zapcore.RegisterHooks(core, cfg.Hooks...) 374 } 375 376 _, perLoggerLevelFn, err := buildPerLoggerLevelFunc(cfg.PerLoggerLevels) 377 if err != nil { 378 return nil, nil, err 379 } 380 381 // wrap the base core with dynamic level 382 opts = append(opts, zap.WrapCore(func(core zapcore.Core) zapcore.Core { 383 return &dynamicLevelCore{ 384 Core: core, 385 baseLevel: atomLevel.zl, 386 levelFunc: perLoggerLevelFn, 387 } 388 })) 389 390 lg := zap.New(core, opts...) 391 prop := &Properties{ 392 cfg: cfg.GlobalConfig, 393 level: atomLevel, 394 } 395 return lg, prop, nil 396 }