feat(log): 全面重构日志系统,支持日志级别、去除ANSI乱码并抑制高频轮询
This commit is contained in:
@@ -8,12 +8,114 @@ import (
|
||||
"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
|
||||
|
||||
@@ -142,13 +244,15 @@ func Setup(logFile string) (*os.File, error) {
|
||||
log.SetOutput(mw)
|
||||
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
||||
|
||||
// Gin access / recovery 日志
|
||||
// 禁用 Gin 的 ANSI 彩色输出,避免在日志文件中留下乱码转义字符
|
||||
gin.DisableConsoleColor()
|
||||
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)
|
||||
Infof("[applog] 日志按天分割输出到 %s-YYYYMMDD.log(保留 %d 天,级别: %s)",
|
||||
strings.TrimSuffix(abs, filepath.Ext(abs)), logRetainDays, GetLevel())
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
@@ -169,3 +273,4 @@ func NewGormLogger(mode string) logger.Interface {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,47 +1,74 @@
|
||||
package applog
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"bytes"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDailyFileWriterRotatesAndCleans(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
logFile := filepath.Join(dir, "app.log")
|
||||
|
||||
w, err := newDailyFileWriter(logFile)
|
||||
if err != nil {
|
||||
t.Fatalf("new writer: %v", err)
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
// 写入当天日志,文件应按 app-YYYYMMDD.log 命名
|
||||
today := time.Now().Format("20060102")
|
||||
if _, err := w.Write([]byte("line1\n")); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
todayPath := filepath.Join(dir, "app-"+today+".log")
|
||||
if _, err := os.Stat(todayPath); err != nil {
|
||||
t.Fatalf("today log file missing: %v", err)
|
||||
}
|
||||
// 旧格式 app.log 不应被创建
|
||||
if _, err := os.Stat(logFile); err == nil {
|
||||
t.Fatalf("old-style app.log should not be created")
|
||||
func TestParseLevel(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected Level
|
||||
}{
|
||||
{"debug", LevelDebug},
|
||||
{"DEBUG", LevelDebug},
|
||||
{"info", LevelInfo},
|
||||
{"INFO", LevelInfo},
|
||||
{"warn", LevelWarn},
|
||||
{"warning", LevelWarn},
|
||||
{"WARN", LevelWarn},
|
||||
{"error", LevelError},
|
||||
{"ERROR", LevelError},
|
||||
{"unknown", LevelInfo},
|
||||
{"", LevelInfo},
|
||||
}
|
||||
|
||||
// 构造过期文件,cleanup 应删除(保留 30 天,这里用 3 天验证)
|
||||
old := filepath.Join(dir, "app-20260701.log")
|
||||
if err := os.WriteFile(old, []byte("old"), 0o644); err != nil {
|
||||
t.Fatalf("create old file: %v", err)
|
||||
}
|
||||
w.cleanup(time.Now())
|
||||
if _, err := os.Stat(old); err == nil {
|
||||
t.Fatalf("old log file should be cleaned")
|
||||
}
|
||||
// 当天的文件应保留
|
||||
if _, err := os.Stat(todayPath); err != nil {
|
||||
t.Fatalf("today log file should be kept")
|
||||
for _, tc := range tests {
|
||||
got := ParseLevel(tc.input)
|
||||
if got != tc.expected {
|
||||
t.Errorf("ParseLevel(%q) = %v, want %v", tc.input, got, tc.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogLevelFiltering(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
log.SetOutput(&buf)
|
||||
defer log.SetOutput(log.Writer())
|
||||
|
||||
SetLevel("warn")
|
||||
|
||||
buf.Reset()
|
||||
Debugf("this is debug: %d", 1)
|
||||
if buf.Len() > 0 {
|
||||
t.Errorf("expected debug to be filtered out, got %q", buf.String())
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
Infof("this is info: %d", 2)
|
||||
if buf.Len() > 0 {
|
||||
t.Errorf("expected info to be filtered out, got %q", buf.String())
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
Warnf("this is warn: %d", 3)
|
||||
if !strings.Contains(buf.String(), "[WARN] this is warn: 3") {
|
||||
t.Errorf("expected warn to be logged, got %q", buf.String())
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
Errorf("this is error: %d", 4)
|
||||
if !strings.Contains(buf.String(), "[ERROR] this is error: 4") {
|
||||
t.Errorf("expected error to be logged, got %q", buf.String())
|
||||
}
|
||||
|
||||
// Now switch to debug
|
||||
SetLevel("debug")
|
||||
buf.Reset()
|
||||
Debugf("debug now visible")
|
||||
if !strings.Contains(buf.String(), "[DEBUG] debug now visible") {
|
||||
t.Errorf("expected debug to be logged after SetLevel(debug), got %q", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user