// Package metrics provides a small transport-neutral request metrics collector. // It is intentionally in-memory and dependency-free; applications can expose // Snapshot through any route or export it to their monitoring system. package metrics import ( "sync/atomic" "time" apierrors "git.yuntuo.hk/k2safe/go-omni/errors" "git.yuntuo.hk/k2safe/go-omni/httpx" ) // Snapshot is a point-in-time view of request metrics collected by Metrics. type Snapshot struct { Requests uint64 `json:"requests"` Errors uint64 `json:"errors"` InFlight int64 `json:"inFlight"` TotalDurationMs float64 `json:"totalDurationMs"` StatusCounts map[string]uint64 `json:"statusCounts"` } // Metrics collects request counts, status counts, in-flight requests and // cumulative handler time. A zero Metrics value is ready for use. type Metrics struct { requests atomic.Uint64 errors atomic.Uint64 inFlight atomic.Int64 durationNs atomic.Int64 statuses [600]atomic.Uint64 } // New creates an empty metrics collector. func New() *Metrics { return &Metrics{} } // Middleware records every request that reaches the middleware chain. Errors // include returned handler errors and responses with a status of 400 or greater. func (m *Metrics) Middleware() httpx.Middleware { return func(next httpx.Handler) httpx.Handler { return func(ctx *httpx.Context) error { if m == nil { return next(ctx) } m.inFlight.Add(1) started := time.Now() err := next(ctx) status := ctx.Status() if err != nil && !ctx.Responded() { status = apierrors.From(err).Status } m.requests.Add(1) if err != nil || status >= 400 { m.errors.Add(1) } if status >= 0 && status < len(m.statuses) { m.statuses[status].Add(1) } m.durationNs.Add(time.Since(started).Nanoseconds()) m.inFlight.Add(-1) return err } } } // Handler exposes the current snapshot through the standard GoOmni response // envelope. Register it explicitly, for example app.GET("/metrics", m.Handler()). func (m *Metrics) Handler() httpx.Handler { return func(ctx *httpx.Context) error { return ctx.OK(m.Snapshot()) } } // Snapshot returns a consistent-enough point-in-time view for operational // reporting. Counters may advance while the snapshot is being assembled. func (m *Metrics) Snapshot() Snapshot { if m == nil { return Snapshot{StatusCounts: map[string]uint64{}} } statuses := make(map[string]uint64) for status := 100; status < len(m.statuses); status++ { if count := m.statuses[status].Load(); count > 0 { statuses[itoa(status)] = count } } return Snapshot{ Requests: m.requests.Load(), Errors: m.errors.Load(), InFlight: m.inFlight.Load(), TotalDurationMs: float64(m.durationNs.Load()) / float64(time.Millisecond), StatusCounts: statuses, } } func itoa(value int) string { if value == 0 { return "0" } var digits [3]byte index := len(digits) for value > 0 { index-- digits[index] = byte('0' + value%10) value /= 10 } return string(digits[index:]) }