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 } // logFileWriter 同时写入文件、内存和终端。 // 终端写入失败时静默忽略(Windows GUI 应用无控制台)。 type logFileWriter struct { file *os.File store *MemoryLogStore } func (w *logFileWriter) Write(p []byte) (int, error) { // 写入文件 if _, err := w.file.Write(p); err != nil { return 0, err } // 写入内存 if _, err := w.store.Write(p); err != nil { return 0, err } // 尝试写入终端(失败时忽略,Windows GUI 应用无控制台) _, _ = os.Stdout.Write(p) return len(p), nil } // InitLogging 初始化日志输出:日志文件和内存缓冲同时写入。 // 终端输出通过 logFileWriter 同步写入(Windows GUI 应用无控制台时自动跳过)。 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(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(store, 2048) log.SetOutput(async) log.Printf("日志文件创建失败,仅写入内存: %v", err) return func() { _ = async.Close() } } store.SetFilePath(filePath) // 使用 logFileWriter 同时写入文件、内存和终端(终端失败时静默忽略) multi := &logFileWriter{file: file, store: store} async := NewAsyncLogWriter(multi, 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" } }