Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Setup and first signal

At a glance

  • What it does: builds a tracer provider with the stdout exporter and a service.name resource, sets it as the global provider, records one span with two attributes and one event, then shuts the provider down.
  • Crates and versions: opentelemetry 0.33.1 (API), opentelemetry_sdk 0.33.1 (SDK), opentelemetry-stdout 0.33.1 (exporter), tokio 1.54.1 (async runtime). Rust edition 2024.
  • Run command (from the examples folder): 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, KeyValue and 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-stdout writes spans to standard output; opentelemetry-otlp sends them over gRPC or HTTP.

The example then goes through these steps:

  1. Build a resource. A resource describes who produced the telemetry. Resource::builder().with_service_name("first-signal") sets service.name, the one field most backends group by. Version note: Resource::builder() is the 0.33.x form; older versions had Resource::new.
  2. Build an exporter. SpanExporter::default() from opentelemetry-stdout gives an exporter that writes spans to standard output. It is for learning and debugging, not for production.
  3. Build the provider. SdkTracerProvider::builder() composes it: with_simple_exporter(exporter) attaches the exporter, with_resource attaches 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.
  4. Install it globally. global::set_tracer_provider(provider) makes every global::tracer call in the process use this provider. The global provider can only be set once; later calls are ignored.
  5. 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_span runs the closure inside a span, ending the span when the closure returns.
  6. 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.
  7. 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 by Drop, so a program that skips it can silently drop spans. The example uses with_simple_exporter, which writes synchronously, so the span is on stdout before shutdown returns.

Pitfalls

  • Forgetting shutdown. The provider does not export on drop. Without provider.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_provider takes ownership of a clone; if your original SdkTracerProvider value 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-otlp instead.
  • Building a resource by hand with an old API. In 0.33.x use Resource::builder(). Older snippets with Resource::new(vec![...]) no longer compile.
  • Confusing the API with the SDK. Library code should depend on opentelemetry only and take a Tracer from 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.