package logger import ( "io" "os" "path/filepath" "time" "go.uber.org/zap" "go.uber.org/zap/zapcore" "gopkg.in/natefinch/lumberjack.v2" ) const ( // DefaultLevel the default log level DefaultLevel = zapcore.InfoLevel // DefaultTimeLayout the default time layout; DefaultTimeLayout = time.DateTime ) // Option custom setup config type Option func(*option) type option struct { level zapcore.Level fields map[string]string file io.Writer timeLayout string disableConsole bool disableCaller bool } // WithDebugLevel only greater than 'level' will output func WithDebugLevel() Option { return func(opt *option) { opt.level = zapcore.DebugLevel } } // WithInfoLevel only greater than 'level' will output func WithInfoLevel() Option { return func(opt *option) { opt.level = zapcore.InfoLevel } } // WithWarnLevel only greater than 'level' will output func WithWarnLevel() Option { return func(opt *option) { opt.level = zapcore.WarnLevel } } // WithErrorLevel only greater than 'level' will output func WithErrorLevel() Option { return func(opt *option) { opt.level = zapcore.ErrorLevel } } // WithField add some field(s) to log func WithField(key, value string) Option { return func(opt *option) { opt.fields[key] = value } } // WithFileP write log to some file func WithFileP(file string) Option { dir := filepath.Dir(file) if err := os.MkdirAll(dir, 0766); err != nil { panic(err) } f, err := os.OpenFile(file, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0766) if err != nil { panic(err) } return func(opt *option) { opt.file = zapcore.Lock(f) } } // WithFileRotationP write log to some file with rotation func WithFileRotationP(file string) Option { dir := filepath.Dir(file) if err := os.MkdirAll(dir, 0766); err != nil { panic(err) } return func(opt *option) { opt.file = &lumberjack.Logger{ // concurrent-safed Filename: file, // 文件路径 MaxSize: 128, // 单个文件最大尺寸,默认单位 M MaxBackups: 300, // 最多保留 300 个备份 MaxAge: 30, // 最大时间,默认单位 day LocalTime: true, // 使用本地时间 Compress: true, // 是否压缩 disabled by default } } } // WithTimeLayout custom time format func WithTimeLayout(timeLayout string) Option { return func(opt *option) { opt.timeLayout = timeLayout } } // WithDisableConsole WithEnableConsole write log to os.Stdout or os.Stderr func WithDisableConsole() Option { return func(opt *option) { opt.disableConsole = true } } // WithDisableCaller disable caller field func WithDisableCaller() Option { return func(opt *option) { opt.disableCaller = true } } // NewJSONLogger return a json-encoder zap logger, func NewJSONLogger(opts ...Option) (*zap.Logger, error) { opt := &option{level: DefaultLevel, fields: make(map[string]string)} for _, f := range opts { f(opt) } timeLayout := DefaultTimeLayout if opt.timeLayout != "" { timeLayout = opt.timeLayout } // similar to zap.NewProductionEncoderConfig() encoderConfig := zapcore.EncoderConfig{ TimeKey: "time", LevelKey: "level", NameKey: "logger", // used by logger.Named(key); optional; useless CallerKey: "caller", MessageKey: "msg", StacktraceKey: "stacktrace", // use by zap.AddStacktrace; optional; useless LineEnding: zapcore.DefaultLineEnding, EncodeLevel: zapcore.LowercaseLevelEncoder, // 小写编码器 EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { enc.AppendString(t.Format(timeLayout)) }, EncodeDuration: zapcore.MillisDurationEncoder, EncodeCaller: zapcore.ShortCallerEncoder, // 全路径编码器 } jsonEncoder := zapcore.NewJSONEncoder(encoderConfig) // lowPriority usd by info\debug\warn lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { return lvl >= opt.level && lvl < zapcore.ErrorLevel }) // highPriority usd by error\panic\fatal highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { return lvl >= opt.level && lvl >= zapcore.ErrorLevel }) stdout := zapcore.Lock(os.Stdout) // lock for concurrent safe stderr := zapcore.Lock(os.Stderr) // lock for concurrent safe core := zapcore.NewTee() if !opt.disableConsole { core = zapcore.NewTee( zapcore.NewCore(jsonEncoder, zapcore.NewMultiWriteSyncer(stdout), lowPriority, ), zapcore.NewCore(jsonEncoder, zapcore.NewMultiWriteSyncer(stderr), highPriority, ), ) } if opt.file != nil { core = zapcore.NewTee(core, zapcore.NewCore(jsonEncoder, zapcore.AddSync(opt.file), zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { return lvl >= opt.level }), ), ) } logger := zap.New(core, zap.WithCaller(!opt.disableCaller), zap.ErrorOutput(stderr), ) for key, value := range opt.fields { logger = logger.WithOptions(zap.Fields(zapcore.Field{Key: key, Type: zapcore.StringType, String: value})) } return logger, nil } var _ Meta = (*meta)(nil) // Meta key-value type Meta interface { Key() string Value() any meta() } type meta struct { key string value any } func (m *meta) Key() string { return m.key } func (m *meta) Value() any { return m.value } func (m *meta) meta() {} // NewMeta create meat func NewMeta(key string, value any) Meta { return &meta{key: key, value: value} } // WrapMeta wrap meta to zap fields func WrapMeta(err error, metas ...Meta) (fields []zap.Field) { capacity := len(metas) + 1 // namespace meta if err != nil { capacity++ } fields = make([]zap.Field, 0, capacity) if err != nil { fields = append(fields, zap.Error(err)) } fields = append(fields, zap.Namespace("meta")) for _, meta := range metas { fields = append(fields, zap.Any(meta.Key(), meta.Value())) } return } // RestyClientLogger use by resty.Client func RestyClientLogger(logger *zap.Logger) *RestyClientLog { return &RestyClientLog{logger: logger} } type RestyClientLog struct { logger *zap.Logger } func (r *RestyClientLog) Errorf(format string, v ...any) { r.logger.Sugar().Errorf(format, v) } func (r *RestyClientLog) Warnf(format string, v ...any) { r.logger.Sugar().Warnf(format, v) } func (r *RestyClientLog) Debugf(format string, v ...any) { r.logger.Sugar().Debugf(format, v) }