Posted in

Go HTTP中间件设计反模式(含Auth、RateLimit、Tracing):3个导致请求上下文丢失的致命bug(附修复diff)

第一章:Go HTTP中间件设计反模式总览

在 Go 的 HTTP 生态中,中间件本应是解耦、可复用、职责单一的横切逻辑载体,但实践中常因设计失当演变为系统脆弱性的温床。以下列举几种高频出现且危害显著的反模式,它们看似便捷,实则侵蚀可维护性、可观测性与并发安全性。

过度依赖全局状态注入

*sql.DB*redis.Client 或配置结构体直接赋值给包级变量,并在中间件中隐式使用,导致测试隔离困难、多实例部署冲突。正确做法是通过闭包捕获依赖:

func LoggingMiddleware(logger *zap.Logger) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            logger.Info("request started", zap.String("path", r.URL.Path))
            next.ServeHTTP(w, r)
        })
    }
}

该模式确保每次中间件实例持有独立依赖,支持按路由粒度定制行为。

忽略 ResponseWriter 包装完整性

仅包装 Write() 方法却遗漏 WriteHeader()Flush()Hijack() 等关键方法,造成 HTTP 状态码丢失、流式响应中断或 WebSocket 升级失败。必须完整实现 http.ResponseWriter 接口,或使用标准库 httptest.ResponseRecorder 的衍生封装。

中间件链中 panic 未统一捕获

未在顶层中间件中 recover(),导致 panic 泄露至 net/http 默认处理器,返回 500 且无日志。应强制要求链首中间件包含错误兜底:

func RecoveryMiddleware() func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            defer func() {
                if err := recover(); err != nil {
                    http.Error(w, "Internal Server Error", http.StatusInternalServerError)
                    log.Printf("PANIC: %+v", err)
                }
            }()
            next.ServeHTTP(w, r)
        })
    }
}

同步阻塞式上下文传递

在中间件中调用 r.Context().Value() 后直接进行耗时 I/O(如 DB 查询),阻塞整个请求协程。应始终将耗时操作移出 ServeHTTP 主线程,或使用 context.WithTimeout 显式约束。

反模式类型 风险表现 修复方向
全局状态滥用 并发写入竞争、测试不可靠 依赖闭包注入
ResponseWriter 残缺 HTTP 协议违规、功能降级 完整接口实现或使用 wrapper
Panic 逸出 服务雪崩、错误信息泄露 链首 recover + 日志记录
上下文同步阻塞 Goroutine 积压、延迟毛刺 异步化 + Context 超时控制

第二章:Auth中间件的上下文丢失陷阱与修复

2.1 基于context.WithValue的认证信息传递原理与生命周期误区

context.WithValue 常被误用于跨层透传用户身份,但其本质是只读键值快照,不触发生命周期联动。

为什么 auth.ContextKey 不该是 string?

// ❌ 危险:全局字符串 key 易冲突
ctx = context.WithValue(ctx, "user_id", 123)

// ✅ 推荐:私有未导出类型,保障类型安全
type userKey struct{}
var UserKey = userKey{}
ctx = context.WithValue(ctx, UserKey, &User{ID: 123})

WithValue 要求 key 可比较(comparable),但 string 无命名空间隔离;自定义未导出 struct 可杜绝第三方意外覆盖。

生命周期陷阱核心表现

场景 行为 后果
goroutine 复用 ctx 子协程修改 value 竞态写入 panic
HTTP handler 中 defer 清理 WithValue 无法自动清理 内存泄漏 + 信息残留

传递链路不可见性问题

graph TD
    A[HTTP Handler] --> B[Service Layer]
    B --> C[DB Query]
    C --> D[Log Middleware]
    D -.->|隐式依赖 ctx.Value| A

所有环节均需显式调用 ctx.Value(key),缺失任意一环即导致 nil 解引用 panic。

2.2 错误复用request.Context()导致Auth值被覆盖的典型场景分析

并发请求中的Context误共享

当多个 goroutine 共享同一 *http.RequestContext()(如通过 req.Context() 获取后直接传入异步任务),Auth 信息(如 context.WithValue(ctx, authKey, token))可能被后续中间件或子请求覆盖。

数据同步机制

以下代码演示了危险的 Context 复用模式:

