This commit is contained in:
parent
0dc459d50a
commit
ea2808f72b
@ -36,13 +36,7 @@ func Run() (err error) {
|
|||||||
build.Version = "TestVersion"
|
build.Version = "TestVersion"
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infoln("*******************************************************************")
|
log.Infof("服务启动,版本:%s", build.Version)
|
||||||
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("*******************************************************************")
|
|
||||||
|
|
||||||
// 启动定时任务。
|
// 启动定时任务。
|
||||||
go cron_server.Start()
|
go cron_server.Start()
|
||||||
|
|||||||
34
backend/services/runtimelog/presentation.go
Normal file
34
backend/services/runtimelog/presentation.go
Normal file
@ -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
|
||||||
|
}
|
||||||
@ -72,16 +72,21 @@ func (w *Writer) Read(ctx context.Context, name string, offset, limit int) (Page
|
|||||||
if at.Before(cutoff) {
|
if at.Before(cutoff) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
// 先过滤再分页,偏移、统计和下一页判断均按实际可见记录计算。
|
||||||
|
visible, keep := presentLine(string(line))
|
||||||
|
if !keep {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
position++
|
position++
|
||||||
if position <= offset {
|
if position <= offset {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if len(page.Lines) >= limit || bytes+len(line) > maxResponseBytes {
|
if len(page.Lines) >= limit || bytes+len(visible) > maxResponseBytes {
|
||||||
page.HasMore = true
|
page.HasMore = true
|
||||||
return finished
|
return finished
|
||||||
}
|
}
|
||||||
page.Lines = append(page.Lines, string(line))
|
page.Lines = append(page.Lines, visible)
|
||||||
bytes += len(line)
|
bytes += len(visible)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if errors.Is(err, finished) {
|
if errors.Is(err, finished) {
|
||||||
|
|||||||
@ -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 运行日志目录简化
|
## 2026-09-09 运行日志目录简化
|
||||||
|
|
||||||
- [✓] 已确认:保留顶层 `log/`,取消 `log/运行日志/` 这一层;修改前检查点 `3d8d1c8` 已推送。
|
- [✓] 已确认:保留顶层 `log/`,取消 `log/运行日志/` 这一层;修改前检查点 `3d8d1c8` 已推送。
|
||||||
|
|||||||
@ -47,7 +47,11 @@ export function parseRuntimeLine(line, sequence) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const separator = /^\*{3,}$/.test(message.trim())
|
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\//, '')
|
const sourceShort = source.replace(/^git\.pcux\.cn\/xuans\/Smail\//, '')
|
||||||
return {
|
return {
|
||||||
sequence, raw, createdAt, level, source, sourceShort, requestId, message, summary,
|
sequence, raw, createdAt, level, source, sourceShort, requestId, message, summary,
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
package runtimelog
|
package runtimelog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -11,6 +15,119 @@ import (
|
|||||||
"git.pcux.cn/xuans/Smail/utils/logpolicy"
|
"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) {
|
func TestRuntimeLogCompressionAndExactRetention(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
w, err := New(root)
|
w, err := New(root)
|
||||||
|
|||||||
@ -115,6 +115,29 @@ test('无请求标识、Windows 代码路径与原文方括号均能安全展示
|
|||||||
assert.equal(row.level, 'info')
|
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('未知格式和非法时间不丢失消息,分隔线只改摘要不改原文', () => {
|
test('未知格式和非法时间不丢失消息,分隔线只改摘要不改原文', () => {
|
||||||
for (const raw of ['[2026-99-99T00:00:00Z] 错误', '<img src=x onerror=alert(1)>', '[业务]消息', '']) {
|
for (const raw of ['[2026-99-99T00:00:00Z] 错误', '<img src=x onerror=alert(1)>', '[业务]消息', '']) {
|
||||||
const row = parseRuntimeLine(raw, 1)
|
const row = parseRuntimeLine(raw, 1)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user