feat(log): 全面重构日志系统,支持日志级别、去除ANSI乱码并抑制高频轮询

This commit is contained in:
yml2213
2026-09-18 15:04:38 +08:00
parent 20bde97d7a
commit ec76844da8
15 changed files with 415 additions and 100 deletions
+107 -2
View File
@@ -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(&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
@@ -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 {
},
)
}
+64 -37
View File
@@ -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())
}
}
+36 -25
View File
@@ -2,10 +2,11 @@ package openlog
import (
"fmt"
"log"
"strings"
"time"
"affiliate_dash/internal/pkg/applog"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
@@ -45,7 +46,7 @@ var Enabled bool
func Init(enabled bool) {
Enabled = enabled
if enabled {
log.Printf("[open] 开放接口调试日志已开启 (OPEN_API_DEBUG)")
applog.Infof("[open] 开放接口调试日志已开启 (OPEN_API_DEBUG)")
}
}
@@ -104,15 +105,17 @@ func APITypeFromSide(side string) string {
}
func IsDebug(c *gin.Context) bool {
if !Enabled {
return false
if applog.IsDebug() || Enabled {
return true
}
if v, ok := c.Get(CtxDebug); ok {
if b, ok := v.(bool); ok {
return b
if c != nil {
if v, ok := c.Get(CtxDebug); ok {
if b, ok := v.(bool); ok {
return b
}
}
}
return Enabled
return false
}
// MaskKey 脱敏 api_key:保留前缀与后 4 位
@@ -145,7 +148,10 @@ func Truncate(s string, max int) string {
return s[:max] + fmt.Sprintf("...(%d bytes)", len(s))
}
func Prefix(c *gin.Context) string {
func Tag(c *gin.Context) string {
if c == nil {
return "open"
}
side, _ := c.Get(CtxSide)
apiType, _ := c.Get(CtxAPIType)
action, _ := c.Get(CtxAction)
@@ -158,32 +164,37 @@ func Prefix(c *gin.Context) string {
} else if sideText != "" {
tag += "." + sideText
}
if actionText != "" {
if actionText != "" && actionText != ActionUnknown {
tag += "." + actionText
}
fields := ""
if apiTypeText != "" {
fields += fmt.Sprintf(" api_type=%s", apiTypeText)
}
if actionText != "" {
fields += fmt.Sprintf(" action=%s", actionText)
}
return fmt.Sprintf("[%s] req_id=%s%s ", tag, GetReqID(c), fields)
return tag
}
func Info(c *gin.Context, format string, args ...interface{}) {
func Prefix(c *gin.Context) string {
return fmt.Sprintf("[%s] req_id=%s ", Tag(c), GetReqID(c))
}
// Debug 输出开放接口调试日志(高频查询、鉴权成功等),在 DEBUG 级别或 OPEN_API_DEBUG 开启时输出
func Debug(c *gin.Context, format string, args ...interface{}) {
if !IsDebug(c) {
return
}
log.Printf(Prefix(c)+format, args...)
applog.Debugf(Prefix(c)+format, args...)
}
// Info 输出开放接口核心业务生命周期事件日志(下单、发货提交、取消、回调等)
func Info(c *gin.Context, format string, args ...interface{}) {
applog.Infof(Prefix(c)+format, args...)
}
// Warn 输出开放接口业务告警/客户端参数错误日志
func Warn(c *gin.Context, format string, args ...interface{}) {
// 鉴权失败等也值得在 debug 时打出
if !Enabled {
return
}
log.Printf(Prefix(c)+format, args...)
applog.Warnf(Prefix(c)+format, args...)
}
// Error 输出开放接口系统故障或下游服务异常日志
func Error(c *gin.Context, format string, args ...interface{}) {
applog.Errorf(Prefix(c)+format, args...)
}
func Elapsed(c *gin.Context) time.Duration {
+1 -1
View File
@@ -15,7 +15,7 @@ func TestPrefixIncludesSideAndAction(t *testing.T) {
SetScope(c, SideSource, ActionPush)
got := Prefix(c)
want := "[open.source_open_v1.push] req_id=req123 api_type=source_open_v1 action=push "
want := "[open.source_open_v1.push] req_id=req123 "
if got != want {
t.Fatalf("prefix mismatch: got %q want %q", got, want)
}