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

Configuration

At a glance

  • Reads standard OpenTelemetry environment variables, reports the effective service name and sampler, and attempts to export ten spans.
  • Version references: opentelemetry 0.33.1, opentelemetry_sdk 0.33.1, opentelemetry-otlp 0.33.1, opentelemetry-stdout 0.33.1, opentelemetry-appender-tracing 0.33.1, tracing-opentelemetry 0.34.0, tracing 0.1.44, tracing-subscriber 0.3.23, tokio 1.54.1, rusqlite 0.42.0, and libsql 0.10.0. The example uses only the dependencies listed in its manifest.
  • Run command: cargo run -p env-config.
  • Expected last line: ok: emitted 10 spans

Dependencies

The example uses Rust edition 2024.

[package]
name = "env-config"
version = "0.1.0"
edition = "2024"
publish = false

[dependencies]
opentelemetry = "=0.33.1"
opentelemetry_sdk = "=0.33.1"
opentelemetry-otlp = { version = "=0.33.1", default-features = false, features = ["trace", "grpc-tonic", "http-proto", "reqwest-blocking-client"] }
tokio = { version = "=1.54.1", features = ["rt-multi-thread", "macros"] }

Code

use std::error::Error;
use std::time::Duration;

use opentelemetry::trace::{Span, Tracer, TracerProvider};
use opentelemetry::KeyValue;
use opentelemetry_otlp::{SpanExporter, WithExportConfig};
use opentelemetry_sdk::trace::Sampler;
use opentelemetry_sdk::{Resource, trace::SdkTracerProvider};

fn setting(name: &str, default: &str) -> String {
    match std::env::var(name) {
        Ok(value) => value,
        Err(_error) => default.to_owned(),
    }
}

fn resource_attributes() -> Vec<KeyValue> {
    setting("OTEL_RESOURCE_ATTRIBUTES", "")
        .split(',')
        .filter_map(|pair| {
            let (key, value) = pair.split_once('=')?;
            let key = key.trim();
            if key.is_empty() {
                return None;
            }
            Some(KeyValue::new(key.to_owned(), value.trim().to_owned()))
        })
        .collect()
}

fn ratio_argument() -> f64 {
    match setting("OTEL_TRACES_SAMPLER_ARG", "1.0").parse::<f64>() {
        Ok(value) if (0.0..=1.0).contains(&value) => value,
        Ok(_) | Err(_) => 1.0,
    }
}

fn resolve_sampler() -> (Sampler, String) {
    let name = setting("OTEL_TRACES_SAMPLER", "parentbased_always_on");
    let ratio = ratio_argument();

    match name.as_str() {
        "always_on" => (Sampler::AlwaysOn, "always_on".to_owned()),
        "always_off" => (Sampler::AlwaysOff, "always_off".to_owned()),
        "traceidratio" => (
            Sampler::TraceIdRatioBased(ratio),
            "traceidratio".to_owned(),
        ),
        "parentbased_always_off" => (
            Sampler::ParentBased(Box::new(Sampler::AlwaysOff)),
            "parentbased_always_off".to_owned(),
        ),
        "parentbased_traceidratio" => (
            Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(ratio))),
            "parentbased_traceidratio".to_owned(),
        ),
        "parentbased_always_on" => (
            Sampler::ParentBased(Box::new(Sampler::AlwaysOn)),
            "parentbased_always_on".to_owned(),
        ),
        _ => (
            Sampler::ParentBased(Box::new(Sampler::AlwaysOn)),
            "parentbased_always_on".to_owned(),
        ),
    }
}

