From 507c2bd11ee42a16e78ed025c2eaae630a644e9e Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 25 Apr 2025 12:58:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20zap=E6=97=A5=E5=BF=97=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config-dev.yaml | 13 +++ config-prod.yaml | 13 +++ config/config.go | 1 + config/zap.go | 81 +++++++++++++++++++ core/internal/cutter.go | 161 ++++++++++++++++++++++++++++++++++++++ core/internal/zap_core.go | 68 ++++++++++++++++ core/zap.go | 44 +++++++++++ global/global.go | 2 + log/2025-04-25/debug.log | 2 + log/2025-04-25/error.log | 4 + log/2025-04-25/info.log | 5 ++ main.go | 13 ++- utils/directory.go | 20 +++++ 13 files changed, 424 insertions(+), 3 deletions(-) create mode 100644 config/zap.go create mode 100644 core/internal/cutter.go create mode 100644 core/internal/zap_core.go create mode 100644 core/zap.go create mode 100644 log/2025-04-25/debug.log create mode 100644 log/2025-04-25/error.log create mode 100644 log/2025-04-25/info.log create mode 100644 utils/directory.go diff --git a/config-dev.yaml b/config-dev.yaml index 2cde784..ad78550 100644 --- a/config-dev.yaml +++ b/config-dev.yaml @@ -7,3 +7,16 @@ redis: host: 127.0.0.1 password: sundynix port: 6379 + + + +zap: + director: log + encode-level: LowercaseColorLevelEncoder + format: console + level: debug + log-in-console: true + prefix: '[sundynix-go]' + retention-day: 5 + show-line: true + stacktrace-key: stacktrace \ No newline at end of file diff --git a/config-prod.yaml b/config-prod.yaml index 3102034..f88f4b1 100644 --- a/config-prod.yaml +++ b/config-prod.yaml @@ -7,3 +7,16 @@ redis: host: 192.168.100.127 password: sundynix port: 6379 + + +# zap日志配置 +zap: + director: log + encode-level: LowercaseColorLevelEncoder + format: console + level: info + log-in-console: true + prefix: '[sundynix-go]' + retention-day: -1 + show-line: true + stacktrace-key: stacktrace \ No newline at end of file diff --git a/config/config.go b/config/config.go index 23a026f..b6f9437 100644 --- a/config/config.go +++ b/config/config.go @@ -3,4 +3,5 @@ package config type Server struct { Mysql Mysql `mapstructure:"mysql" json:"mysql" yaml:"mysql"` Redis Redis `mapstructure:"redis" json:"redis" yaml:"redis"` + Zap Zap `mapstructure:"zap" json:"zap" yaml:"zap"` } diff --git a/config/zap.go b/config/zap.go new file mode 100644 index 0000000..6ee8b8b --- /dev/null +++ b/config/zap.go @@ -0,0 +1,81 @@ +package config + +import ( + "go.uber.org/zap/zapcore" + "time" +) + +type Zap struct { + Level string `mapstructure:"level" json:"level" yaml:"level"` + Prefix string `mapstructure:"prefix" json:"prefix" yaml:"prefix"` + Format string `mapstructure:"format" json:"format" yaml:"format"` + Director string `mapstructure:"director" json:"director" yaml:"director"` + EncodeLevel string `mapstructure:"encode-level" json:"encode-level" yaml:"encode-level"` + StacktraceKey string `mapstructure:"stacktrace-key" json:"stacktrace-key" yaml:"stacktrace-key"` + ShowLine bool `mapstructure:"show-line" json:"show-line" yaml:"show-line"` + LogInConsole bool `mapstructure:"log-in-console" json:"log-in-console" yaml:"log-in-console"` + RetentionDay int `mapstructure:"retention-day" json:"retention-day" yaml:"retention-day"` +} + +// Levels 返回一个基于 Zap 实例中配置的日志级别的 zapcore.Level 切片。 +// 该切片从配置的日志级别开始,包含所有更高严重性级别,直到 FatalLevel。 +// 如果无法解析配置的日志级别,则默认使用 DebugLevel。 +// +// 返回值: +// - []zapcore.Level: 包含从配置的日志级别(或解析失败时的 DebugLevel)到 FatalLevel 的所有日志级别的切片。 +func (c *Zap) Levels() []zapcore.Level { + // 初始化一个容量为 7 的空切片,用于存储日志级别。 + levels := make([]zapcore.Level, 0, 7) + + // 解析配置的日志级别。如果解析失败,则默认使用 DebugLevel。 + level, err := zapcore.ParseLevel(c.Level) + if err != nil { + level = zapcore.DebugLevel + } + + // 从解析的(或默认的)日志级别开始,迭代到 FatalLevel,并将每个级别追加到切片中。 + for ; level <= zapcore.FatalLevel; level++ { + levels = append(levels, level) + } + + // 返回填充好的日志级别切片 + return levels +} + +// Encoder 返回一个 zapcore.Encoder,用于编码日志记录。 +func (c *Zap) Encoder() zapcore.Encoder { + config := zapcore.EncoderConfig{ + TimeKey: "time", + NameKey: "name", + LevelKey: "level", + CallerKey: "caller", + MessageKey: "message", + StacktraceKey: c.StacktraceKey, + LineEnding: zapcore.DefaultLineEnding, + EncodeTime: func(t time.Time, encoder zapcore.PrimitiveArrayEncoder) { + encoder.AppendString(c.Prefix + t.Format("2006-01-02 15:04:05")) + }, + EncodeLevel: c.LevelEncoder(), + EncodeCaller: zapcore.FullCallerEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + } + if c.Format == "json" { + return zapcore.NewJSONEncoder(config) + } + return zapcore.NewConsoleEncoder(config) +} + +func (c *Zap) LevelEncoder() zapcore.LevelEncoder { + switch { + case c.EncodeLevel == "LowercaseLevelEncoder": + return zapcore.LowercaseLevelEncoder + case c.EncodeLevel == "LowercaseColorLevelEncoder": + return zapcore.LowercaseColorLevelEncoder + case c.EncodeLevel == "CapitalLevelEncoder": + return zapcore.CapitalLevelEncoder + case c.EncodeLevel == "CapitalColorLevelEncoder": + return zapcore.CapitalColorLevelEncoder + default: + return zapcore.LowercaseLevelEncoder + } +} diff --git a/core/internal/cutter.go b/core/internal/cutter.go new file mode 100644 index 0000000..4eaa3ad --- /dev/null +++ b/core/internal/cutter.go @@ -0,0 +1,161 @@ +package internal + +import ( + "os" + "path/filepath" + "sync" + "time" +) + +type Cutter struct { + level string // 日志级别 + layout string //时间格式 + formats []string //自定义参数 + director string //日志文件夹 + retentionDay int //保留天数 + file *os.File //文件 + mutex *sync.RWMutex // 读写锁 +} + +type CutterOption func(c *Cutter) + +// 设置时间格式 +func CutterWithLayout(layout string) CutterOption { + return func(c *Cutter) { + c.layout = layout + } +} + +// 格式化参数 +func CutterWithFormats(format ...string) CutterOption { + return func(c *Cutter) { + if len(format) > 0 { + c.formats = format + } + } +} + +// NewCutter 创建一个新的 Cutter 实例,用于管理日志文件的切割和保留。 +// +// 参数: +// - directory: 日志文件存储的目录路径。 +// - level: 日志级别,用于标识日志的严重程度。 +// - retentionDay: 日志文件保留的天数,超过该天数的日志文件将被删除。 +// - options: 可选的 CutterOption 函数,用于对 Cutter 实例进行额外的配置。 +// +// 返回值: +// - *Cutter: 返回一个初始化后的 Cutter 实例。 +func NewCutter(directory string, level string, retentionDay int, options ...CutterOption) *Cutter { + // 初始化 Cutter 实例,设置日志级别、目录、保留天数以及互斥锁 + rotate := &Cutter{ + level: level, + director: directory, + retentionDay: retentionDay, + mutex: new(sync.RWMutex), + } + + // 应用所有传入的 CutterOption 配置函数 + for i := 0; i < len(options); i++ { + options[i](rotate) + } + + return rotate +} + +// Write 方法将给定的字节数据写入到日志文件中。该方法会确保日志文件的目录存在,并根据配置的格式生成文件名。 +// 如果日志文件已经存在,数据将被追加到文件末尾。如果文件不存在,则会创建新文件。 +// 该方法还会定期清理超过保留天数的日志文件夹。 +// +// 参数: +// - bytes: 要写入的字节数据。 +// +// 返回值: +// - n: 成功写入的字节数。 +// - err: 如果发生错误,返回错误信息;否则返回 nil。 +func (c *Cutter) Write(bytes []byte) (n int, err error) { + // 加锁以确保并发安全 + c.mutex.Lock() + defer func() { + // 在函数结束时关闭文件并释放锁 + if c.file != nil { + _ = c.file.Close() + c.file = nil + } + c.mutex.Unlock() + }() + + // 生成日志文件名 + length := len(c.formats) + values := make([]string, 0, 3+length) + values = append(values, c.director) + if c.layout != "" { + values = append(values, time.Now().Format(c.layout)) + } + for i := 0; i < length; i++ { + values = append(values, c.formats[i]) + } + values = append(values, c.level+".log") + filename := filepath.Join(values...) + + // 确保日志文件所在的目录存在 + directory := filepath.Dir(filename) + err = os.MkdirAll(directory, os.ModePerm) + if err != nil { + return 0, nil + } + + // 清理超过保留天数的日志文件夹 + err = removeNDaysFolders(c.director, c.retentionDay) + if err != nil { + return 0, err + } + + // 打开或创建日志文件,并追加写入数据 + c.file, err = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return 0, err + } + + // 将数据写入文件并返回写入的字节数 + return c.file.Write(bytes) +} + +// Sync 方法用于将当前文件的内容同步到磁盘,确保所有缓冲区的数据都写入磁盘。 +// 该方法在调用时会先获取互斥锁,以确保在同步过程中不会有其他操作干扰。 +// 如果当前 Cutter 实例中的文件对象不为 nil,则调用文件对象的 Sync 方法进行同步操作。 +// 如果文件对象为 nil,则直接返回 nil,表示无需同步。 +// +// 返回值: +// - error: 如果同步过程中发生错误,则返回该错误;否则返回 nil。 +func (c *Cutter) Sync() error { + c.mutex.Lock() + defer c.mutex.Unlock() + + // 如果文件对象存在,则调用其 Sync 方法进行同步 + if c.file != nil { + return c.file.Sync() + } + + // 文件对象不存在,直接返回 nil + return nil +} + +// removeNDaysFolders 删除指定目录下,指定天数前的文件夹 +func removeNDaysFolders(dir string, days int) error { + if days <= 0 { + return nil + } + cutoff := time.Now().AddDate(0, 0, -days) + return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() && info.ModTime().Before(cutoff) && path != dir { + err = os.RemoveAll(path) + if err != nil { + return err + } + } + return nil + }) +} diff --git a/core/internal/zap_core.go b/core/internal/zap_core.go new file mode 100644 index 0000000..9121ebe --- /dev/null +++ b/core/internal/zap_core.go @@ -0,0 +1,68 @@ +package internal + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "os" + "sundynix-go/global" + "time" +) + +type ZapCore struct { + level zapcore.Level + zapcore.Core +} + +// NewZapCore 创建一个 zapcore.Core +func NewZapCore(level zapcore.Level) *ZapCore { + entity := &ZapCore{level: level} + syncer := entity.WriteSyncer() + levelEnabler := zap.LevelEnablerFunc(func(l zapcore.Level) bool { return l == level }) + entity.Core = zapcore.NewCore(global.Config.Zap.Encoder(), syncer, levelEnabler) + return entity +} + +// WriteSyncer 创建一个 zapcore.WriteSyncer +func (z *ZapCore) WriteSyncer(formats ...string) zapcore.WriteSyncer { + cutter := NewCutter( + global.Config.Zap.Director, + z.level.String(), + global.Config.Zap.RetentionDay, + CutterWithLayout(time.DateOnly), + CutterWithFormats(formats...), + ) + if global.Config.Zap.LogInConsole { + multiSyncer := zapcore.NewMultiWriteSyncer(os.Stdout, cutter) + return zapcore.AddSync(multiSyncer) + } + return zapcore.AddSync(cutter) +} + +func (z *ZapCore) Enabled(level zapcore.Level) bool { + return z.level == level +} + +func (z *ZapCore) With(fields []zapcore.Field) zapcore.Core { + return z.Core.With(fields) +} + +func (z *ZapCore) Check(entry zapcore.Entry, check *zapcore.CheckedEntry) *zapcore.CheckedEntry { + if z.Enabled(entry.Level) { + return check.AddCore(entry, z) + } + return check +} + +func (z *ZapCore) Write(entry zapcore.Entry, fields []zapcore.Field) error { + for i := 0; i < len(fields); i++ { + if fields[i].Key == "business" || fields[i].Key == "folder" || fields[i].Key == "directory" { + syncer := z.WriteSyncer(fields[i].String) + z.Core = zapcore.NewCore(global.Config.Zap.Encoder(), syncer, z.level) + } + } + return z.Core.Write(entry, fields) +} + +func (z *ZapCore) Sync() error { + return z.Core.Sync() +} diff --git a/core/zap.go b/core/zap.go new file mode 100644 index 0000000..c31117f --- /dev/null +++ b/core/zap.go @@ -0,0 +1,44 @@ +package core + +import ( + "fmt" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "os" + "sundynix-go/core/internal" + "sundynix-go/global" + "sundynix-go/utils" +) + +// Zap 函数用于初始化并返回一个 zap.Logger 实例。 +// 该函数会检查日志目录是否存在,如果不存在则创建该目录。 +// 根据配置中的日志级别,创建对应的 zapcore.Core,并将它们合并为一个 zap.Logger。 +// 如果配置中启用了显示行号,则会在日志中添加调用者信息。 +// 返回值: +// - logger: 初始化后的 zap.Logger 实例,用于记录日志。 +func Zap() (logger *zap.Logger) { + // 检查日志目录是否存在,如果不存在则创建 + if ok, _ := utils.PathExist(global.Config.Zap.Director); !ok { + fmt.Printf("日志目录 %v 不存在,创建中...\n", global.Config.Zap.Director) + _ = os.Mkdir(global.Config.Zap.Director, os.ModePerm) + } + + // 获取配置中的日志级别,并初始化对应的 zapcore.Core + levels := global.Config.Zap.Levels() + length := len(levels) + cores := make([]zapcore.Core, 0, length) + for i := 0; i < length; i++ { + core := internal.NewZapCore(levels[i]) + cores = append(cores, core) + } + + // 将所有的 zapcore.Core 合并为一个 zap.Logger + logger = zap.New(zapcore.NewTee(cores...)) + + // 如果配置中启用了显示行号,则添加调用者信息 + if global.Config.Zap.ShowLine { + logger = logger.WithOptions(zap.AddCaller()) + } + + return logger +} diff --git a/global/global.go b/global/global.go index 93c99ac..61212da 100644 --- a/global/global.go +++ b/global/global.go @@ -2,11 +2,13 @@ package global import ( "github.com/spf13/viper" + "go.uber.org/zap" "sundynix-go/config" ) // 全局变量 加载在内存中 var ( Viper *viper.Viper + Logger *zap.Logger Config *config.Server ) diff --git a/log/2025-04-25/debug.log b/log/2025-04-25/debug.log new file mode 100644 index 0000000..0143c27 --- /dev/null +++ b/log/2025-04-25/debug.log @@ -0,0 +1,2 @@ +2025-04-25 12:55:18 debug /Users/blizzard/sourceCode/GolandProjects/src/sundynix-go/main.go:18 this is debug log +[sundynix-go]2025-04-25 12:57:15 debug /Users/blizzard/sourceCode/GolandProjects/src/sundynix-go/main.go:18 this is debug log diff --git a/log/2025-04-25/error.log b/log/2025-04-25/error.log new file mode 100644 index 0000000..f94780e --- /dev/null +++ b/log/2025-04-25/error.log @@ -0,0 +1,4 @@ +2025-04-25 12:53:32 error /Users/blizzard/sourceCode/GolandProjects/src/sundynix-go/main.go:18 this is error log +2025-04-25 12:53:43 error /Users/blizzard/sourceCode/GolandProjects/src/sundynix-go/main.go:19 this is error log +2025-04-25 12:55:18 error /Users/blizzard/sourceCode/GolandProjects/src/sundynix-go/main.go:19 this is error log +[sundynix-go]2025-04-25 12:57:15 error /Users/blizzard/sourceCode/GolandProjects/src/sundynix-go/main.go:19 this is error log diff --git a/log/2025-04-25/info.log b/log/2025-04-25/info.log new file mode 100644 index 0000000..4241cfe --- /dev/null +++ b/log/2025-04-25/info.log @@ -0,0 +1,5 @@ +2025-04-25 12:53:14 info /Users/blizzard/sourceCode/GolandProjects/src/sundynix-go/main.go:17 this is debug log +2025-04-25 12:53:32 info /Users/blizzard/sourceCode/GolandProjects/src/sundynix-go/main.go:17 this is debug log +2025-04-25 12:53:43 info /Users/blizzard/sourceCode/GolandProjects/src/sundynix-go/main.go:17 this is debug log +2025-04-25 12:55:18 info /Users/blizzard/sourceCode/GolandProjects/src/sundynix-go/main.go:17 this is debug log +[sundynix-go]2025-04-25 12:57:15 info /Users/blizzard/sourceCode/GolandProjects/src/sundynix-go/main.go:17 this is debug log diff --git a/main.go b/main.go index b71d09a..6fbb3b2 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,7 @@ package main import ( - "fmt" + "go.uber.org/zap" "sundynix-go/core" "sundynix-go/global" ) @@ -9,6 +9,13 @@ import ( func main() { //初始化viper global.Viper = core.Viper() - fmt.Println(global.Config.Mysql) - fmt.Println(global.Config.Redis) + //初始化zap + global.Logger = core.Zap() + //替换zap + zap.ReplaceGlobals(global.Logger) + + global.Logger.Info("this is debug log") + global.Logger.Debug("this is debug log") + global.Logger.Error("this is error log") + } diff --git a/utils/directory.go b/utils/directory.go new file mode 100644 index 0000000..c2bd8ae --- /dev/null +++ b/utils/directory.go @@ -0,0 +1,20 @@ +package utils + +import ( + "errors" + "os" +) + +func PathExist(path string) (bool, error) { + stat, err := os.Stat(path) + if err == nil { + if stat.IsDir() { + return true, nil + } + return false, errors.New("存在同名文件") + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +}