Files
dygou_go/internal/logger/logger_test.go
T
yml2213 dce9ce2a5a 优化日志系统:网络请求降噪并按接口聚合,补齐双时间说明与实例标识,发布0.3.2
- 逐条网络日志(请求完成 / 接口返回异常 / 响应刷新CSRF已同步)从 info/warn 降为 debug:
  实测一次 6 账号的模拟抢购里 233 条日志有 174 条(74%)属于这类;真实抢购会产生几千条,
  把界面仅 800 条的内存日志冲掉,反而看不到关键信息。HTTP/解析/传输层错误仍为 error 级
- 新增按接口聚合的网络统计,抢购任务结束时输出一行,替代逐条刷屏:
  "接口统计: 锁定兑换商品 ×341 失败341 平均=95ms 最慢=312ms 最后错误=活动火爆,请稍后再试"
- 日志文件开头新增表头:实例名、数据目录、双时间格式说明(本地时间 + D 斗鱼估算时间)、
  并说明"校准完成前偏移为 0,只显示本地时间"。双时间一直存在却没有任何地方解释过,
  多实例日志复制或汇总后也分不清来源
- 界面日志面板:显示日志级别并按级别着色(此前级别只存在于 CSS class,复制出去就丢);
  新增"复制"按钮,按与日志文件一致的单行格式导出(此前直接复制会变成 5 行一条、
  1204 行里其实只有 233 条日志)
- 设置页"日志显示等级"补充说明:Info 起只显示关键日志,需要逐请求细节时切到 Debug
- 新增测试:TestRequestStatsAggregatesByLabel、TestLogFileHeaderDocumentsFormat、
  TestRunNowLogsAggregateInsteadOfPerRequest(断言必须有聚合行、且不得出现逐条请求日志)
2026-10-03 19:33:14 +08:00

138 lines
4.2 KiB
Go

package logger
import (
"os"
"path/filepath"
"strings"
"testing"
)
// TestStructuredLogging 验证 LogEvent→sink→MemoryLogStore 结构化链路:
// LogEntry 的 category/level/account 应直接由调用方填入,而非字符串反解析。
func TestStructuredLogging(t *testing.T) {
store := NewMemoryLogStore(100)
cleanup := InitLogging(store, t.TempDir(), "测试实例", "/tmp/douyu-go-test")
defer func() {
cleanup()
globalSink = nil
}()
LogEvent("抢购", "success", "兑换成功: %s", "商品A")
LogAccountEvent("抢购", "warn", "testuser", "冷却: %ds", 5)
entries := store.Since(0)
// InitLogging 自身产生 1 条"日志文件已启用",加上上述 2 条,共 3 条
if len(entries) != 3 {
t.Fatalf("Since(0) = %d entries, want 3", len(entries))
}
if entries[0].Category != "日志" {
t.Errorf("entry0 Category = %q, want '日志' (InitLogging 启动日志)", entries[0].Category)
}
e1 := entries[1]
if e1.ID != 2 {
t.Errorf("entry1 ID = %d, want 2", e1.ID)
}
if e1.Category != "抢购" {
t.Errorf("entry1 Category = %q, want '抢购'", e1.Category)
}
if e1.Level != "success" {
t.Errorf("entry1 Level = %q, want 'success'", e1.Level)
}
if e1.Account != "" {
t.Errorf("entry1 Account = %q, want empty", e1.Account)
}
if e1.Time == "" {
t.Error("entry1 Time is empty")
}
if !strings.Contains(e1.Message, "[抢购][success]") {
t.Errorf("entry1 Message = %q, want contains '[抢购][success]'", e1.Message)
}
if !strings.Contains(e1.Message, "兑换成功: 商品A") {
t.Errorf("entry1 Message = %q, want contains '兑换成功: 商品A'", e1.Message)
}
e2 := entries[2]
if e2.ID != 3 {
t.Errorf("entry2 ID = %d, want 3", e2.ID)
}
if e2.Account != "testuser" {
t.Errorf("entry2 Account = %q, want 'testuser'", e2.Account)
}
if !strings.Contains(e2.Message, "[账号:testuser]") {
t.Errorf("entry2 Message = %q, want contains '[账号:testuser]'", e2.Message)
}
if !strings.Contains(e2.Message, "冷却: 5s") {
t.Errorf("entry2 Message = %q, want contains '冷却: 5s'", e2.Message)
}
// 验证 minLevel 过滤:设为 error 后 info 日志不应进入内存
SetMinLevel("error")
LogEvent("测试", "info", "这条应被过滤")
if got := store.Since(e2.ID); len(got) != 0 {
t.Errorf("after SetMinLevel(error), Since(%d) = %d entries, want 0", e2.ID, len(got))
}
SetMinLevel("info") // 恢复,避免影响后续测试
}
// TestCreateUniqueLogFileAvoidsCollision 同一目录同秒启动的两个进程不能写同一个文件,
// 否则日志会交错、分不清实例。
func TestCreateUniqueLogFileAvoidsCollision(t *testing.T) {
dir := t.TempDir()
base := filepath.Join(dir, "douyu_20261003_160856")
first, firstPath, err := createUniqueLogFile(base)
if err != nil {
t.Fatal(err)
}
_ = first.Close()
if got := filepath.Base(firstPath); got != "douyu_20261003_160856.log" {
t.Fatalf("first = %q", got)
}
second, secondPath, err := createUniqueLogFile(base)
if err != nil {
t.Fatal(err)
}
_ = second.Close()
if got := filepath.Base(secondPath); got != "douyu_20261003_160856-2.log" {
t.Fatalf("second = %q, want 同目录 -2 后缀", got)
}
}
// TestLogFileHeaderDocumentsFormat 日志文件开头必须说明双时间含义、实例名与数据目录,
// 否则把日志复制或汇总后看不出两个时间分别是什么、来自哪台机器的哪个实例。
func TestLogFileHeaderDocumentsFormat(t *testing.T) {
dir := t.TempDir()
store := NewMemoryLogStore(20)
cleanup := InitLogging(store, dir, "实例 2", `C:\Users\Administrator\AppData\Roaming\douyu-go\instances\instance-2`)
LogEvent("系统", "success", "已启动实例 2")
cleanup()
globalSink = nil
path := store.FilePath()
if path == "" {
t.Fatal("日志文件路径为空")
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
content := string(data)
for _, want := range []string{
"实例: 实例 2",
"数据目录: ",
"时间格式: 本地时间(D斗鱼估算时间)",
"校准完成前偏移为 0",
"logLevel 改成 debug",
"已启动实例 2",
} {
if !strings.Contains(content, want) {
t.Errorf("日志文件缺少 %q\n实际内容:\n%s", want, content)
}
}
if !strings.HasPrefix(content, "====") {
t.Errorf("表头应在文件最开始:\n%s", content)
}
}