package logger import ( "bytes" "encoding/json" "fmt" "io" "log" "os" "path/filepath" "time" "mini-chat/internal/repository/mysql/dao" "mini-chat/internal/repository/mysql/model" "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.RFC3339 ) // Option custom setup config type Option func(*option) type option struct { level zapcore.Level fields map[string]string file io.Writer timeLayout string outputInConsole 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 } } // WithOutputInConsole write log to os.Stdout or os.Stderr func WithOutputInConsole() Option { return func(opt *option) { opt.outputInConsole = 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.outputInConsole { 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.AddCaller(), 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 } // CustomLogger 自定义日志记录器 type customLogger struct { db *dao.Query logger *zap.Logger } var _ CustomLogger = (*customLogger)(nil) type CustomLogger interface { i() Info(msg string, fields ...zap.Field) Debug(msg string, fields ...zap.Field) Warn(msg string, fields ...zap.Field) Error(msg string, fields ...zap.Field) Fatal(msg string, fields ...zap.Field) Sync() error } // NewCustomLogger 创建自定义日志记录器 func NewCustomLogger(db *dao.Query, opts ...Option) (CustomLogger, error) { logger, err := NewJSONLogger(opts...) if err != nil { return nil, err } return &customLogger{ db: db, logger: logger, }, nil } func (c *customLogger) i() { } // fieldsToJSON 将 zap.Field 转换为 JSON 字符串 func (c *customLogger) fieldsToJSON(msg string, fields []zap.Field) (string, error) { // 创建一个 buffer 来存储编码后的 JSON buffer := &bytes.Buffer{} // 创建一个 zapcore.Encoder 对象 encoderConfig := zap.NewProductionEncoderConfig() encoder := zapcore.NewJSONEncoder(encoderConfig) // 创建一个 zapcore.Core 对象 core := zapcore.NewCore(encoder, zapcore.AddSync(buffer), zap.InfoLevel) // 创建一个 zap.Logger 对象 tempLogger := zap.New(core) // 使用 tempLogger 记录一条日志,只包含 fields tempLogger.Info(msg, fields...) // 获取编码后的 JSON 字符串 jsonStr := buffer.String() // 去掉最后的换行符 if len(jsonStr) > 0 && jsonStr[len(jsonStr)-1] == '\n' { jsonStr = jsonStr[:len(jsonStr)-1] } // 解析 JSON 字符串以确保格式正确 var parsedJSON map[string]interface{} err := json.Unmarshal([]byte(jsonStr), &parsedJSON) if err != nil { return "", err } // 重新编码为 JSON 字符串 jsonBytes, err := json.Marshal(parsedJSON) if err != nil { return "", err } return string(jsonBytes), nil } // fieldsJsonToDB 将 zap.Field 转换为数据库记录 func (c *customLogger) fieldsJsonToDB(level, msg string, fields []zap.Field) { content := "" jsonFields, err := c.fieldsToJSON(msg, fields) if err != nil { c.logger.Error("Failed to convert zap fields to JSON", zap.Error(err)) } else { content = jsonFields } if c.db != nil { if err := c.db.LogOperation.Create(&model.LogOperation{ Level: level, Msg: msg, Content: content, CreatedAt: time.Now(), }); err != nil { log.Println(fmt.Sprintf("Failed to create log operation record: %s", err.Error())) } } else { log.Println(level, msg, content) } } // Info 重写 Info 方法 func (c *customLogger) Info(msg string, fields ...zap.Field) { c.logger.Info(msg, fields...) c.fieldsJsonToDB("info", msg, fields) } // Debug 调用底层 Debug 方法 func (c *customLogger) Debug(msg string, fields ...zap.Field) { c.logger.Debug(msg, fields...) c.fieldsJsonToDB("debug", msg, fields) } // Warn 调用底层 Warn 方法 func (c *customLogger) Warn(msg string, fields ...zap.Field) { c.logger.Warn(msg, fields...) c.fieldsJsonToDB("warn", msg, fields) } // Error 调用底层 Error 方法 func (c *customLogger) Error(msg string, fields ...zap.Field) { c.logger.Error(msg, fields...) c.fieldsJsonToDB("error", msg, fields) } // Fatal 调用底层 Fatal 方法 func (c *customLogger) Fatal(msg string, fields ...zap.Field) { c.logger.Fatal(msg, fields...) c.fieldsJsonToDB("fatal", msg, fields) } // Sync 调用底层 Sync 方法 func (c *customLogger) Sync() error { return c.logger.Sync() } // With 调用底层 With 方法 //func (c *customLogger) With(db mysql.Repo, fields ...zap.Field) *customLogger { // return &customLogger{db: db, logger: c.logger.With(fields...)} //} var _ Meta = (*meta)(nil) // Meta key-value type Meta interface { Key() string Value() interface{} meta() } type meta struct { key string value interface{} } func (m *meta) Key() string { return m.key } func (m *meta) Value() interface{} { return m.value } func (m *meta) meta() {} // NewMeta create meat func NewMeta(key string, value interface{}) 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 }