package log import ( "fmt" "os" "path/filepath" "regexp" "strings" "sync" "time" ) // DateRotator 日期轮转日志 type DateRotator struct { // 基本配置 Filename string // 日志文件路径模板,如 "./logs/app-%s.log" MaxSize int // 单个文件最大大小,单位MB MaxBackups int // 最多保留多少个备份 MaxAge int // 文件最多保存多少天 Compress bool // 是否压缩 LogFormat string // 日期格式,如 "2006-01-02" // 内部状态 mu sync.Mutex // 保护以下字段 file *os.File // 当前日志文件 currentDate string // 当前日期 size int64 // 当前文件大小 } // NewDateRotator 创建一个基于日期轮转的日志器 func NewDateRotator(filename string, maxSize, maxBackups, maxAge int, compress bool, logFormat string) *DateRotator { if logFormat == "" { logFormat = "2006-01-02" // 默认按天轮转 } r := &DateRotator{ Filename: filename, MaxSize: maxSize, MaxBackups: maxBackups, MaxAge: maxAge, Compress: compress, LogFormat: logFormat, currentDate: time.Now().Format(logFormat), } return r } // Write 写入日志,同时检查是否需要轮转 func (r *DateRotator) Write(p []byte) (n int, err error) { r.mu.Lock() defer r.mu.Unlock() // 检查日期是否变化 currentDate := time.Now().Format(r.LogFormat) if r.file == nil || currentDate != r.currentDate { if err := r.rotate(currentDate); err != nil { return 0, err } } // 检查文件大小是否超限 if r.size+int64(len(p)) >= int64(r.MaxSize*1024*1024) { if err := r.rotate(currentDate); err != nil { return 0, err } } // 写入日志 n, err = r.file.Write(p) r.size += int64(n) return n, err } // Close 关闭当前日志文件 func (r *DateRotator) Close() error { r.mu.Lock() defer r.mu.Unlock() if r.file != nil { err := r.file.Close() r.file = nil return err } return nil } // rotate 轮转日志文件 func (r *DateRotator) rotate(newDate string) error { // 关闭旧文件 if r.file != nil { r.file.Close() r.file = nil } // 更新日期 r.currentDate = newDate // 根据文件名格式更新日期部分 var filename string if strings.Contains(r.Filename, "%s") { // 如果文件名中有格式化占位符,直接使用新日期替换 filename = fmt.Sprintf(r.Filename, newDate) } else { // 如果没有占位符,则检查文件名是否已经包含日期格式 ext := filepath.Ext(r.Filename) base := r.Filename[:len(r.Filename)-len(ext)] // 如果文件名中已经有日期格式(如api-2025-05-26.log),则替换日期部分 datePattern := regexp.MustCompile(`-\d{4}-\d{2}-\d{2}`) if datePattern.MatchString(base) { base = datePattern.ReplaceAllString(base, "-"+newDate) filename = base + ext } else { // 否则在扩展名前添加日期 filename = base + "-" + newDate + ext } } // 确保目录存在 dir := filepath.Dir(filename) if err := os.MkdirAll(dir, 0755); err != nil { return err } // 打开新文件 f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { return err } // 获取文件大小 info, err := f.Stat() if err != nil { f.Close() return err } // 更新状态 r.file = f r.size = info.Size() // 清理旧文件(异步) go r.cleanup() return nil } // cleanup 清理旧日志文件 func (r *DateRotator) cleanup() { // 实现基于日期和数量的旧文件清理 if r.MaxAge <= 0 && r.MaxBackups <= 0 { return } pattern := "" dir := filepath.Dir(r.Filename) base := filepath.Base(r.Filename) ext := filepath.Ext(base) prefix := base[:len(base)-len(ext)] // 构造通配符模式 pattern = filepath.Join(dir, prefix+"*"+ext) // 查找匹配的文件 matches, err := filepath.Glob(pattern) if err != nil { return } // 按修改时间排序 type fileInfo struct { path string modTime time.Time } var files []fileInfo for _, match := range matches { info, err := os.Stat(match) if err != nil { continue } files = append(files, fileInfo{match, info.ModTime()}) } // 按修改时间从新到旧排序 sort := func(i, j int) bool { return files[i].modTime.After(files[j].modTime) } for i := 0; i < len(files)-1; i++ { for j := i + 1; j < len(files); j++ { if !sort(i, j) { files[i], files[j] = files[j], files[i] } } } // 根据保留数量删除旧文件 if r.MaxBackups > 0 && len(files) > r.MaxBackups { for i := r.MaxBackups; i < len(files); i++ { os.Remove(files[i].path) } files = files[:r.MaxBackups] } // 根据保留天数删除旧文件 if r.MaxAge > 0 { cutoff := time.Now().Add(-time.Duration(r.MaxAge) * 24 * time.Hour) for _, f := range files { if f.modTime.Before(cutoff) { os.Remove(f.path) } } } } // Sync 将缓存数据同步到磁盘 func (r *DateRotator) Sync() error { r.mu.Lock() defer r.mu.Unlock() if r.file != nil { return r.file.Sync() } return nil }