接入运维真实 metrics:负载、请求错误率与服务探测
- 新增 GET /admin/ops/metrics 与进程内请求统计中间件 - 运维页展示 CPU/内存/磁盘、24h 曲线与 WS/DB/存储状态 - 去掉未使用的消息队列占位项
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRecordAndSnapshot(t *testing.T) {
|
||||
c := NewCollector()
|
||||
c.Record(200)
|
||||
c.Record(200)
|
||||
c.Record(500)
|
||||
c.Record(404)
|
||||
|
||||
reqs, rates, hours, totalReq, totalErr := c.Snapshot24h()
|
||||
if len(reqs) != 24 || len(rates) != 24 || len(hours) != 24 {
|
||||
t.Fatalf("snapshot 长度应为 24: req=%d rates=%d hours=%d", len(reqs), len(rates), len(hours))
|
||||
}
|
||||
if totalReq != 4 || totalErr != 2 {
|
||||
t.Fatalf("窗口统计错误: totalReq=%d totalErr=%d", totalReq, totalErr)
|
||||
}
|
||||
// 当前小时桶应有 4 请求
|
||||
if reqs[23] != 4 {
|
||||
t.Fatalf("当前小时请求量 = %d, 期望 4", reqs[23])
|
||||
}
|
||||
if rates[23] < 49 || rates[23] > 51 {
|
||||
t.Fatalf("当前小时错误率 = %v, 期望约 50", rates[23])
|
||||
}
|
||||
tr, te, t5, _ := c.Totals()
|
||||
if tr != 4 || te != 2 || t5 != 1 {
|
||||
t.Fatalf("累计统计错误: req=%d err=%d 5xx=%d", tr, te, t5)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user