Skip to content

aster_forge_metrics ​

Forge 拥有实例级指标内核、通用基础设施 schema、typed instruments、采样和导出机制。产品拥有业务指标、标签分类、配置来源、认证策略及错误映射。日志和 traces 不属于本次 metrics 接口。

适用场景与概念 ​

用于需要隔离实例、限制高基数、为基础设施和产品指标统一选择导出后端的 Rust 服务。HTTP、数据库、任务等模块依赖 recorder trait;产品启动层持有 provider。每个 provider 独立管理 schema、系列、reader 和关闭状态,不安装全局 OpenTelemetry provider。

名称在本实现中负责什么
metric一个被观测的量,如请求数、当前活跃数、耗时分布
OpenTelemetry / OTel指标 API、SDK 聚合和 resource 模型;并非另一套业务指标
OTLP把聚合后的数据推送给 collector 的协议;本 crate 仅实现 metrics 的 HTTP/protobuf
Prometheus定期抓取 HTTP exposition,存储时间序列并运行 PromQL
Grafana查询 Prometheus 等数据源,展示 dashboard 与告警
Tempo保存与查询 traces;本 crate 不产生或导出 traces

一次业务记录进入同一个 OTel MeterProvider,Prometheus reader 和 OTLP reader 分别读取自己的累计聚合。启用双导出不需要在业务调用处双写。耗时 histogram 提供次数、总耗时和桶分布,可计算速率或分位数;它不能告诉你某个具体请求在哪一层耗时。后者需要单独接入 tracing 与上下文传播。

text
HTTP / DB / auth / tasks / 产品事件
  -> InfrastructureMetricsRecorder / product_metrics! typed handles
  -> 实例 MetricsProvider(校验、系列预算、OTel SDK)
       -> Prometheus reader -> exposition adapter <- Prometheus <- Grafana
       -> periodic OTLP reader -> collector -> 指标存储 <- Grafana

Cargo 与启用边界 ​

Cargo feature编译能力运行方式
默认descriptors、typed instruments、Noop providerbuilder 不添加 reader 时不采集
backend-prometheusPrometheus reader显式 with_prometheus()
otlpOTLP HTTP/protobuf reader显式 with_otlp(config)
两者同一个 MeterProvider 的两个 reader同时添加,不重复业务埋点
allocator-metricsallocator 快照infrastructure updater 采样
runtime-healthhealth report adapterrecorder.record_health(scope, report)

feature 只提供能力,不初始化全局 provider,也不注册 HTTP 路由。配置和部署默认值由产品决定。

toml
[dependencies]
aster_forge_metrics = { git = "https://github.com/AsterCommunity/AsterForge", features = ["backend-prometheus", "runtime-health"] }

最小接入 ​

rust
use aster_forge_metrics::{MetricsProvider, InfrastructureMetricsRecorder, MetricResult};

aster_forge_metrics::product_metrics! {
    pub struct ProductMetrics {
        bytes: counter("file", "bytes_total", "Observed bytes.", &["direction"]).with_unit("By"),
        active: gauge("file", "active", "Active transfers.", &["direction"]),
        duration: histogram_with_buckets("file", "duration_seconds", "Transfer duration.", &["outcome"], &[0.01, 0.1, 1.0, 10.0]).with_unit("s"),
    }
}

fn create() -> MetricResult<(MetricsProvider, InfrastructureMetricsRecorder, ProductMetrics)> {
    let provider = MetricsProvider::builder().with_prometheus()?.build()?;
    let shared = InfrastructureMetricsRecorder::new(provider.clone())?;
    let product = ProductMetrics::register(&provider)?;
    product.bytes.inc(&["ingress"], 1024);
    Ok((provider, shared, product))
}

一个 metric set 先校验全部描述,再一次注册。重复名称、histogram 派生名称冲突、错误 kind、重复/非法 label、非法 unit/buckets 和资源超限返回 MetricError,失败不留下半套 schema。handles 直接绑定创建它们的实例;没有通过名称查全局 registry 的记录路径。

