优化日志系统:网络请求降噪并按接口聚合,补齐双时间说明与实例标识,发布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:
@@ -141,7 +141,10 @@ var globalSink *sink
|
||||
|
||||
// InitLogging 初始化日志输出:日志文件和内存缓冲同时写入。
|
||||
// 终端输出通过 sink 同步写入(Windows GUI 应用无控制台时自动跳过)。
|
||||
func InitLogging(store *MemoryLogStore, logDir string) func() {
|
||||
//
|
||||
// instanceName / dataDir 只用于写文件表头,便于把日志复制/汇总后仍能看出
|
||||
// 是哪台机器的哪个实例。
|
||||
func InitLogging(store *MemoryLogStore, logDir, instanceName, dataDir string) func() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("")
|
||||
|
||||
@@ -166,6 +169,10 @@ func InitLogging(store *MemoryLogStore, logDir string) func() {
|
||||
return func() {}
|
||||
}
|
||||
|
||||
// 表头同步写入,确保排在任何日志之前(异步写入器还没启动)。
|
||||
// 写失败不影响日志本身。
|
||||
_, _ = file.WriteString(logFileHeader(instanceName, dataDir))
|
||||
|
||||
store.SetFilePath(filePath)
|
||||
globalSink = &sink{
|
||||
async: NewAsyncLogWriter(file, asyncLogBufferSize),
|
||||
@@ -181,6 +188,28 @@ func InitLogging(store *MemoryLogStore, logDir string) func() {
|
||||
}
|
||||
}
|
||||
|
||||
// logFileHeader 说明日志格式与运行上下文。
|
||||
//
|
||||
// 双时间(本地时间 + 斗鱼估算时间)一直存在,但日志里没有任何地方解释过;
|
||||
// 而且时间校准完成前偏移为 0、只显示本地时间,看起来像格式不一致。
|
||||
// 把格式、实例名、数据目录写在文件开头,复制或汇总多实例日志时就分得清。
|
||||
func logFileHeader(instanceName, dataDir string) string {
|
||||
if instanceName == "" {
|
||||
instanceName = "未知实例"
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString("==================================================================\n")
|
||||
fmt.Fprintf(&b, "实例: %s\n", instanceName)
|
||||
if dataDir != "" {
|
||||
fmt.Fprintf(&b, "数据目录: %s\n", dataDir)
|
||||
}
|
||||
b.WriteString("时间格式: 本地时间(D斗鱼估算时间) [类别][级别][账号] 正文\n")
|
||||
b.WriteString(" D 为经服务端校准的斗鱼时间估算;校准完成前偏移为 0,只显示本地时间\n")
|
||||
b.WriteString("网络逐条请求为 debug 级,默认不显示;排查时把配置 logLevel 改成 debug\n")
|
||||
b.WriteString("==================================================================\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// createUniqueLogFile 以 O_EXCL 递增后缀创建日志文件。
|
||||
//
|
||||
// 早期实现用 O_CREATE|O_APPEND + 秒级时间戳:同一目录下同秒启动的两个进程会
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -10,7 +11,7 @@ import (
|
||||
// LogEntry 的 category/level/account 应直接由调用方填入,而非字符串反解析。
|
||||
func TestStructuredLogging(t *testing.T) {
|
||||
store := NewMemoryLogStore(100)
|
||||
cleanup := InitLogging(store, t.TempDir())
|
||||
cleanup := InitLogging(store, t.TempDir(), "测试实例", "/tmp/douyu-go-test")
|
||||
defer func() {
|
||||
cleanup()
|
||||
globalSink = nil
|
||||
@@ -98,3 +99,39 @@ func TestCreateUniqueLogFileAvoidsCollision(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user