Files
huya-go/internal/logger/entry.go
T

180 lines
4.7 KiB
Go

// Package logger 提供全应用共用的结构化日志。
//
// 设计要点(多开场景下尤其重要):
// - 每个进程启动时创建一个独占的日志文件(O_EXCL + 自增后缀),
// 多个实例同时运行也不会写进同一个文件;
// - 文件异步写入且队列有界,队列满时丢弃日志,避免磁盘 I/O 阻塞抢购主链路;
// - 内存环形缓冲带自增 ID,界面可以按 ID 增量拉取;
// - 条目带分类与账号标签,便于按账号观察抢购链路。
package logger
import (
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
)
// 日志级别。
const (
LevelDebug = "debug"
LevelInfo = "info"
LevelSuccess = "success"
LevelWarn = "warn"
LevelError = "error"
)
const (
entryTimeLayout = "15:04:05.000"
lineTimeLayout = "2006-01-02 15:04:05.000"
)
// Entry 是一条结构化日志。
type Entry struct {
ID int64 `json:"id"`
Time string `json:"time"`
Level string `json:"level"`
Category string `json:"category"`
Account string `json:"account"`
Message string `json:"message"`
// at 只用于落盘,保证文件里带完整日期(不参与 JSON 序列化)。
at time.Time
}
var minLevel atomic.Int32
var (
emitterMu sync.RWMutex
emitter func(Entry)
)
// SetMinLevel 设置日志最低输出等级,低于该等级的日志(例如逐条网络请求的 debug)
// 不会进入内存缓冲,也不会落盘。
func SetMinLevel(level string) {
minLevel.Store(int32(levelPriority(level)))
}
// MinLevel 返回当前最低输出等级名。
func MinLevel() string {
switch minLevel.Load() {
case -1:
return LevelDebug
case 1:
return LevelWarn
case 2:
return LevelError
default:
return LevelInfo
}
}
// SetEmitter 注册日志推送回调(界面用来实时显示日志)。传 nil 表示取消。
func SetEmitter(fn func(Entry)) {
emitterMu.Lock()
emitter = fn
emitterMu.Unlock()
}
func currentEmitter() func(Entry) {
emitterMu.RLock()
defer emitterMu.RUnlock()
return emitter
}
func levelPriority(level string) int {
switch strings.ToLower(strings.TrimSpace(level)) {
case LevelDebug:
return -1
case LevelWarn:
return 1
case LevelError:
return 2
case "", LevelInfo, LevelSuccess:
return 0
default:
return 0
}
}
func normalizeLevel(level string) string {
switch strings.ToLower(strings.TrimSpace(level)) {
case LevelDebug:
return LevelDebug
case LevelWarn:
return LevelWarn
case LevelError:
return LevelError
case LevelSuccess:
return LevelSuccess
default:
return LevelInfo
}
}
// LogEvent 输出带分类和级别的结构化日志。
func LogEvent(category, level, format string, args ...any) {
logEntry(category, level, "", format, args...)
}
// LogAccountEvent 输出带账号标签的结构化日志,便于界面按账号观察抢购链路。
func LogAccountEvent(category, level, account string, format string, args ...any) {
logEntry(category, level, account, format, args...)
}
func logEntry(category, level, account, format string, args ...any) {
level = normalizeLevel(level)
if levelPriority(level) < int(minLevel.Load()) {
return
}
if category = strings.TrimSpace(category); category == "" {
category = "系统"
}
now := time.Now()
entry := Entry{
Time: now.Format(entryTimeLayout),
Level: level,
Category: category,
Account: account,
Message: fmt.Sprintf(format, args...),
at: now,
}
if current := currentSink(); current != nil {
current.write(entry)
}
}
// formatLogLine 把条目渲染成落盘的一行文本。
func formatLogLine(entry Entry) string {
at := entry.at
if at.IsZero() {
at = time.Now()
}
prefix := at.Format(lineTimeLayout)
if entry.Account != "" {
return fmt.Sprintf("%s [%s][%s][账号:%s] %s\n", prefix, entry.Category, entry.Level, entry.Account, entry.Message)
}
return fmt.Sprintf("%s [%s][%s] %s\n", prefix, entry.Category, entry.Level, entry.Message)
}
// logFileHeader 说明日志格式与运行上下文,多实例日志汇总后仍能分清来源。
func logFileHeader(instanceName, instanceID, dataDir string) string {
if instanceName == "" {
instanceName = "未知实例"
}
if instanceID == "" {
instanceID = "unknown"
}
var b strings.Builder
b.WriteString("==================================================================\n")
fmt.Fprintf(&b, "实例: %s (%s)\n", instanceName, instanceID)
if dataDir != "" {
fmt.Fprintf(&b, "数据目录: %s\n", dataDir)
}
b.WriteString("时间格式: YYYY-MM-DD HH:MM:SS.mmm [分类][级别][账号] 正文\n")
b.WriteString("逐条网络请求为 debug 级,默认不显示;排查时把日志等级改成 debug\n")
b.WriteString("==================================================================\n")
return b.String()
}