优化日志系统:网络请求降噪并按接口聚合,补齐双时间说明与实例标识,发布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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user