真正的全局崩溃自动告警需在gin.recovery()中扩展recoveryfunc,写入独立错误日志、推送sentry、异步发邮件,并确保中间件顺序正确、文件句柄长期持有、环境配置隔离且敏感信息不硬编码。

Gin 服务线上崩溃,靠人工盯日志或等用户反馈才察觉?不行。真正的全局崩溃自动告警,核心不是“捕获 panic”,而是“捕获后立刻触发可观察、可响应的动作”——比如写入独立错误文件 + 推送到 Sentry + 发邮件。默认的 gin.Recovery() 只做两件事:终止 panic 波及、返回 500,其余全靠你补。
panic 崩溃没进 Sentry?检查 Recovery 中间件注册顺序
Recovery 必须是第一个中间件,否则 panic 发生时,前面未执行的中间件(比如鉴权、日志)可能已 panic,或者后续中间件(比如自定义 error handler)干扰了 recover 流程。
-
router.Use(gin.Recovery())必须在router.Use(...)所有其他中间件之前调用 - 如果用了
gin.Default(),它内部已调用gin.Recovery(),但你无法替换其输出目标——此时必须改用gin.New()+ 手动注册gin.RecoveryWithWriter() - 切勿在业务 handler 内再套一层
defer/recover:这会绕过 Recovery 中间件,导致 Sentry 埋点漏报、错误日志不统一
错误日志写入文件但没内容?确认文件句柄未被提前关闭
用 os.OpenFile("error.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) 创建的文件句柄,必须在整个服务生命周期内保持打开状态。常见错误是把它写在某个函数里,函数返回后句柄被 GC 或显式 Close(),后续 panic 日志就全丢进黑洞。
- 把
errFile声明为包级变量,或注入到 Engine 实例中长期持有 - 务必在
main()结束前调用errFile.Close(),而不是在中间件初始化函数末尾关 - 验证是否生效:手动
panic("test"),看error.log是否追加了完整堆栈(含 goroutine 和 source line)
想发邮件或推 Sentry?别改 Recovery 源码,用自定义 recoveryfunc
gin.RecoveryWithWriter() 支持传入第二个参数 recoveryfunc,这才是埋点扩展点。默认的 defaultHandleRecovery 只打印到 writer,而你可以让它同时调用 sentry.CaptureException() 和 sendAlertEmail()。
- 示例签名:
func(c *gin.Context, err interface{}),err是原始 panic 值(可能是string、error或任意类型) - 注意:Sentry 的
CaptureException()需要*errors.ErrorStack,建议用debug.Stack()拼装上下文后再传 - 邮件发送必须异步(go func() {...}()),否则阻塞 HTTP 请求线程,导致超时雪崩
为什么本地测试能告警,线上却静默?环境隔离和权限问题
线上容器或 systemd 服务常限制网络外连(Sentry)、文件写入路径(/tmp 可能只读)、SMTP 端口(25 被屏蔽)。这些不会报 panic,只会让告警逻辑静默失败。
- 所有告警调用必须包裹
if err != nil { log.Printf("[ALERT FAIL] %v", err) },确保失败可查 - 写入错误日志的路径不要硬编码
"./error.log",改用绝对路径如/var/log/myapp/error.log,并提前chown myapp:myapp /var/log/myapp - Sentry DSN、邮箱密码等敏感配置必须从环境变量读取,禁止写死;启动时校验
os.Getenv("SENTRY_DSN") != ""
真正难的不是写一个 recover,而是让每一次 panic 都留下足够线索:哪条路由、哪个用户 ID、什么请求体、panic 前最后三行代码是什么。这些信息不在 recover() 返回值里,得靠 c.Request、c.Keys 和 runtime.Caller() 自己捞。漏掉任意一环,告警就只剩 “internal server error”,等于没告。











