需在中间件中用 if r := recover(); r != nil 安全捕获 panic 并以 fmt.sprintf("%v", r) 记录,附带请求路径与方法;避免响应已提交后写入;按响应状态码分级日志,500+ 须记 error 并捕获堆栈。

中间件里捕获 panic 并记录错误日志
Echo 默认不自动 recover panic,直接崩溃。要在中间件中统一记录错误日志,必须手动 recover(),否则 500 错误不会进日志,更没法打堆栈。
常见错误是只记录 err.Error(),漏掉 panic 值类型(比如 nil、string、error)导致 panic: interface conversion 二次崩溃。
- 务必用
if r := recover(); r != nil判断,再根据r类型做安全转换 - 推荐统一转成
fmt.Sprintf("%v", r),避免类型断言失败 - 记录时带上
c.Request().URL.Path和c.Request().Method,方便定位请求上下文 - 别在 recover 后调用
c.JSON()等写响应的方法——此时 response writer 可能已提交,会 panic
区分 HTTP 错误和 panic 错误的日志级别
不是所有错误都该记为 ERROR 级别。400、404 这类客户端错误适合 WARN,而 panic、数据库连接失败、空指针解引用必须是 ERROR 或更高。
如果用 zap 或 logrus,建议在中间件里按 c.Response().Status() 分流:
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
-
Status() >= 500→ 记ERROR,附带stack(需提前用debug.Stack()捕获) Status() >= 400 && Status() → 记 <code>WARN,不打堆栈- 显式调用
c.Error(err)的场景(如业务校验失败),默认走WARN,除非你主动重设err的类型或包装
避免中间件中日志重复写入
Echo 的 c.Error() 本身会触发 echo.HTTPErrorHandler,如果这个 handler 也记日志,再加上你自定义的中间件日志,同一错误就记两遍。
最稳妥的做法是:禁用默认错误处理,把所有错误收敛到一个中间件里处理。
- 启动时设置
e.HTTPErrorHandler = func(err error, c echo.Context) {},清空默认行为 - 确保你的日志中间件是第一个注册的(
e.Use(logMiddleware)放在最前) - 不要在 handler 里调用
c.Error()后又手动log.Error(...),这会造成冗余 - 注意:Echo v4.10+ 的
c.Logger()默认不输出 error,它只是个 placeholder,不能替代真实日志实例
性能与 context 生命周期的坑
日志中间件里若用了 zap.String("user_id", c.Get("user_id").(string)) 这类取值,一旦 c.Get() 返回 nil 或类型不对,就会 panic —— 而此时你刚 recover 完上一个 panic,又掉进下一个。
更隐蔽的问题是:在 defer 中访问 c.Request().Context().Done() 或依赖 request body 的字段(比如 c.FormValue()),可能因 body 已被读过而返回空或 error。
- 所有从
c取值的操作,先判空、再断言,或用value, ok := c.Get("key") - 不要在 defer 里解析 body;如需记录请求体,应在中间件开头用
ioutil.ReadAll(c.Request().Body)备份,并重置c.Request().Body - 高并发下频繁打 stack trace 会影响性能,建议仅在
Status() >= 500时调用debug.Stack()
c.Get() 和 c.Request().Body 这两个地方,出问题时往往没报错,只是日志变空或者少字段。










