276 lines
6.1 KiB
Go
276 lines
6.1 KiB
Go
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(¤tLevel, int32(parsed))
|
||
}
|
||
|
||
// GetLevel 获取当前日志级别
|
||
func GetLevel() Level {
|
||
return Level(atomic.LoadInt32(¤tLevel))
|
||
}
|
||
|
||
// 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,
|
||
},
|
||
)
|
||
}
|