SuperAPI AI 工具集

maple-rust-style

部署与运维

查看项目

更新于 10-16 10:31

技能介绍

用途

Maple 的 Rust OpenTelemetry 接入风格。目标是让 Rust 服务把 trace、log、metrics 通过 OTLP HTTP 协议发送到 Maple 的 ingest 端点。

主要能力

  • 使用官方 opentelemetry + opentelemetry_sdk crate,配合 opentelemetry-otlp 导出(HTTP exporter,不是 gRPC)。
  • 桥接 tracing crate:tracing-opentelemetry 把 tracing span 转成 OTel span;opentelemetry-appender-tracing 把 info! / error! 事件转成 OTLP log records。
  • 同时导出 traces、logs、metrics 三类信号。
  • 提供资源属性设置,包括 service name、deployment.environment.name、vcs.repository.url.full、vcs.ref.head.revision。

使用方式

依赖(Cargo.toml)

[dependencies]
opentelemetry = "0.32"
opentelemetry_sdk = { version = "0.32", features = ["trace", "logs", "metrics"] }
opentelemetry-otlp = { version = "0.32", features = ["http-proto", "reqwest-blocking-client", "reqwest-rustls", "trace", "logs", "metrics"] }
opentelemetry-appender-tracing = "0.32"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-opentelemetry = "0.33"

版本约束说明:

  • opentelemetry* 系列 crate 保持同一 minor 版本。
  • tracing-opentelemetry 需与为其构建的版本配对(0.32 对应 0.33)。
  • builder 默认的 batch processors 从自己的线程导出,因此必须使用 blocking reqwest client;async reqwest-client 在该场景会 panic,报错 "there is no reactor running"。
  • HTTPS 需要 reqwest-rustls:opentelemetry-otlp 构建 reqwest 时不带默认 TLS。

Bootstrap

端点与 ingest key 采用内联方式。key 是项目级、只写的 token(形如 Sentry DSN)。代码示例:

use opentelemetry::{global, trace::TracerProvider as _, KeyValue};
use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
use opentelemetry_otlp::{
    ExporterBuildError, LogExporter, MetricExporter, Protocol, SpanExporter, WithExportConfig,
    WithHttpConfig,
};
use opentelemetry_sdk::{
    logs::SdkLoggerProvider, metrics::SdkMeterProvider, trace::SdkTracerProvider, Resource,
};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

const MAPLE_ENDPOINT: &str = "https://ingest.maple.dev"; // EU: https://ingest.eu.maple.dev
const MAPLE_KEY: &str = "MAPLE_TEST"; // public ingest key (maple_pk_…), or MAPLE_TEST until the user has one

pub fn init() -> Result<(SdkTracerProvider, SdkLoggerProvider, SdkMeterProvider), ExporterBuildError> {
    let auth = format!("Bearer {MAPLE_KEY}");
    let mut headers = std::collections::HashMap::new();
    headers.insert("authorization".to_string(), auth);

    let resource = Resource::builder()
        .with_service_name("orders-api")
        .with_attributes([
            KeyValue::new("deployment.environment.name", std::env::var("DEPLOYMENT_ENV").unwrap_or_else(|_| "development".into())),
            KeyValue::new("vcs.repository.url.full", "https://github.com/acme/orders-api"),
            KeyValue::new("vcs.ref.head.revision", std::env::var("GITHUB_SHA").unwrap_or_default()),
        ])
        .build();

    let trace_exporter = SpanExporter::builder()
        .with_http()
        .with_endpoint(format!("{MAPLE_ENDPOINT}/v1/traces"))
        .with_headers(headers.clone())
        .with_protocol(Protocol::HttpBinary)
        .build()?;
    let tracer_provider = SdkTracerProvider::builder()
        .with_batch_exporter(trace_exporter)
        .with_resource(resource.clone())
        .build();
    global::set_tracer_provider(tracer_provider.clone());

    let log_exporter = LogExporter::builder()
        .with_http()
        .with_endpoint(format!("{MAPLE_ENDPOINT}/v1/logs"))
        .with_headers(headers.clone())
        .with_protocol(Protocol::HttpBinary)
        .build()?;
    let logger_provider = SdkLoggerProvider::builder()
        .with_batch_exporter(log_exporter)
        .with_resource(resource.clone())
        .build();

    let metric_exporter = MetricExporter::builder()
        .with_http()
        .with_endpoint(format!("{MAPLE_ENDPOINT}/v1/metrics"))
        .with_headers(headers)
        .with_protocol(Protocol::HttpBinary)
        .build()?;
    let meter_provider = SdkMeterProvider::builder()
        .with_periodic_exporter(metric_exporter)
        .with_resource(resource)
        .build();
    global::set_meter_provider(meter_provider.clone());

    let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer_provider.tracer("orders.api"));
    let otel_log_layer = OpenTelemetryTracingBridge::new(&logger_provider);

    tracing_subscriber::registry()
        .with(tracing_subscriber::EnvFilter::from_default_env())
        .with(tracing_subscriber::fmt::layer())
        .with(otel_layer)
        .with(otel_log_layer)
        .init();

    Ok((tracer_provider, logger_provider, meter_provider))
}

在 main 中调用并在退出时关闭:

#[tokio::main]
async fn main() {
    // A telemetry setup error is logged, never fatal: the app runs without export.
    let providers = telemetry::init()
        .inspect_err(|err| eprintln!("telemetry disabled: {err}"))
        .ok();

    // app run …

    if let Some((tracer_provider, logger_provider, meter_provider)) = providers {
        let _ = tracer_provider.shutdown();
        let _ = logger_provider.shutdown();
        let _ = meter_provider.shutdown();
    }
}

遥测初始化失败只记录日志,不视为致命错误:应用在没有导出的情况下继续运行。

通过 tracing 定义有界业务 span

桥接 tracing 可保持既有埋点不变。对有界异步操作使用 #[tracing::instrument]:

#[tracing::instrument(name = "order.submit", skip_all, err, fields(order.id = %order_id))]
async fn submit_order(order_id: &str) -> Result<(), Error> {
    charge_order(order_id).await?;
    Ok(())
}

在 #[instrument] 中加上 err。返回 Err 时会发出带 error 字段的事件,tracing-opentelemetry 会将其记录为 exception event 以及 span 状态 Error(二者默认开启)。

共存

如果项目已在使用 tracing 并接有 Honeycomb、Datadog 或 Jaeger 的 layer,保持其原样,把 Maple 的 layer 并行加上即可。除非用户要求,不要移除已有的导出器。

适用场景

  • Rust 服务需要接入 Maple 的 trace、log、metrics 上报。
  • 已有 tracing 埋点的项目,希望在不改动现有 instrumentation 的情况下增加 OTel 导出。
  • 需要 OTLP HTTP(非 gRPC)导出并且要求 HTTPS 支持的场景。

限制

  • 导出协议为 HTTP(Protocol::HttpBinary),非 gRPC。
  • 必须使用 blocking reqwest client,async client 会在 batch 导出线程中 panic。
  • HTTPS 必须启用 reqwest-rustls。
  • opentelemetry* crate 需保持同一 minor 版本,tracing-opentelemetry 需配对其对应版本(0.32 对应 0.33)。
  • 文档中除示例中的服务名、仓库地址与端点外,暂未提供其他平台、收费或价格信息。