Skip to main content

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:

  1. Core SDKopentelemetry provides the API and basic implementation.
  2. Exporters — specialized crates that send data to backends (e.g., opentelemetry-prometheus, opentelemetry-jaeger).
  3. Integrations — optional crates that bridge tracing or tokio to 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:

  • opentelemetry with rt-tokio feature ensures metrics use the Tokio runtime for background batch exports.
  • opentelemetry-prometheus lets you export metrics in Prometheus text format; it runs an HTTP scrape endpoint.
  • opentelemetry-jaeger exports distributed traces to Jaeger; it batches spans and sends them to a Jaeger agent or collector.
  • tracing and tracing-opentelemetry are a matched pair: you instrument using tracing macros, and tracing-opentelemetry pipes those events into OpenTelemetry traces.
  • tracing-subscriber configures how tracing outputs 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:

  1. Initializes Prometheus: PrometheusBuilder::new().install_simple() configures Prometheus metric collection and launches an HTTP server on :9090/metrics.
  2. Initializes Jaeger: new_pipeline().install_simple() sets up trace export to Jaeger's default agent endpoint (localhost:6831 via UDP).
  3. Wires tracing to OpenTelemetry: The OpenTelemetryLayer makes every tracing event (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, and opentelemetry-jaeger to your Cargo.toml with tokio runtime features.
  • Initialize a Prometheus exporter with PrometheusBuilder to enable metric scraping on :9090/metrics.
  • Initialize a Jaeger exporter with new_pipeline() to export traces to a local or remote Jaeger agent.
  • Wire tracing events to OpenTelemetry using OpenTelemetryLayer for 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.

Further Reading