Golang Web开发中如何做请求日志_Golang日志中间件实现思路

1次阅读

默认的 http.servemux 不记录请求日志,因其仅负责路由分发,无内置日志能力;完整日志需通过外层中间件拦截整个请求生命周期,捕获状态码、字节数、耗时等关键字段。

Golang Web开发中如何做请求日志_Golang日志中间件实现思路

为什么默认的 http.ServeMux 不记录请求日志

go 标准库的 http.ServeMux 本身不带日志能力,它只负责路由分发。你看到的 log.printf("received request") 这类写法如果直接塞进 handler,容易漏掉 panic、超时、提前返回等路径,导致日志不完整或丢失状态码。

真正可控的日志必须在请求生命周期最外层拦截——也就是用中间件包装所有 handler,确保无论是否 panic、是否重定向、是否提前 return,都能捕获开始时间、结束时间、状态码、字节数等关键字段。

http.Handler 实现可复用的日志中间件

核心是实现一个接收 http.Handler 并返回新 http.Handler 的函数。注意不要直接包装 http.HandlerFunc,否则会丢失对底层 http.Handler 接口的兼容性(比如第三方 routergorilla/mux 返回的是自定义 Handler)。

  • time.Now() 记录起始时间,defer 中计算耗时
  • io.TeeReader 或自定义 ResponseWriter 拦截响应体和状态码(推荐后者)
  • 避免在日志中打印完整 r.Body,容易阻塞或泄露敏感数据;如需 body 内容,应限制长度并做脱敏
  • 状态码必须从自定义 ResponseWriterWriteHeaderWrite 方法中捕获,不能依赖 defer 里读 rw.Status —— 因为有些 handler 可能根本不调用 WriteHeader
type loggingResponseWriter struct { 	http.ResponseWriter 	statusCode int 	written    int }  func (lrw *loggingResponseWriter) WriteHeader(code int) { 	lrw.statusCode = code 	lrw.ResponseWriter.WriteHeader(code) }  func (lrw *loggingResponseWriter) Write(b []byte) (int, error) { 	if lrw.statusCode == 0 { 		lrw.statusCode = http.StatusOK 	} 	n, err := lrw.ResponseWriter.Write(b) 	lrw.written += n 	return n, err }  func LoggingMiddleware(next http.Handler) http.Handler { 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 		start := time.Now() 		lrw := &loggingResponseWriter{ 			ResponseWriter: w, 			statusCode:     http.StatusOK, 		} 		next.ServeHTTP(lrw, r) 		log.Printf("[%s] %s %s %d %d %v", 			r.Method, 			r.URL.Path, 			r.Proto, 			lrw.statusCode, 			lrw.written, 			time.Since(start), 		) 	}) }

生产环境要注意的三个坑

本地调试时日志看着没问题,一上生产就出问题,常见于:

立即学习go语言免费学习笔记(深入)”;

  • log.Printf 默认输出到 os.Stderrkubernetes 或 systemd 下可能被截断或丢弃;应显式配置 log.SetOutput 到文件或结构化 logger(如 zap
  • 并发下大量字符串拼接 + log.Printf 会成为性能瓶颈;建议用 fmt.Sprintf 预格式化或直接用 zap.Stringer 类型避免分配
  • 未过滤健康检查路径(如 /healthz),导致日志刷屏;应在中间件开头加白名单/黑名单判断:if strings.HasPrefix(r.URL.Path, "/healthz") { next.ServeHTTP(w, r); return }

要不要集成结构化日志(如 zap

纯文本日志查问题效率低,尤其要按 status、path、latency 做聚合分析时。用 zap 替换 log 几乎零成本:

  • log.Printf(...) 改成 logger.Info("http request", zap.String("method", r.Method), ...)
  • 注意 zap.Duration 要传 time.Since(start),别传 start.Sub(...) 错方向
  • 如果用了 ginecho,它们自带日志中间件,但默认不记录 body size 和精确耗时;仍建议自己包一层或 patch 其 ResponseWriter

真正麻烦的不是怎么记日志,而是日志字段是否统一、能否被日志采集系统(如 filebeat + elk)正确解析。路径、方法、状态码、耗时、字节数这五个字段缺一不可,且命名最好跟 OpenTelemetry HTTP 规范对齐。

text=ZqhQzanResources