Configuration
At a glance
- Reads standard OpenTelemetry environment variables, reports the effective service name and sampler, and attempts to export ten spans.
- Version references:
opentelemetry0.33.1,opentelemetry_sdk0.33.1,opentelemetry-otlp0.33.1,opentelemetry-stdout0.33.1,opentelemetry-appender-tracing0.33.1,tracing-opentelemetry0.34.0,tracing0.1.44,tracing-subscriber0.3.23,tokio1.54.1,rusqlite0.42.0, andlibsql0.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
| Name | Meaning | Default | Example |
|---|---|---|---|
OTEL_SERVICE_NAME | Resource service name | unknown_service | checkout |
OTEL_RESOURCE_ATTRIBUTES | Comma-separated resource key-value pairs | Empty | deployment.environment=dev,region=west |
OTEL_EXPORTER_OTLP_ENDPOINT | OTLP endpoint; the default depends on protocol | http://localhost:4317 for gRPC; http://localhost:4318 for HTTP | http://localhost:4317 |
OTEL_EXPORTER_OTLP_PROTOCOL | Export protocol | grpc | http/protobuf |
OTEL_EXPORTER_OTLP_HEADERS | Comma-separated exporter headers | Empty | key=value |
OTEL_EXPORTER_OTLP_TIMEOUT | Export timeout in milliseconds | 2000 | 5000 |
OTEL_TRACES_SAMPLER | Head sampler | parentbased_always_on | traceidratio |
OTEL_TRACES_SAMPLER_ARG | Ratio for ratio-based samplers | 1.0 | 0.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.25for a 25% ratio.