107 lines
3.0 KiB
Go
107 lines
3.0 KiB
Go
// 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:])
|
|
}
|