Files
affiliate_dash/backend/internal/pkg/applog/applog.go
T
yml2213 44ec142395 style: 清理 gofmt 偏差
纯对齐与行尾空行调整,无逻辑变更。gofmt -l 现输出为空,
配合 make fmt 可将格式检查直接纳入 CI。
2026-10-06 19:43:43 +08:00

276 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package applog
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm/logger"
)
// Level 定义日志级别
type Level int32
const (
LevelDebug Level = iota
LevelInfo
LevelWarn
LevelError
)
func (l Level) String() string {
switch l {
case LevelDebug:
return "DEBUG"
case LevelInfo:
return "INFO"
case LevelWarn:
return "WARN"
case LevelError:
return "ERROR"
default:
return "INFO"
}
}
// ParseLevel 将字符串解析为日志级别,默认 INFO
func ParseLevel(s string) Level {
switch strings.ToLower(strings.TrimSpace(s)) {
case "debug":
return LevelDebug
case "warn", "warning":
return LevelWarn
case "error":
return LevelError
case "info":
return LevelInfo
default:
return LevelInfo
}
}
var currentLevel int32 = int32(LevelInfo)
// SetLevel 设置全局日志过滤级别
func SetLevel(lvl string) {
parsed := ParseLevel(lvl)
atomic.StoreInt32(&currentLevel, int32(parsed))
}
// GetLevel 获取当前日志级别
func GetLevel() Level {
return Level(atomic.LoadInt32(&currentLevel))
}
// IsDebug 是否允许输出 DEBUG 级别日志
func IsDebug() bool {
return GetLevel() <= LevelDebug
}
// IsInfo 是否允许输出 INFO 级别日志
func IsInfo() bool {
return GetLevel() <= LevelInfo
}
// IsWarn 是否允许输出 WARN 级别日志
func IsWarn() bool {
return GetLevel() <= LevelWarn
}
// Debugf 输出 DEBUG 级别日志
func Debugf(format string, args ...any) {
if GetLevel() > LevelDebug {
return
}
log.Printf("[DEBUG] "+format, args...)
}
// Infof 输出 INFO 级别日志
func Infof(format string, args ...any) {
if GetLevel() > LevelInfo {
return
}
log.Printf("[INFO] "+format, args...)
}
// Warnf 输出 WARN 级别日志
func Warnf(format string, args ...any) {
if GetLevel() > LevelWarn {
return
}
log.Printf("[WARN] "+format, args...)
}
// Errorf 输出 ERROR 级别日志
func Errorf(format string, args ...any) {
if GetLevel() > LevelError {
return
}
log.Printf("[ERROR] "+format, args...)
}
// 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 的 ANSI 彩色输出,避免在日志文件中留下乱码转义字符
gin.DisableConsoleColor()
gin.DefaultWriter = mw
gin.DefaultErrorWriter = mw
if logFile != "" {
abs, _ := filepath.Abs(logFile)
Infof("[applog] 日志按天分割输出到 %s-YYYYMMDD.log(保留 %d 天,级别: %s)",
strings.TrimSuffix(abs, filepath.Ext(abs)), logRetainDays, GetLevel())
}
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,
},
)
}