mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 00:48:00 +00:00
feat(stats): gateway request stats/audit API + polish scratch sort animations (link chain, reset, cleanup)
This commit is contained in:
167
internal/gateway/stats.go
Normal file
167
internal/gateway/stats.go
Normal file
@ -0,0 +1,167 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Req is one recorded gateway request (audit trail + per-key/per-model stats).
|
||||
type Req struct {
|
||||
Time int64 `json:"time"` // unix milliseconds
|
||||
Key string `json:"key"` // gateway key id
|
||||
Type string `json:"type"` // chat | stream | image
|
||||
Model string `json:"model"` // effective model used upstream
|
||||
// Source provider/source name
|
||||
Source string `json:"source"`
|
||||
// Prompt prompt tokens
|
||||
Prompt int64 `json:"prompt_tokens"`
|
||||
// Compl completion tokens
|
||||
Compl int64 `json:"completion_tokens"`
|
||||
// LatMs total handling time ms
|
||||
LatMs int64 `json:"latency_ms"`
|
||||
OK bool `json:"ok"`
|
||||
// Status http status code
|
||||
Status int `json:"status"`
|
||||
// Err short error message
|
||||
Err string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Stat aggregates counters for one dimension row.
|
||||
type Stat struct {
|
||||
Reqs int64 `json:"reqs"`
|
||||
OK int64 `json:"ok"`
|
||||
Err int64 `json:"err"`
|
||||
Tokens int64 `json:"tokens"`
|
||||
Prompt int64 `json:"prompt_tokens"`
|
||||
Compl int64 `json:"completion_tokens"`
|
||||
LatSum int64 `json:"latency_sum_ms"`
|
||||
LatMax int64 `json:"latency_max_ms"`
|
||||
}
|
||||
|
||||
type agrRow struct {
|
||||
Name string `json:"name"`
|
||||
Stat
|
||||
}
|
||||
|
||||
// Stats collects per-key / per-model / per-source aggregates plus a bounded
|
||||
// ring of raw request records, all guarded by one mutex.
|
||||
type Stats struct {
|
||||
mu sync.Mutex
|
||||
active int64
|
||||
byKey map[string]*Stat
|
||||
byModel map[string]*Stat
|
||||
bySrc map[string]*Stat
|
||||
recs []Req
|
||||
maxRecs int
|
||||
}
|
||||
|
||||
func NewStats(maxRecords int) *Stats {
|
||||
if maxRecords <= 0 {
|
||||
maxRecords = 3000
|
||||
}
|
||||
return &Stats{
|
||||
byKey: map[string]*Stat{},
|
||||
byModel: map[string]*Stat{},
|
||||
bySrc: map[string]*Stat{},
|
||||
maxRecs: maxRecords,
|
||||
}
|
||||
}
|
||||
|
||||
// Begin accounts an in-flight request; the returned func must be called once
|
||||
// the request finished (defer ok).
|
||||
func (s *Stats) Begin() func() {
|
||||
s.mu.Lock()
|
||||
s.active++
|
||||
s.mu.Unlock()
|
||||
return func() {
|
||||
s.mu.Lock()
|
||||
s.active--
|
||||
s.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func inc(m map[string]*Stat, name string, r Req) {
|
||||
a := m[name]
|
||||
if a == nil {
|
||||
a = &Stat{}
|
||||
m[name] = a
|
||||
}
|
||||
a.Reqs++
|
||||
if r.OK {
|
||||
a.OK++
|
||||
} else {
|
||||
a.Err++
|
||||
}
|
||||
a.Tokens += r.Prompt + r.Compl
|
||||
a.Prompt += r.Prompt
|
||||
a.Compl += r.Compl
|
||||
a.LatSum += r.LatMs
|
||||
if r.LatMs > a.LatMax {
|
||||
a.LatMax = r.LatMs
|
||||
}
|
||||
}
|
||||
|
||||
// Record appends a finished request to the aggregates and ring buffer.
|
||||
func (s *Stats) Record(r Req) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
inc(s.byKey, r.Key, r)
|
||||
inc(s.byModel, r.Model, r)
|
||||
inc(s.bySrc, r.Source, r)
|
||||
s.recs = append(s.recs, r)
|
||||
if len(s.recs) > s.maxRecs {
|
||||
s.recs = s.recs[len(s.recs)-s.maxRecs:]
|
||||
}
|
||||
}
|
||||
|
||||
func rows(m map[string]*Stat) []StatsRow {
|
||||
out := make([]StatsRow, 0, len(m))
|
||||
for k, v := range m {
|
||||
out = append(out, StatsRow{Name: k, Stat: *v})
|
||||
}
|
||||
for i := 1; i < len(out); i++ {
|
||||
for j := i; j > 0 && out[j].Reqs > out[j-1].Reqs; j-- {
|
||||
out[j], out[j-1] = out[j-1], out[j]
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// StatsRow is one aggregated row for the dashboard.
|
||||
type StatsRow struct {
|
||||
Name string `json:"name"`
|
||||
Stat
|
||||
}
|
||||
|
||||
// Snapshot returns the whole dashboard payload.
|
||||
func (s *Stats) Snapshot(limit int) map[string]interface{} {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if limit <= 0 {
|
||||
limit = s.maxRecs
|
||||
}
|
||||
start := 0
|
||||
if len(s.recs) > limit {
|
||||
start = len(s.recs) - limit
|
||||
}
|
||||
var total Stat
|
||||
for _, a := range s.byModel {
|
||||
total.Reqs += a.Reqs
|
||||
total.OK += a.OK
|
||||
total.Err += a.Err
|
||||
total.Tokens += a.Tokens
|
||||
total.Prompt += a.Prompt
|
||||
total.Compl += a.Compl
|
||||
total.LatSum += a.LatSum
|
||||
if a.LatMax > total.LatMax {
|
||||
total.LatMax = a.LatMax
|
||||
}
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"active": s.active,
|
||||
"total": total,
|
||||
"by_key": rows(s.byKey),
|
||||
"by_model": rows(s.byModel),
|
||||
"by_source": rows(s.bySrc),
|
||||
"records": append([]Req(nil), s.recs[start:]...),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user