From ea2808f72bc271e205b235ad6073d6fa42e0f1a9 Mon Sep 17 00:00:00 2001 From: smail Date: Wed, 9 Sep 2026 00:39:01 +0800 Subject: [PATCH] =?UTF-8?q?=E7=B2=BE=E7=AE=80=E5=90=AF=E5=8A=A8=E6=A8=AA?= =?UTF-8?q?=E5=B9=85=E6=97=A5=E5=BF=97=E5=B9=B6=E4=BF=9D=E8=AF=81=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E8=BF=87=E6=BB=A4=E5=88=86=E9=A1=B5=E5=87=86=E7=A1=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/app/app.go | 8 +- backend/services/runtimelog/presentation.go | 34 +++++ backend/services/runtimelog/query.go | 11 +- docs/日志改造任务.md | 10 ++ .../components/LogSettings/RuntimeLog.js | 6 +- test/backend/runtime_logs_test.go | 117 ++++++++++++++++++ test/frontend/logs.test.mjs | 23 ++++ 7 files changed, 198 insertions(+), 11 deletions(-) create mode 100644 backend/services/runtimelog/presentation.go diff --git a/backend/internal/app/app.go b/backend/internal/app/app.go index 0c3ed07..13885ff 100644 --- a/backend/internal/app/app.go +++ b/backend/internal/app/app.go @@ -36,13 +36,7 @@ func Run() (err error) { build.Version = "TestVersion" } - log.Infoln("*******************************************************************") - log.Infof("***\tServer Start Success \n") - log.Infof("***\tServer Version: %s \n", build.Version) - log.Infof("***\tGit Commit Hash: %s ", build.GitHash) - log.Infof("***\tBuild Date: %s ", build.BuildTime) - log.Infof("***\tBuild GoLang Version: %s ", build.GoVersion) - log.Infoln("*******************************************************************") + log.Infof("服务启动,版本:%s", build.Version) // 启动定时任务。 go cron_server.Start() diff --git a/backend/services/runtimelog/presentation.go b/backend/services/runtimelog/presentation.go new file mode 100644 index 0000000..fd23beb --- /dev/null +++ b/backend/services/runtimelog/presentation.go @@ -0,0 +1,34 @@ +package runtimelog + +import ( + "regexp" + "strings" +) + +var legacyStartupLine = regexp.MustCompile(`^(\[[^\]\r\n]+\]\s+\[info\]\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\](?:\[[^\]\r\n]*\])?\[([^\]\r\n]+):\d+\])([^\r\n]*)$`) + +// presentLine 只精简旧版主程序的启动横幅,不修改磁盘原文或隐藏其他来源和级别的记录。 +func presentLine(line string) (string, bool) { + parts := legacyStartupLine.FindStringSubmatch(line) + if parts == nil { + return line, true + } + source := strings.ReplaceAll(parts[2], `\`, "/") + if source != "internal/app/app.go" && !strings.HasSuffix(source, "/internal/app/app.go") { + return line, true + } + message := strings.TrimSpace(parts[3]) + if len(message) >= 3 && strings.Trim(message, "*") == "" { + return "", false + } + if !strings.HasPrefix(message, "***\t") && !strings.HasPrefix(message, "*** ") { + return line, true + } + message = strings.TrimSpace(message[3:]) + for _, prefix := range []string{"Server Version:", "Git Commit Hash:", "Build Date:", "Build GoLang Version:"} { + if strings.HasPrefix(message, prefix) { + return "", false + } + } + return line, true +} diff --git a/backend/services/runtimelog/query.go b/backend/services/runtimelog/query.go index 22b15c8..c5af781 100644 --- a/backend/services/runtimelog/query.go +++ b/backend/services/runtimelog/query.go @@ -72,16 +72,21 @@ func (w *Writer) Read(ctx context.Context, name string, offset, limit int) (Page if at.Before(cutoff) { return nil } + // 先过滤再分页,偏移、统计和下一页判断均按实际可见记录计算。 + visible, keep := presentLine(string(line)) + if !keep { + return nil + } position++ if position <= offset { return nil } - if len(page.Lines) >= limit || bytes+len(line) > maxResponseBytes { + if len(page.Lines) >= limit || bytes+len(visible) > maxResponseBytes { page.HasMore = true return finished } - page.Lines = append(page.Lines, string(line)) - bytes += len(line) + page.Lines = append(page.Lines, visible) + bytes += len(visible) return nil }) if errors.Is(err, finished) { diff --git a/docs/日志改造任务.md b/docs/日志改造任务.md index 3e45da6..34ac954 100644 --- a/docs/日志改造任务.md +++ b/docs/日志改造任务.md @@ -1,5 +1,15 @@ # 日志存储与可视化改造 +## 2026-09-09 精简启动日志 + +- [✓] 范围已核对:不再显示无业务价值的启动分隔线,以及单独占行的版本、提交号、编译时间、Go 版本;修改前检查点 `0dc459d` 已推送。 +- [x] 新程序只生成一条带运行版本的“服务启动”记录;不把进入启动流程误写成所有监听服务已就绪。 +- [x] 历史启动横幅在后端查询分页前过滤,旧启动事件显示为一条中文记录;过滤仅匹配主程序来源和信息级别,其他来源、未知格式、警告和错误保留。 +- [x] 不修改原日志文件,不调整三十天保留、七天压缩、数据库和访问权限。 +- [✓] 回归测试明文和压缩日志、分页、尾部冗余记录、仅含冗余记录及原文件不变;历史启动摘要中文化,详情保留原文。 +- [✓] 最终全量回归 `test-log/run-2026-09-08T16-36-44-859Z-5048`,前端 150 项、部署 4 项及后端全包通过;并发检测 `test-log/run-2026-09-08T16-37-55-398Z-17556`,运行日志与程序启动包通过。 +- [~] 完整编译并更新 `yunx-test`,重启后用 MCP 验证。 + ## 2026-09-09 运行日志目录简化 - [✓] 已确认:保留顶层 `log/`,取消 `log/运行日志/` 这一层;修改前检查点 `3d8d1c8` 已推送。 diff --git a/frontend/src/views/components/LogSettings/RuntimeLog.js b/frontend/src/views/components/LogSettings/RuntimeLog.js index 12d5034..ac6684b 100644 --- a/frontend/src/views/components/LogSettings/RuntimeLog.js +++ b/frontend/src/views/components/LogSettings/RuntimeLog.js @@ -47,7 +47,11 @@ export function parseRuntimeLine(line, sequence) { } } const separator = /^\*{3,}$/.test(message.trim()) - const summary = separator ? '启动分隔线' : message.trim() || '(空消息)' + const legacyStartup = level === 'info' + && /(?:^|\/)internal\/app\/app\.go:\d+$/.test(source.replace(/\\/g, '/')) + && /^\*{3}\s+Server Start Success\s*(?:\\r)?(?:\\n)?\s*$/.test(message) + // 历史启动信息只简化摘要,详情仍保留完整消息和原始记录。 + const summary = legacyStartup ? '服务启动' : separator ? '启动分隔线' : message.trim() || '(空消息)' const sourceShort = source.replace(/^git\.pcux\.cn\/xuans\/Smail\//, '') return { sequence, raw, createdAt, level, source, sourceShort, requestId, message, summary, diff --git a/test/backend/runtime_logs_test.go b/test/backend/runtime_logs_test.go index b923651..2f5cc2e 100644 --- a/test/backend/runtime_logs_test.go +++ b/test/backend/runtime_logs_test.go @@ -1,9 +1,13 @@ package runtimelog import ( + "bytes" + "compress/gzip" "context" + "fmt" "os" "path/filepath" + "reflect" "strings" "testing" "time" @@ -11,6 +15,119 @@ import ( "git.pcux.cn/xuans/Smail/utils/logpolicy" ) +func TestRuntimeLogStartupPresentation(t *testing.T) { + header := "[2026-09-08T14:55:23Z] [info][2026-09-08 22:55:23]" + source := "git.pcux.cn/xuans/Smail/internal/app/app.go" + for _, location := range []string{source, "internal/app/app.go", "/srv/Smail/internal/app/app.go", `D:\Desktop\Smail\backend\internal\app\app.go`} { + for _, request := range []string{"", "[request-42]"} { + prefix := header + request + "[" + location + ":39]" + for _, message := range []string{ + "*******************************************************************", "***\tServer Version: dev-test ", + "***\tGit Commit Hash: abc123 ", "***\tBuild Date: 2026-09-08T14:55:12Z ", + "***\tBuild GoLang Version: go version go1.26.6 windows/amd64 ", + } { + if _, keep := presentLine(prefix + message); keep { + t.Fatalf("冗余启动记录仍可见:%s", prefix+message) + } + } + for _, tail := range []string{" ", ` \n`, `\r\n`} { + line := prefix + "***\tServer Start Success" + tail + if got, keep := presentLine(line); !keep || got != line { + t.Fatalf("旧启动事件未保留完整原文:%q %v", got, keep) + } + } + } + } + for _, line := range []string{ + header + "[" + source + ":39]服务启动,版本:dev-test", + header + "[" + source + ":39]***\tServer Start Success 后续内容", + header + "[" + source + ":39]***\t未知启动消息", + header + "[other/app.go:39]***\tBuild Date: 保留业务消息", + header + "[other/app.go:39]********", + header + "[" + source + ":39]Build Date: 不属于横幅", + strings.Replace(header, "[info]", "[warning]", 1) + "[" + source + ":39]***\tBuild Date: 保留警告", + strings.Replace(header, "[info]", "[error]", 1) + "[" + source + ":39]********", + "[2026-09-08T14:55:23Z] 未知格式保留", + } { + if got, keep := presentLine(line); !keep || got != line { + t.Fatalf("误改正常或未知记录:%q => %q %v", line, got, keep) + } + } +} + +func TestRuntimeLogStartupFilteringBeforePagination(t *testing.T) { + for _, compressed := range []bool{false, true} { + for _, onlyNoise := range []bool{false, true} { + t.Run(fmt.Sprintf("compressed=%v/onlyNoise=%v", compressed, onlyNoise), func(t *testing.T) { + w, err := New(t.TempDir()) + if err != nil { + t.Fatal(err) + } + defer w.Close() + now := time.Date(2026, 9, 8, 12, 0, 0, 0, time.UTC) + w.now = func() time.Time { return now } + header := "[2026-09-08T12:00:00Z] [info][2026-09-08 20:00:00][internal/app/app.go:39]" + noise := header + "********\n" + original := strings.Repeat(noise, 60) + var expected []string + if !onlyNoise { + original += header + "***\tServer Start Success \n" + noise + original += header + "***\tServer Version: old \n" + header + "HTTP 服务就绪\n" + original += header + "***\tBuild Date: old \n" + strings.Replace(header, "[info]", "[warning]", 1) + "需处理的警告\n" + expected = []string{header + "***\tServer Start Success ", header + "HTTP 服务就绪", strings.Replace(header, "[info]", "[warning]", 1) + "需处理的警告"} + } + original += strings.Repeat(noise, 60) + name := prefix + "2026-09-08.log" + data := []byte(original) + if compressed { + name += ".gz" + var buffer bytes.Buffer + archive := gzip.NewWriter(&buffer) + if _, err := archive.Write(data); err != nil { + t.Fatal(err) + } + if err := archive.Close(); err != nil { + t.Fatal(err) + } + data = buffer.Bytes() + } + if err := w.root.WriteFile(name, data, 0600); err != nil { + t.Fatal(err) + } + var received []string + for offset := 0; ; { + page, err := w.Read(context.Background(), name, offset, 2) + if err != nil { + t.Fatal(err) + } + received = append(received, page.Lines...) + if page.HasMore && len(page.Lines) != 2 { + t.Fatalf("过滤导致空页或短页:%+v", page) + } + if !page.HasMore { + break + } + offset += len(page.Lines) + if offset > 4 { + t.Fatal("分页重复返回记录") + } + } + if !reflect.DeepEqual(received, expected) { + t.Fatalf("分页丢失或重复记录:%q,期望 %q", received, expected) + } + page, err := w.Read(context.Background(), name, len(expected), 2) + if err != nil || len(page.Lines) != 0 || page.HasMore { + t.Fatalf("尾部隐藏记录触发多余下一页:%+v %v", page, err) + } + after, err := w.root.ReadFile(name) + if err != nil || !bytes.Equal(after, data) { + t.Fatalf("只读过滤修改了原文件:%v", err) + } + }) + } + } +} + func TestRuntimeLogCompressionAndExactRetention(t *testing.T) { root := t.TempDir() w, err := New(root) diff --git a/test/frontend/logs.test.mjs b/test/frontend/logs.test.mjs index e28c2e7..950fc49 100644 --- a/test/frontend/logs.test.mjs +++ b/test/frontend/logs.test.mjs @@ -115,6 +115,29 @@ test('无请求标识、Windows 代码路径与原文方括号均能安全展示 assert.equal(row.level, 'info') }) +test('旧启动信息只精简摘要,不改原始记录或其他业务消息', () => { + const header = '[2026-09-08T14:55:23Z] [info][2026-09-08 22:55:23]' + for (const source of ['git.pcux.cn/xuans/Smail/internal/app/app.go', 'D:\\source\\internal\\app\\app.go']) { + for (const suffix of [' ', ' \\n', '\\r\\n']) { + const message = `***\tServer Start Success${suffix}` + const raw = `${header}[${source}:40]${message}` + const row = parseRuntimeLine(raw, 1) + assert.equal(row.summary, '服务启动') + assert.equal(row.message, message) + assert.equal(row.raw, raw) + } + } + for (const raw of [ + `${header}[other/app.go:40]***\tServer Start Success`, + `${header}[internal/app/app.go:40]***\tServer Start Success 后续内容`, + `${header.replace('[info]', '[warning]')}[internal/app/app.go:40]***\tServer Start Success` + ]) { + const row = parseRuntimeLine(raw, 1) + assert.equal(row.summary, row.message.trim()) + assert.equal(row.raw, raw) + } +}) + test('未知格式和非法时间不丢失消息,分隔线只改摘要不改原文', () => { for (const raw of ['[2026-99-99T00:00:00Z] 错误', '', '[业务]消息', '']) { const row = parseRuntimeLine(raw, 1)