不能直接在 handler 里用 log.Printf 记录访问日志,因其缺乏请求上下文(如 IP、路径、耗时、状态码),无法统一格式与输出目标,且 panic 或提前 return 时无法获取完整响应信息;必须用 echo.MiddlewareFunc 封装结构化访问日志中间件,在请求进入和响应返回时精准捕获字段。

为什么不能直接在 handler 里用 log.Printf 记录访问日志
因为 Echo 的 Context 是请求生命周期的载体,而裸调 log.Printf 缺乏请求上下文(如 IP、路径、耗时、状态码),也难以统一控制日志格式、采样或输出目标。更关键的是:如果 handler panic 或提前 return,你可能根本拿不到完整响应状态和耗时。
用 echo.MiddlewareFunc 套一层访问日志中间件
这是最标准、最可控的方式——利用 Echo 的中间件机制,在请求进入和响应返回两个时机捕获必要字段。注意中间件必须在路由注册前 use,且顺序影响日志完整性(比如要在 echo.Logger() 之前还是之后?通常放最外层)。
- 记录起点时间用
c.Request().RemoteAddr和c.Path(),别依赖c.Request().URL.Path(可能被重写过) - 耗时必须用
time.Since()配合 defer,不能用time.Now()在结尾再取一次(有延迟误差) - 状态码要从
c.Response().Status读,不是w.WriteHeader()的参数(Echo 封装了 ResponseWriter) - 若需记录请求体或响应体,得用
echo.WrapHandler或自定义ResponseWriter,但会显著影响性能,慎开
func AccessLog() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
start := time.Now()
path := c.Path()
method := c.Request().Method
ip := c.RealIP()
err := next(c)
status := c.Response().Status
latency := time.Since(start)
log.Printf("[%s] %s %s %s %d %v", ip, method, path, time.Now().Format("15:04:05"), status, latency)
return err
}
}
}
// 使用
e := echo.New()
e.Use(AccessLog())
怎么把日志结构化并接入 zap 或 logrus
原生 log 包不支持字段注入,硬拼字符串难过滤、难聚合。换成结构化日志库后,要把 ip、path、status、latency 等作为字段传入,而不是塞进 msg 字符串。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
- 用
zap.String("ip", ip)而不是fmt.Sprintf("ip=%s", ip) - 注意
latency用zap.Duration("latency", latency),便于指标统计 - 如果用了
echo.HTTPErrorHandler,错误日志要单独处理,避免重复记录 500 状态 - 不要在中间件里调
c.Logger()—— 它是为业务日志设计的,和访问日志语义不同,且默认不带请求上下文字段
为什么 c.Logger() 不适合记访问日志
c.Logger() 返回的是绑定当前请求的 echo.Logger 实例,但它底层仍走 Go 标准 log,不支持结构化字段;更重要的是它的默认输出不含 status 和 latency,你得手动补全,反而破坏中间件的职责单一性。它更适合 handler 内部打「业务行为」日志,比如 “用户 ID 123 修改了订单状态”,而不是“GET /api/order 200 12ms”。
真正容易被忽略的是:当服务启用了 gzip、JWT 验证、CORS 等中间件时,访问日志中间件必须放在最外层,否则拿到的 latency 不包含这些开销,监控数据就失真了。










