主人 2026-10-03 拍板「注释一律中文」;gateway 至此整包注释中文化完成(本会话共 3 批: 已推库 16 个文件 + 本批剩余文件)。 校验:机械校验(非注释改动行)= 空;剥注释后代码文本逐行比对全等(不一致文件数 0); 全树 go test ./... 58 包 ok / 0 FAIL;交叉编译绿。
156 lines
4.5 KiB
Go
156 lines
4.5 KiB
Go
package gateway
|
|
|
|
import (
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
|
|
)
|
|
|
|
// 指标收集器调参。编译期常量。
|
|
const (
|
|
// metricsWindow 限定请求时间戳为 QPS 保留多久。
|
|
metricsWindow = 2 * time.Second
|
|
// metricsQPSSpan 是测量 QPS 的滑动窗口。
|
|
metricsQPSSpan = time.Second
|
|
)
|
|
|
|
// AdminMetrics 是 GET /api/v1/admin/observability/metrics 的线上结构。
|
|
// LLM/Token 成本刻意缺位:网关不跟踪 token 用量或
|
|
// 花费,编造一个数字会违反「不伪造指标」红线。
|
|
type AdminMetrics struct {
|
|
TotalRequests int64 `json:"totalRequests"`
|
|
QPS float64 `json:"qps"`
|
|
AvgLatencyMs float64 `json:"avgLatencyMs"`
|
|
MaxLatencyMs int64 `json:"maxLatencyMs"`
|
|
PerMethod map[string]int64 `json:"perMethod"`
|
|
PerPath map[string]int64 `json:"perPath"`
|
|
StatusCounts map[string]int64 `json:"statusCounts"`
|
|
LLMCalls int64 `json:"llmCalls"`
|
|
SubagentEvents int64 `json:"subagentEvents"`
|
|
}
|
|
|
|
// MetricsCollector 为管理可观测面板累积请求指标与派生
|
|
// 计数器。它对并发安全。
|
|
type MetricsCollector struct {
|
|
mu sync.Mutex
|
|
total int64
|
|
perMethod map[string]int64
|
|
perPath map[string]int64
|
|
statuses map[int]int64
|
|
sumDur int64
|
|
maxDur int64
|
|
recent []time.Time
|
|
llm int64
|
|
subagent int64
|
|
}
|
|
|
|
// newMetricsCollector 构建一个空指标收集器。
|
|
func newMetricsCollector() *MetricsCollector {
|
|
return &MetricsCollector{
|
|
perMethod: make(map[string]int64),
|
|
perPath: make(map[string]int64),
|
|
statuses: make(map[int]int64),
|
|
}
|
|
}
|
|
|
|
// Record 用给定的 method、path、status 与 duration 计一次请求。
|
|
func (m *MetricsCollector) Record(method, path string, status int, durMs int64) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
now := time.Now()
|
|
m.total++
|
|
m.perMethod[method]++
|
|
m.perPath[path]++
|
|
m.statuses[status]++
|
|
m.sumDur += durMs
|
|
if durMs > m.maxDur {
|
|
m.maxDur = durMs
|
|
}
|
|
m.recent = append(m.recent, now)
|
|
m.prune(now)
|
|
}
|
|
|
|
// Observe 在记录到该类 span 时递增 LLM/子代理计数器。
|
|
// 它由 ObserveSpan 调用,使计数器反映真实工作。
|
|
func (m *MetricsCollector) Observe(rec trace.SpanRecord) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
switch rec.Kind {
|
|
case trace.SpanKindLLM:
|
|
m.llm++
|
|
case trace.SpanKindSubagent:
|
|
m.subagent++
|
|
}
|
|
}
|
|
|
|
// prune 丢弃早于 metricsWindow 的请求时间,使 recent 保持小。
|
|
// 调用方必须持有 m.mu。
|
|
func (m *MetricsCollector) prune(now time.Time) {
|
|
min := now.Add(-metricsWindow)
|
|
i := 0
|
|
for i < len(m.recent) && m.recent[i].Before(min) {
|
|
i++
|
|
}
|
|
if i > 0 {
|
|
m.recent = append([]time.Time(nil), m.recent[i:]...)
|
|
}
|
|
}
|
|
|
|
// Snapshot 返回指标的时间点副本。qps 统计在 now 的
|
|
// metricsQPSSpan 之内开始的请求。
|
|
//
|
|
// 结果中的每个 map 都是全新副本,绝不是收集器自己的:调用方
|
|
// 在本锁释放之后才序列化该值(管理 handler 对它做 JSON
|
|
// 编码),因此返回活 map 会让并发的 Record 在编码器
|
|
// 遍历它时改动该 map —— Go 运行时对此的回答是不可恢复的
|
|
// 致命错误 "concurrent map iteration and map write",它会让整个进程
|
|
// 倒下(2026-10-03 21:00:28 在 GET /api/v1/admin/observability/metrics 上的崩溃)。
|
|
func (m *MetricsCollector) Snapshot(now time.Time) AdminMetrics {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
var qps int64
|
|
for _, t := range m.recent {
|
|
if now.Sub(t) <= metricsQPSSpan {
|
|
qps++
|
|
}
|
|
}
|
|
avg := int64(0)
|
|
if m.total > 0 {
|
|
avg = m.sumDur / m.total
|
|
}
|
|
return AdminMetrics{
|
|
TotalRequests: m.total,
|
|
QPS: float64(qps),
|
|
AvgLatencyMs: float64(avg),
|
|
MaxLatencyMs: m.maxDur,
|
|
PerMethod: copyCounts(m.perMethod),
|
|
PerPath: copyCounts(m.perPath),
|
|
StatusCounts: copyStatusCounts(m.statuses),
|
|
LLMCalls: m.llm,
|
|
SubagentEvents: m.subagent,
|
|
}
|
|
}
|
|
|
|
// copyCounts 返回字符串键计数器 map 的脱离副本,使
|
|
// 快照能在收集器锁释放后编码。调用方在复制时
|
|
// 必须持有 m.mu(复制会遍历活 map)。
|
|
func copyCounts(src map[string]int64) map[string]int64 {
|
|
out := make(map[string]int64, len(src))
|
|
for k, v := range src {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
// copyStatusCounts 把 int 键的状态计数器渲染为 AdminMetrics JSON
|
|
// 形状所声明的字符串键线上 map。调用方在复制时必须持有 m.mu。
|
|
func copyStatusCounts(src map[int]int64) map[string]int64 {
|
|
out := make(map[string]int64, len(src))
|
|
for k, v := range src {
|
|
out[strconv.Itoa(k)] = v
|
|
}
|
|
return out
|
|
}
|