func handleRequest(w http.ResponseWriter, req *http.Request) {
    ctx := req.Context() // ❌ 危险:复用原始req.Context()
    go processAsync(ctx) // 异步中可能被中间件修改ctx
}

func processAsync(ctx context.Context) {
    token := ctx.Value(authKey) // 可能为 nil 或旧值
    // ... 业务逻辑
}

逻辑分析req.Context() 是 request 生命周期绑定的可变上下文。中间件调用 req.WithContext(newCtx) 后,新 Context 不会自动传播至已派生的 goroutine;原 ctx 引用仍指向旧实例,但其内部 value 字段可能已被并发写入覆盖。

常见错误模式对比

场景 是否安全 原因
ctx := req.Context(); ctx = context.WithValue(ctx, key, val) ✅ 安全(新建) 显式派生新 Context
ctx := req.Context(); go f(ctx) ❌ 危险(复用) 并发中 ctx 的 value 映射被共享修改
graph TD
    A[HTTP Request] --> B[Middleware A: WithValue]
    A --> C[goroutine: use req.Context()]
    B --> D[修改 ctx.value map]
    C --> E[读取同一 map → 脏读/竞态]

2.3 中间件链中Auth上下文未显式传递引发的nil panic实战复现

在 Gin 框架中间件链中,若认证中间件将 auth.User 存入 c.Request.Context(),但后续中间件或 handler 未显式从 c.Request.Context() 取值,而是直接解引用未初始化的指针字段,将触发 panic: runtime error: invalid memory address or nil pointer dereference

复现场景代码

func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 模拟鉴权成功,注入用户信息
        user := &model.User{ID: 123, Role: "admin"}
        c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), "user", user))
        c.Next()
    }
}

func ProfileHandler(c *gin.Context) {
    user := c.MustGet("user").(*model.User) // ✅ 正确:从 gin.Context 取
    // user := c.Request.Context().Value("user").(*model.User) // ❌ 危险:若上层未注入,Value 返回 nil
    c.JSON(200, gin.H{"id": user.ID}) // 若 user 为 nil,此处 panic
}

逻辑分析c.Request.Context().Value("user") 在未被 AuthMiddleware 注入时返回 nil;强制类型断言 .(*model.User) 不触发 panic,但后续访问 user.ID 时因 user == nil 导致运行时崩溃。关键在于 Context.Value 的“静默失败”特性。

常见错误链路

  • 认证中间件注入 context.WithValue
  • 日志中间件跳过 c.Next() 提前终止链
  • 后续 handler 直接 ctx.Value("user") 而非 c.MustGet()
  • 缺少空值校验(如 if user == nil { c.AbortWithStatusJSON(401, ...) }
风险环节 是否显式校验 后果
c.Request.Context().Value() 返回 nil,延迟 panic
c.MustGet() 是(panic) 立即暴露缺失问题
c.Get() + 判断 是(需手动) 安全但易被忽略

2.4 使用自定义ContextKey+类型安全封装替代字符串Key的重构实践

问题根源:字符串Key的脆弱性

传统 context.WithValue(ctx, "user_id", id) 存在三大风险:

  • 类型擦除(interface{} 导致运行时panic)
  • 拼写错误无法被编译器捕获
  • Key复用冲突难以追踪

解决方案:强类型ContextKey

// 自定义Key类型,避免与其他包Key冲突
type userKey struct{}
var UserKey = userKey{}

// 安全存取封装
func WithUser(ctx context.Context, u *User) context.Context {
    return context.WithValue(ctx, UserKey, u)
}
func UserFrom(ctx context.Context) (*User, bool) {
    u, ok := ctx.Value(UserKey).(*User)
    return u, ok
}

逻辑分析userKey 是未导出空结构体,确保唯一地址语义;WithUserUserFrom 提供类型约束入口,编译期即校验 *User 类型,杜绝 interface{} 强转失败。

迁移收益对比

维度 字符串Key 自定义Key+封装
类型安全 ❌ 运行时panic ✅ 编译期检查
IDE支持 ❌ 无自动补全 ✅ Key常量可跳转
单元测试覆盖 需反射验证类型 直接断言 *User 类型
graph TD
    A[原始ctx.WithValue] -->|string key + interface{}| B[类型断言失败 panic]
    C[WithUser/ UserFrom] -->|userKey + *User| D[编译通过即安全]

2.5 修复diff详解:从panic到可测试、可审计的Auth中间件演进

痛点溯源:未处理的JWT解析panic

早期中间件在jwt.Parse()失败时直接panic("invalid token"),导致HTTP服务崩溃——无错误传播、无日志上下文、不可恢复。

重构核心:错误分类与结构化响应

func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        tokenStr := c.GetHeader("Authorization")
        if tokenStr == "" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, 
                map[string]string{"error": "missing auth header"})
            return
        }
        // 使用自定义Error类型,支持code、traceID、reason字段
        token, err := parseAndValidateToken(tokenStr)
        if err != nil {
            c.AbortWithStatusJSON(http.StatusUnauthorized, 
                map[string]string{"error": err.Error(), "code": err.Code()})
            return
        }
        c.Set("user_id", token.Claims["sub"])
        c.Next()
    }
}

