feat: add transport-neutral request metrics
This commit is contained in:
106
metrics/metrics.go
Normal file
106
metrics/metrics.go
Normal file
@@ -0,0 +1,106 @@
|
||||
// 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:])
|
||||
}
|
||||
Reference in New Issue
Block a user