Documentation
¶
Overview ¶
Package metrics records per-endpoint request counts + errors so the dashboard can show at-a-glance health next to each op: how busy it is, whether it's failing, and if so, what the last error was.
The middleware layer lives in middleware.go; this file is the data plane — a Store interface + in-memory implementation. Swap the store for a Prometheus- or StatsD-backed version later without touching the handlers or UI.
Index ¶
Constants ¶
const RecentErrorsCap = 1000
RecentErrorsCap is the ring-buffer depth per endpoint. Large enough to survive a burst without losing context; /stats polling strips the events so the hot path doesn't pay for this depth — the dashboard fetches the full ring on demand via the per-op errors endpoint.
Variables ¶
This section is empty.
Functions ¶
func NewMiddleware ¶
func NewMiddleware(store Store, key string) middleware.Middleware
NewMiddleware returns a transport-agnostic middleware bundle that counts requests + errors for key. Same bundle serves REST (via Gin) and GraphQL (via Graph); nexus auto-attaches it to every reflective registration so the dashboard populates without extra wiring.
For custom transports (raw gin routes not registered via AsRest, etc.) you can call NewMiddleware directly and thread the Gin realization through c.Next.
Types ¶
type EndpointStats ¶
type EndpointStats struct {
Key string `json:"key"`
Count int64 `json:"count"`
Errors int64 `json:"errors"`
LastError string `json:"lastError,omitempty"`
// LastErrStack is the stack trace captured at the most recent
// errored request (when the error was a panic wrapped via
// trace.StackError). Surfaces in the drawer's last-error panel as
// a collapsible disclosure so an operator can see the failing
// call chain without opening the per-op error ring.
LastErrStack string `json:"lastErrStack,omitempty"`
LastAt time.Time `json:"lastAt,omitempty"` // time of last request
LastErrAt time.Time `json:"lastErrAt,omitempty"` // time of last errored request
RecentErrors []ErrorEvent `json:"recentErrors,omitempty"`
}
EndpointStats is the serializable per-op snapshot the dashboard shows next to each op row on the Architecture tab — request counter, error counter, plus the most recent error's message and timestamp for a "what broke?" quick read.
RecentErrors is a ring-capped list of recent error events (most recent first) so the UI can pop a dialog with timestamps + IPs + messages when an operator clicks the error badge.
type ErrorEvent ¶
type ErrorEvent struct {
Timestamp time.Time `json:"timestamp"`
IP string `json:"ip,omitempty"`
Message string `json:"message"`
// Stack is the captured Go runtime stack trace when the error
// originated from a panic (extracted via trace.StackOf). Empty
// for plain returned errors — those don't carry a stack unless
// user code explicitly wraps them in a *trace.StackError.
Stack string `json:"stack,omitempty"`
}
ErrorEvent captures one error occurrence for the dashboard's click- through panel. Rolling per-endpoint ring keeps the latest few so operators see both "who's hitting this" (IP) and "what's wrong" (message) without flipping between tabs.
type MemoryStore ¶
type MemoryStore struct {
// contains filtered or unexported fields
}
MemoryStore is the default Store. Per-key entries use atomic ints for counters and a mutex for the last-error text, which mutates rarely compared to the counter path.
func NewMemoryStore ¶
func NewMemoryStore() *MemoryStore
NewMemoryStore returns an in-process Store. Atomic counters keep the hot path lock-free; a mutex only guards LastError / LastAt updates.
func (*MemoryStore) Errors ¶
func (s *MemoryStore) Errors(key string) []ErrorEvent
Errors returns the ring of recent error events for key, newest first. Empty slice when the key is unknown. Safe to call concurrently with Record — we snapshot under the entry's lock before returning.
func (*MemoryStore) Get ¶
func (s *MemoryStore) Get(key string) (EndpointStats, bool)
func (*MemoryStore) Record ¶
func (s *MemoryStore) Record(key, ip string, err error)
func (*MemoryStore) Snapshot ¶
func (s *MemoryStore) Snapshot() []EndpointStats
type Store ¶
type Store interface {
// Record one request outcome against key. ip is the caller's client
// address (empty when unavailable); err is the handler's return
// (nil on success). Error events get stashed in a per-endpoint ring
// so the dashboard can surface IP + message together.
Record(key, ip string, err error)
// Snapshot returns a point-in-time copy of every key's stats. The
// returned EndpointStats omits RecentErrors so the /stats polling
// payload stays small at any ring depth; use Errors(key) for the
// full per-endpoint error list. Sorted by key.
Snapshot() []EndpointStats
// Get returns a single endpoint's stats (without RecentErrors).
Get(key string) (EndpointStats, bool)
// Errors returns the ring of recent error events for key, newest
// first. Called by the dashboard when a user clicks the error
// badge — lazy fetch means a 1000-entry ring doesn't inflate the
// hot-polling /stats payload.
Errors(key string) []ErrorEvent
}
Store is the backend contract. Record is called once per request with nil err on success; Snapshot returns every key's current stats for the dashboard. Safe for concurrent use.
func NewCacheStore ¶
NewCacheStore returns a Store backed by a nexus cache.Manager — meaning the in-memory path uses go-cache (the same primitive every other nexus cache does), and the Redis path persists counters so multi-replica deploys see aggregated totals.
Semantics: counters are best-effort. Every Record does a read-modify- write against the cache under the endpoint key, so concurrent writers from different goroutines (or replicas in Redis mode) can step on each other's increments. For a first-pass dashboard view that's fine — if you need exact counts under heavy contention, point your app at a Prometheus collector instead. The API shape stays identical either way so swapping later is a one-line change.
Keys in the cache are namespaced under "nexus.metrics." so they don't collide with whatever else the app caches. TTL is 24h; rolling out restarts within a day preserves counters.