package metrics import ( "sync" "sync/atomic" "time" "github.com/gin-gonic/gin" ) // Collector 进程内请求统计:近 24 小时按整点小时桶累加。 type Collector struct { mu sync.Mutex byHour map[int64]*hourBucket // 自进程启动以来的累计 totalReq atomic.Int64 totalErr atomic.Int64 // HTTP status >= 400 total5xx atomic.Int64 inFlight atomic.Int64 startedAt time.Time } type hourBucket struct { requests int64 errors int64 status5 int64 } // Default 全局采集器 var Default = NewCollector() func NewCollector() *Collector { return &Collector{ byHour: make(map[int64]*hourBucket), startedAt: time.Now(), } } func hourKey(t time.Time) int64 { return t.Unix() / 3600 } func (c *Collector) Record(status int) { c.totalReq.Add(1) isErr := status >= 400 is5 := status >= 500 if isErr { c.totalErr.Add(1) } if is5 { c.total5xx.Add(1) } h := hourKey(time.Now()) c.mu.Lock() b := c.byHour[h] if b == nil { b = &hourBucket{} c.byHour[h] = b c.pruneLocked(h) } b.requests++ if isErr { b.errors++ } if is5 { b.status5++ } c.mu.Unlock() } func (c *Collector) pruneLocked(nowHour int64) { cutoff := nowHour - 25 for k := range c.byHour { if k < cutoff { delete(c.byHour, k) } } } // Snapshot24h 返回近 24 个整点小时(含当前小时)的请求量与错误率(0–100)。 // 下标 0 为 23 小时前,23 为当前小时;hours 为当地小时 0–23。 func (c *Collector) Snapshot24h() (requests []int64, errorRates []float64, hours []int, totalReq, totalErr int64) { now := time.Now() nowH := hourKey(now) loc := now.Location() requests = make([]int64, 24) errorRates = make([]float64, 24) hours = make([]int, 24) c.mu.Lock() defer c.mu.Unlock() for i := 0; i < 24; i++ { hKey := nowH - int64(23-i) t := time.Unix(hKey*3600, 0).In(loc) hours[i] = t.Hour() b := c.byHour[hKey] var r, e int64 if b != nil { r, e = b.requests, b.errors } requests[i] = r if r > 0 { errorRates[i] = float64(e) / float64(r) * 100 } else { errorRates[i] = 0 } totalReq += r totalErr += e } return } func (c *Collector) Totals() (req, err4, err5 int64, startedAt time.Time) { return c.totalReq.Load(), c.totalErr.Load(), c.total5xx.Load(), c.startedAt } func (c *Collector) InFlight() int64 { return c.inFlight.Load() } // Middleware 记录每个 HTTP 请求的状态码(跳过 WebSocket 升级失败后的统计由 status 决定)。 func Middleware() gin.HandlerFunc { return func(c *gin.Context) { // 健康检查可不计入业务错误曲线,但仍记请求量更直观;这里全部计入 Default.inFlight.Add(1) c.Next() Default.inFlight.Add(-1) // WebSocket 升级成功后 status 可能是 101 status := c.Writer.Status() if status == 0 { status = 200 } Default.Record(status) } }