inc、set、add、observe 在观测失败时跳过该次记录;需要断言错误的边界使用 try_*。未知、高基数 label 不自动合并进一个误导性的正常系列。MetricsLimits 默认限制 512 instruments、每 instrument 4096 系列、全实例 16384 系列、单 label 256 bytes、exposition 16 MiB。

计数器在内核中用 u64 并检查溢出;gauge 使用有限 f64,支持有序 add/set;histogram 接受有限非负值。Prometheus 的数值格式存在 IEEE-754 精度边界,超过 2^53 的整数可能近似表示。OTLP 整数样本是 i64,超界 u64 在转换前拒绝该批导出,不能被转换成 0 或负数。没有承诺无限精度。

系列首次出现时分配 label 和绑定状态;预热后的记录复用借用查询和 OTel bound instruments。typed handle 记录路径不执行 exporter 网络调用,也不为预热系列复制标签字符串;上层 adapter 自身构造标签的成本仍需单独评估。系列在 provider 生命周期内保留,SDK 不提供 unregister;不能靠删除产品侧 handle 来回收 SDK 的系列预算。

Provider、resource 与产品配置 ​

MetricsProvider::builder().build() 创建无 reader 的 Noop provider。enabled() 为 false,仍可检查注册的 schema,但记录不会创建实际时间序列。编译 exporter feature 本身不改变这个默认行为。产品应把“编译支持”和“静态启用”分开:请求启用未编译能力时在启动阶段拒绝配置,关闭时跳过采样任务和昂贵标签计算。

rust
use aster_forge_metrics::{MetricResult, MetricsProvider, MetricsResource};

fn create(instance_id: String) -> MetricResult<MetricsProvider> {
    MetricsProvider::builder()
        .with_resource(MetricsResource {
            service_name: "example-service".into(),
            service_version: env!("CARGO_PKG_VERSION").into(),
            environment: "production".into(),
            role: "api".into(),
            instance_id,
        })?
        .with_prometheus()?
        .build()
}

with_resource 要求非空 service_name;每个字段至多 128 bytes,不能包含控制字符。空的可选字段不导出。产品应生成每次进程启动唯一、进程内稳定的 instance ID,在创建 provider 前完成角色和环境判定。

Rust 字段OTel resource attributePrometheus label
service_nameservice.nameservice_name
service_versionservice.versionservice_version
environmentdeployment.environment.namedeployment_environment_name
roleaster.roleaster_role
instance_idservice.instance.idservice_instance_id

Prometheus 侧不另生成 target_info;上述属性直接附到样本。job、instance 通常由 Prometheus 抓取配置附加,不能混同于进程的 service_instance_id。OTLP 使用显式 resource,不运行 SDK 环境资源检测器。

provider 的 clone 共享实例,创建第二个 provider 才有独立系列和预算。共享 schema 和每套产品 schema 各注册一次,再 clone recorder/handles 注入消费者;重复注册名称会返回错误。

数值、schema 与资源预算 ​

类型记录 API值的边界与含义
Counterinc / try_incu64 非负增量,累计溢出时拒绝
Gaugeset、add / try_set、try_add有限 f64;可升可降,set/add 按系列同步
Histogramobserve / try_observe有限非负 f64;显式递增桶边界

macro 中的 labels 是有序 schema;调用传入的值必须按相同顺序且数量相同。subsystem 非空时名称为 subsystem_name。单位存入 descriptor;Prometheus exporter 不自动添加单位或 _total 后缀,名称应由 schema 明确决定。histogram 的 _bucket、_sum、_count 派生名称也参与冲突检查。

MetricsLimits 字段默认值构建时上限
max_instruments5124096
max_series_per_instrument409665536
max_total_series163841048576
max_label_bytes256 bytes4096 bytes
max_exposition_bytes16 MiB64 MiB