逻辑分析parseAndValidateToken返回实现了interface{ Error() string; Code() string }的错误实例(如auth.ErrInvalidToken),使错误可分类、可审计、可被监控系统提取code标签。AbortWithStatusJSON替代panic,保障服务可用性。

可测试性保障

  • 所有依赖(JWT解析、用户查询)通过接口注入,支持mock;
  • 每个错误分支均有单元测试覆盖(含空header、过期token、签名无效等12种场景)。

审计增强能力

事件类型 日志字段 是否持久化至审计库
Token解析失败 trace_id, error_code, ip
成功鉴权 user_id, scope, ua
超时重试 retry_count, latency_ms ❌(仅debug日志)
graph TD
    A[HTTP Request] --> B{Auth Header?}
    B -->|No| C[401 + structured error]
    B -->|Yes| D[Parse & Validate JWT]
    D -->|Fail| C
    D -->|OK| E[Inject user_id → context]
    E --> F[Next handler]

第三章:RateLimit中间件的并发上下文污染问题

3.1 基于goroutine本地状态(如defer闭包捕获)引发的Context泄漏

defer 闭包意外捕获携带取消信号的 context.Context,且该 goroutine 生命周期远超预期时,Context 树无法被及时 GC,导致内存与 goroutine 泄漏。

defer 闭包隐式持有 Context 示例

func leakyHandler(ctx context.Context, ch chan<- string) {
    // ❌ 错误:defer 中闭包捕获 ctx,延长其生命周期
    defer func() {
        log.Printf("cleanup with ctx: %v", ctx.Err()) // ctx 无法释放
    }()
    select {
    case <-ctx.Done():
        return
    case ch <- "done":
        return
    }
}

逻辑分析defer 函数在函数返回后才执行,但闭包持续引用 ctx;若 ch 阻塞或 ctx 被长期持有(如父 Context 未取消),ctx 及其 cancelFunctimer 等关联对象无法回收。

安全替代方案

  • ✅ 显式传入仅需的值(如 ctx.Err() 结果)
  • ✅ 使用 context.WithTimeout 并确保 cancel() 调用
  • ✅ 避免在长生命周期 goroutine 中 defer 捕获整个 ctx
风险模式 安全模式
defer func(){ use(ctx) } err := ctx.Err(); defer func(){ log.Println(err) }
匿名闭包捕获 ctx 提前解构、按需传递字段

3.2 限流器回调中异步写入response.WriteHeader()破坏Context继承链

问题根源:WriteHeader() 的隐式状态变更

http.ResponseWriter.WriteHeader() 一旦调用,即触发 HTTP 状态行发送,并永久锁定响应头。若在限流器回调(如 limiter.OnReject(func(){...}))中异步执行该操作,将导致:

  • Context 被提前取消(因 handler goroutine 已返回)
  • response 实例所属的 http.response 内部 ctx 字段仍指向已失效的 parent Context
  • 后续 Write() 或日志记录因 ctx.Err() 返回 context.Canceled

典型错误模式

limiter.OnReject(func(w http.ResponseWriter, r *http.Request) {
    go func() { // ⚠️ 异步协程脱离原始请求生命周期
        w.WriteHeader(http.StatusTooManyRequests) // ❌ 破坏 Context 继承链
        w.Write([]byte("rate limited"))
    }()
})

