package middleware import ( "crypto/rand" "encoding/hex" "log/slog" "os" "strconv" "time" "github.com/gin-gonic/gin" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) // CtxRequestID 是请求 ID 在 gin.Context 中的键。 const CtxRequestID = "request_id" // ---- Prometheus 指标 ---- var ( httpRequests = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "sundynix_http_requests_total", Help: "HTTP 请求总数(按方法/路由模板/状态码)。", }, []string{"method", "route", "status"}) httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ Name: "sundynix_http_request_duration_seconds", Help: "HTTP 请求耗时(秒)。", Buckets: prometheus.DefBuckets, }, []string{"method", "route"}) httpInFlight = promauto.NewGauge(prometheus.GaugeOpts{ Name: "sundynix_http_requests_in_flight", Help: "当前处理中的 HTTP 请求数。", }) ) // accessLogger 是结构化访问日志器(JSON 到 stderr)。 var accessLogger = slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})) // RequestID 为每个请求生成/透传 X-Request-ID,写入上下文与响应头,供日志关联。 func RequestID() gin.HandlerFunc { return func(c *gin.Context) { id := c.GetHeader("X-Request-ID") if id == "" { id = newRequestID() } c.Set(CtxRequestID, id) c.Header("X-Request-ID", id) c.Next() } } // Observe 记录 Prometheus 指标 + 结构化访问日志。放在中间件链较前位置。 func Observe() gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() httpInFlight.Inc() c.Next() httpInFlight.Dec() route := c.FullPath() // 路由模板(/tasks/:id/...),避免按真实路径产生高基数 if route == "" { route = "unmatched" } status := c.Writer.Status() dur := time.Since(start) method := c.Request.Method httpRequests.WithLabelValues(method, route, strconv.Itoa(status)).Inc() httpDuration.WithLabelValues(method, route).Observe(dur.Seconds()) uid, _ := c.Get(CtxUserID) rid, _ := c.Get(CtxRequestID) accessLogger.Info("http", "request_id", rid, "method", method, "route", route, "path", c.Request.URL.Path, "status", status, "latency_ms", dur.Milliseconds(), "ip", c.ClientIP(), "uid", uid, "bytes", c.Writer.Size(), ) } } func newRequestID() string { var b [8]byte _, _ = rand.Read(b[:]) return hex.EncodeToString(b[:]) }