预算均须大于零;通过 builder.with_limits(...) 显式调整。每个 descriptor 最多八个 label。预算控制内存和输出规模,不能代替产品有限枚举分类。路由使用模板;用户 ID、文件名、对象 key、完整 URL、SQL、token 和其他凭据不能进入 labels。未知分类应在产品边界映射到有限的 unknown/other,不要把原始输入直接交给 recorder。

内核超限拒绝新系列,已有系列可继续记录;不会悄悄把不同业务对象汇入正常标签。系列不能 unregister,删除 handle 不释放 SDK 已注册的系列。长时间运行服务必须按最大可能组合数设计标签,而不是按一次请求的标签数估算。

通用指标与兼容 ​

InfrastructureMetricsRecorder 提供 HTTP、DB、auth、application events、config reload/mutation、background tasks、external operations、health 与 process 指标。原 Prometheus 名称、HELP、类型、labels 和显式 buckets 保持;subsystem 不为空的产品指标仍按 subsystem_name 导出。单位由 descriptor 单独表达,exporter 不自动添加单位/_total 后缀。

process_cpu_milliseconds_total 仍是 毫秒累计值的 gauge,没有悄悄改为秒 counter。process_memory_rss_bytes、process_uptime_seconds 和可选 process_heap_memory_mib{kind} 的采样移到实例 updater;scrape 不刷新系统或 allocator 数据。

HTTP route 必须是路由模板;DB 只记录 backend/kind/status/elapsed,不记录 SQL。资源身份通过 MetricsResource 显式注入,不读取 SDK 环境资源检测器;产品应在每次进程启动时生成、并在进程内保持稳定的 service.instance.id,避免多个 OTLP 实例覆盖同一累计系列。产品用户、文件、对象 key、URL、SQL 和凭据不能成为指标 labels。

OTLP ​

OtlpMetricsConfig 显式持有完整 /v1/metrics endpoint、headers、timeout、interval、请求/响应大小上限。只实现 HTTP/protobuf,HTTPS 使用系统信任;不提供无效的 gRPC、traces 或 logs 开关。不读取 OTEL_*、代理环境变量或 SDK credential headers;禁止重定向、URL 用户信息、query/fragment、保留 transport headers 和畸形 header。

同一 OTel MeterProvider 为 Prometheus 与 OTLP 分别维护 reader aggregation。OTLP 使用 cumulative temporality 与 descriptor buckets;重启会有新的 start timestamp。失败在下一周期重发累计快照,没有业务重试队列;scrape 不触发 push。只有 collector 成功确认才更新本地成功时间,不代表下游平台已经持久化。

provider.otlp_diagnostics() 提供本地成功/失败累计值与最后成功时间,不依赖故障 exporter 自报。infrastructure updater 同步到 telemetry_export_successes、telemetry_export_failures、telemetry_export_last_success_timestamp_seconds{exporter="otlp"}。endpoint、headers 和远端错误响应不出现在 Debug、错误或这些 labels 中。

OTLP 完整配置示例 ​

OtlpMetricsConfig 没有隐式部署默认值;产品明确填写各个字段。以下同时启用 pull 与 push,需要两个 exporter feature:

rust
use aster_forge_metrics::{MetricResult, MetricsProvider, OtlpMetricsConfig};
use std::{collections::BTreeMap, time::Duration};

fn create() -> MetricResult<MetricsProvider> {
    let builder = MetricsProvider::builder().with_prometheus()?;
    let (builder, diagnostics) = builder.with_otlp(OtlpMetricsConfig {
        endpoint: "http://127.0.0.1:4318/v1/metrics".into(),
        headers: BTreeMap::new(),
        timeout: Duration::from_secs(5),
        interval: Duration::from_secs(30),
        max_request_bytes: 16 * 1024 * 1024,
        max_response_bytes: 64 * 1024,
    })?;
    let provider = builder.build()?;
    let _initial_snapshot = diagnostics.snapshot();
    Ok(provider)
}