// The OTLP gRPC exporter requires the provider to be created inside a Tokio
// runtime, and the blocking HTTP client is driven by the batch processor's own
// background thread.
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let service_name = setting("OTEL_SERVICE_NAME", "unknown_service");
    let protocol = setting("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc");
    let default_endpoint = if protocol == "http/protobuf" {
        "http://localhost:4318"
    } else {
        "http://localhost:4317"
    };
    let endpoint = setting("OTEL_EXPORTER_OTLP_ENDPOINT", default_endpoint);
    // Keep an unavailable collector from holding the example open for the default 10 seconds.
    let timeout_ms = match setting("OTEL_EXPORTER_OTLP_TIMEOUT", "2000").parse::<u64>() {
        Ok(value) => value,
        Err(_error) => 2_000,
    };
    let (sampler, sampler_name) = resolve_sampler();

    println!("service name: {service_name}");
    println!("sampler: {sampler_name}");

    let exporter_builder = SpanExporter::builder();
    let exporter = match protocol.as_str() {
        "grpc" => exporter_builder
            .with_tonic()
            .with_endpoint(endpoint)
            .with_timeout(Duration::from_millis(timeout_ms))
            .build()?,
        "http/protobuf" => exporter_builder
            .with_http()
            .with_endpoint(endpoint)
            .with_timeout(Duration::from_millis(timeout_ms))
            .build()?,
        _ => {
            return Err(format!("unsupported OTLP protocol: {protocol}").into());
        }
    };

    let resource = Resource::builder()
        .with_service_name(service_name)
        .with_attributes(resource_attributes())
        .build();
    let provider = SdkTracerProvider::builder()
        .with_batch_exporter(exporter)
        .with_sampler(sampler)
        .with_resource(resource)
        .build();
    let tracer = provider.tracer("env-config");

    for index in 0..10 {
        let mut span = tracer.start(format!("example-span-{index}"));
        span.end();
    }

    drop(tracer);
    let shutdown_result = provider.shutdown_with_timeout(Duration::from_secs(2));
    drop(provider);

    if let Err(error) = shutdown_result {
        eprintln!("export or shutdown failed: {error}");
        eprintln!("ok: emitted 10 spans");
    } else {
        println!("ok: emitted 10 spans");
    }
    Ok(())
}

Run it

Run the command:

cargo run -p env-config

Have an OTLP receiver listening at the configured endpoint. With no overrides, the example uses the gRPC endpoint http://localhost:4317. The program prints the effective service name and sampler, then the expected final line:

ok: emitted 10 spans

How it works

The example reads the service name, resource attributes, exporter settings, and sampler settings from the process environment. The OpenTelemetry Rust SDK 0.33.1 does not resolve these environment variables for this example; the program reads and applies them directly.

OTEL_SERVICE_NAME sets the resource service name. OTEL_RESOURCE_ATTRIBUTES adds comma-separated key-value pairs to the resource. The exporter uses the endpoint, protocol, headers, and timeout selected from the environment. The program supports grpc and http/protobuf; the HTTP protocol’s default endpoint is http://localhost:4318, while gRPC defaults to http://localhost:4317.

The sampler is selected before the tracer provider is built. The example creates ten spans, shuts down the provider so its batch exporter can flush, and prints the final line.

Head sampling makes the sampling decision when a trace starts. always_on samples every trace and always_off samples none. traceidratio samples according to a trace ID ratio. Parent-based samplers follow the parent decision when a parent exists and use their configured root sampler otherwise.

Environment variables

NameMeaningDefaultExample
OTEL_SERVICE_NAMEResource service nameunknown_servicecheckout
OTEL_RESOURCE_ATTRIBUTESComma-separated resource key-value pairsEmptydeployment.environment=dev,region=west
OTEL_EXPORTER_OTLP_ENDPOINTOTLP endpoint; the default depends on protocolhttp://localhost:4317 for gRPC; http://localhost:4318 for HTTPhttp://localhost:4317
OTEL_EXPORTER_OTLP_PROTOCOLExport protocolgrpchttp/protobuf
OTEL_EXPORTER_OTLP_HEADERSComma-separated exporter headersEmptykey=value
OTEL_EXPORTER_OTLP_TIMEOUTExport timeout in milliseconds20005000
OTEL_TRACES_SAMPLERHead samplerparentbased_always_ontraceidratio
OTEL_TRACES_SAMPLER_ARGRatio for ratio-based samplers1.00.25

The sampler names supported by this example are always_on, always_off, traceidratio, parentbased_always_on, parentbased_always_off, and parentbased_traceidratio. The ratio argument must be between zero and one; otherwise the example uses 1.0.

Pitfalls

  • A syntactically valid endpoint is not proof that a receiver is running. The provider’s shutdown can return an export error if it cannot reach the receiver.
  • The endpoint default changes with the protocol. Set both variables explicitly when switching between gRPC and HTTP.
  • Resource attributes and headers are comma-separated in this example. Values containing commas are not parsed as a single value.
  • A ratio sampler’s argument is not a percentage: use 0.25 for a 25% ratio.