fix(gateway): 日志页脱敏改用「可带上限」变体(4096),摘要侧仍是 80
塔 2026-10-26 复审裁定:复用 trace.RedactText 让面板日志页每条 msg 都被切到 80 rune,是实质性 UX 回归(日志页就是排障面)。修法不是网关侧另写脱敏,而是 在 trace 包给上限加一个参数。 - trace/redact.go:新增导出 `RedactTextLimited(s, limit)`,语义 = 同一套凭据形状 清洗 + 按调用方给的上限截断;`RedactText` 改为它的 `maxSummaryRunes`(80) 调用形态,既有调用方零改动,清洗逻辑仍只有一份(redactSecrets)。 limit <= 0 = 不截断,留给体积已自己界定的调用方。 - observability_logs.go:新增常量 `maxLogMsgRunes = 4096`(含注释说明为什么不是 80、也不是无上限:单条日志可能是 %v 的 MiB 级对象,1000 条会撑爆 JSON), adminLogOf 改用 `RedactTextLimited(e.Msg, maxLogMsgRunes)`。 - span props / 摘要侧不动,仍是 80(一个要摘要,一个要全文,两边不该被统一)。 测试(均验过变异打红): - TestAdminObservabilityLogs_LongLineKeepsFullText:547 rune 的日志行(行首凭据 + 行尾哨兵)⇒ 凭据打码、末尾哨兵仍在、rune 数只差「凭据→[redacted]」那一处; 变异(改回 RedactText 80)⇒ 红:拿到 81 rune、末尾哨兵消失。 - TestAdminObservabilityTrace_PropsStayAtSummaryBound:200 rune 的 args_summary ⇒ 81 rune(80 + 省略号);变异(把 maxSummaryRunes 提到 4096)⇒ 红。 验收:go build ./... / go vet ./... 全绿;go test -count=1 -race ./internal/services/gateway/... ./internal/runtime/trace/... 全绿。未 push。
This commit is contained in:
1 parent
fa5d3d23bf
commit
3157abcf3c
3 files changed
+152
-8
No files matched your search
@@ -26,6 +26,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/auth"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||
@@ -193,3 +194,113 @@ func TestAdminObservabilityTrace_RedactsPropsAndEvents(t *testing.T) {
|
||||
t.Fatalf("事件非正文键 ok 应原样保留:%+v", sp.Events[0].Props)
|
||||
}
|
||||
}
|
||||
|
||||
// obsLogTailMarker 是长日志行**末尾**的哨兵:按 80 rune 截断必然把它切掉,按
|
||||
// 4096(`maxLogMsgRunes`)必然留下它 —— 它就是「这一行有没有被拦腰截断」的判据。
|
||||
const obsLogTailMarker = "TAIL_OF_LONG_LOG_LINE"
|
||||
|
||||
// longLogMsg 造一条 ~500 rune 的日志行:凭据放在**行首**(所以 80 rune 截断也能
|
||||
// 打码成功,红的是「截断」这一条而不是「没打码」),末尾带哨兵 —— 一旦被切到 80,
|
||||
// 哨兵必然消失,判据因此指向问题本身。
|
||||
// 「排障上下文一行 」是 8 rune,×60 = 480 rune ⇒ 远超 80 的摘要上限。
|
||||
func longLogMsg() string {
|
||||
return "upstream rejected " + obsLeakKey + " " +
|
||||
strings.Repeat("排障上下文一行 ", 60) + obsLogTailMarker
|
||||
}
|
||||
|
||||
// TestAdminObservabilityLogs_LongLineKeepsFullText 钉住日志页的**上限与出图不同**
|
||||
// (M42 r2,塔 2026-10-26 裁定):日志页是排障面,一条 500 rune 的错误行必须完整
|
||||
// 保留(凭据仍然打码),不能被 `maxSummaryRunes`(80)拦腰截断。
|
||||
//
|
||||
// 变异:把 `adminLogOf` 改回 `trace.RedactText`(80)⇒ 本用例必红(末尾哨兵消失、
|
||||
// rune 数掉到 81)。
|
||||
func TestAdminObservabilityLogs_LongLineKeepsFullText(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv, ts, jwt := buildEnv(t, cfgDefault(), Deps{})
|
||||
ring := logger.NewRingBuffer(8)
|
||||
log := logger.New(logger.Options{Level: "debug", Format: "text", Out: io.Discard, Ring: ring})
|
||||
msg := longLogMsg()
|
||||
if utf8.RuneCountInString(msg) <= maxLogMsgRunes/8 {
|
||||
t.Fatalf("夹具只有 %d rune,不足以证明「不被 80 截断」", utf8.RuneCountInString(msg))
|
||||
}
|
||||
log.Error(msg)
|
||||
srv.SetLogBuffer(ring)
|
||||
tok := signAny(t, jwt, "acct", auth.RoleOwner)
|
||||
|
||||
resp, body := doReq(t, http.MethodGet, ts.URL+"/api/v1/admin/observability/logs", nil, tok)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200 (body=%s)", resp.StatusCode, body)
|
||||
}
|
||||
var env logEnvelope
|
||||
if err := json.Unmarshal(body, &env); err != nil {
|
||||
t.Fatalf("decode: %v (body=%s)", err, body)
|
||||
}
|
||||
if len(env.Data) != 1 {
|
||||
t.Fatalf("日志条数 = %d, want 1", len(env.Data))
|
||||
}
|
||||
got := env.Data[0].Msg
|
||||
if strings.Contains(got, obsLeakPrefix) {
|
||||
t.Fatalf("长行里的凭据没打码(%q):%q", obsLeakPrefix, got)
|
||||
}
|
||||
if !strings.Contains(got, "[redacted]") {
|
||||
t.Fatalf("长行里的凭据位置应换成 [redacted]:%q", got)
|
||||
}
|
||||
if !strings.Contains(got, obsLogTailMarker) {
|
||||
t.Fatalf("日志页把 %d rune 的行截断了(末尾哨兵 %q 消失),拿到 %d rune:…%s",
|
||||
utf8.RuneCountInString(msg), obsLogTailMarker, utf8.RuneCountInString(got),
|
||||
string([]rune(got)[max(0, utf8.RuneCountInString(got)-20):]))
|
||||
}
|
||||
// 长度只允许差「凭据 → [redacted]」那一处替换,不许多掉任何东西。
|
||||
wantLen := utf8.RuneCountInString(msg) - utf8.RuneCountInString(obsLeakKey) +
|
||||
utf8.RuneCountInString("[redacted]")
|
||||
if n := utf8.RuneCountInString(got); n != wantLen {
|
||||
t.Fatalf("日志行 rune 数 = %d, want %d(只允许打码带来的差额,不许截断)", n, wantLen)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdminObservabilityTrace_PropsStayAtSummaryBound 是上一条的**对偶**:日志页
|
||||
// 的上限调大到 4096 之后,出图这一侧(span props)不许被顺手统一 —— 那里要的是
|
||||
// 摘要(`maxSummaryRunes` = 80),不是一个更长的全文。
|
||||
//
|
||||
// 变异:把 `maxSummaryRunes` 提到 4096(或让出图侧改用日志上限)⇒ 本用例必红。
|
||||
func TestAdminObservabilityTrace_PropsStayAtSummaryBound(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv, ts, jwt := buildEnv(t, cfgDefault(), Deps{})
|
||||
started := time.Date(2026, 10, 26, 3, 4, 5, 0, time.UTC)
|
||||
long := strings.Repeat("摘", 200)
|
||||
srv.ObserveSpan(trace.SpanRecord{
|
||||
ID: "span-bound",
|
||||
Name: "tool.execution",
|
||||
Kind: trace.SpanKindTool,
|
||||
Status: trace.SpanStatusOK,
|
||||
StartedAt: started,
|
||||
EndedAt: started.Add(time.Millisecond),
|
||||
Duration: time.Millisecond,
|
||||
Props: map[string]any{"tool": "web_search", "args_summary": long},
|
||||
})
|
||||
tok := signAny(t, jwt, "acct", auth.RoleOwner)
|
||||
|
||||
resp, body := doReq(t, http.MethodGet, ts.URL+"/api/v1/admin/observability/trace", nil, tok)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200 (body=%s)", resp.StatusCode, body)
|
||||
}
|
||||
var env struct {
|
||||
Data []AdminTraceSpan `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &env); err != nil {
|
||||
t.Fatalf("decode: %v (body=%s)", err, body)
|
||||
}
|
||||
if len(env.Data) != 1 {
|
||||
t.Fatalf("span 条数 = %d, want 1", len(env.Data))
|
||||
}
|
||||
summary, _ := env.Data[0].Props["args_summary"].(string)
|
||||
// 80 是 trace 包的摘要上限(`maxSummaryRunes`,未导出 ⇒ 这里按契约写常量)。
|
||||
const summaryBound = 80
|
||||
if n := utf8.RuneCountInString(summary); n != summaryBound+1 {
|
||||
t.Fatalf("args_summary rune 数 = %d, want %d(%d rune 摘要 + 省略号)—— "+
|
||||
"出图侧不许跟日志页一起放宽到 %d", n, summaryBound+1, summaryBound, maxLogMsgRunes)
|
||||
}
|
||||
if !strings.HasSuffix(summary, "…") {
|
||||
t.Fatalf("截断结果应以省略号收尾:%q", summary)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user