package applog import ( "fmt" "io" "log" "os" "path/filepath" "strings" "sync" "time" "github.com/gin-gonic/gin" "gorm.io/gorm/logger" ) // logRetainDays 保留最近多少天的日志文件;<=0 不清理。 var logRetainDays = 30 // SetRetainDays 设置日志文件保留天数(可在 main 里按配置覆盖)。 func SetRetainDays(days int) { if days > 0 { logRetainDays = days } } // dailyFileWriter 按天轮转的日志 writer。 // 文件命名:<前缀>-20060102<扩展>,如 app-20260803.log;跨天自动切换并清理过期文件。 type dailyFileWriter struct { dir string prefix string ext string mu sync.Mutex current *os.File currentDate string } func newDailyFileWriter(logFile string) (*dailyFileWriter, error) { abs, err := filepath.Abs(logFile) if err != nil { return nil, err } dir := filepath.Dir(abs) if err := os.MkdirAll(dir, 0o755); err != nil { return nil, err } base := filepath.Base(abs) ext := filepath.Ext(base) prefix := strings.TrimSuffix(base, ext) w := &dailyFileWriter{dir: dir, prefix: prefix, ext: ext} if err := w.rotateIfNeeded(time.Now()); err != nil { return nil, err } return w, nil } // pathFor 当天日志文件路径。 func (w *dailyFileWriter) pathFor(t time.Time) string { return filepath.Join(w.dir, fmt.Sprintf("%s-%s%s", w.prefix, t.Format("20060102"), w.ext)) } func (w *dailyFileWriter) rotateIfNeeded(now time.Time) error { date := now.Format("20060102") if w.current != nil && w.currentDate == date { return nil } if w.current != nil { _ = w.current.Close() w.current = nil } f, err := os.OpenFile(w.pathFor(now), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) if err != nil { return err } w.current = f w.currentDate = date go w.cleanup(now) return nil } // cleanup 删除超出保留天数的历史日志文件。 func (w *dailyFileWriter) cleanup(now time.Time) { if logRetainDays <= 0 { return } cutoff := now.AddDate(0, 0, -logRetainDays).Format("20060102") entries, err := os.ReadDir(w.dir) if err != nil { return } for _, entry := range entries { if entry.IsDir() { continue } name := entry.Name() if !strings.HasPrefix(name, w.prefix+"-") || !strings.HasSuffix(name, w.ext) { continue } datePart := strings.TrimSuffix(strings.TrimPrefix(name, w.prefix+"-"), w.ext) if len(datePart) != 8 || datePart >= cutoff { continue } _ = os.Remove(filepath.Join(w.dir, name)) } } func (w *dailyFileWriter) Write(p []byte) (int, error) { w.mu.Lock() defer w.mu.Unlock() if err := w.rotateIfNeeded(time.Now()); err != nil { return 0, err } return w.current.Write(p) } func (w *dailyFileWriter) Close() error { w.mu.Lock() defer w.mu.Unlock() if w.current != nil { err := w.current.Close() w.current = nil return err } return nil } // Setup 将标准库 log、Gin 请求日志同时写到控制台与按天分割的文件。 // logFile 为空则仅控制台。返回文件句柄供进程结束时 Close(可为 nil)。 func Setup(logFile string) (*os.File, error) { writers := []io.Writer{os.Stdout} var f *os.File if logFile != "" { daily, err := newDailyFileWriter(logFile) if err != nil { return nil, err } f = daily.current writers = append(writers, daily) } mw := io.MultiWriter(writers...) log.SetOutput(mw) log.SetFlags(log.LstdFlags | log.Lmicroseconds) // Gin access / recovery 日志 gin.DefaultWriter = mw gin.DefaultErrorWriter = mw if logFile != "" { abs, _ := filepath.Abs(logFile) log.Printf("[applog] 日志按天分割输出到 %s-YYYYMMDD.log(保留 %d 天)", strings.TrimSuffix(abs, filepath.Ext(abs)), logRetainDays) } return f, nil } // NewGormLogger 返回写到当前 log 输出(已含 MultiWriter)的 GORM 日志 func NewGormLogger(mode string) logger.Interface { level := logger.Info if mode == "release" { level = logger.Warn } return logger.New( log.New(log.Writer(), "\r\n", log.LstdFlags), logger.Config{ SlowThreshold: 200 * time.Millisecond, LogLevel: level, IgnoreRecordNotFoundError: true, Colorful: false, }, ) }