Traces
At a glance
- What it does: creates nested spans with
tracingand exports them to stdout. - Crates and versions:
opentelemetry0.33.1,opentelemetry_sdk0.33.1,opentelemetry-stdout0.33.1,tracing-opentelemetry0.34.0,tracing0.1.44,tracing-subscriber0.3.23, andtokio1.54.1. - Run command:
cargo run -p traces - Expected last line:
ok: traces exported
Dependencies
[package]
name = "traces"
version = "0.1.0"
edition = "2024"
publish = false
[dependencies]
opentelemetry = { version = "0.33.1" }
opentelemetry_sdk = { version = "0.33.1", features = ["trace"] }
opentelemetry-stdout = { version = "0.33.1", features = ["trace"] }
tokio = { version = "1.54.1", features = ["macros", "rt-multi-thread"] }
tracing = { version = "0.1.44", features = ["attributes"] }
tracing-opentelemetry = "0.34.0"
tracing-subscriber = { version = "0.3.23", features = ["registry"] }
Code
use std::io::{self, Write};
use opentelemetry::trace::TracerProvider;
use opentelemetry_sdk::trace::SdkTracerProvider;
use tracing::Instrument;
use tracing_subscriber::prelude::*;
#[tracing::instrument(fields(error = tracing::field::Empty))]
fn process_order(order_id: u64) {
let span = tracing::info_span!("validate_order", item_count = 2_u64);
let _entered = span.enter();
let error = "missing item";
span.record("error", error);
tracing::error!(order_id, error, "order validation failed");
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let exporter = opentelemetry_stdout::SpanExporter::default();
let provider = SdkTracerProvider::builder()
.with_simple_exporter(exporter)
.build();
let tracer = provider.tracer("traces-example");
let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
let subscriber_result = tracing_subscriber::registry()
.with(telemetry)
.try_init();
if let Err(error) = subscriber_result {
provider.shutdown()?;
return Err(error.into());
}
process_order(42);
let async_span = tracing::info_span!("async_work", job = "demo");
let task = tokio::spawn(
async {
tracing::info!("work completed");
}
.instrument(async_span),
);
task.await?;
provider.shutdown()?;
let mut stdout = io::stdout().lock();
stdout.write_all(b"ok: traces exported\n")?;
Ok(())
}
Run it
Run:
cargo run -p traces
The stdout exporter writes span data before the final line. The final line is:
ok: traces exported
How it works
The SDK tracer provider owns the span exporter and sends completed spans to stdout.
The subscriber combines normal tracing subscribers with the OpenTelemetry layer. The layer converts tracing spans and events into OpenTelemetry spans.
#[instrument] creates a span for process_order. The validate_order span is entered inside it, so it becomes a child span.
Fields on a span are attributes. Events add timestamped records to the current span. The example records an error field and emits an error event.
A failed operation should also set an OpenTelemetry error status when the application has a direct status API available. An error event alone is useful evidence, but it is not the same as a status.
The current span is available through tracing context. Use an instrumentation helper such as Instrument when spawning an async task so the task keeps its parent context. Thread hand-offs need the equivalent context propagation step.
The provider is shut down after the work completes. This flushes the exporter before the process exits.
Pitfalls
- Initializing the subscriber more than once can fail, so initialize it in one place.
- Creating a span does not make it a child until it is entered or attached to a context.
- Recording an error as an event is not the same as recording it as a span field or setting status.
- Spawning an async task without instrumenting it loses the intended parent relationship.
- Exiting without shutting down the provider can lose buffered spans.
- Keep exporter configuration separate from application code when changing from stdout to OTLP.