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  }