上例为便于展示省略 resource;生产环境应像前例一样注入唯一实例身份。endpoint 是完整 signal URL,库不会自动追加 /v1/metrics;部署时必须填 collector 实际接收 metrics 的路径。

配置约束
endpointHTTP/HTTPS、必须有 host,不能有 URL 用户信息、query 或 fragment
timeout大于零、不超过 60s,且不超过 interval
interval1s 到 1h
headers最多 16 个;每个值最多 4096 bytes,名称和值必须为合法 HTTP header
保留 headers不接受 content-type、content-length、host、connection、transfer-encoding
请求预算大于零,最多 64 MiB
响应预算大于零,最多 1 MiB

不接受重定向,也不读取代理环境变量;需要代理时使用明确可达的 collector endpoint。SDK 定期 worker 负责后台发送,业务记录不执行网络 I/O。HTTP 成功还必须得到可接受的 OTLP acknowledgement;部分拒绝等情况按失败处理。Debug、错误、诊断计数不暴露 endpoint、headers 或远端响应体。

通用基础设施 schema ​

所有名称为实际导出名;C = counter,G = gauge,H = histogram。health、allocator 和 exporter 项分别需要对应 feature。

名称类型/单位labels
http_requests_totalC / 1method, route, status
http_request_duration_secondsH / smethod, route, status
db_queries_totalC / 1backend, kind, status
db_query_duration_secondsH / sbackend, kind, status
auth_events_totalC / 1action, status, reason
application_events_totalC / 1category, event, status
config_reloads_totalC / 1source, decision, status
config_reload_duration_secondsH / ssource, decision, status
config_reload_changed_keysH / 1source, decision, status
config_mutations_totalC / 1source, operation, status
config_mutation_changed_keysH / 1source, operation, status
background_tasks_totalC / 1kind, status
background_tasks_pendingG / 1无
background_task_retries_totalC / 1kind
external_operations_totalC / 1system, operation, status
external_operation_duration_secondsH / ssystem, operation, status
health_report_statusG / 1scope
health_report_duration_secondsH / sscope, status
health_component_statusG / 1scope, component
health_component_duration_secondsH / sscope, component, status
process_memory_rss_bytesG / By无
process_cpu_milliseconds_totalG / ms无
process_uptime_secondsG / s无
process_heap_memory_mibG / MiBykind
telemetry_export_successesG / 未指定exporter
telemetry_export_failuresG / 未指定exporter
telemetry_export_last_success_timestamp_secondsG / sexporter

health 状态数值是 healthy=0、degraded=1、unhealthy=2。background task 的 status="retry" 同时增加 retry counter。heap 的 kind 为 allocated 与 peak_or_resident。导出成功/失败虽然是累计数,当前 schema 的类型仍为 gauge。

库提供 schema 与记录入口,不会自动拦截全部 HTTP/SQL/auth 流程。产品须显式接入 middleware、数据库 adapter、任务状态转换和认证流程。auth recorder 只接收有限的 action/status/reason,不拥有产品登录策略或用户身份。histogram 精确桶定义可通过 provider.descriptors() 或 src/shared.rs 查询;变更桶、名称、labels 和单位均应视作监控 schema 变更。

Runtime 与关闭 ​

产品持有显式 provider/recorder,向 DB、HTTP middleware 和任务注入 recorder;通过 aster_forge_observability 注入 exposition resource。系统 updater 每个实例只启动一次,取消时结束,不在 scrape 中执行。

停止 HTTP/worker producer,等待仍产生指标的 audit、mail 和业务任务退出后,再 provider.shutdown(timeout).await。force_flush 和 shutdown 共用有界生命周期额度。超时仅表示调用者停止等待;blocking job 仍持有额度,可通过 shutdown_result() 读取真正结果,不允许超时后无限堆后台任务。成功/失败结果均缓存,关闭后的 handles 拒绝记录。产品决定 exporter 故障是否影响启动、readiness 或退出码。

