- 人工智能
- AI Agent
- Agent 沙箱
- 云原生
- 容器运行时
- 零信任
【免费下载链接】substrate
Agent Substrate: the core system
本文是 Agent Substrate 核心仓库(substrate)中关于如何在子系统内新增一个 OpenTelemetry 指标的工程指南,对应 docs/dev/best-practices/metrics.md。它回答四个问题:什么时候该加指标(以及什么时候不该加)、该选哪种 Instrument、指标与标签如何命名、如何用 Go 编写、测试并把它注册进指标注册表。读完本文,你将掌握 Substrate 体系中"从一次缓存命中率到一条ate.*指标"的全套规范,并能在atelet、ateapi等二进制中按同一标准落地新指标。
在动手之前,建议先阅读 Actor Observability(docs/observability.md):它说明了系统目前已经有哪些遥测、注册表如何使用。本文只讲"如何往里加东西"。
什么时候该加指标,什么时候不该加
判断的第一原则是:这个值是否会被"聚合"。
- 应该加指标:当有人会对该值做 rate(速率)、ratio(比率)、percentile(百分位)、按结果计数、或全集群汇总时。缓存命中率、按阶段拆分的延迟分布、队列深度——这些是指标。
- 不应该加指标:当问题只针对某一个 actor时。由于 actor 身份被禁止出现在指标标签中(见下文"标签与基数"),指标无法回答"actor X 花了多久"。此时应该用
ateattr.ActorLogAttrs写一条结构化日志记录,atelet中的Restore timing breakdown就是典范。 - 指标与日志并非二选一:
atelet对一次 restore 同时发出两者——直方图用于集群视图,日志记录用于单个 actor 视图。 - 不要为一次性/启动期事实加指标(如配置值、版本号):把它们放在启动日志行或资源对象上即可。
新指标 PR 的检查清单
每个指标 PR 都包含以下部分,评审者会逐项核对:
- 指标在 Go 中定义于注入的
metric.Meter之上,nil 安全,并带metric.WithUnit与metric.WithDescription; - 每个标签键都是
internal/ateattr中的常量,每个标签值要么来自其中定义的有界集合,要么命名一个 operator 创建的对象(模板、池),符合标签规则; - 单元测试通过
ManualReader采集指标,断言名称、单位、Instrument 类型以及每个序列的标签集合; - 指标与任何新属性都登记在
docs/metrics/registry/metrics.yaml,且hack/verify/metrics.sh通过; - 若指标包含注册表无法承载的规则、或填补了某个盲区,则更新
docs/metrics/substrate.yaml; docs/observability.md的指标表为它增加一行。
选择 Instrument
| 你想知道什么 | Instrument | Go 构造器 | 仓库内示例 |
|---|---|---|---|
| 某件事发生了多少次,及其速率 | Counter | Int64Counter | ate.imagecache.requests(internal/imagecache/metrics.go) |
| 每次发生的时长或大小分布 | Histogram | Float64Histogram(秒)、Int64Histogram(字节) | ate.actor.restore.duration(cmd/atelet/metrics.go)、atelet.snapshot.size(cmd/atelet/main.go) |
| 当前存在多少东西,通过加减维护 | UpDownCounter | Int64ObservableUpDownCounter(可在采集时枚举)、Int64UpDownCounter(自己维护增量,罕见) | ate.workerpool.workers(cmd/ateapi/internal/controlapi/metrics.go) |
| 一个需要读取并记录的"表盘"读数 | Gauge | Int64ObservableGauge/Float64ObservableGauge | ate.actor.stats.memory.working_set(cmd/atelet/statspoller.go) |
几条经验法则:
- "有多少"优先用 observable 形式:observable 回调在采集时读取真值(缓存、map),因此漏掉的递减不会让值漂移,从数据源消失的组也会从导出中消失。当变化发生在请求路径上、且"最后递减后序列保留末值"可以接受时(如 router 的 parked-request 计数),用同步 UpDownCounter;当你要复制一份已经拥有的数据结构时才用 observable 形式。
- UpDownCounter 还是 Gauge:你在计数,还是在读表盘?UpDownCounter 是你自己维护的计数:某件事开始就加一,结束就减一。
ate.workerpool.workers是 UpDownCounter,因为 ateapi 负责分配和释放每个 worker,它是那个维护计数的人。Gauge 是你读取的表盘:memory working set 是 atelet 轮询 sandbox 得到的值(gVisor 上是 cgroup,micro-VM 上是 guest agent),按模板记录。问自己:"这个数是我加减出来的,还是我看到的?"答案就是 Instrument 类型。dashboard 之后如何聚合与此无关——类型是对管道关于数据点性质的承诺,rollup 和 actor relay 会在不检查的情况下按此承诺行事。 - 不要在一个 success counter 旁边再加一个 failure counter:一个 Instrument 承载成败,失败挂在
error.type或ate.failure.reason上,键的缺席即成功。见下文报告失败。 - 一个直方图、多个阶段,而不是每个阶段一个直方图(当阶段共享维度时)。
ate.actor.restore.duration用ate.snapshot.phase作为标签,让所有阶段落在同一张图上。
命名
- Instrument 名称使用点号分隔的小写:substrate 全局概念用
ate.<subsystem>.<noun>(如ate.actor.crashes、ate.imagecache.requests);指标描述组件自身机制时用<component>.<subsystem>.<noun>(如atenet.router.parking.active、atelet.snapshot.size)。拿不准就用ate.*:以组件开头会把指标绑定到那个二进制,同一测量若在第二个组件中出现就需要第二个名字。 - 命名被测的事物,而不是聚合:
duration、size、requests、workers。单位放在 Instrument 的 unit 字段,每个 exporter 按自己的方式渲染:kind 上的 Prometheus exporter 会追加_seconds、_bytes、_total,而 Cloud Monitoring 保留 OpenTelemetry 名称、只追加_bucket、_count、_sum(随附 dashboard 查询atelet.snapshot.size_bucket)。不要在名称里放单位或聚合词,否则某个后端会显示两次。 - 存在上游语义约定指标时直接复用:
rpc.server.call.duration原样来自otelgrpc。上游有形态但没有对应概念时,在ate.*名下镜像该形态:ate.workerpool.desired_workers与ate.workerpool.ready_workers遵循k8s.deployment.desired_pods和k8s.deployment.available_pods——是两个 Instrument 而不是一个按状态打标签的 Instrument,因为 desired 加 ready 不是求和。 - 存在上游属性模式时遵循它:
*.operation.name、*.duration、error.type。原样复用上游属性而不是把它别名化进ate.*(error.type与file.name都是原样使用的)。 - 属于子系统的标签键以子系统为根(
ate.imagecache.outcome),而不是放在actor下——当每个 actor 都共享该标签描述的事物时。
单位与桶(Units and buckets)
使用 UCUM 单位,与仓库其余部分一致:
| 量 | 单位 | Go 值 |
|---|---|---|
| 时间 | s | time.Since(start).Seconds()为float64 |
| 大小 | By | int64字节 |
| 事物的计数 | 花括号{thing}:{request}、{worker}、{crash} | int64 |
| 比率 | 1(UCUM;目前树内尚无 Instrument 使用) | float64 |
即使对快路径也记录秒而不是毫秒:分辨率由桶承载,而跨 Instrument 混用单位会破坏每个做除法运算的 dashboard。
每个直方图必须显式设置桶边界。SDK 默认值(0, 5, 10, 25 … 10000)是为毫秒设计的,对以秒记录的值是错的:0 到 5 秒之间的一切都会落进同一个桶。要选择覆盖所见两端的边界,并在注释中写明这两端是什么。量可比时复用已有集合:
| 量 | 树内使用的边界 |
|---|---|
| 生命周期操作 | 0.005 … 30(cmd/ateapi/internal/controlapi/metrics.go) |
| 调度步骤 | 0.0005 … 5(同一文件) |
| 快照阶段 | 0.005 … 60(cmd/atelet/metrics.go 中的snapshotPhaseBuckets) |
| 请求等待 | 0.001 … 60(cmd/atenet/internal/router/ingress/metrics.go) |
| 字节大小 | 1e6 … 1e10(cmd/atelet/main.go) |
| 小计数(worker 数) | 0, 1, 2, 3, 5, 10, 20, 50, 100, 250(cmd/ateapi/internal/scheduling/metrics.go) |
以ate.actor.restore.duration为例,实际实现(cmd/atelet/metrics.go)中的桶边界是:
// snapshotPhaseBuckets have to cover both ends of a phase breakdown: a warm OCI // unpack or a local rename lands in single-digit milliseconds, while a cold node // fetching a multi-GiB snapshot runs for tens of seconds. var snapshotPhaseBuckets = []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 20, 30, 60}注册表条目会在annotations.substrate.buckets下重复这些边界,让读者不用读代码就能找到。
标签与基数(Labels and cardinality)
规则集中在 docs/metrics/substrate.yaml 的cardinality_rules下。每个新指标都必须满足以下规则:
- 禁止 actor 身份:
ate.actor.name、ate.actor.uid、ate.atespace、ate.actor.version、ate.actor.container.name永远不能是指标标签。它们属于 span 和日志记录。这是让序列数量与"历史上存在过多少 actor"无关的规则。 - 有界或目录作用域:每个
ate.*标签要么是带固定值列表的枚举,要么命名 operator 创建的对象(模板、池)。指标标签上的任何值都不得来自请求负载、错误消息、主机名或路径。 - 在生产端归一化:当值来自网络或未经校验的文件时,在记录前把它映射到有界集合上:
ateattr.NormalizeSandboxClass、ateattr.NormalizeOperationName、ateattr.SnapshotScopeValue。无法识别的值上报unknown,绝不报告原始字符串。这些归一化函数都实现在 internal/ateattr/ateattr.go 中,例如NormalizeSandboxClass会把快照清单里未经校验的 class 折叠到gvisor、microvm或unknown。 - 不知道的标签就省略,而不是空着发:一个在选中 worker 之前就失败的 resume 没有池,池键应缺席而不是
""。ateattr.WorkerPoolAttributes在这种情况下返回 nil。 - 成对键一起出现:
ate.workerpool.namespace与ate.workerpool.name;ate.failure.reason与ate.failure.domain。使用返回成对的ateattr辅助函数(ateattr.FailureAttributes就是一个:它同时返回 reason 与由 reason 严格推导出的 domain)。
每个标签键都是 internal/ateattr/ateattr.go 中的常量,每个有界值集合都是它旁边的常量组。指标绝不在本地声明attribute.Key("...");唯一例外的 Instrument(router 的 parkingoutcome标签)早于该规则,被记录在substrate.yaml的lint_exceptions下。所以顺序是:先把键和值加进ateattr,注册表条目与代码便"按构造"达成一致。
加标签前,先问 dashboard 会按什么分组。一个没人会去分组或过滤的标签只会白白翻倍序列数。三四个标签是常态;标签最多的 restore 直方图有八个,且其中几个是有条件的。
SDK 还有硬限制:每个 Instrument 2000 个不同的属性集合,按 reader 持有(OTEL_GO_X_CARDINALITY_LIMIT可修改;未设置即 2000)。超过后,每个新组合都被静默折叠进一个只带otel.metric.overflow=true的序列。把你标签的值数量相乘,让乘积远低于该值。该限制作用于单进程内,因此 atelet 上的 per-template 标签受限于单节点托管的模板数,而不是整个集群。
报告失败(Reporting failures)
一个 Instrument 同时承载成功与失败。成功即失败键的缺席。用哪个键取决于错误在何处被分类:
error.type:用于 RPC 边界,携带 gRPC 状态码(status.Code(err).String()),如ate.actor.lifecycle.operation.duration所做。也用于有界协议状态集,如ate.imagecache.requests用一个 HTTP 状态码 allow-list 加_OTHER兜底。ate.failure.reason与ate.failure.domain成对:当失败属于 substrate 自身的 reason 分类(internal/ateerrors)时。使用ateattr.FailureAttributes(reason),绝不手工设置这两个键。这是 atelet 与 ateom handler 内的正确选择——那里的 gRPC 状态只在 handler 返回后才被赋值,读出来会是Unknown。
把"调用方放弃"与"失败"分开:context.Canceled与context.DeadlineExceeded是 outcome 标签上自己的结果(cancelled、timeout),不是error。而且必须记录它们:一次被取消的 pull 仍然运行过。把请求上下文原样传给Add和Record,无论是否已取消——SDK 从不检查ctx.Err(),所以什么都不会被丢弃;它从上下文里唯一读取的是 span,以便 exemplar 指向被采样的 trace。
internal/imagecache/metrics.go是这套做法的完整实现:failureOutcome先读 cancellation(被放弃的请求也携带 transport error,否则会被误读为 registry 故障),errorType只对reportedStatuses(401/403/404/429/500/502/503/504)内的状态返回具体码,其余一律返回_OTHER,从而把标签钉在有界集合内。
编写 Go 代码
结构形态(Shape)
把包内所有 Instrument 放进一个metrics.go,包含:Instrument 名称常量、一个 Instrument 结构体、一个接收metric.Meter的构造函数、以及未导出的 nil 安全record*方法。internal/imagecache、cmd/atelet、cmd/ateapi/internal/controlapi 都遵循此形态。核心样板如下:
const requestsMetric = "ate.snapshotcache.requests" // Instruments holds the snapshot cache's instruments. A nil *Instruments is a // valid no-op, so call sites need no guard. type Instruments struct { requests metric.Int64Counter } func NewInstruments(meter metric.Meter) (*Instruments, error) { requests, err := meter.Int64Counter( requestsMetric, metric.WithUnit("{request}"), metric.WithDescription("Number of snapshot lookups in the node-local snapshot cache, by outcome."), ) if err != nil { return nil, fmt.Errorf("create %s counter: %w", requestsMetric, err) } return &Instruments{requests: requests}, nil } // recordRequest counts one lookup. kind is known before the lookup starts, so // the label is present on every outcome, which is what lets the registry mark // it required. failureOutcome and errorType are the same two helpers as in // internal/imagecache/metrics.go. func (i *Instruments) recordRequest(ctx context.Context, kind, outcome string, err error) { if i == nil || i.requests == nil { return } if err != nil { outcome = failureOutcome(err) } attrs := []attribute.KeyValue{ ateattr.SnapshotCacheOutcomeKey.String(outcome), ateattr.SnapshotKindKey.String(kind), } if outcome == ateattr.SnapshotCacheOutcomeError { attrs = append(attrs, ateattr.ErrorTypeKey.String(errorType(err))) } i.requests.Add(ctx, 1, metric.WithAttributes(attrs...)) }nil 安全至关重要:测试、benchmark 和无指标部署都会在无 Instrument 的情况下构造子系统,调用点必须保持无条件。
注册表中标记为required的每个标签都必须在 record 方法的每条路径上设置。这里kind是参数而非事后推导,所以 miss、hit、failure 都携带它。如果某标签只在部分路径可知,在注册表中把它标为conditionally_required并在其他路径省略它,而不要空着发。
获取 Meter
把 meter 作为依赖注入,不要在库包深处调用otel.Meter。二进制的主函数传入otel.Meter("<component>"),组件名作为 scope("atelet"、"ateapi"、"atecontroller"):
- 带 options 结构体的包增加一个
WithMeter(metric.Meter) Option(如internal/imagecache); - 在
main中只构造一次的包接收在那里构建的*Instruments(如cmd/atelet)。
Meter provider 每个二进制只设置一次,由serverboot的三个函数之一完成(internal/serverboot/serverboot.go):
serverboot.InitMetrics:Prometheus reader 加 OTLP push;serverboot.InitMetricsPushOnly:仅 OTLP push(atecontroller);serverboot.InitMetricsPushOnlyVia:通过 atelet relay 走 OTLP push(ateom)。
新组件调用其中一个并 deferShutdownProvider;已有组件内的新包无需在此添加任何东西。
可观测 Instrument(Observable instruments)
对可在采集时枚举的值,注册回调而不是维护增量。这是完整示例中的 size Instrument:
const sizeMetric = "ate.snapshotcache.size" // The cache owns its index and the bytes per kind partition it, so this is // an UpDownCounter and not a gauge; observable, because the index is the // truth to read at collection time. size, err := meter.Int64ObservableUpDownCounter(sizeMetric, metric.WithUnit("By"), metric.WithDescription("Bytes of snapshots held in the node-local cache, by snapshot kind.")) ... _, err = meter.RegisterCallback(func(_ context.Context, o metric.Observer) error { for kind, n := range cache.bytesByKind() { o.ObserveInt64(size, n, metric.WithAttributes(ateattr.SnapshotKindKey.String(kind))) } return nil }, size)来自 ateapiRegisterWorkerCount的两个习惯(cmd/ateapi/internal/controlapi/metrics.go):数据源不可用时返回 nil、本周期不观测任何值(而不是观测 0);为警报依赖的序列显式播种一个 0,让"没有空闲 worker"是0而不是缺失序列。
回调要尽量廉价、尽量无锁——它每次采集都会运行(kind 上每 10 秒一次、生产环境每 60 秒一次),每次 Prometheus 抓取也会运行。statspoller(cmd/atelet/statspoller.go)用原子指针发布预计算的快照(latest atomic.Pointer[map[templateKey]*templateAggregate]),回调只读它。
测试
通过ManualReader测试,绝不通过全局 provider(那会让测试产生顺序依赖并阻塞t.Parallel)。模式见 cmd/atelet/metrics_test.go 与 internal/imagecache/metrics_test.go:
func newTestInstruments(t *testing.T) (*Instruments, *sdkmetric.ManualReader) { t.Helper() reader := sdkmetric.NewManualReader() mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)) inst, err := NewInstruments(mp.Meter("atelet")) if err != nil { t.Fatalf("NewInstruments: %v", err) } return inst, reader }然后reader.Collect(ctx, &rm)并遍历rm.ScopeMetrics[*].Metrics按名称找到 Instrument。断言:
- 名称与单位(这里的笔误会静默破坏每个 dashboard);
- 数据类型(counter 是
metricdata.Sum[int64]且IsMonotonic为 true,时长直方图是metricdata.Histogram[float64],gauge 是Gauge); - 每个序列的标签集合,包括某标签应在场缺席时确实缺席(成功时的
error.type、分配前的池对); - 值归一化:未知的线上值落为
unknown,被取消的调用落为cancelled而非error。
要测试维持基数有界的行为,而不只是快乐路径。image cache 测试喂入一个未列出状态的 registry 错误并断言_OTHER。
指标在 kind 上存在后,e2e 指标套件(internal/e2e/suites/metrics)可以通过抓取 collector 的 Prometheus 端点(e2e.ScrapeCollectorMetrics)断言它到达了 collector。当指标是 operator 会设警报的对象时,把它加入套件检查的列表。
注册(Registering it)
docs/metrics/registry/metrics.yaml
该注册表是一个OpenTelemetry Weaver 语义约定注册表,是 substrate 发出内容的唯一权威清单(hack/verify/metrics.sh在 CI 中运行weaver registry check -r docs/metrics/registry验证它;本地 weaver 仅当恰好是 v0.25.1 时可用,其他版本会报错,否则走 docker 中的固定镜像)。两类条目:
属性(attribute):放在其子系统的attribute_group中,每个允许值作为一个成员。把新组放在registry.ate.imagecache旁边:
- id: registry.ate.snapshotcache type: attribute_group brief: > The labels of the snapshot cache on the node. The key starts with the name of the subsystem and not with actor: every actor on the node shares it. attributes: - id: ate.snapshotcache.outcome stability: development brief: > The result of one snapshot lookup. Calculate the hit ratio as hit / (hit + miss). Do not put the failures or the stopped lookups in the denominator. type: members: - id: hit stability: development value: hit brief: The node holds the snapshot. - id: miss stability: development value: miss brief: The lookup must download the snapshot. - id: error stability: development value: error brief: The lookup failed. Only this outcome has an error.type key. - id: cancelled stability: development value: cancelled brief: The caller stopped the lookup. - id: timeout stability: development value: timeout brief: The time limit of the caller ended.指标(metric):放在其组件对应的 section:
- id: metric.ate.snapshotcache.requests type: metric metric_name: ate.snapshotcache.requests instrument: counter unit: "{request}" stability: development brief: The number of snapshot lookups in the snapshot cache on the node, by outcome. note: > A miss causes a download. Thus the hit ratio of a node is an early sign of the resume time. annotations: substrate: emitted_by: [atelet] golden_signals: [latency, errors] code_anchor: cmd/atelet/internal/snapshotcache/metrics.go cuj: Resumes became slower on one node. Does the snapshot cache miss? attributes: - ref: ate.snapshotcache.outcome requirement_level: required - ref: ate.snapshot.kind requirement_level: required - ref: error.type requirement_level: conditionally_required: The outcome is error.annotations.substrate块是 substrate 自有、Weaver 会原样透传的:emitted_by列出二进制,code_anchor指向创建该 Instrument 的文件,cuj是 operator 用它回答的问题,golden_signals说明它服务四个黄金信号中的哪些(latency、traffic、errors、saturation;把适用的都列上,这样 dashboard 作者不用读每个 brief 就能找到饱和度 Instrument),buckets是直方图边界。所有适用字段都要填,包括直方图的buckets——注册表读者不应需要看代码。
已存在的属性(ate.template.name、ate.snapshot.kind、error.type、ate.failure.reason)用ref:引用而非重定义。若代码在某些路径省略某标签,标为conditionally_required并说明条件,而不是required。
然后运行 CI 同款检查:
hack/verify/metrics.sh # local weaver only if it is exactly v0.25.1 (another version is an error), else the pinned image via dockerdocs/metrics/substrate.yaml
当新指标填补了某个列出的blind_spot(删除该条目)、包含 Weaver 无法表达的规则(加到cardinality_rules并说明什么能强制它)、或有意打破某个约定(加lint_exceptions条目并写明原因)时,更新此文件。该文件当前记录了六条基数规则(no-actor-identity、bounded-or-catalog-scoped、error-type-not-parallel-counters、failure-keys-paired、workload-domain-is-a-report、pool-keys-paired)、两条 lint 例外,以及若干盲区(store、worker cache、actor population、image cache cost and eviction、podcertcontroller、golden snapshot pipeline、router shutdown outcome),并注明每条规则目前尚无机器强制执行——它由人和 agent 阅读。
docs/observability.md
在 "2. Metrics" 下的指标表中加一行:名称、发出组件、Instrument 类型、一句话说明测量内容并列出标签。若标签语义需要超过一句话(如快照标签那样),在表格下方加一小段,而不是拉长表格行。
Dashboards
Cloud Monitoring dashboard 位于tools/setup-gcp/dashboards/。新指标不必在同一 PR 内新增 dashboard,但 operator 会设警报的指标应在后续 PR 中加面板,且 PR 描述应说明该面板回答什么问题。
完整示例:节点本地快照缓存
假设在 atelet 中为快照下载前加一个缓存,想回答:缓存有没有用、一次 miss 花多少钱、它占多少磁盘。这需要四个 Instrument,而不是一个:
| 问题 | Instrument | 标签 |
|---|---|---|
| 有用吗? | ate.snapshotcache.requestscounter,按 outcome | ate.snapshotcache.outcome、ate.snapshot.kind、error 时加error.type |
| miss 花多少钱? | ate.snapshotcache.fill.duration直方图,秒,与cmd/atelet/metrics.go中snapshotPhaseBuckets相同的边界(那里未导出,需复制或提取) | ate.snapshot.kind、ate.template.atespace、ate.template.name,失败时加 failure 对 |
| 占多少磁盘? | ate.snapshotcache.sizeobservable UpDownCounter,字节(缓存拥有总量且按 kind 分区,所以不是 gauge),外加ate.snapshotcache.evictionscounter | ate.snapshot.kind;evictions 还带ate.snapshotcache.eviction.reason枚举(capacity、ttl、explicit) |
刻意不作为标签的东西:快照名称或 digest(每个 actor 一个,无界)、对象存储 URL(路径)、actor。"这个 actor 的 resume 命中了哪个快照"这类 per-actor 问题,用带ateattr.ActorLogAttrs的日志记录写在 restore 路径上——那里已有Restore timing breakdown。
实施步骤,按顺序:
- 在 internal/ateattr/ateattr.go 中:
SnapshotCacheOutcomeKey、SnapshotCacheEvictionReasonKey及其值常量。复用SnapshotKindKey、TemplateAtespaceKey、TemplateNameKey、ErrorTypeKey、FailureAttributes。 cmd/atelet/internal/snapshotcache/metrics.go(单二进制使用的包放在cmd/<binary>/internal/下):Instruments结构体、接收metric.Meter的构造函数、nil 安全的record*方法、在既有锁下读取缓存索引的 size 回调。- cmd/atelet/main.go:用
otel.Meter("atelet")构建 Instruments 并传入缓存。 cmd/atelet/internal/snapshotcache/metrics_test.go:ManualReader;一个 miss-then-hit 测试、一个断言error.type只在error上出现的失败测试、一个按 reason 的 eviction 测试、一个 insert 和 evict 后的 size 测试。- docs/metrics/registry/metrics.yaml:
registry.ate.snapshotcache属性组与四条指标条目;运行hack/verify/metrics.sh。 - docs/metrics/substrate.yaml:无需新增。size 与 eviction Instrument 随缓存一起发布,所以起点没有盲区;那里的
image cache cost and eviction条目是 image cache 的缺口,与本例无关。 - docs/observability.md:表格四行,outcome 语义一小段。
关键路径速查
- 标签键与有界值集(唯一事实来源):internal/ateattr/ateattr.go
- 计数器实现样板(失败分类、
_OTHER、allow-list):internal/imagecache/metrics.go - 直方图与阶段桶实现:
ate.actor.restore.duration(cmd/atelet/metrics.go) - 可观测 gauge 与原子快照发布:
ate.actor.stats.memory.working_set(cmd/atelet/statspoller.go) - observable UpDownCounter 与 0 播种习惯:
ate.workerpool.workers(cmd/ateapi/internal/controlapi/metrics.go) - 指标注册表与检查命令:docs/metrics/registry/metrics.yaml、hack/verify/metrics.sh
- 基数规则、lint 例外与盲区清单:docs/metrics/substrate.yaml
- 面向 operator 的指标总表:docs/observability.md
- 指标的对偶文档(tracing):docs/dev/best-practices/tracing.md
- 人工智能
- AI Agent
- Agent 沙箱
- 云原生
- 容器运行时
- 零信任
【免费下载链接】substrate
Agent Substrate: the core system
相关推荐
wazero最佳实践指南:从开发到生产的完整流程
wazero最佳实践指南:从开发到生产的完整流程 WebAssembly 运行时 wazero 是一个零依赖的 Go 语言 WebAssembly 运行时,让
开发工具系统底层3行代码加速卷积神经网络:wincnn Winograd最小卷积算法生成器完全入门指南
3行代码加速卷积神经网络:wincnn Winograd最小卷积算法生成器完全入门指南 wincnn 是一个轻量级 Python 开源模块,用于计算 卷积神经网
StepCI 最佳实践:10个技巧让你的API测试更高效
StepCI 最佳实践:10个技巧让你的API测试更高效 StepCI 是一个强大的开源 API 质量保证框架,专门为开发者和测试工程师提供高效的API测试解决
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考