示例说明
请求日志的三个细节决定可用性: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)
})
}
}