逻辑分析w.WriteHeader() 内部调用 r.Context().Done() 监听,但此时 r.Context() 已随 handler 函数退出而被 cancel;异步 goroutine 中访问 w 时,其底层 response.conn.rwc 可能已关闭,且 response.ctx 不再可派生子 Context。

安全替代方案对比

方案 是否保持 Context 链 是否线程安全 推荐度
同步 WriteHeader + Write ✅ 完整继承 ★★★★★
使用 http.Hijacker 自定义响应 ⚠️ 需手动管理 ctx ★★☆☆☆
限流中间件前置拦截(不进入 handler) ✅ 最早生效 ★★★★☆
graph TD
    A[HTTP Handler] --> B{是否超限?}
    B -- 是 --> C[同步写入 Header+Body]
    B -- 否 --> D[执行业务逻辑]
    C --> E[Context 生命周期完整]
    D --> E

3.3 修复方案:将限流决策与响应写入解耦,确保ctx贯穿整个HTTP生命周期

核心改造思路

将限流器的 Check() 决策逻辑与 WriteResponse() 响应写入彻底分离,所有中间状态通过 context.Context 携带,避免中间件间隐式状态传递。

数据同步机制

使用 context.WithValue() 注入限流结果,确保下游 handler 可安全读取:

// 在限流中间件中
ctx = context.WithValue(r.Context(), "rate_limit_result", &LimitResult{
    Allowed:  true,
    Remaining: 99,
    RetryAfter: 0,
})

逻辑分析LimitResult 结构体封装决策元数据;r.Context() 确保 ctx 从请求入口延续至 ServeHTTP 末尾;WithValue 仅用于传递请求级只读数据(非业务状态)。

执行时序保障

graph TD
    A[HTTP Request] --> B[限流中间件:Check+ctx.WithValue]
    B --> C[业务Handler:读取ctx.Value]
    C --> D[响应中间件:按Result写入Status/Headers]

关键参数说明

字段 类型 含义
Allowed bool 是否放行请求
Remaining int 当前窗口剩余配额
RetryAfter time.Duration 触发拒绝时建议重试延迟

第四章:Tracing中间件的Span上下文断裂根源剖析

4.1 OpenTracing/OpenTelemetry中span.Context()与http.Request.Context()的隐式绑定失效场景

当 HTTP 中间件未显式传递 span context,或使用 context.WithValue() 覆盖原始 request.Context() 时,OpenTelemetry 的自动注入机制会断裂。

数据同步机制

OpenTelemetry SDK 依赖 http.Request.Context() 中的 oteltrace.SpanContextKey 值进行 span 关联。若中间件执行:

req = req.WithContext(context.WithValue(req.Context(), "user-id", "123"))
// ❌ 覆盖后原 span context 丢失(因 WithValue 不继承 parent values)

此处 context.WithValue 创建新 context,但未保留 oteltrace.SpanContextKey 对应的 SpanContext,导致后续 span.FromContext(req.Context()) 返回空 span。

典型失效场景对比

场景 是否保留 span.Context 原因
req.WithContext(ctx)(ctx 含 span) 显式继承
context.WithValue(req.Context(), k, v) 底层 valueCtx 不透传未声明 key
goroutine 中直接 req.Context() ⚠️ 若 req 已被 WithContext 替换则安全,否则可能为原始 background

隐式绑定断裂流程

graph TD
    A[HTTP Handler] --> B[Middleware A]
    B --> C[Middleware B: req.WithContext<br>with custom value]
    C --> D[otelhttp.Handlers<br>无法提取 span]
    D --> E[新建 root span]

4.2 中间件中错误调用req.WithContext(span.Context())却忽略返回值导致traceID丢失

req.WithContext() 是不可变操作,返回新请求对象;原 req 保持不变。

常见错误写法

// ❌ 错误:忽略返回值,原req未更新
req.WithContext(span.Context()) // 返回值被丢弃
handler.ServeHTTP(w, req)       // 仍使用无span的req

逻辑分析:WithContext() 创建新 *http.Request 并携带 span 的 context.Context,但 Go 中 http.Request 是不可变结构体,所有 WithXXX 方法均返回新实例。此处未赋值,下游中间件/Handler 读取的仍是原始无 trace 上下文的 req

