3. 请求与响应拦截:中间件模式、请求日志记录、响应状态码处理、自定义 Transport

在业务环境回放编排中,拦截能力是保障可观测性与稳定性的核心。Go 的 net/http 包提供了两种拦截粒度:中间件模式(Handler 级别)与 自定义 Transport(RoundTripper 级别)。本章将系统讲解如何利用这两种机制实现请求日志记录、响应状态码处理,并构建可复用的拦截链路。

3.1 中间件模式与 Transport 拦截的职责边界

两种拦截方式在回放编排中承担不同职责,下表梳理了核心差异:

维度 中间件模式 (Handler) 自定义 Transport (RoundTripper)
拦截层级 HTTP 处理函数层 HTTP 客户端传输层
适用场景 请求路由、认证、业务日志 请求重试、代理、回放流量标记
可访问数据 Request + ResponseWriter Request + Response(只读)
回放编排价值 记录回放请求上下文 注入回放标识、拦截响应码
架构原则:中间件处理“业务语义”,Transport 处理“传输语义”。在回放编排中,建议将日志记录放在中间件层,将状态码分类与重试逻辑放在 Transport 层。

3.2 中间件模式:请求日志记录

中间件模式通过包装 http.Handler 实现横切关注点。以下是一个标准的结构化日志中间件:

func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        // 包装 ResponseWriter 以捕获状态码
        lrw := &loggingResponseWriter{ResponseWriter: w, statusCode: http.StatusOK}

        next.ServeHTTP(lrw, r)

        duration := time.Since(start)
        log.Printf(
            "[REPLAY] method=%s path=%s status=%d duration=%v trace_id=%s",
            r.Method, r.URL.Path, lrw.statusCode, duration, r.Header.Get("X-Trace-ID"),
        )
    })
}

type loggingResponseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (lrw *loggingResponseWriter) WriteHeader(code int) {
    lrw.statusCode = code
    lrw.ResponseWriter.WriteHeader(code)
}
回放日志关键字段:除 method/path/status/duration 外,务必记录 X-Replay-ID 和 X-Trace-ID,用于关联回放会话与原始请求。

3.3 响应状态码处理:分类与决策

回放编排中,响应状态码决定了后续编排动作。我们将状态码分为三类:

  • 成功族 (2xx):直接返回响应,记录回放成功。
  • 客户端错误 (4xx):记录回放差异,可能触发告警。
  • 服务端错误 (5xx):触发重试或降级策略。

状态码处理中间件示例:

func StatusCodeHandler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        lrw := &statusRecorder{ResponseWriter: w, code: http.StatusOK}
        next.ServeHTTP(lrw, r)

        switch {
        case lrw.code >= 500:
            // 服务端错误:记录并触发重试
            log.Printf("[REPLAY_ERROR] 5xx detected: %d for %s", lrw.code, r.URL.Path)
            // 此处可注入重试 Transport
        case lrw.code >= 400:
            // 客户端错误:记录差异
            log.Printf("[REPLAY_DIFF] 4xx detected: %d for %s", lrw.code, r.URL.Path)
        }
    })
}
注意:不要在所有 4xx 场景下都触发告警。例如 404 可能是回放数据自然过期,应结合业务上下文判断。

3.4 自定义 Transport:回放流量拦截

自定义 Transport 通过实现 http.RoundTripper 接口,在请求发出前和响应返回后执行拦截逻辑。这是回放编排中注入标识、控制重试的核心机制。

type ReplayTransport struct {
    Base http.RoundTripper
}

func (t *ReplayTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    // 注入回放标识
    req.Header.Set("X-Replay-Mode", "true")
    req.Header.Set("X-Replay-Timestamp", time.Now().UTC().Format(time.RFC3339))

    // 执行原始请求
    resp, err := t.Base.RoundTrip(req)
    if err != nil {
        return nil, fmt.Errorf("replay transport error: %w", err)
    }

    // 响应后处理:记录状态码
    log.Printf("[REPLAY_TRANSPORT] %s %s -> %d", req.Method, req.URL.String(), resp.StatusCode)

    return resp, nil
}

// 使用方式
client := &http.Client{
    Transport: &ReplayTransport{Base: http.DefaultTransport},
}

3.5 拦截链路架构图

下图展示了回放编排中完整的拦截链路:

回放编排拦截链路架构 回放请求入口 中间件层:日志记录 + 状态码分类 LoggingMiddleware → StatusCodeHandler 业务 Handler 自定义 Transport:注入回放标识 + 响应拦截 ReplayTransport.RoundTrip() 下游目标服务 拦截职责划分 • 中间件:业务日志 • Transport:传输拦截 • 两者可组合使用 回放关键 Header • X-Replay-Mode • X-Replay-ID • X-Trace-ID

3.6 组合使用:构建完整拦截链

在实际回放编排中,中间件与 Transport 需要协同工作。以下是一个完整的初始化示例:

func NewReplayServer(handler http.Handler) *http.Server {
    // 1. 构建中间件链
    var h http.Handler = handler
    h = StatusCodeHandler(h)   // 状态码处理
    h = LoggingMiddleware(h)   // 日志记录

    // 2. 构建自定义 Transport 客户端
    client := &http.Client{
        Transport: &ReplayTransport{
            Base: &http.Transport{
                MaxIdleConns:        100,
                IdleConnTimeout:     90 * time.Second,
                DisableCompression:  true,
            },
        },
        Timeout: 30 * time.Second,
    }

    // 3. 将 client 注入 handler 上下文
    ctx := context.WithValue(context.Background(), "replay_client", client)
    handlerWithClient := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        r = r.WithContext(ctx)
        h.ServeHTTP(w, r)
    })

    return &http.Server{
        Addr:    ":8080",
        Handler: handlerWithClient,
    }
}

3.7 拦截链路数据流图

下图展示了请求在拦截链中的数据流转与关键操作点:

拦截链路数据流与关键操作 阶段1:请求到达 解析 URL + Header 阶段2:中间件 日志 + 状态码分类 阶段3:业务处理 执行回放逻辑 阶段4 Transport 数据流:请求 Header → 中间件注入 Trace-ID → 业务处理生成响应 → Transport 注入 Replay-Mode → 下游服务 各阶段关键操作 阶段 操作 数据变更 阶段1 解析请求路径与查询参数 提取回放目标 URL 阶段2 记录请求日志 + 包装 ResponseWriter 生成 Trace-ID,捕获状态码 阶段4 注入回放标识 + 响应拦截 设置 X-Replay-Mode Header

3.8 最佳实践与避坑指南

  • 中间件顺序:日志中间件应放在最外层,确保所有请求都被记录;状态码处理中间件紧随其后。
  • Transport 复用:自定义 Transport 应使用连接池配置,避免每次请求创建新连接。
  • 错误传播:Transport 层返回的错误应包含上下文信息,便于中间件层记录。
  • 性能考量:日志记录使用异步写入或缓冲池,避免阻塞请求处理。
回放编排拦截口诀:中间件记日志,Transport 打标记;状态码分三类,4xx 记差异 5xx 重试。