Rust OpenTelemetry style for Maple: opentelemetry + opentelemetry_sdk + opentelemetry-otlp HTTP exporter, tracing-opentelemetry bridge for the tracing crate, inline endpoint + ingest key, semconv resource attributes.
66
78%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
High
Do not use without reviewing
Fix and improve this skill with Tessl
tessl review fix ./skills/maple-rust-style/SKILL.mdUse the official opentelemetry + opentelemetry_sdk crates with opentelemetry-otlp (HTTP exporter, not gRPC). Bridge the tracing crate: tracing-opentelemetry turns tracing spans into OTel spans, and opentelemetry-appender-tracing turns info! / error! events into OTLP log records.
[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"Keep the opentelemetry* crates on one minor version, and pair tracing-opentelemetry with the release built for it (0.33 for 0.32). The builder-default batch processors export from their own thread, so use the blocking reqwest client. The async reqwest-client panics there with "there is no reactor running". reqwest-rustls is required for HTTPS: opentelemetry-otlp builds reqwest without default TLS.
Inline the endpoint and ingest key. The key is a project-scoped, write-only token (shaped like a 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))
}Call from main and shut down on exit:
#[tokio::main]
async fn main() {
let (tracer_provider, logger_provider, meter_provider) =
telemetry::init().expect("telemetry init");
// app run …
let _ = tracer_provider.shutdown();
let _ = logger_provider.shutdown();
let _ = meter_provider.shutdown();
}tracingBridging tracing keeps existing instrumentation unchanged. Use #[tracing::instrument] on bounded async operations:
#[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(())
}Add err to #[instrument]. An Err return then emits an event with an error field, which tracing-opentelemetry records as an exception event and span status Error (both on by default).
If the project already uses tracing with a Honeycomb, Datadog, or Jaeger layer, leave it in place and add Maple's layers alongside. Do not strip the existing exporter unless the user asks.
1a03fa7
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.