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  }