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 拦截链路架构图
下图展示了回放编排中完整的拦截链路:
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 拦截链路数据流图
下图展示了请求在拦截链中的数据流转与关键操作点:
3.8 最佳实践与避坑指南
- 中间件顺序:日志中间件应放在最外层,确保所有请求都被记录;状态码处理中间件紧随其后。
- Transport 复用:自定义 Transport 应使用连接池配置,避免每次请求创建新连接。
- 错误传播:Transport 层返回的错误应包含上下文信息,便于中间件层记录。
- 性能考量:日志记录使用异步写入或缓冲池,避免阻塞请求处理。
回放编排拦截口诀:中间件记日志,Transport 打标记;状态码分三类,4xx 记差异 5xx 重试。