正确写法

// ✅ 正确:显式接收并传递新req
req = req.WithContext(span.Context())
handler.ServeHTTP(w, req)

影响对比

场景 traceID 是否透传 后续 Span 是否可关联
忽略返回值 否(新建 Span 成孤立节点)
正确赋值 是(形成完整调用链)
graph TD
    A[Middleware] -->|req.WithContext| B[New req with span]
    B -->|未赋值| C[Original req passed]
    C --> D[Handler sees no traceID]

4.3 跨goroutine(如日志异步刷盘、metric上报)中context.WithValue传播中断分析

问题根源:context非继承式传递

context.WithValue 创建的新 context 仅对直接子调用可见;若启动新 goroutine 时未显式传入该 context,其内部 ctx.Value() 将回退到父 context(通常是 context.Background()),导致键值丢失。

典型失效场景

  • 日志异步刷盘:go func() { log.Write(ctx, msg) }()ctx 未重新传入
  • Metric 上报协程:go reportMetrics(ctx)ctx 是外层变量而非参数,实际使用的是捕获时的原始 context

修复方案对比

方案 是否安全 说明
go f(ctx, args...) 显式传参,保证 context 链完整
go func() { f(ctx, args...) }() 匿名函数闭包捕获正确 ctx
go f(parentCtx, args...) parentCtx 可能不含所需 value
// ❌ 危险:ctx 在 goroutine 启动时未传入
ctx := context.WithValue(context.Background(), key, "trace-123")
go func() {
    val := ctx.Value(key) // nil!因 ctx 未作为参数传入,且无显式引用
    log.Printf("value: %v", val)
}()

// ✅ 正确:显式传参确保 context 值可达
go func(c context.Context) {
    val := c.Value(key) // "trace-123"
    log.Printf("value: %v", val)
}(ctx)

逻辑分析:Go 的 goroutine 启动不自动继承 caller 的 context;ctx.Value() 查找依赖 context 链,而闭包捕获或参数传递是唯一可控路径。key 类型应为 any(推荐 struct{})以避免冲突。

graph TD
    A[main goroutine] -->|WithValues| B[ctx with traceID]
    B --> C[spawn goroutine]
    C -->|❌ 未传ctx| D[ctx.Value→nil]
    B -->|✅ 显式传入| E[goroutine ctx]
    E --> F[ctx.Value→'trace-123']

4.4 修复diff详解:基于context.WithValue + span.Context()双校验的健壮追踪中间件

核心校验逻辑

中间件在请求入口同时注入 traceID(via context.WithValue)与 span.Context(),形成冗余但互补的上下文源。

双校验触发条件

  • span.Context() 可用 → 优先采用 OpenTracing 标准上下文
  • ⚠️ span.Context() 为空但 ctx.Value(traceKey) 存在 → 回退至手动注入值
  • ❌ 两者均缺失 → 拒绝追踪,避免污染链路

关键代码片段

func TraceMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        span := opentracing.SpanFromContext(ctx)
        var traceID string

        // 双校验:先尝试 span.Context(),再 fallback 到 context.WithValue
        if span != nil {
            traceID = span.Context().(opentracing.SpanContext).TraceID().String()
        } else if val := ctx.Value("trace_id"); val != nil {
            traceID = val.(string)
        }

        // 注入统一 traceID 到日志与下游 context
        ctx = context.WithValue(ctx, "trace_id", traceID)
        r = r.WithContext(ctx)
        next.ServeHTTP(w, r)
    })
}

逻辑分析span.Context() 提供分布式链路原生语义,而 context.WithValue 作为防御性兜底——二者类型不同(SpanContext vs string),校验时需显式类型断言。traceID 作为唯一标识贯穿日志、指标与链路,双源保障避免因 Span 提前结束导致的上下文丢失。

