Files
dygou_go/log_store.go
T
2026-06-19 13:02:28 +08:00

240 lines
5.8 KiB
Go

package main
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"sync/atomic"
"time"
)
// newMemoryLogStore 创建内存日志缓冲,最多保留 max 条最新日志。
func newMemoryLogStore(max int) *memoryLogStore {
if max <= 0 {
max = 500
}
return &memoryLogStore{
max: max,
entries: make([]LogEntry, 0, max),
}
}
// initLogging 初始化日志输出:终端、日志文件、内存缓冲三处同时写入。
func initLogging(store *memoryLogStore, logDir string) func() {
if logDir == "" {
logDir = defaultLogDir
}
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(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()
}
}
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(cst).Format("2006/01/02 15:04:05.000000"), dropped)
}
}
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
}
// Write 实现 io.Writer,把标准 log 输出转存为内存日志。
func (s *memoryLogStore) Write(p []byte) (int, error) {
text := strings.TrimRight(string(p), "\r\n")
if text == "" {
return len(p), nil
}
s.mu.Lock()
defer s.mu.Unlock()
for _, line := range strings.Split(text, "\n") {
line = strings.TrimRight(line, "\r")
if strings.TrimSpace(line) == "" {
continue
}
category, level := parseLogMeta(line)
if level == "" {
level = detectLogLevel(line)
}
s.nextID++
s.entries = append(s.entries, LogEntry{
ID: s.nextID,
Time: time.Now().In(cst).Format("2006-01-02 15:04:05.000"),
Level: level,
Category: category,
Message: line,
})
if len(s.entries) > s.max {
copy(s.entries, s.entries[len(s.entries)-s.max:])
s.entries = s.entries[:s.max]
}
}
return len(p), nil
}
// logEvent 输出带分类和级别的结构化文本日志。
func logEvent(category, level, format string, args ...any) {
if category == "" {
category = "系统"
}
if level == "" {
level = "info"
}
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
}
func isLogLevel(level string) bool {
switch strings.ToLower(level) {
case "success", "error", "warn", "info":
return true
default:
return false
}
}
// setFilePath 保存当前日志文件路径,供前端展示或排查使用。
func (s *memoryLogStore) setFilePath(path string) {
s.mu.Lock()
defer s.mu.Unlock()
s.filePath = path
}
// Since 返回 ID 大于 since 的日志。
func (s *memoryLogStore) Since(since int64) []LogEntry {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]LogEntry, 0, len(s.entries))
for _, entry := range s.entries {
if entry.ID > since {
out = append(out, entry)
}
}
return out
}
// FilePath 返回当前日志文件路径。
func (s *memoryLogStore) FilePath() string {
s.mu.RLock()
defer s.mu.RUnlock()
return s.filePath
}
// detectLogLevel 根据日志文本粗略分类,方便前端染色。
func detectLogLevel(line string) string {
lower := strings.ToLower(line)
switch {
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"
}
}