- 新增设置弹窗: trace开关/正文上限、日志等级(debug/info/warn/error)、并发数、重试次数 - 配置运行时生效,持久化到 config.json - 日志系统支持 debug 级别过滤 - 工具栏展示版本号(v0.0.2),来源 wails.json + constants.go - 抢购并发数和重试次数改为读取配置,不再写死常量 - 新增 build.sh 构建脚本,Windows exe 文件名带版本号 - config.json 加入 .gitignore
236 lines
6.0 KiB
Go
236 lines
6.0 KiB
Go
package logger
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"douyu_go/internal/constants"
|
|
)
|
|
|
|
// minLevel 日志最低等级,低于此等级的日志不输出。0=info, 1=warn, 2=error。
|
|
var minLevel atomic.Int32
|
|
|
|
// SetMinLevel 设置日志最低显示等级。
|
|
func SetMinLevel(level string) {
|
|
minLevel.Store(int32(levelPriority(level)))
|
|
}
|
|
|
|
func levelPriority(level string) int {
|
|
switch strings.ToLower(level) {
|
|
case "debug":
|
|
return -1
|
|
case "info":
|
|
return 0
|
|
case "warn":
|
|
return 1
|
|
case "error":
|
|
return 2
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// AsyncLogWriter 异步日志写入器,队列满时丢弃日志以保护主链路。
|
|
type AsyncLogWriter struct {
|
|
dst io.Writer
|
|
ch chan []byte
|
|
wg sync.WaitGroup
|
|
closed uint32
|
|
dropped uint64
|
|
}
|
|
|
|
// NewAsyncLogWriter 创建异步日志写入器。
|
|
func NewAsyncLogWriter(dst io.Writer, buffer int) *AsyncLogWriter {
|
|
if buffer <= 0 {
|
|
buffer = 1024
|
|
}
|
|
w := &AsyncLogWriter{
|
|
dst: dst,
|
|
ch: make(chan []byte, buffer),
|
|
}
|
|
w.wg.Add(1)
|
|
go w.run()
|
|
return w
|
|
}
|
|
|
|
// Write 异步写日志;队列满时丢弃,避免抢购请求被磁盘 I/O 阻塞。
|
|
func (w *AsyncLogWriter) Write(p []byte) (int, error) {
|
|
if w == nil {
|
|
return len(p), nil
|
|
}
|
|
if atomic.LoadUint32(&w.closed) == 1 {
|
|
return len(p), nil
|
|
}
|
|
buf := append([]byte(nil), p...)
|
|
select {
|
|
case w.ch <- buf:
|
|
default:
|
|
atomic.AddUint64(&w.dropped, 1)
|
|
}
|
|
return len(p), nil
|
|
}
|
|
|
|
func (w *AsyncLogWriter) run() {
|
|
defer w.wg.Done()
|
|
for p := range w.ch {
|
|
_, _ = w.dst.Write(p)
|
|
}
|
|
if dropped := atomic.LoadUint64(&w.dropped); dropped > 0 {
|
|
_, _ = fmt.Fprintf(w.dst, "%s [日志][warn] 日志队列已丢弃 %d 条,已优先保护抢购链路\n",
|
|
time.Now().In(constants.CST).Format("2006/01/02 15:04:05.000000"), dropped)
|
|
}
|
|
}
|
|
|
|
// Close 关闭写入器,等待队列清空。
|
|
func (w *AsyncLogWriter) Close() error {
|
|
if w == nil {
|
|
return nil
|
|
}
|
|
if atomic.CompareAndSwapUint32(&w.closed, 0, 1) {
|
|
close(w.ch)
|
|
w.wg.Wait()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// InitLogging 初始化日志输出:终端、日志文件、内存缓冲三处同时写入。
|
|
func InitLogging(store *MemoryLogStore, logDir string) func() {
|
|
if logDir == "" {
|
|
logDir = constants.DefaultLogDir
|
|
}
|
|
|
|
// 解析为绝对路径,避免打包后工作目录不同导致日志写到意外位置
|
|
logDir = resolveAbsolutePath(logDir)
|
|
|
|
if err := os.MkdirAll(logDir, 0755); err != nil {
|
|
async := NewAsyncLogWriter(io.MultiWriter(os.Stdout, store), 2048)
|
|
log.SetOutput(async)
|
|
log.Printf("日志目录创建失败,仅写入终端和内存: %v", err)
|
|
return func() { _ = async.Close() }
|
|
}
|
|
|
|
filePath := filepath.Join(logDir, "douyu_"+time.Now().In(constants.CST).Format("20060102_150405")+".log")
|
|
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
|
if err != nil {
|
|
async := NewAsyncLogWriter(io.MultiWriter(os.Stdout, store), 2048)
|
|
log.SetOutput(async)
|
|
log.Printf("日志文件创建失败,仅写入终端和内存: %v", err)
|
|
return func() { _ = async.Close() }
|
|
}
|
|
|
|
store.SetFilePath(filePath)
|
|
async := NewAsyncLogWriter(io.MultiWriter(os.Stdout, file, store), 2048)
|
|
log.SetOutput(async)
|
|
log.Printf("日志文件已启用: %s", filePath)
|
|
return func() {
|
|
_ = async.Close()
|
|
_ = file.Close()
|
|
}
|
|
}
|
|
|
|
// LogEvent 输出带分类和级别的结构化文本日志。低于当前最低等级的日志会被丢弃。
|
|
func LogEvent(category, level, format string, args ...any) {
|
|
if category == "" {
|
|
category = "系统"
|
|
}
|
|
if level == "" {
|
|
level = "info"
|
|
}
|
|
if levelPriority(level) < int(minLevel.Load()) {
|
|
return
|
|
}
|
|
log.Printf("[%s][%s] %s", category, level, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// ParseLogMeta 从标准日志行中提取类似 [网络][warn] 的分类和级别。
|
|
func ParseLogMeta(line string) (string, string) {
|
|
idx := strings.Index(line, "[")
|
|
if idx < 0 {
|
|
return "", ""
|
|
}
|
|
category, rest, ok := cutBracket(line[idx:])
|
|
if !ok {
|
|
return "", ""
|
|
}
|
|
level, _, ok := cutBracket(rest)
|
|
if !ok || !IsLogLevel(level) {
|
|
return category, ""
|
|
}
|
|
return category, level
|
|
}
|
|
|
|
func cutBracket(s string) (string, string, bool) {
|
|
if !strings.HasPrefix(s, "[") {
|
|
return "", s, false
|
|
}
|
|
end := strings.Index(s, "]")
|
|
if end <= 1 {
|
|
return "", s, false
|
|
}
|
|
return s[1:end], s[end+1:], true
|
|
}
|
|
|
|
// IsLogLevel 判断是否为有效日志级别。
|
|
func IsLogLevel(level string) bool {
|
|
switch strings.ToLower(level) {
|
|
case "success", "error", "warn", "info", "debug":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// LogDebug 输出 debug 级别日志,仅在日志等级设为 debug 时可见。
|
|
func LogDebug(category, format string, args ...any) {
|
|
LogEvent(category, "debug", format, args...)
|
|
}
|
|
|
|
// resolveAbsolutePath 将相对路径解析为绝对路径。
|
|
// 如果路径已经是绝对路径则直接返回;否则基于可执行文件所在目录解析。
|
|
func resolveAbsolutePath(path string) string {
|
|
if filepath.IsAbs(path) {
|
|
return path
|
|
}
|
|
// 先尝试基于当前工作目录解析
|
|
if abs, err := filepath.Abs(path); err == nil {
|
|
return abs
|
|
}
|
|
// 兜底:基于可执行文件所在目录
|
|
if exe, err := os.Executable(); err == nil {
|
|
return filepath.Join(filepath.Dir(exe), path)
|
|
}
|
|
return path
|
|
}
|
|
|
|
// DetectLogLevel 根据日志文本粗略分类,方便前端染色。
|
|
func DetectLogLevel(line string) string {
|
|
lower := strings.ToLower(line)
|
|
switch {
|
|
case strings.Contains(lower, "[debug]"):
|
|
return "debug"
|
|
case strings.Contains(lower, "[success]"):
|
|
return "success"
|
|
case strings.Contains(lower, "[error]"):
|
|
return "error"
|
|
case strings.Contains(lower, "[warn]"):
|
|
return "warn"
|
|
case strings.Contains(lower, "[info]"):
|
|
return "info"
|
|
case strings.Contains(line, "成功"):
|
|
return "success"
|
|
case strings.Contains(line, "失败") || strings.Contains(line, "错误") || strings.Contains(lower, "error") || strings.Contains(lower, "panic"):
|
|
return "error"
|
|
case strings.Contains(line, "降级") || strings.Contains(line, "跳过") || strings.Contains(line, "不存在") || strings.Contains(lower, "warn"):
|
|
return "warn"
|
|
default:
|
|
return "info"
|
|
}
|
|
}
|