应在 gin 中用 io.teereader + bytes.buffer 拦截请求体并重置 body,包装 responsewriter 拦截响应体,避免直接读取导致下游无法消费;对二进制内容做 hex 编码或截断;大文件限长记录;用 zap 结构化日志,过滤敏感字段,透传 trace_id 实现链路追踪,异步写入防阻塞,非核心路径及采样控制降负载。

如何在 Gin 中拦截并记录完整请求/响应体
直接读 ctx.Request.Body 会导致后续 handler 读不到数据,因为 Go 的 io.ReadCloser 只能消费一次。必须用 httputil.DumpRequestOut 或手动复制 body 并重置 ctx.Request.Body。
- 推荐用
io.TeeReader+bytes.Buffer拦截请求体:先写入 buffer 记录,再透传给下游 - 响应体拦截更麻烦——Gin 默认不暴露 response body,需包装
http.ResponseWriter实现Write和WriteHeader拦截 - 注意二进制内容(如图片上传、Protobuf)不要强制 UTF-8 解码,用
hex.EncodeToString或截断处理避免日志乱码或崩溃 - 大文件上传场景下,记录完整 body 会吃光内存,建议只记录前 1KB 或开关控制
用 Zap 替代 log.Printf 实现结构化日志输出
log.Printf 输出纯文本,难过滤、难聚合;Zap 提供高性能结构化日志,字段可直接被 ELK 或 Loki 解析。
- 初始化时用
zap.NewProduction()(线上)或zap.NewDevelopment()(调试) - 记录字段必须显式命名:
logger.Info("request completed", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.Int("status", w.status)) - 避免在日志中拼接字符串(如
"user:" + uid),Zap 的字段函数已做逃逸优化 - 敏感字段(如
Authorization、password)必须过滤,可用中间件预处理 header map 后再传入 logger
按 trace_id 关联请求链路日志
Gin 默认不带分布式追踪上下文,需从 header(如 X-Request-ID 或 traceparent)提取 trace_id,并透传到下游服务和日志中。
- 用
gin.Context.Set("trace_id", tid)存入 context,后续 handler 和 logger 都从中取值 - 若集成 OpenTelemetry,优先用
otel.GetTextMapPropagator().Extract解析traceparent,比硬编码 header 名更健壮 - 确保所有日志调用都带上
zap.String("trace_id", traceID),否则链路无法串联 - 注意:同一请求的多个 goroutine(如异步发消息)需用
context.WithValue传递 trace_id,不能只靠 gin.Context
避免日志插件拖慢网关吞吐量
日志写磁盘或网络是阻塞操作,高频请求下容易成为瓶颈。关键不是“要不要记”,而是“怎么记才不影响性能”。
- 绝对不要在 handler 主流程里直接
logger.Info(...)写文件——改用异步 channel + worker 模式批量刷盘 - Zap 的
Core支持自定义写入逻辑,可对接 Kafka 或 Loki 的 HTTP API,但需加超时和失败重试 - 非核心路径(如 OPTIONS 预检、健康检查 /healthz)建议跳过日志,用
if r.Method == "OPTIONS" { return }快速过滤 - 采样率控制很实用:
if rand.Intn(100) ,线上可设为 1%~5%,调试期开 100%
真正难的不是记录日志,而是在高并发下不丢日志、不拖慢请求、不泄露敏感信息——这三点漏掉任何一项,日志就从排查工具变成事故源头。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











