gin中注册全局panic捕获并转发飞书机器人需:1)禁用gin.default(),改用gin.new()手动注入增强recovery中间件;2)主goroutine panic由中间件recover捕获,异步goroutine需额外defer-recover;3)捕获后提取请求信息、堆栈,通过带重试(2次/500ms)和本地降级的日志落盘机制调用飞书webhook;4)消息体采用post卡片格式,含title摘要、code块堆栈、日志平台url,并从环境变量读取webhook地址。

如何在 Gin 中注册全局 panic 捕获并转发到飞书机器人
Gin 默认不捕获 panic,线上服务一旦 panic 就会直接 500 或断连,根本没机会发告警。必须在 gin.Engine 启动前用 gin.SetMode(gin.ReleaseMode) 关闭调试输出,并手动注入 recovery 中间件的增强版。
- 不要直接用
gin.Default(),它自带的recovery只打印日志,不支持自定义回调 - 自己重写 recovery:用
recover()拿到 panic 堆栈后,调用飞书 webhook 发送text类型消息,内容至少含time、url、error和stack - 注意
panic可能发生在异步 goroutine(比如中间件里启了 go func),这种要额外用recover()包一层,否则飞书收不到
飞书 webhook 发送失败时怎么避免告警丢失
飞书接口不稳定或网络抖动时,http.Post 直接返回 error,如果只做一次请求就放弃,关键异常就会漏报。不能靠“发一次看运气”。
- 必须加重试:建议最多 2 次重试,间隔 500ms,用
time.Sleep控制节奏,避免瞬间打爆飞书限流(默认 60 次/分钟) - 重试仍失败,要降级写本地文件(如
/tmp/gin-alert.log),带上时间戳和完整 error,方便后续人工补发 - 别用
log.Fatal或os.Exit,这会让整个服务退出,得不偿失
怎样构造飞书消息体让告警更易读、可操作
纯 text 消息容易被忽略,且无法折叠堆栈、没法跳转到日志系统。飞书支持 interactive 类型卡片,但 Gin 里发 JSON 要严格校验字段,错一个 key 就 400。
- 优先用
post类型卡片:支持title、content数组,把错误摘要放title,堆栈用code块包裹在content里 - 加
url字段指向公司内部日志平台(如https://logs.example.com?trace_id=xxx),需要 Gin 请求上下文里提前注入 trace_id - 别硬编码飞书 webhook 地址,从环境变量读取:
os.Getenv("FEISHU_WEBHOOK_URL"),本地开发设为空字符串即可跳过发送
Gin 中间件里怎么安全获取请求上下文用于告警
panic 发生时,c.Request 可能已 nil(比如在 defer 里 panic),直接取 c.Request.URL.Path 会 panic 二次。必须在中间件入口就提取关键字段并缓存。
- 在自定义 recovery 中间件开头,立刻读取
c.Request.Method、c.Request.URL.Path、c.ClientIP(),存进 map 或 struct - 避免在 recover 后再访问
c的任何方法,尤其是c.MustGet()—— 如果 key 不存在,它会 panic - 如果用了 gin-contrib/zap 等日志中间件,确保 zap logger 实例是全局复用的,不要在 panic 处理里 new 一个,否则可能阻塞或内存泄漏
飞书告警不是加个 URL 就完事,真正难的是 panic 上下文不丢、重试不雪崩、消息体不过期——这些细节没对齐,告警就只是个摆设。











