Substrate 指标开发最佳实践:从 Instrument 选型、命名到注册的完整流程
2026/9/23 18:12:47 网站建设 项目流程
  • 人工智能
  • AI Agent
  • Agent 沙箱
  • 云原生
  • 容器运行时
  • 零信任

【免费下载链接】substrate

Agent Substrate: the core system

项目地址:https://gitcode.com/GitHub_Trending/substrate7/substrate
点击查看免费下载

本文是 Agent Substrate 核心仓库(substrate)中关于如何在子系统内新增一个 OpenTelemetry 指标的工程指南,对应 docs/dev/best-practices/metrics.md。它回答四个问题:什么时候该加指标(以及什么时候不该加)、该选哪种 Instrument、指标与标签如何命名、如何用 Go 编写、测试并把它注册进指标注册表。读完本文,你将掌握 Substrate 体系中"从一次缓存命中率到一条ate.*指标"的全套规范,并能在ateletateapi等二进制中按同一标准落地新指标。

在动手之前,建议先阅读 Actor Observability(docs/observability.md):它说明了系统目前已经有哪些遥测、注册表如何使用。本文只讲"如何往里加东西"。

什么时候该加指标,什么时候不该加

判断的第一原则是:这个值是否会被"聚合"

  • 应该加指标:当有人会对该值做 rate(速率)、ratio(比率)、percentile(百分位)、按结果计数、或全集群汇总时。缓存命中率、按阶段拆分的延迟分布、队列深度——这些是指标。
  • 不应该加指标:当问题只针对某一个 actor时。由于 actor 身份被禁止出现在指标标签中(见下文"标签与基数"),指标无法回答"actor X 花了多久"。此时应该用ateattr.ActorLogAttrs写一条结构化日志记录,atelet中的Restore timing breakdown就是典范。
  • 指标与日志并非二选一atelet对一次 restore 同时发出两者——直方图用于集群视图,日志记录用于单个 actor 视图。
  • 不要为一次性/启动期事实加指标(如配置值、版本号):把它们放在启动日志行或资源对象上即可。

新指标 PR 的检查清单

每个指标 PR 都包含以下部分,评审者会逐项核对:

  1. 指标在 Go 中定义于注入的metric.Meter之上,nil 安全,并带metric.WithUnitmetric.WithDescription
  2. 每个标签键都是internal/ateattr中的常量,每个标签值要么来自其中定义的有界集合,要么命名一个 operator 创建的对象(模板、池),符合标签规则;
  3. 单元测试通过ManualReader采集指标,断言名称、单位、Instrument 类型以及每个序列的标签集合;
  4. 指标与任何新属性都登记在docs/metrics/registry/metrics.yaml,且hack/verify/metrics.sh通过;
  5. 若指标包含注册表无法承载的规则、或填补了某个盲区,则更新docs/metrics/substrate.yaml
  6. docs/observability.md的指标表为它增加一行。

选择 Instrument

你想知道什么InstrumentGo 构造器仓库内示例
某件事发生了多少次,及其速率CounterInt64Counterate.imagecache.requests(internal/imagecache/metrics.go)
每次发生的时长或大小分布HistogramFloat64Histogram(秒)、Int64Histogram(字节)ate.actor.restore.duration(cmd/atelet/metrics.go)、atelet.snapshot.size(cmd/atelet/main.go)
当前存在多少东西,通过加减维护UpDownCounterInt64ObservableUpDownCounter(可在采集时枚举)、Int64UpDownCounter(自己维护增量,罕见)ate.workerpool.workers(cmd/ateapi/internal/controlapi/metrics.go)
一个需要读取并记录的"表盘"读数GaugeInt64ObservableGauge/Float64ObservableGaugeate.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.typeate.failure.reason上,键的缺席即成功。见下文报告失败。
  • 一个直方图、多个阶段,而不是每个阶段一个直方图(当阶段共享维度时)。ate.actor.restore.durationate.snapshot.phase作为标签,让所有阶段落在同一张图上。

命名

  • Instrument 名称使用点号分隔的小写:substrate 全局概念用ate.<subsystem>.<noun>(如ate.actor.crashesate.imagecache.requests);指标描述组件自身机制时用<component>.<subsystem>.<noun>(如atenet.router.parking.activeatelet.snapshot.size)。拿不准就用ate.*:以组件开头会把指标绑定到那个二进制,同一测量若在第二个组件中出现就需要第二个名字。
  • 命名被测的事物,而不是聚合durationsizerequestsworkers。单位放在 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_workersate.workerpool.ready_workers遵循k8s.deployment.desired_podsk8s.deployment.available_pods——是两个 Instrument 而不是一个按状态打标签的 Instrument,因为 desired 加 ready 不是求和。
  • 存在上游属性模式时遵循它*.operation.name*.durationerror.type。原样复用上游属性而不是把它别名化进ate.*error.typefile.name都是原样使用的)。
  • 属于子系统的标签键以子系统为根ate.imagecache.outcome),而不是放在actor下——当每个 actor 都共享该标签描述的事物时。

单位与桶(Units and buckets)

使用 UCUM 单位,与仓库其余部分一致:

单位Go 值
时间stime.Since(start).Seconds()float64
大小Byint64字节
事物的计数花括号{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.nameate.actor.uidate.atespaceate.actor.versionate.actor.container.name永远不能是指标标签。它们属于 span 和日志记录。这是让序列数量与"历史上存在过多少 actor"无关的规则。
  • 有界或目录作用域:每个ate.*标签要么是带固定值列表的枚举,要么命名 operator 创建的对象(模板、池)。指标标签上的任何值都不得来自请求负载、错误消息、主机名或路径。
  • 在生产端归一化:当值来自网络或未经校验的文件时,在记录前把它映射到有界集合上:ateattr.NormalizeSandboxClassateattr.NormalizeOperationNameateattr.SnapshotScopeValue。无法识别的值上报unknown,绝不报告原始字符串。这些归一化函数都实现在 internal/ateattr/ateattr.go 中,例如NormalizeSandboxClass会把快照清单里未经校验的 class 折叠到gvisormicrovmunknown
  • 不知道的标签就省略,而不是空着发:一个在选中 worker 之前就失败的 resume 没有池,池键应缺席而不是""ateattr.WorkerPoolAttributes在这种情况下返回 nil。
  • 成对键一起出现ate.workerpool.namespaceate.workerpool.nameate.failure.reasonate.failure.domain。使用返回成对的ateattr辅助函数(ateattr.FailureAttributes就是一个:它同时返回 reason 与由 reason 严格推导出的 domain)。

每个标签键都是 internal/ateattr/ateattr.go 中的常量,每个有界值集合都是它旁边的常量组。指标绝不在本地声明attribute.Key("...");唯一例外的 Instrument(router 的 parkingoutcome标签)早于该规则,被记录在substrate.yamllint_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.reasonate.failure.domain成对:当失败属于 substrate 自身的 reason 分类(internal/ateerrors)时。使用ateattr.FailureAttributes(reason),绝不手工设置这两个键。这是 atelet 与 ateom handler 内的正确选择——那里的 gRPC 状态只在 handler 返回后才被赋值,读出来会是Unknown

把"调用方放弃"与"失败"分开context.Canceledcontext.DeadlineExceeded是 outcome 标签上自己的结果(cancelledtimeout),不是error。而且必须记录它们:一次被取消的 pull 仍然运行过。把请求上下文原样传给AddRecord,无论是否已取消——SDK 从不检查ctx.Err(),所以什么都不会被丢弃;它从上下文里唯一读取的是 span,以便 exemplar 指向被采样的 trace。

internal/imagecache/metrics.go是这套做法的完整实现:failureOutcome先读 cancellation(被放弃的请求也携带 transport error,否则会被误读为 registry 故障),errorType只对reportedStatuses401/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说明它服务四个黄金信号中的哪些(latencytrafficerrorssaturation;把适用的都列上,这样 dashboard 作者不用读每个 brief 就能找到饱和度 Instrument),buckets是直方图边界。所有适用字段都要填,包括直方图的buckets——注册表读者不应需要看代码。

已存在的属性(ate.template.nameate.snapshot.kinderror.typeate.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 docker

docs/metrics/substrate.yaml

当新指标填补了某个列出的blind_spot(删除该条目)、包含 Weaver 无法表达的规则(加到cardinality_rules并说明什么能强制它)、或有意打破某个约定(加lint_exceptions条目并写明原因)时,更新此文件。该文件当前记录了六条基数规则(no-actor-identitybounded-or-catalog-scopederror-type-not-parallel-countersfailure-keys-pairedworkload-domain-is-a-reportpool-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,按 outcomeate.snapshotcache.outcomeate.snapshot.kind、error 时加error.type
miss 花多少钱?ate.snapshotcache.fill.duration直方图,秒,与cmd/atelet/metrics.gosnapshotPhaseBuckets相同的边界(那里未导出,需复制或提取)ate.snapshot.kindate.template.atespaceate.template.name,失败时加 failure 对
占多少磁盘?ate.snapshotcache.sizeobservable UpDownCounter,字节(缓存拥有总量且按 kind 分区,所以不是 gauge),外加ate.snapshotcache.evictionscounterate.snapshot.kind;evictions 还带ate.snapshotcache.eviction.reason枚举(capacityttlexplicit

刻意不作为标签的东西:快照名称或 digest(每个 actor 一个,无界)、对象存储 URL(路径)、actor。"这个 actor 的 resume 命中了哪个快照"这类 per-actor 问题,用带ateattr.ActorLogAttrs的日志记录写在 restore 路径上——那里已有Restore timing breakdown

实施步骤,按顺序:

  1. 在 internal/ateattr/ateattr.go 中:SnapshotCacheOutcomeKeySnapshotCacheEvictionReasonKey及其值常量。复用SnapshotKindKeyTemplateAtespaceKeyTemplateNameKeyErrorTypeKeyFailureAttributes
  2. cmd/atelet/internal/snapshotcache/metrics.go(单二进制使用的包放在cmd/<binary>/internal/下):Instruments结构体、接收metric.Meter的构造函数、nil 安全的record*方法、在既有锁下读取缓存索引的 size 回调。
  3. cmd/atelet/main.go:用otel.Meter("atelet")构建 Instruments 并传入缓存。
  4. cmd/atelet/internal/snapshotcache/metrics_test.goManualReader;一个 miss-then-hit 测试、一个断言error.type只在error上出现的失败测试、一个按 reason 的 eviction 测试、一个 insert 和 evict 后的 size 测试。
  5. docs/metrics/registry/metrics.yaml:registry.ate.snapshotcache属性组与四条指标条目;运行hack/verify/metrics.sh
  6. docs/metrics/substrate.yaml:无需新增。size 与 eviction Instrument 随缓存一起发布,所以起点没有盲区;那里的image cache cost and eviction条目是 image cache 的缺口,与本例无关。
  7. 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

项目地址:https://gitcode.com/GitHub_Trending/substrate7/substrate
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询