← 中文资源

简体中文版本

Chi 中间件请求日志

为 chi 路由器编写请求日志中间件:请求 ID 透传、状态与字节数经包装写捕获、记录路由模板而非具体路径防基数爆炸。

阅读英文原文

示例说明

请求日志的三个细节决定可用性:request_id 让分布式日志可串联;路由模板(/orders/{orderID})替代具体路径让指标可聚合——每次真实 ID 都会成为新标签是监控的大忌;状态码与字节数必须从包装响应写器读取,直接的 ResponseWriter 拿不到。defer 确保记录发生在响应完成后。

技术要点

  • RoutePattern 输出参数化路由模板
  • WrapResponseWriter 捕获状态与字节
  • RequestID 中间件生成透传标识

代码解读

中间件链依次注册 RequestID、RealIP、自定义日志与 Recoverer——顺序即洋葱层次,日志在 Recoverer 外层保证 panic 也能被记录。requestLogger 返回标准签名,defer 里用 LogAttrs 结构化输出七组键值。处理器内 InfoContext 继承请求上下文。

使用建议

日志量大的服务配采样或分级。慢请求阈值告警可在此埋点。JSON 输出直接对接采集器,无需再格式化。

代码示例

package main

import (
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
)

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelInfo,
	}))

	r := chi.NewRouter()
	r.Use(middleware.RequestID)
	r.Use(middleware.RealIP)
	r.Use(requestLogger(logger))
	r.Use(middleware.Recoverer)

	r.Get("/orders/{orderID}", func(w http.ResponseWriter, r *http.Request) {
		id := chi.URLParam(r, "orderID")
		logger.InfoContext(r.Context(), "order fetched",
			slog.String("order_id", id),
		)
		w.Write([]byte("order " + id))
	})

	http.ListenAndServe(":8080", r)
}

func requestLogger(logger *slog.Logger) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			started := time.Now()
			wrapped := middleware.NewWrapResponseWriter(w, r.ProtoMajor)

			defer func() {
				logger.LogAttrs(r.Context(), slog.LevelInfo, "request",
					slog.String("request_id", middleware.GetReqID(r.Context())),
					slog.String("method", r.Method),
					slog.String("route", chi.RouteContext(r.Context()).RoutePattern()),
					slog.Int("status", wrapped.Status()),
					slog.Int("bytes", wrapped.BytesWritten()),
					slog.Duration("duration", time.Since(started)),
				)
			}()

			next.ServeHTTP(wrapped, r)
		})
	}
}