日志中间件无法获取真实路径因r.URL.Path被代理或路由改写,可靠方式是使用未解析的r.RequestURI并脱敏处理;需包装ResponseWriter捕获状态码与响应长度;读取Request.Body应复制流而非直接读取;JSON日志中文转义需用go-json或zap避免性能损耗。

为什么日志中间件总打不出真实路径
因为 r.URL.Path 在反向代理、URL 重写或某些路由库(如 gorilla/mux)下会被改写,甚至变成 /;而 r.URL.String() 还会补 ?、漏 fragment、触发 ParseForm 污染 r.Body。真正可靠的是 r.RequestURI——它是服务端收到的原始请求行字符串,未被标准库解析篡改。
但要注意:r.RequestURI 带查询参数,含敏感字段(如 token=xxx)时必须脱敏;别直接打印整个字符串,应先用正则或 url.ParseQuery 拆解后过滤。
怎么包装 ResponseWriter 捕获状态码和响应长度
标准 http.ResponseWriter 不暴露状态码和已写字节数,必须自己实现 wrapper 类型。关键不是“记下状态”,而是要覆盖所有隐式 200 场景:比如 handler 没调 WriteHeader 就直接 Write,Go 会默认发 200,但你的 wrapper 必须能感知并记录这个 fallback 行为。
-
status字段初始设为0,WriteHeader中更新并标记wroteHeader = true -
Write方法里,若!wroteHeader,先调rw.ResponseWriter.WriteHeader(http.StatusOK),再设status = 200 - 用
io.MultiWriter同时写向原始ResponseWriter和一个bytes.Buffer,统计实际字节数
如何安全读取并复用 Request.Body
直接 io.ReadAll(r.Body) 会导致后续 handler 读到 EOF,报 json: cannot unmarshal object into Go value。你不能“重放”原 Body,只能复制流。
立即学习“go语言免费学习笔记(深入)”;
- 用
io.TeeReader(r.Body, buf)把 body 流一边读一边写进bytes.Buffer,再用bytes.NewReader(buf.Bytes())构造新Body - 别用
r.Clone(ctx)—— Go 1.19+ 的Clone不复制 body 内容,返回的Body仍是空的 - 如果只记录 method/path/status,完全不用碰
Body;读 body 是高成本操作,仅在必要时做(比如审计 POST payload)
JSON 日志里中文变 \u4f60\u597d 怎么办
这是 json.Marshal 默认行为,不是 bug,但生产日志中毫无必要。禁用 Unicode 转义不能靠 SetEscapeHTML(false)(它只管 <code>>),得换方式:
- 用
json.Encoder+ 自定义MarshalJSON方法,或直接上go-json库(它默认不转义 Unicode) - 字段加
json:",string"标签会导致数字变字符串,ES/Kibana 会当keyword处理,查耗时、聚合全失效 - 用
zap时,优先走zap.Stringer+String()方法,比 JSON 序列化快一个数量级,且天然不转义
最易被忽略的一点:时间戳别用 time.Now().Format("2006-01-02T15:04:05Z07:00") 拼接,格式化开销大;直接用 time.Now().UnixMicro() 输出整数,下游再转——日志量大时,这一步省下的 CPU 很可观。


















