// 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() }