Setup and first signal
At a glance
- What it does: builds a tracer provider with the stdout exporter and a
service.nameresource, sets it as the global provider, records one span with two attributes and one event, then shuts the provider down. - Crates and versions:
opentelemetry0.33.1 (API),opentelemetry_sdk0.33.1 (SDK),opentelemetry-stdout0.33.1 (exporter),tokio1.54.1 (async runtime). Rust edition 2024. - Run command (from the
examplesfolder):cargo run -p first-signal - Expected last line:
ok: first signal exported
Get the examples
Download examples.zip from the site root and unpack it, or create a new folder with the Cargo.toml and src/main.rs shown below. After unpacking, run the commands from the examples folder.
Dependencies
[package]
name = "first-signal"
edition = "2024"
publish = false
[dependencies]
opentelemetry = "0.33.1"
opentelemetry_sdk = { version = "0.33.1", features = ["rt-tokio"] }
opentelemetry-stdout = "0.33.1"
tokio = { version = "1.54.1", features = ["full"] }
Code
use opentelemetry::global;
use opentelemetry::trace::{TraceContextExt, Tracer};
use opentelemetry_sdk::Resource;
use opentelemetry_sdk::trace::SdkTracerProvider;
use opentelemetry_stdout::SpanExporter;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let exporter = SpanExporter::default();
let provider = SdkTracerProvider::builder()
.with_simple_exporter(exporter)
.with_resource(
Resource::builder()
.with_service_name("first-signal")
.build(),
)
.build();
global::set_tracer_provider(provider.clone());
let tracer = global::tracer("first-signal");
tracer
.in_span("first-span", |cx| {
let span = cx.span();
span.set_attribute(opentelemetry::KeyValue::new("example.kind", "hello"));
span.set_attribute(opentelemetry::KeyValue::new("example.count", 1));
span.add_event(
"starting",
vec![opentelemetry::KeyValue::new("event.source", "main")],
);
});
provider.shutdown()?;
println!("ok: first signal exported");
Ok(())
}
Run it
$ cargo run -p first-signal
...
ok: first signal exported
The stdout exporter prints one JSON line per span while the program runs. The last line the program prints is ok: first signal exported.
How it works
OpenTelemetry for Rust comes in three kinds of package (all 0.33.x here):
- The API crate,
opentelemetry. It defines the types your application uses:Tracer,Span,KeyValueand the global functions. Code against the API alone stays easy to test. - The SDK crate,
opentelemetry_sdk. It implements the API: providers that own the work of processing and exporting, and the samplers and processors behind them. Only the binary at the edge of your application touches it. - The exporter crates, one per destination.
opentelemetry-stdoutwrites spans to standard output;opentelemetry-otlpsends them over gRPC or HTTP.
The example then goes through these steps:
- Build a resource. A resource describes who produced the telemetry.
Resource::builder().with_service_name("first-signal")setsservice.name, the one field most backends group by. Version note:Resource::builder()is the 0.33.x form; older versions hadResource::new. - Build an exporter.
SpanExporter::default()fromopentelemetry-stdoutgives an exporter that writes spans to standard output. It is for learning and debugging, not for production. - Build the provider.
SdkTracerProvider::builder()composes it:with_simple_exporter(exporter)attaches the exporter,with_resourceattaches the resource. The simple exporter writes each ended span immediately, which is the easiest thing to watch. The batch exporter,with_batch_exporter, queues spans and writes them in the background; it is what you want once output volume matters. - Install it globally.
global::set_tracer_provider(provider)makes everyglobal::tracercall in the process use this provider. The global provider can only be set once; later calls are ignored. - Get a tracer and make a span. A tracer creates spans; a span is one timed operation with a name, attributes and events.
global::tracer("first-signal")names the instrumentation library.in_spanruns the closure inside a span, ending the span when the closure returns. - Attach data. Attributes are key-value pairs that describe the span (
example.kind,example.count). An event is a timestamped note inside the span (starting), itself with attributes. - Shut down.
provider.shutdown()flushes and exports anything still in flight and stops accepting new spans. Call it before your program exits so nothing is lost. It is not called byDrop, so a program that skips it can silently drop spans. The example useswith_simple_exporter, which writes synchronously, so the span is on stdout beforeshutdownreturns.
Pitfalls
- Forgetting
shutdown. The provider does not export on drop. Withoutprovider.shutdown()before exit, buffered spans vanish. With the batch exporter this is the default failure mode: the program exits while spans are still in the queue. - Dropping the provider early.
global::set_tracer_providertakes ownership of a clone; if your originalSdkTracerProvidervalue is dropped and you keep using the global one, shutdown happens twice or never. Keep one owner of the provider and shut it down deliberately. - Setting the global provider twice. The second call is ignored and the first provider stays installed, so your new exporter never sees anything.
- Using the stdout exporter in production. It writes every span to the console. Fine for this chapter; in a service you would send spans to a collector with
opentelemetry-otlpinstead. - Building a resource by hand with an old API. In 0.33.x use
Resource::builder(). Older snippets withResource::new(vec![...])no longer compile. - Confusing the API with the SDK. Library code should depend on
opentelemetryonly and take aTracerfrom its caller; the SDK belongs in the binary. Pulling the SDK into a library forces a provider on everyone who links it. - Looking for the span on stdout before the closure ends. The simple exporter writes a span when it ends, not while it is open. If the program seems to hang after printing nothing, it is waiting on the runtime, not on telemetry.