Files
2026-06-22 08:55:57 +08:00

109 lines
4.1 KiB
Go

// Package metrics 提供进程内的 Prometheus 指标注册中心与一组类型化的记录助手。
//
// 设计要点:
// - 用私有 *prometheus.Registry(NOT prometheus.DefaultRegisterer),避免在
// 热重载 / 测试中重复注册默认注册表导致 panic。
// - 通过 promhttp.HandlerFor(reg, ...) 暴露 Handler();调用方(router)负责
// 鉴权门禁(/metrics 泄露成本/会话数,必须 admin/内网受限)。
// - Record* 助手对外提供窄 API:其它模块只依赖这几个方法(或 acp 包内定义的
// 窄接口),无需 import prometheus。
//
// 指标清单(spec §11 §1205-1207):
// - acp_sessions_active GaugeFunc —— 当前活跃 ACP 会话数
// - acp_session_exit_total{status} CounterVec —— 会话退出计数(exited/crashed/…)
// - acp_permission_decisions_total{outcome} CounterVec —— 权限决策计数
// (auto/approved/denied/expired)
// - jobs_dead_letter_total{type} CounterVec —— 死信任务计数(按 job type)
// - llm_cost_usd_total GaugeFunc —— 累计 LLM 成本(USD)
package metrics
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// Metrics 持有私有注册表与各 collector。零值不可用,必须经 New 构造。
type Metrics struct {
reg *prometheus.Registry
sessionExits *prometheus.CounterVec
permDecisions *prometheus.CounterVec
deadLetters *prometheus.CounterVec
}
// New 构造 Metrics。activeSessions / llmCostTotal 是供 GaugeFunc 回调的取值
// 函数;二者可为 nil(nil 时对应 gauge 恒为 0),便于测试 / feature-off。
func New(activeSessions func() float64, llmCostTotal func() float64) *Metrics {
reg := prometheus.NewRegistry()
m := &Metrics{
reg: reg,
sessionExits: prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "acp_session_exit_total",
Help: "Total number of ACP agent session exits, labeled by terminal status.",
}, []string{"status"}),
permDecisions: prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "acp_permission_decisions_total",
Help: "Total number of ACP tool-permission decisions, labeled by outcome.",
}, []string{"outcome"}),
deadLetters: prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "jobs_dead_letter_total",
Help: "Total number of jobs that exhausted retries and were dead-lettered, by job type.",
}, []string{"type"}),
}
reg.MustRegister(m.sessionExits, m.permDecisions, m.deadLetters)
if activeSessions == nil {
activeSessions = func() float64 { return 0 }
}
if llmCostTotal == nil {
llmCostTotal = func() float64 { return 0 }
}
reg.MustRegister(prometheus.NewGaugeFunc(prometheus.GaugeOpts{
Name: "acp_sessions_active",
Help: "Current number of active (starting/running) ACP agent sessions.",
}, activeSessions))
reg.MustRegister(prometheus.NewGaugeFunc(prometheus.GaugeOpts{
Name: "llm_cost_usd_total",
Help: "Cumulative LLM usage cost in USD across all endpoints.",
}, llmCostTotal))
return m
}
// Handler 返回 /metrics 的 http.Handler,绑定到本 Metrics 的私有注册表。
func (m *Metrics) Handler() http.Handler {
return promhttp.HandlerFor(m.reg, promhttp.HandlerOpts{})
}
// Registry 暴露底层注册表,便于测试直接 Gather。
func (m *Metrics) Registry() *prometheus.Registry { return m.reg }
// RecordSessionExit 记录一次会话退出。status 取 acp.SessionStatus 字符串值
// (exited/crashed/…)。
func (m *Metrics) RecordSessionExit(status string) {
if m == nil {
return
}
m.sessionExits.WithLabelValues(status).Inc()
}
// RecordPermissionDecision 记录一次权限决策。outcome 取 auto/approved/denied/expired。
func (m *Metrics) RecordPermissionDecision(outcome string) {
if m == nil {
return
}
m.permDecisions.WithLabelValues(outcome).Inc()
}
// RecordDeadLetter 记录一次死信任务,按 job type 标签累加。
func (m *Metrics) RecordDeadLetter(jobType string) {
if m == nil {
return
}
m.deadLetters.WithLabelValues(jobType).Inc()
}