校验项 类型来源 容错能力 是否参与 OpenTracing 标准传播
span.Context() OpenTracing SDK 弱(Span 结束即失效) ✅ 是
ctx.Value() 手动注入 强(生命周期同 request.Context) ❌ 否(需显式透传)
graph TD
    A[HTTP Request] --> B{span.Context() valid?}
    B -->|Yes| C[采用 SpanContext.TraceID]
    B -->|No| D{ctx.Value(trace_id) exists?}
    D -->|Yes| E[采用 context.Value]
    D -->|No| F[traceID = \"\"]
    C --> G[注入统一 ctx]
    E --> G
    F --> G

第五章:构建可观察、可维护的Go HTTP中间件体系

中间件链的显式组装与生命周期管理

在生产级服务中,我们摒弃 mux.Use() 的隐式叠加,采用函数式组合构建可读性强的中间件链。例如:

func NewHTTPHandler() http.Handler {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/users", userHandler)

    return loggingMiddleware(
        metricsMiddleware(
            recoveryMiddleware(
                authMiddleware(
                    rateLimitMiddleware(mux, 100, time.Minute),
                ),
            ),
        ),
    )
}

该模式确保每个中间件职责单一,且调用顺序一目了然,便于调试与灰度替换。

结构化日志注入请求上下文

使用 zerolog 将 trace ID、request ID、路径、状态码、耗时等字段注入每条日志,避免字符串拼接:

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        reqID := uuid.New().String()
        logCtx := zerolog.Ctx(ctx).With().
            Str("req_id", reqID).
            Str("method", r.Method).
            Str("path", r.URL.Path).
            Logger()

        r = r.WithContext(logCtx.WithContext(ctx))
        start := time.Now()

        rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
        next.ServeHTTP(rw, r)

        logCtx.Info().
            Int("status", rw.statusCode).
            Dur("duration_ms", time.Since(start)).
            Msg("http_request")
    })
}

Prometheus指标采集与标签正交化

定义三类核心指标并严格分离标签维度:

指标名 类型 标签示例 采集方式
http_requests_total Counter method="POST",path="/api/users",status_code="201" 每次响应后 Inc()
http_request_duration_seconds Histogram method="GET",path="/healthz" Observe(time.Since(start).Seconds())
http_active_requests Gauge method="PUT" 请求进入+1,退出−1

所有指标注册统一通过 promauto.NewCounterVec() 实例化,避免重复注册导致 panic。

分布式追踪上下文透传

集成 OpenTelemetry SDK,在入口中间件解析 traceparent 头,并将 SpanContext 注入 context.Context

func tracingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        spanCtx := otel.GetTextMapPropagator().Extract(ctx, propagation.HeaderCarrier(r.Header))
        ctx, span := tracer.Start(
            trace.ContextWithRemoteSpanContext(ctx, spanCtx),
            "http_server",
            trace.WithSpanKind(trace.SpanKindServer),
        )
        defer span.End()

        r = r.WithContext(ctx)
        next.ServeHTTP(w, r)
    })
}

配置驱动的中间件开关与降级

通过 YAML 配置动态启用/禁用中间件,并支持运行时热重载:

middleware:
  logging: true
  metrics: true
  auth: 
    enabled: true
    skip_paths: ["/healthz", "/metrics"]
  rate_limit:
    enabled: true
    limit: 500
    window: 60s

使用 fsnotify 监听配置文件变更,触发 atomic.Value.Store() 更新当前中间件链实例,实现毫秒级生效。

错误分类与结构化上报

自定义 AppError 类型封装业务错误码、HTTP 状态、日志等级与 Sentry 事件级别:

type AppError struct {
    Code    string `json:"code"`
    Message string `json:"message"`
    Status  int    `json:"status"`
    Level   Level  `json:"level"` // Info, Warn, Error, Fatal
}

func recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                appErr := &AppError{
                    Code:    "INTERNAL_PANIC",
                    Message: fmt.Sprintf("panic: %v", err),
                    Status:  http.StatusInternalServerError,
                    Level:   Error,
                }
                sentry.CaptureException(fmt.Errorf("%+v", err))
                zerolog.Ctx(r.Context()).Err(err).Msg("panic recovered")
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

可观测性仪表盘联动设计

Grafana 中配置告警规则:当 rate(http_requests_total{status_code=~"5.."}[5m]) > 0.01http_request_duration_seconds_bucket{le="0.5"} < 0.95 同时触发时,自动关联展示 http_active_requestsotel_trace_span_count,定位是否为慢查询引发连接池耗尽。

热爱算法,相信代码可以改变世界。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注