用中间件记录HTTP请求指标需包装handler,通过自定义responseWriter捕获状态码,用带缓冲channel异步收集StatsLog,聚合前归一化路径、分类状态码,并用sync.Map实现/debug/stats实时验证。

如何用中间件记录每次 HTTP 请求的基础指标
Go 的 http.Handler 天然适合做请求拦截,统计入口必须从这里切入。别试图在每个 handler 里手动埋点——漏掉一个就断层。
核心是包装原始 handler,用闭包捕获开始时间、状态码、路径、方法等关键字段:
func StatsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(rw, r)
duration := time.Since(start).Milliseconds()
<pre class="brush:php;toolbar:false;"> // 记录到内存 map / channel / 或直接发往 metrics 后端
logEntry := StatsLog{
Path: r.URL.Path,
Method: r.Method,
Status: rw.statusCode,
Duration: int64(duration),
Time: time.Now(),
}
statsChan <p>}</p>
-
responseWriter需实现http.ResponseWriter接口并覆盖WriteHeader,否则拿不到真实状态码(默认 200) - 别用
time.Now().Sub(start)做耗时计算——纳秒级精度下,两次调用可能有微小偏差,直接用time.Since - 所有日志结构体字段尽量用基础类型(
int64,string),避免指针或接口,减少 GC 压力
为什么用 channel + goroutine 聚合比直接写文件/DB 更稳
高频 API 下,同步写磁盘或 DB 极易拖慢响应,且单点失败会导致整个请求 hang 住。channel 是 Go 提供的轻量缓冲机制,天然适配“采集快、处理慢”的场景。
典型错误是把 statsChan 声明成无缓冲 channel:make(chan StatsLog) —— 一旦消费者卡住,生产者会永久阻塞。
- 务必设缓冲:
statsChan := make(chan StatsLog, 1000),容量按 QPS × 平均处理延迟预估 - 消费者 goroutine 要 recover panic,否则崩溃后 channel 满了就再也写不进去了
- 聚合周期建议 10–30 秒一次 flush,太短 IO 密集,太长内存堆积;用
time.Ticker控制,别用time.Sleep阻塞主循环
聚合维度怎么选:path、method、status 还是 label 化路由
原始 r.URL.Path 直接当维度用,会导致 /user/123 和 /user/456 被算作两个独立接口,失去统计意义。必须做路径归一化。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
简单方案是正则替换 ID 类段:/user/[0-9]+ → /user/{id};复杂路由建议用 gorilla/mux 或 chi 的 Route.GetPathTemplate() 方法获取模板路径。
- 不要对 query 参数做归一化(如
?page=1&size=20和?page=2&size=20应视为同一接口) - 如果用了 OpenAPI 规范,可提前从 spec 中提取
operationId作为逻辑接口标识,比路径更准确 - status 统计建议分三类:2xx、4xx、5xx,而不是记全部 40+ 状态码,否则基数爆炸,查询变慢
本地调试时如何快速验证统计逻辑没丢数据
线上跑着看不见,本地就得有即时反馈。最有效的是加一个 /debug/stats 端点,返回最近 60 秒的实时聚合结果,用 sync.Map 存每条 path-method 组合的 count/duration。
注意这个端点本身也会被统计进去——得在中间件里加白名单绕过:
if r.URL.Path == "/debug/stats" {
next.ServeHTTP(w, r)
return
}
- 别用全局变量存聚合数据,多个 goroutine 并发读写会 panic;
sync.Map或sync.RWMutex必须配对使用 - 调试端点返回 JSON 即可,不用 HTML;加个
Cache-Control: no-cache头,防止浏览器缓存旧数据 - 如果发现统计数明显少于访问数(比如压测 1000 次只记了 800 条),优先检查
responseWriter.WriteHeader是否被第三方库覆盖、或中间件顺序是否把统计放到了 panic 恢复之后
真正难的不是记录,是让聚合结果在高并发下不偏移、不丢失、不拖慢业务。buffer size、panic 恢复、路径归一化这三点,漏掉任何一个,上线后都得半夜爬起来修。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!










