metrics

package
v0.35.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 13, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package metrics 内核指标契约面(issue #100):指标名 / 类型 / 维度 / 基数上界的 **单一来源**。

生命周期: Experimental(v0.34.1 定级)——口径面仍会随宿主看板的需求增补, 不承诺任何兼容,升级风险自负;定级理由见 api-lifecycle.yaml,分级规则见 docs/api-compat.md。

为什么是「契约面」而不是一个 SDK

与 trajectory 的 OTel 映射同一条分工:**business 选指标、transport 负责编码与 投递**。本仓知道一个量是什么意思(执行耗时 / 重试 / 租约竞争 / 恢复成败 / 未知结果积压),不知道宿主把数投给谁(OTel / Prometheus / 自建看板)。本包若直连 任何 SDK,就会把「导出失败算不算执行失败」这类可用性问题引进内核——遥测后端 抖一下就影响执行,正是 trajectory/otel.go 头注拒绝的那条路。

没有约定时各宿主自造指标名,同一个「租约竞争次数」在不同系统里可以是不同含义, 跨宿主的看板与查询全部拼不起来——而这**不会报错**:数照常上行,只是拼不起来。 故名称与标签在这里写成常量,宿主的看板按本包的口径查询,本包按口径埋点。

命名与口径纪律(与 trajectory 同源)

  1. **前缀 @harness.@**:与 trajectory 的属性命名同一条理由——本仓不在 OTel 官方命名空间里,冒用官方前缀会让「按官方约定解析」的工具读到半真半假的数据。
  2. **耗时单位毫秒(ms)且只记实测**:与 trajectory.AttrDurationMs 同一口径—— 起止相减现算,估算值不许冒充实测(trajectory「结构上不设时长字段」的镜像)。
  3. **无界字段不进标签**:与 trajectory/otelmap.go 的基数纪律同源。session_id / run_id / task_id / 幂等键这类值进标签就是无界基数——指标时序数按业务量 膨胀,后端先爆;要关联用 trace_id(真相源在 oekit/tracing),那是**标识** 不是维度。禁区清单见 ForbiddenLabels。
  4. **先登记再埋点**:每个指标的名称 / 类型 / 维度 / 基数上界在 Specs 里登记, 自校验(metrics_test)保证登记表自洽。埋一个没登记的指标 = 看板口径外 的暗数,Validate 会显式报错。

覆盖清单(issue #100 验收的五类量)

| 量             | 指标                        | 类型      |
|----------------|-----------------------------|-----------|
| 执行耗时       | harness.exec.duration       | histogram |
| 重试次数       | harness.retry.total         | counter   |
| 租约竞争       | harness.lease.contended     | counter   |
| 恢复成功率     | harness.recovery.total      | counter   |
| 未知结果积压   | harness.sideeffect.uncertain| gauge     |

各量的**接入点**(在哪个包哪个函数上埋)见 [ACCESS-POINTS.md]。

recorder.go —— 指标投递接缝(骨架)+ 进程内参考实现。

分工(与 trajectory 的 OTel 映射同一条)

内核只产出「名称 + 标签 + 数值」;编码与投递归宿主的 Recorder 实现。 本包不 import 任何 OTel / Prometheus SDK——接了谁,「导出失败算不算执行失败」 就得由谁回答,而那个问题不该出现在内核里。

埋点形态

生产代码经包级便利入口埋(一行,不持有任何后端引用):

metrics.Count(metrics.NameLeaseContended, 1, map[string]string{
    metrics.LabelLeaseSubsystem: metrics.LeaseSubsystemSideEffect,
    metrics.LabelLeaseOutcome:   metrics.LeaseOutcomeContended,
})

后端未装配时缺省是 Nop——「还没接看板」不该让执行路径变红;宿主装配期调 SetDefault 后,全部埋点即刻生效(logx.SetDefault 同款装配语义)。

测试期校验

埋错标签(越界取值、禁区键、未登记指标名)不会报错——数照常上行,只是看板 拼不起来。把这类错拦在测试期:宿主测试用 Mem 接进 SetDefault,对自己的 埋点跑 Validate(Mem.Collect 已内置校验开关,见其头注)。

Index

Constants

View Source
const (
	// NameExecDuration 一次编排单元的实测耗时(直方图,ms)。
	// 口径与 trajectory.AttrDurationMs 对齐:起止相减现算,只记实测。
	NameExecDuration = "harness.exec.duration"
	// NameRetryTotal 重试决策计数(计数器)。每次 retryplan.Decide 落地一条。
	NameRetryTotal = "harness.retry.total"
	// NameLeaseContended 租约竞争 / 未知结果拒绝计数(计数器)。
	// 「没轮上我」与「须先对账」都进这里——两者都是「这次没执行成」的竞争面信号。
	NameLeaseContended = "harness.lease.contended"
	// NameRecoveryTotal 恢复尝试计数(计数器,按 outcome 分成功 / 失败)。
	// 恢复成功率 = outcome=success 与 outcome=failure 的比值,不在本包另算——
	// 派生量只算一遍(宿主看板算),内核不产出第二个真相。
	NameRecoveryTotal = "harness.recovery.total"
	// NameUncertainBacklog 未知结果积压量(仪表):当前处于「结果未知」态的
	// 副作用 Key 数。这是本轮 P0/P1 的核心运营指标——积压不清偿 = 有动作
	// 可能已施加而无人知晓。
	NameUncertainBacklog = "harness.sideeffect.uncertain"
)

── 指标名 ──────────────────────────────────────────────────────────────────

名字是契约:看板与查询按它对应,改名 = 看板静默断链。新增指标先登记 Specs 再埋点;改名走 BREAKING(Experimental 阶段允许,但要同步 ACCESS-POINTS.md)。

View Source
const (
	// LabelExecKind 执行单元类目。取值对齐 trajectory 的行类目(Kind* 常量):
	// 轨迹上已经按这个类目分了 span,指标沿用同一枚举,看板两边拼得起来。
	LabelExecKind = "harness.exec.kind"
	// LabelExecStatus 执行语义状态(三值,与 trajectory 状态三值纪律同源:
	// 「仍在途」记 unset 不记 error——把在途记成 error 会把事故淹进噪声里)。
	LabelExecStatus = "harness.exec.status"
	// LabelRetryPath 重试路径,取值 = retryplan.Path 的三个值。
	LabelRetryPath = "harness.retry.path"
	// LabelLeaseSubsystem 租约竞争发生在哪个子系统。
	LabelLeaseSubsystem = "harness.lease.subsystem"
	// LabelLeaseOutcome 租约竞争的拒绝形态。
	LabelLeaseOutcome = "harness.lease.outcome"
	// LabelRecoverySubsystem 恢复发生在哪个子系统。
	LabelRecoverySubsystem = "harness.recovery.subsystem"
	// LabelRecoveryOutcome 恢复结局(success / failure 两值;blocked 也是一种
	// failure——按「没恢复成」计)。
	LabelRecoveryOutcome = "harness.recovery.outcome"
)

── 标签键 ──────────────────────────────────────────────────────────────────

标签键同样有界:每个键的取值域在 Specs 的 LabelDim 里写死。

View Source
const (
	ExecKindTurn       = "turn"
	ExecKindStep       = "step"
	ExecKindToolCall   = "tool_call"
	ExecKindToolResult = "tool_result"
	ExecKindSideEffect = "sideeffect"
	ExecKindCheckpoint = "checkpoint"
)

执行单元类目:逐字对齐 trajectory 的行类目(KindTurn/…/KindCheckpoint 的 字符串值)。metrics_test 会拿 trajectory 的常量做逐字比对,口径漂移即红。

View Source
const (
	ExecStatusUnset = "unset"
	ExecStatusOK    = "ok"
	ExecStatusError = "error"
)

执行语义状态三值(「缺省是 unset 不是 error」与 trajectory 同源)。

View Source
const (
	RetryPathNone     = "none"
	RetryPathDirect   = "direct"
	RetryPathReplayed = "replayed"
)

重试路径:逐字对齐 retryplan.Path 的 String()(none / direct / replayed)。

View Source
const (
	LeaseSubsystemSideEffect = "sideeffect"
	LeaseSubsystemTaskBoard  = "taskboard"
)

租约子系统。

View Source
const (
	LeaseOutcomeContended = "contended"
	LeaseOutcomeUncertain = "uncertain"
)

租约拒绝形态:contended=「还没轮上我」(等一等再来), uncertain=「可能已施加,须先对账」(硬阻塞)。语义来自 sideeffect.Outcome。

View Source
const (
	RecoverySubsystemCheckpoint = "checkpoint"
	RecoverySubsystemSideEffect = "sideeffect"
)

恢复子系统。

View Source
const (
	RecoveryOutcomeSuccess = "success"
	RecoveryOutcomeFailure = "failure"
)

恢复结局。blocked(计划判 blocked)也计 failure——「没恢复成」只有一种计法, 看板才不用先学会内核的细分语义。

Variables

View Source
var ErrUnregisteredMetric = errors.New("未登记的指标名(先在 metrics.specs 登记再埋点)")

ErrUnregisteredMetric 埋了没登记的指标名(先登记 Specs 再埋点)。

View Source
var ForbiddenLabels = []ForbiddenLabel{
	{Name: "session_id", Why: "一次会话一个取值:进 label 会让指标时序数按会话数膨胀,且会话标识是可关联标识"},
	{Name: "agent_slug", Why: "取值数随部署的 agent 目录增长,本仓定不出上界"},
	{Name: "payload", Why: "自由文本、基数无界,且可能含未脱敏内容"},
	{Name: "run_id", Why: "编排运行标识,随业务量无界增长;关联走轨迹的 trace_id(轮边界)"},
	{Name: "task_id", Why: "任务标识随业务量无界增长;关联走日志与 taskboard 的 List 面"},
	{Name: "key", Why: "副作用幂等键,基数等于业务写入量;积压量只报 gauge 总数,明细走日志"},
	{Name: "resource", Why: "资源标识随业务量无界增长,同 key"},
}

ForbiddenLabels 禁区清单。

前三项与 trajectory.ForbiddenLabels 同源(同一批「看起来很像维度」的无界键); 后三项是本包补的内核侧键:run_id / task_id 随业务量无界增长,key / resource 是幂等键与资源标识——基数等于业务写入量,进标签后端必爆。要关联它们, 走日志 / 轨迹(trace_id / 行 ID),不走指标标签。

Functions

func Count

func Count(name string, delta int64, labels Labels)

Count 经全局后端记一条计数。

func Observe

func Observe(name string, ms float64, labels Labels)

Observe 经全局后端记一次直方图观测(毫秒)。

func Set

func Set(name string, v int64, labels Labels)

Set 经全局后端设一次仪表值。

func SetDefault

func SetDefault(r Recorder)

SetDefault 换全局投递后端。传 nil 等于换回 Nop(不 panic,也不当「没配置」静默)。 装配期调用一次;运行期换后端是宿主自己的并发决定,本包只保证调用安全。

func Validate

func Validate(name string, labels Labels) error

Validate 校验一次埋点是否在契约面内:指标名已登记、标签键都在登记的维度里、 取值落在封闭枚举内、禁区键不出现。宿主测试用它把「埋错标签」拦在测试期 ——这类错在生产里不报错,只会在看板上静默拼不起来。

Types

type ForbiddenLabel

type ForbiddenLabel struct {
	Name string
	Why  string
}

ForbiddenLabel 明确禁止作为 metric label 的键(宿主不得自行补进 label)。

type Kind

type Kind string

Kind 指标类型(OTel 数据模型里本仓用到的三类)。

const (
	// KindCounter 只增计数。
	KindCounter Kind = "counter"
	// KindGauge 可升可降的瞬时量。
	KindGauge Kind = "gauge"
	// KindHistogram 分布观测(本仓只用它记毫秒耗时)。
	KindHistogram Kind = "histogram"
)

type LabelDim

type LabelDim struct {
	// Key 标签键(Label* 常量之一)。
	Key string
	// Values 有界取值域(封闭枚举;基数上界 = len(Values))。
	Values []string
	// Why 这套取值域为什么封闭(为什么将来不会悄悄涨)。
	Why string
}

LabelDim 一个标签维度:键、有界取值域、基数上界与**依据**。

基数上界没写依据 = 声明自相矛盾(与 trajectory/otelmap_test 的判据同源): 「上界是几」与「为什么敢这么上界」是两件事,后者才是让人敢在阈值判定里依赖它的理由。

func (LabelDim) Card

func (d LabelDim) Card() int

Card 基数上界(= len(Values);单列一个方法是为了让「上界有主」可机器判定)。

type Labels

type Labels = map[string]string

Labels 指标标签。值必须落在登记的取值域内(Validate 会判);禁区清单里的键 不得出现(ForbiddenLabels)。

type Mem

type Mem struct {

	// Strict 为真时 Collect 对每次埋点跑 Validate,违规计入 Bad(缺省 false:
	// 生产语义是旁路不反噬,严格档留给测试)。
	Strict bool
	// Bad 累计的违规埋点数(Strict 模式下测试断言它为 0)。
	Bad int
	// contains filtered or unexported fields
}

Mem 进程内参考 Recorder:计数 / 观测 / 设值全部收进内存。

两个用途:宿主测试把它接进 SetDefault 断言「埋点真的响了」;自写 OTel / Prometheus 适配器时照它的形状抄(标签规范化成排序键、并发安全、采集不阻塞埋点)。

func (*Mem) Count

func (m *Mem) Count(name string, delta int64, labels Labels)

Count 实现 Recorder。

func (*Mem) Counts

func (m *Mem) Counts() map[string]int64

Counts 返回计数快照:键 = 指标名 + "|" + 排序后的 k=v 对(与 key() 同一规范), 无标签时就是指标名。副本——调用方改不动内部账本。

func (*Mem) Gauges

func (m *Mem) Gauges() map[string]int64

Gauges 返回仪表快照(键规范同 Counts)。

func (*Mem) Observations

func (m *Mem) Observations(key string) []float64

Observations 返回某键(规范同 Counts)的直方图观测副本。

func (*Mem) Observe

func (m *Mem) Observe(name string, ms float64, labels Labels)

Observe 实现 Recorder。

func (*Mem) Set

func (m *Mem) Set(name string, v int64, labels Labels)

Set 实现 Recorder。

func (*Mem) Sum

func (m *Mem) Sum(key string) float64

Sum 返回某键的观测总和(耗时看板最常用的聚合;键规范同 Counts)。

type Metric

type Metric struct {
	// Name 指标名(Name* 常量之一)。
	Name string
	// Kind 指标类型。
	Kind Kind
	// Unit 单位(耗时恒 ms;计数与仪表用 "1"——OTel 约定无量纲写作 "1")。
	Unit string
	// Desc 看板读得懂的语义说明(写清「数的是什么」而不是「在哪里埋的」)。
	Desc string
	// Labels 维度(无维度时为 nil——零维指标不是缺陷,积压总量就该是零维)。
	Labels []LabelDim
}

Metric 一个指标的登记项。

func SpecOf

func SpecOf(name string) (Metric, bool)

SpecOf 按名取登记项。未登记的指标名 = 埋点面之外的暗数,Validate 会拦。

func Specs

func Specs() []Metric

Specs 返回登记表的副本(调用方改不动登记表——口径只有一处能改)。

type Recorder

type Recorder interface {
	// Count 计数器加 delta(NameRetryTotal / NameLeaseContended / NameRecoveryTotal)。
	Count(name string, delta int64, labels Labels)
	// Observe 直方图观测(NameExecDuration;ms 实测值,只记实测不记估算)。
	Observe(name string, ms float64, labels Labels)
	// Set 仪表设值(NameUncertainBacklog;整数量)。
	Set(name string, v int64, labels Labels)
}

Recorder 指标投递接缝。三个方法对应三类指标(counter / gauge / histogram)。

实现纪律:**非阻塞、不 panic、不返回错误**。指标是旁路观测,投递层的任何 故障都不该反噬执行路径(buffer 满了就丢——丢了是观测损失,堵住是可用性事故)。

func Default

func Default() Recorder

Default 返回当前全局后端。

func Nop

func Nop() Recorder

Nop 零开销实现:不投递任何数。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL