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:
AskaEth committed 2026-10-26 13:58:02 +08:00
1 parent fa5d3d23bf
commit 3157abcf3c
3 files changed
+152 -8

No files matched your search

@@ -16,6 +16,17 @@ import (
const (
defaultLogLimit = 200
maxLogLimit = 1000
// maxLogMsgRunes 是日志页**单行 msg** 的截断上限(M42 r2)。
//
// 🔴 为什么不是 80:`trace.RedactText` 的 80 rune 是**出图摘要**那一档(span
// props / 工具结果摘要要的是「一眼看完的摘要」),而日志页是**排障面** ——
// 把一条长错误行拦腰截断就是丢诊断信息(实测一条 500 rune 的上游报错会被切到
// 80 + 省略号,恰好切掉「谁拒了、为什么」)。
//
// 🔴 为什么也不是无上限:单条日志可能是 `%v` 一个大对象(MiB 级),面板一次
// 要 1000 条 ⇒ 原样透出会把 JSON 响应撑到 GB 量级。4096 rune 足够装下完整的
// 技术栈 / 长错误行 / 一次请求的回显,而 4096 × 1000 条仍只是几 MB 量级。
maxLogMsgRunes = 4096
)
// AdminLogEntry 是 GET /api/v1/admin/observability/logs 中一个条目的
@@ -94,22 +105,24 @@ func (s *Server) adminObservabilityLogs(w http.ResponseWriter, r *http.Request)
// adminLogOf 把捕获的日志条目映射为驼峰式管理线上结构。
//
// 🔴 读侧脱敏(M42 #3):`msg` 是日志正文,也是唯一可能夹带凭据/正文的字段
// (本仓有过「把上游报文整段打进日志」的先例),因此出面板前过一遍
// `trace.RedactText` —— 与写侧(trace.JSONRecorder)**同一个函数**,不另写一套;
// 两套脱敏必然漂移,而漂移的方向总是「其中一面开始原样吐凭据」。
// (本仓有过「把上游报文整段打进日志」的先例),因此出面板前过一遍 trace 包的
// 共享清洗 —— 与写侧同一条路径,**不另写一套**:两套脱敏必然漂移,而漂移的方向
// 总是「其中一面开始原样吐凭据」。
//
// 🔴 上限与出图摘要**故意不同**(M42 r2,塔 2026-10-26 裁定):这里用
// `trace.RedactTextLimited(msg, maxLogMsgRunes)`(4096),而不是 `RedactText`
// 的 80 —— 日志页是排障面,80 会把长错误行拦腰截断(实质性 UX 回归)。出图那一侧
// (span props / 摘要)仍是 80,两边**不该被统一**:一个要摘要,一个要全文。
//
// 排障字段一律保住:`time`(RFC3339)、`level`、`source`(模块来源标签,编译期
// 常量构成,不含内容)原样透出,且 `msg` 的打码不改变条目条数、排序与级别过滤
// —— 面板因此照旧可诊断「谁在什么时候按什么级别报了哪一类错」。
//
// 注意 `RedactText` 同时按 `maxSummaryRunes` 截断(与出图/落盘同一口径):超长
// 正文会带省略号。这是复用同一个 helper 的既定代价,远小于「凭据原样上面板」。
func adminLogOf(e logger.Entry) AdminLogEntry {
return AdminLogEntry{
Time: e.Time.UTC().Format(time.RFC3339),
Level: e.Level,
Source: e.Source,
Msg: trace.RedactText(e.Msg),
Msg: trace.RedactTextLimited(e.Msg, maxLogMsgRunes),
}
}