Setting Up OpenTelemetry in Rust: Dependencies and Init
Setting up OpenTelemetry in your Rust project takes five minutes: add three crates to Cargo.toml, initialize a meter and tracer in your main.rs, and start emitting data. This article walks you through every step, from dependency selection to your first working meter.
The key insight is that OpenTelemetry separates concerns: the SDK (measurement collection), exporters (data transmission), and backends (storage/visualization) are pluggable. You decide your backend (Prometheus? Jaeger?) at initialization time, not at compile time. This allows the same code to emit to different systems without recompilation.
Choosing Your Dependencies
OpenTelemetry in Rust requires three categories of crates:
- Core SDK —
opentelemetryprovides the API and basic implementation. - Exporters — specialized crates that send data to backends (e.g.,
opentelemetry-prometheus,opentelemetry-jaeger). - Integrations — optional crates that bridge
tracingortokioto OpenTelemetry (e.g.,tracing-opentelemetry,opentelemetry-jaeger-propagator).
For a typical Rust service, you will add:
[dependencies]
opentelemetry = { version = "0.23", features = ["rt-tokio"] }
opentelemetry-prometheus = "0.16"
opentelemetry-jaeger = { version = "0.22", features = ["rt-tokio"] }
tracing = "0.1"
tracing-opentelemetry = "0.25"
tracing-subscriber = { version = "0.3", features = ["fmt", "json"] }
tokio = { version = "1", features = ["full"] }
Breaking this down:
opentelemetrywithrt-tokiofeature ensures metrics use the Tokio runtime for background batch exports.opentelemetry-prometheuslets you export metrics in Prometheus text format; it runs an HTTP scrape endpoint.opentelemetry-jaegerexports distributed traces to Jaeger; it batches spans and sends them to a Jaeger agent or collector.tracingandtracing-opentelemetryare a matched pair: you instrument usingtracingmacros, andtracing-opentelemetrypipes those events into OpenTelemetry traces.tracing-subscriberconfigures howtracingoutputs data (to stdout, JSON, filtering levels).
As of 2026, these crates are all stable and compatible with Rust 1.70+. Check crates.io for the latest versions in your project.
Initializing a Meter and Tracer
Create a new Rust binary:
cargo new my_observable_app --bin
cd my_observable_app
Add the dependencies above to Cargo.toml. Then, in src/main.rs, initialize OpenTelemetry:
use opentelemetry::global;
use opentelemetry_prometheus::PrometheusBuilder;
use opentelemetry_jaeger::new_pipeline;
use tracing::{info, instrument};
use tracing_opentelemetry::OpenTelemetryLayer;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::fmt;
use std::sync::Arc;
#[tokio::main]
async fn main() {
// Initialize Prometheus exporter for metrics
let prometheus_exporter = PrometheusBuilder::new()
.install_simple()
.expect("Prometheus exporter failed");
// Initialize Jaeger exporter for traces
let jaeger_tracer = new_pipeline()
.install_simple()
.expect("Jaeger exporter failed");
// Create a tracing subscriber that combines fmt output and OpenTelemetry
let otel_layer = OpenTelemetryLayer::new(jaeger_tracer);
tracing_subscriber::registry()
.with(fmt::layer())
.with(otel_layer)
.init();
info!("Application started with observability");
// Now you can emit metrics and traces
let meter = global::meter("my_app");
let counter = meter.u64_counter("requests_total")
.with_description("Total HTTP requests")
.init();
// Increment counter when a request arrives
counter.add(1, &[]);
info!("Counter incremented");
// Start the Prometheus scrape endpoint on :9090
// (See next section for full example)
}
This code does three things:
- Initializes Prometheus:
PrometheusBuilder::new().install_simple()configures Prometheus metric collection and launches an HTTP server on:9090/metrics. - Initializes Jaeger:
new_pipeline().install_simple()sets up trace export to Jaeger's default agent endpoint (localhost:6831via UDP). - Wires tracing to OpenTelemetry: The
OpenTelemetryLayermakes everytracingevent (log, span) flow into OpenTelemetry traces.
When you run this, metrics are scraped from http://localhost:9090/metrics (Prometheus format), and traces are sent to a local Jaeger agent.
Exposing Metrics via HTTP
By default, opentelemetry-prometheus exposes metrics on :9090/metrics. If you want to change the port or run your application's own HTTP server on :8080, you must manually wire the Prometheus scraper. Here is a complete example using axum:
use axum::{Router, routing::get};
use opentelemetry_prometheus::PrometheusBuilder;
use tracing::info;
use std::net::SocketAddr;
#[tokio::main]
async fn main() {
// Initialize Prometheus
let prometheus_exporter = PrometheusBuilder::new()
.install_simple()
.expect("Prometheus exporter failed");
// Create a metrics endpoint
let app = Router::new()
.route("/metrics", get(metrics_handler));
let addr = SocketAddr::from(([127, 0, 0, 1], 9090));
info!("Metrics server listening on {}", addr);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn metrics_handler() -> String {
// This returns the current state of all metrics
prometheus_exporter.render()
}
Wait — the Prometheus builder's .install_simple() already starts a background HTTP server. If you want full control, use .build() instead and manually serve the /metrics endpoint:
let (prometheus_exporter, _) = PrometheusBuilder::new()
.build()
.expect("Prometheus builder failed");
// Serve via your HTTP framework
let metrics_body = prometheus_exporter.render();
The prometheus_exporter.render() method returns the current metrics in Prometheus text format (lines like requests_total 42). You expose that on any endpoint you choose.
Configuring Jaeger Export (Advanced)
By default, .install_simple() sends traces to localhost:6831 (Jaeger's UDP agent endpoint). For production, configure the collector endpoint:
use opentelemetry_jaeger::new_pipeline;
use opentelemetry::sdk::Resource;
use opentelemetry::KeyValue;
let jaeger_tracer = new_pipeline()
.install_batch(opentelemetry::runtime::TokioCurrentThread)
.expect("Jaeger tracer failed");
This uses batch export (collects spans in a buffer, flushes periodically) instead of simple export (immediate, blocking). Batch export is much cheaper for high-volume applications.
You can also set service name and tags:
let resource = Resource::new(vec![
KeyValue::new("service.name", "my_observable_app"),
KeyValue::new("service.version", "1.0.0"),
]);
let jaeger_tracer = new_pipeline()
.install_batch(opentelemetry::runtime::TokioCurrentThread)
.expect("Jaeger tracer failed");
This makes it easy to identify your service in Jaeger's UI.
Verifying Your Setup
After initializing, start your app:
cargo run
Check that Prometheus metrics are emitted:
curl http://localhost:9090/metrics
You should see output like:
# HELP requests_total Total HTTP requests
# TYPE requests_total counter
requests_total 1
For traces, verify that your Jaeger agent is running (or start it locally with Docker):
docker run -p 6831:6831/udp -p 16686:16686 jaegertracing/all-in-one
Then visit http://localhost:16686 and you should see your service name and traces appearing.
Key Takeaways
- Add
opentelemetry,opentelemetry-prometheus, andopentelemetry-jaegerto yourCargo.tomlwith tokio runtime features. - Initialize a Prometheus exporter with
PrometheusBuilderto enable metric scraping on:9090/metrics. - Initialize a Jaeger exporter with
new_pipeline()to export traces to a local or remote Jaeger agent. - Wire
tracingevents to OpenTelemetry usingOpenTelemetryLayerfor automatic trace propagation. - Use
.install_simple()for development and.install_batch()for production to minimize overhead.
Frequently Asked Questions
Can I use OpenTelemetry without tracing?
Yes. You can call OpenTelemetry APIs directly (e.g., meter.u64_counter()), but the tracing crate is more ergonomic and ubiquitous in the Rust ecosystem. Most teams use both: tracing for logs and structured data, OpenTelemetry for backends.
What port does Prometheus use by default?
:9090. If that conflicts with your application, use .build() instead of .install_simple() and manually serve the metrics on a different port via your HTTP framework.
Does initializing OpenTelemetry block my application startup?
No. Prometheus and Jaeger exporters start background threads and return immediately. However, ensure you call .shutdown() before the process exits, or traces may be lost. Most frameworks handle this for you.
How do I run Jaeger locally without Docker?
Download the Jaeger all-in-one binary from jaegertracing.io and run it directly. It listens on port 6831 (UDP, agents), 16686 (web UI), and 14268 (collector HTTP).
What is the difference between .install_simple() and .install_batch()?
.install_simple() flushes traces immediately (low latency, higher overhead). .install_batch() buffers spans and exports them in batches every few seconds (lower overhead, slight delay). Use .install_batch() in production.