必须用包装器劫持读写过程:对请求体用bytes.buffer或io.nopcloser包装并重置body,对响应体实现自定义responsewriter拦截write/writeheader并缓存内容,同时组合实现flusher、hijacker等接口以兼容流式响应。

中间件里怎么拿到请求和响应的完整信息
直接用 echo.Context 的 c.Request() 和 c.Response() 只能拿到原始对象,没法读取请求体或响应体内容——因为 http.Request.Body 是单次读取流,http.ResponseWriter 本身不暴露已写入内容。必须用包装器劫持读写过程。
常见错误是直接 io.ReadAll(c.Request().Body),结果后续 handler 拿不到 body;或者想从 c.Response() 取状态码/响应体却始终为空。
- 对请求体:用
bytes.Buffer或io.NopCloser包装原始Body,读完后重置为可再次读取的流 - 对响应:实现自定义
ResponseWriter,拦截WriteHeader和Write调用,缓存状态码、header 和 body 片段 - 注意:大文件上传或流式响应(如 SSE)不适合全量缓存,需按需截断或跳过日志
怎么写一个带耗时和状态码的结构化日志中间件
别用 fmt.Printf 打印裸字符串,用结构化日志库(如 zerolog 或 zap)输出 JSON,方便后续采集和过滤。关键字段包括:时间戳、HTTP 方法、路径、状态码、耗时(ms)、客户端 IP、User-Agent、请求 ID(如果已有)。
示例中用 zerolog:
func Logger() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
start := time.Now()
req := c.Request()
res := c.Response()
// 包装 ResponseWriter
w := &responseWriter{ResponseWriter: res, statusCode: http.StatusOK}
c.SetResponse(echo.NewResponse(w, c.Echo()))
err := next(c)
// 记录日志
zerolog.Ctx(c.Request().Context()).Info().
Str("method", req.Method).
Str("path", req.URL.Path).
Int("status", w.statusCode).
Dur("duration", time.Since(start)).
Str("ip", c.RealIP()).
Msg("http_request")
return err
}
}
}
type responseWriter struct {
echo.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
func (rw *responseWriter) Write(b []byte) (int, error) {
if rw.statusCode == 0 {
rw.statusCode = http.StatusOK
}
return rw.ResponseWriter.Write(b)
}
注意:c.SetResponse() 必须在调用 next(c) 前完成,否则 handler 写入的是原始 ResponseWriter,你的包装器收不到数据。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
为什么日志里看不到 POST 请求体内容
因为 c.Request().Body 默认是不可重复读取的。即使你在中间件里读了一次,后续 handler(比如 c.Bind())会读到空内容,导致绑定失败或 panic。
- 正确做法:只在需要记录时才读取,并立即重置为新
io.ReadCloser,例如用io.NopCloser(bytes.NewReader(buf.Bytes())) - 但生产环境慎开「记录请求体」开关——敏感字段(密码、token)、大 payload(文件上传)会拖慢性能并泄露风险
- 更安全的做法是:仅记录非敏感字段(如
Content-Type、Content-Length),或对特定路径(如/api/debug)开启完整 body 日志
如何避免中间件影响响应头和流式接口
某些场景下,handler 会提前写 header(如 c.Response().Header().Set("X-Stream", "true"))或分块写响应(flusher.Flush())。若你的包装 ResponseWriter 没实现 http.Flusher、http.Hijacker 等接口,会导致 panic 或静默失败。
解决方法是让包装器组合实现这些接口:
type responseWriter struct {
echo.ResponseWriter
statusCode int
flusher http.Flusher
hijacker http.Hijacker
}
func (rw *responseWriter) Flush() {
if rw.flusher != nil {
rw.flusher.Flush()
}
}
func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if rw.hijacker != nil {
return rw.hijacker.Hijack()
}
return nil, nil, errors.New("hijack not supported")
}
// 初始化时检测并赋值:
if f, ok := res.(http.Flusher); ok {
rw.flusher = f
}
if h, ok := res.(http.Hijacker); ok {
rw.hijacker = h
}
没做这一步,遇到 WebSocket 升级、SSE 或长连接接口就会出问题,而且错误不明显——可能只是连接被意外关闭或 header 丢失。










