优化日志系统:网络请求降噪并按接口聚合,补齐双时间说明与实例标识,发布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(断言必须有聚合行、且不得出现逐条请求日志)
This commit is contained in:
yml2213
2026-10-03 19:33:14 +08:00
parent 43270e04a6
commit dce9ce2a5a
12 changed files with 328 additions and 11 deletions
+73 -3
View File
@@ -32,6 +32,22 @@ type DouyuClient struct {
xiaodianEmbedMu sync.Mutex
xiaodianEmbedCache map[string]xiaodianEmbedCacheItem
statsMu sync.Mutex
reqStats map[string]*RequestStat
}
// RequestStat 累计某一类接口的调用情况。
//
// 逐条网络日志现在降为 debug 级(一次真实抢购会产生几千条),靠这份聚合结果
// 保留诊断价值:哪类接口调了多少次、失败多少、平均/最慢多久、最后一次错在哪。
type RequestStat struct {
Label string
Count int
Failed int
Total time.Duration
Slowest time.Duration
LastErr string
}
type xiaodianEmbedCacheItem struct {
@@ -1698,7 +1714,7 @@ func (c *DouyuClient) postMultipart(url, cookie string, csrf *string, label stri
if resp != nil {
if nextCSRF := csrfFromResponse(resp); nextCSRF != "" && csrf != nil {
if nextCSRF != *csrf {
logger.LogEvent("网络", "info", "响应刷新CSRF已同步: %s %s %s", label, req.Method, requestPath(req))
logger.LogEvent("网络", "debug", "响应刷新CSRF已同步: %s %s %s", label, req.Method, requestPath(req))
}
if ctnVal != "" {
*csrf = nextCSRF + "\x00" + ctnVal
@@ -1788,7 +1804,57 @@ func shouldLogNetworkSuccess(label string) bool {
return label != "服务时间"
}
// recordRequest 记录一次请求结果(每类接口一条聚合)。
func (c *DouyuClient) recordRequest(label string, cost time.Duration, failed bool, errMsg string) {
c.statsMu.Lock()
defer c.statsMu.Unlock()
if c.reqStats == nil {
c.reqStats = make(map[string]*RequestStat)
}
stat := c.reqStats[label]
if stat == nil {
stat = &RequestStat{Label: label}
c.reqStats[label] = stat
}
stat.Count++
stat.Total += cost
if cost > stat.Slowest {
stat.Slowest = cost
}
if failed {
stat.Failed++
if errMsg != "" {
stat.LastErr = utils.TrimForLog(errMsg, 60)
}
}
}
// ResetRequestStats 清空聚合统计(每次抢购任务开始时调用)。
func (c *DouyuClient) ResetRequestStats() {
c.statsMu.Lock()
c.reqStats = make(map[string]*RequestStat)
c.statsMu.Unlock()
}
// RequestStats 返回按调用次数降序排列的聚合统计。
func (c *DouyuClient) RequestStats() []RequestStat {
c.statsMu.Lock()
out := make([]RequestStat, 0, len(c.reqStats))
for _, stat := range c.reqStats {
out = append(out, *stat)
}
c.statsMu.Unlock()
sort.Slice(out, func(i, j int) bool {
if out[i].Count != out[j].Count {
return out[i].Count > out[j].Count
}
return out[i].Label < out[j].Label
})
return out
}
func (c *DouyuClient) logNetworkSuccess(label string, req *http.Request, cost time.Duration, apiError int, apiMsg string) {
c.recordRequest(label, cost, apiError != 0, apiMsg)
if !shouldLogNetworkSuccess(label) {
return
}
@@ -1796,25 +1862,29 @@ func (c *DouyuClient) logNetworkSuccess(label string, req *http.Request, cost ti
if apiMsg != "" || apiError != 0 {
extra = fmt.Sprintf(" apiError=%d msg=%s", apiError, utils.TrimForLog(apiMsg, 80))
}
logger.LogEvent("网络", "info", "请求完成: %s 耗时=%s%s", c.networkDiagnostic(label, req), utils.RoundDuration(cost), extra)
logger.LogEvent("网络", "debug", "请求完成: %s 耗时=%s%s", c.networkDiagnostic(label, req), utils.RoundDuration(cost), extra)
}
func (c *DouyuClient) logNetworkAPIResult(label string, req *http.Request, cost time.Duration, apiError int, apiMsg string) {
logger.LogEvent("网络", "warn", "接口返回异常: %s 耗时=%s apiError=%d msg=%s",
c.recordRequest(label, cost, true, apiMsg)
logger.LogEvent("网络", "debug", "接口返回异常: %s 耗时=%s apiError=%d msg=%s",
c.networkDiagnostic(label, req), utils.RoundDuration(cost), apiError, utils.TrimForLog(apiMsg, 120))
}
func (c *DouyuClient) logNetworkHTTPError(label string, req *http.Request, status int, cost time.Duration, body []byte) {
c.recordRequest(label, cost, true, fmt.Sprintf("HTTP %d", status))
logger.LogEvent("网络", "error", "HTTP异常: %s 状态=%d 耗时=%s 响应=%s",
c.networkDiagnostic(label, req), status, utils.RoundDuration(cost), utils.TrimForLog(string(body), 160))
}
func (c *DouyuClient) logNetworkParseError(label string, req *http.Request, cost time.Duration, body []byte, err error) {
c.recordRequest(label, cost, true, err.Error())
logger.LogEvent("网络", "error", "响应解析失败: %s 耗时=%s 错误=%v 响应=%s",
c.networkDiagnostic(label, req), utils.RoundDuration(cost), err, utils.TrimForLog(string(body), 160))
}
func (c *DouyuClient) logNetworkError(label string, req *http.Request, cost time.Duration, err error) {
c.recordRequest(label, cost, true, err.Error())
logger.LogEvent("网络", "error", "请求失败: %s 耗时=%s 错误=%v",
c.networkDiagnostic(label, req), utils.RoundDuration(cost), err)
}
+42
View File
@@ -8,6 +8,7 @@ import (
"strings"
"sync/atomic"
"testing"
"time"
"douyu_go/internal/activity"
"douyu_go/internal/trace"
@@ -499,3 +500,44 @@ func testCTNClient(t *testing.T, ctnURL string) *DouyuClient {
cli.HTTPClient = http.DefaultClient
return cli
}
// TestRequestStatsAggregatesByLabel 验证网络日志降噪后仍保留诊断价值:
// 同类接口聚合成一条统计(次数/失败/平均/最慢/最后错误),并可按任务重置。
func TestRequestStatsAggregatesByLabel(t *testing.T) {
cli := &DouyuClient{}
cli.recordRequest("锁定兑换商品", 80*time.Millisecond, true, "活动火爆,请稍后再试")
cli.recordRequest("锁定兑换商品", 120*time.Millisecond, true, "当前网络繁忙,请稍后再试")
cli.recordRequest("锁定兑换商品", 60*time.Millisecond, false, "")
cli.recordRequest("支付锁单", 300*time.Millisecond, false, "")
stats := cli.RequestStats()
if len(stats) != 2 {
t.Fatalf("统计条目 = %d, want 2: %#v", len(stats), stats)
}
// 次数多的排前面
if stats[0].Label != "锁定兑换商品" {
t.Fatalf("首条应为调用最多的接口, got %q", stats[0].Label)
}
lock := stats[0]
if lock.Count != 3 || lock.Failed != 2 {
t.Fatalf("锁定兑换商品 次数=%d 失败=%d, want 3/2", lock.Count, lock.Failed)
}
if lock.Slowest != 120*time.Millisecond {
t.Fatalf("最慢 = %v, want 120ms", lock.Slowest)
}
if lock.Total != 260*time.Millisecond {
t.Fatalf("总耗时 = %v, want 260ms", lock.Total)
}
if lock.LastErr != "当前网络繁忙,请稍后再试" {
t.Fatalf("最后一次错误 = %q", lock.LastErr)
}
if stats[1].Failed != 0 || stats[1].LastErr != "" {
t.Fatalf("支付锁单不应有失败记录: %#v", stats[1])
}
cli.ResetRequestStats()
if got := cli.RequestStats(); len(got) != 0 {
t.Fatalf("重置后应为空, got %#v", got)
}
}