因为sentry的panic捕获依赖全局handler注册,而gin的http server启动后才开始接收请求,若sentry.init()在router.run()之后调用,则所有中间件和handler中的panic均无法被捕获,导致生产环境错误静默丢失。

为什么Gin里init()放对位置比写对代码还重要
因为Sentry的panic捕获依赖全局handler注册,而Gin的HTTP server启动后才开始接收请求——如果sentry.Init()在router.Run()之后调用,所有中间件、handler里的panic都收不到。常见现象是本地调试能报错,一上生产就静默。
必须把sentry.Init()放在main()函数最开头,早于gin.Default()和任何日志初始化:
func main() {
if err := sentry.Init(sentry.ClientOptions{
Dsn: "https://xxx@o123.ingest.sentry.io/456",
Environment: "production",
Release: "myapp@1.2.0",
Debug: false, // 生产环境必须关
}); err != nil {
log.Fatal(err)
}
defer sentry.Flush(2 * time.Second)
<pre class="brush:php;toolbar:false;">r := gin.Default()
r.Use(SentryRecovery()) // 中间件注册必须在Init之后
r.GET("/health", func(c *gin.Context) { panic("test") })
r.Run(":8080")}
-
Debug: false:生产环境开启会打印内部日志,干扰业务输出且暴露配置 -
defer sentry.Flush(...):进程退出前强制刷出缓冲事件,否则SIGTERM杀进程时错误全丢 - 别在
init()函数里调sentry.Init()——包初始化阶段panic无法被捕获
Gin中间件里怎么让panic自动进Sentry
Gin默认不接管panic,panic("xxx")只会返回500页面,Sentry完全不知情。必须自己加中间件兜底,但不能简单recover()完就完事——要确保堆栈完整、上下文可追溯。
推荐写法:sentry.Recover() + AttachStacktrace: true:
func SentryRecovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
sentry.WithScope(func(scope *sentry.Scope) {
scope.SetTag("gin-handler", c.HandlerName())
scope.SetTag("path", c.Request.URL.Path)
scope.SetExtra("method", c.Request.Method)
sentry.CaptureException(fmt.Errorf("%v", err))
})
}
}()
c.Next()
}
}
- 别用
sentry.Recover()直接替换——它只在全局生效,对Gin handler内的panic无效 -
scope.SetTag()手动补路径和handler名,否则所有错误都显示为GET / - 避免在scope里塞
c.Request.Header等敏感字段,Sentry控制台默认可公开访问
业务error为什么不进Sentry?CaptureException()不是万能的
if err != nil { sentry.CaptureException(err) }这行代码本身没错,但问题常出在上游:err是nil、被提前处理、或类型不对。Sentry-go只认error接口,fmt.Errorf("xxx")可以,string或int不行。
典型漏报场景:
- 第三方库返回
nilerror但实际失败(比如某些HTTP client超时没设Timeout,resp为nil但err也是nil) - try/catch后只
log.Printf没CaptureException,错误停留在本地日志 - goroutine里出错没显式上报——Go的goroutine panic不传播,
sentry.Recover()对它无效
高频低价值error(如参数校验失败)建议过滤,否则刷爆quota:
if errors.Is(err, ErrValidationFailed) {
return // 不上报
}
sentry.CaptureException(err)
为什么Sentry看不到用户ID和请求头
Sentry-go默认不解析*http.Request,ConfigureScope()得你自己调,而且必须在请求生命周期内——不能在main()里一次配完。
最佳实践是在中间件里绑定:
r.Use(func(c *gin.Context) {
sentry.ConfigureScope(func(scope *sentry.Scope) {
scope.SetTag("user_id", c.GetString("user_id")) // 假设JWT中间件已注入
scope.SetTag("ip", c.ClientIP())
scope.SetExtra("ua", c.Request.UserAgent())
})
c.Next()
})
- 别在
WithScope里直接读c.Request.Header.Get("X-Auth-Token")——token可能含敏感信息,Sentry控制台默认可见 -
SetTag适合结构化字段(如user_id、region),SetExtra适合调试用的非结构化数据(如ua) - 如果用了gin-contrib/sessions,从session取
user_id比从header取更安全
最易被忽略的是goroutine上下文丢失:你在主goroutine里ConfigureScope,但异步任务新开的goroutine里scope是空的——每个goroutine要单独绑。