系统 updater 的显式启动 ​

rust
use aster_forge_metrics::{InfrastructureMetricsRecorder, MetricsRecorder};
use tokio_util::sync::CancellationToken;

async fn run_sampler(recorder: InfrastructureMetricsRecorder) {
    let stop = CancellationToken::new();
    if let Some(task) = recorder.system_metrics_updater_task(stop.clone()) {
        let worker = tokio::spawn(task);
        // 实际服务在 shutdown hook 中取消,并等待 worker 退出。
        stop.cancel();
        let _result = worker.await;
    }
}

每个实例只启动一次 updater。它立即开始首轮采样,之后每 15s 采样;错过 tick 时跳过补偿,不堆积采样任务。系统刷新在 blocking pool 中进行,Noop 不返回 updater。OTLP diagnostics 在采样时同步到通用指标,因此通过 metrics 看到的诊断可能落后一个采样周期;直接 snapshot() 可读取本地最新计数。

错误分类与排查 ​

MetricError常见原因与处理边界
InvalidConfiguration / Initialization启动配置或 exporter 初始化失败;产品映射为启动错误
InvalidDescriptor / DuplicateRegistration / WrongKindschema 或 typed handle 使用错误;启动/测试时修正
InstrumentLimit / SeriesLimit / InvalidLabels预算或标签设计错误;检查有限分类及系列组合
InvalidValue非有限/负 histogram、无效 gauge、counter 累计溢出
Unavailable当前实例没有所需 exporter,例如 OTLP-only 被用于 pull
Busy / Timeoutgather 或生命周期额度占满、等待超时;避免无限重试
Export编码、大小或 reader 导出失败;检查预算及本地诊断
Closed / Shutdown关闭后记录或 SDK 关闭失败;检查 producer 退出顺序

热路径 convenience API 有意丢弃该次观测错误,不应让指标失败改变业务事务结果。要求检查数值、预算或关闭合同的测试与控制边界使用 try_*。库不决定 exporter 故障是否降低 readiness,产品应明确该策略。

API 迁移 ​

旧接口替换
全局 init_configured_or_noop / prometheus::init_metricsMetricsProvider::builder() + 显式 reader + build()
PrometheusMetricsRecorderInfrastructureMetricsRecorder::new(provider)
ProductMetrics::register()ProductMetrics::register(&provider)
prometheus::Product* handles/errorscrate 根的 ProductCounter/Gauge/Histogram、MetricError
全局 export_metrics注入 provider 的 exposition resource
全局 health report注入的 recorder record_health

旧全局实现直接删除,不保留平行 registry 或双写。AsterDrive 已合并实例级消费迁移;准确 consumer/provider revision、历史验证范围和未核验的发行状态见消费者证据索引。其他外部产品需要按本表升级,不能将 Forge 库内通过当作已经接入所有消费者。

验证入口 ​

bash
cargo test -p aster_forge_metrics --no-default-features
cargo test -p aster_forge_metrics --features backend-prometheus
cargo test -p aster_forge_metrics --features otlp
cargo test -p aster_forge_metrics --all-features
cargo run --release -p aster_forge_metrics --features backend-prometheus --example recording_cost

测试覆盖实例隔离、整批注册、schema、数值/基数、并发记录、Noop、关闭、真实本地 OTLP receiver 和双导出;微基准只测预热后记录成本,不代表产品端到端吞吐损失。

参考消费者为 AsterDrive crates/aster_drive_metrics 和 bundled Actix template;产品 upload、quota、blob cleanup 和 remote 语义不进入 Forge。

当前消费版本与后续 traces 的边界统一见消费者证据索引。

Shared crates for Aster services.