Files
affiliate_dash/backend/internal/pkg/applog/applog.go
T
yml2213 5f547546c3 日志按天分割并自动清理:app-YYYYMMDD.log,保留天数可配
- dailyFileWriter 按天轮转:跨天自动切换到新文件(app-20260804.log)
- LOG_RETAIN_DAYS 控制保留天数(默认 30),到期自动删除
- docker-compose 透传 LOG_RETAIN_DAYS,README 更新环境变量说明
- 补充轮转/清理单测
2026-08-04 09:42:20 +08:00

172 lines
4.1 KiB
Go

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,
},
)
}