From d5fdcd06856e70f9e89ba42bbae0ea53aadc2053 Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Mon, 29 Jun 2026 19:35:19 +0200 Subject: [PATCH 01/13] feat(api): add experimental env var propagators Add experimental EnvVarExtractor and EnvVarInjector helpers behind the otel_unstable feature flag, document the new API, and add an env-var-propagation example for parent/child process TraceContext propagation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- examples/env-var-propagation/Cargo.toml | 19 ++ examples/env-var-propagation/README.md | 34 ++ examples/env-var-propagation/src/main.rs | 84 +++++ opentelemetry/CHANGELOG.md | 3 + opentelemetry/Cargo.toml | 2 + opentelemetry/README.md | 7 + opentelemetry/src/lib.rs | 8 +- opentelemetry/src/propagation/env.rs | 385 +++++++++++++++++++++++ opentelemetry/src/propagation/mod.rs | 7 + 9 files changed, 547 insertions(+), 2 deletions(-) create mode 100644 examples/env-var-propagation/Cargo.toml create mode 100644 examples/env-var-propagation/README.md create mode 100644 examples/env-var-propagation/src/main.rs create mode 100644 opentelemetry/src/propagation/env.rs diff --git a/examples/env-var-propagation/Cargo.toml b/examples/env-var-propagation/Cargo.toml new file mode 100644 index 0000000000..d6e8104eac --- /dev/null +++ b/examples/env-var-propagation/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "env-var-propagation" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +rust-version = "1.75.0" +publish = false +autobenches = false + +[[bin]] +name = "env-var-propagation" +path = "src/main.rs" +doc = false +bench = false + +[dependencies] +opentelemetry = { workspace = true, features = ["trace", "otel_unstable"] } +opentelemetry_sdk = { workspace = true, features = ["trace"] } +opentelemetry-stdout = { workspace = true, features = ["trace"] } diff --git a/examples/env-var-propagation/README.md b/examples/env-var-propagation/README.md new file mode 100644 index 0000000000..be70492d72 --- /dev/null +++ b/examples/env-var-propagation/README.md @@ -0,0 +1,34 @@ +# Environment Variable Propagation Example + +This example demonstrates distributed tracing across a parent process and a +child process using environment variables as the propagation carrier. + +## What This Example Does + +1. The parent process configures the W3C `TraceContextPropagator` and starts a + root span. +2. The parent injects that span context into an `EnvVarInjector`, then passes + the resulting environment variables to a child process. +3. The child process captures its startup environment with `EnvVarExtractor`, + extracts the propagated context, and starts its own span as a child of the + parent's span. + +The example prints the parent's `TraceId` and `SpanId`, then prints the child's +`TraceId` and remote parent `SpanId` so you can see that the trace was +propagated across the process boundary. + +## Usage + +From `examples/env-var-propagation`, run: + +```shell +cargo run +``` + +The output should show: + +- the parent and child spans sharing the same `TraceId` +- the child's reported `parent_span_id` matching the parent's `span_id` + +Both processes also export their spans to stdout using the OpenTelemetry stdout +exporter. diff --git a/examples/env-var-propagation/src/main.rs b/examples/env-var-propagation/src/main.rs new file mode 100644 index 0000000000..324c606d26 --- /dev/null +++ b/examples/env-var-propagation/src/main.rs @@ -0,0 +1,84 @@ +use opentelemetry::{ + global, + propagation::{EnvVarExtractor, EnvVarInjector}, + trace::{Span, TraceContextExt, Tracer}, + Context, +}; +use opentelemetry_sdk::{propagation::TraceContextPropagator, trace::SdkTracerProvider}; +use opentelemetry_stdout::SpanExporter; +use std::{env, error::Error, io, process::Command}; + +fn init_tracer() -> SdkTracerProvider { + global::set_text_map_propagator(TraceContextPropagator::new()); + + let provider = SdkTracerProvider::builder() + .with_simple_exporter(SpanExporter::default()) + .build(); + global::set_tracer_provider(provider.clone()); + + provider +} + +fn run_parent() -> Result<(), Box> { + let tracer = global::tracer("examples/env-var-propagation/parent"); + let span = tracer.start("parent"); + let span_context = span.span_context().clone(); + let cx = Context::current_with_span(span); + + println!( + "parent trace_id={} span_id={}", + span_context.trace_id(), + span_context.span_id() + ); + + let mut child_env = EnvVarInjector::new(); + global::get_text_map_propagator(|propagator| propagator.inject_context(&cx, &mut child_env)); + + let status = Command::new(env::current_exe()?) + .arg("--child") + .envs(child_env.into_inner()) + .status()?; + + cx.span().end(); + + if status.success() { + Ok(()) + } else { + Err(io::Error::other(format!("child process failed with status {status}")).into()) + } +} + +fn run_child() -> Result<(), Box> { + let extractor = EnvVarExtractor::from_env(); + let parent_cx = global::get_text_map_propagator(|propagator| propagator.extract(&extractor)); + let remote_parent = parent_cx.span().span_context().clone(); + + if !remote_parent.is_valid() { + return Err(io::Error::other("missing propagated span context").into()); + } + + let tracer = global::tracer("examples/env-var-propagation/child"); + let mut span = tracer.start_with_context("child", &parent_cx); + let span_context = span.span_context().clone(); + + println!( + "child trace_id={} parent_span_id={}", + span_context.trace_id(), + remote_parent.span_id() + ); + + span.end(); + + Ok(()) +} + +fn main() -> Result<(), Box> { + let provider = init_tracer(); + let result = match env::args().nth(1).as_deref() { + Some("--child") => run_child(), + _ => run_parent(), + }; + + provider.shutdown()?; + result +} diff --git a/opentelemetry/CHANGELOG.md b/opentelemetry/CHANGELOG.md index 05511dac4d..3c71dc443e 100644 --- a/opentelemetry/CHANGELOG.md +++ b/opentelemetry/CHANGELOG.md @@ -2,6 +2,9 @@ ## vNext +- **Added** experimental `EnvVarExtractor` and `EnvVarInjector` helpers for + propagating OpenTelemetry context through environment variables. This API is + gated behind the `otel_unstable` feature flag. - `otel_info!`, `otel_warn!`, `otel_debug!`, and `otel_error!` macros now accept quoted-key fields (e.g. `"otel.component.type" = "value"`) for dotted attribute names. - **Added** `BoundGauge` and `BoundUpDownCounter` types (and the diff --git a/opentelemetry/Cargo.toml b/opentelemetry/Cargo.toml index 5efba0cf2a..edb80c7bcf 100644 --- a/opentelemetry/Cargo.toml +++ b/opentelemetry/Cargo.toml @@ -39,12 +39,14 @@ metrics = [] testing = ["trace"] logs = [] internal-logs = ["tracing"] +otel_unstable = [] experimental_metrics_bound_instruments = ["metrics"] [dev-dependencies] opentelemetry_sdk = { path = "../opentelemetry-sdk"} # for documentation tests criterion = { workspace = true } rand = { workspace = true, features = ["os_rng", "thread_rng"] } +temp-env = { workspace = true } tokio = { version = "1.0", features = ["full"] } [[bench]] diff --git a/opentelemetry/README.md b/opentelemetry/README.md index ad0912b19e..4da9054cd6 100644 --- a/opentelemetry/README.md +++ b/opentelemetry/README.md @@ -129,6 +129,13 @@ additional exporters and other related components as well. See [docs](https://docs.rs/opentelemetry). +Experimental environment-variable propagation helpers are available behind the +`otel_unstable` feature flag via +`opentelemetry::propagation::EnvVarExtractor` and +`opentelemetry::propagation::EnvVarInjector`. See +[`examples/env-var-propagation`](https://github.com/open-telemetry/opentelemetry-rust/tree/main/examples/env-var-propagation) +for an end-to-end parent/child process example using `TraceContextPropagator`. + ## Release Notes You can find the release notes (changelog) [here](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry/CHANGELOG.md). diff --git a/opentelemetry/src/lib.rs b/opentelemetry/src/lib.rs index 0b77c9d373..7e3d9bb080 100644 --- a/opentelemetry/src/lib.rs +++ b/opentelemetry/src/lib.rs @@ -39,7 +39,9 @@ //! which demonstrates creating spans and propagating trace context across a //! gRPC client and server. See the //! [examples](https://github.com/open-telemetry/opentelemetry-rust/tree/main/examples) -//! directory for additional integration patterns. +//! directory for additional integration patterns, including +//! [`examples/env-var-propagation`](https://github.com/open-telemetry/opentelemetry-rust/tree/main/examples/env-var-propagation) +//! for parent/child process propagation via environment variables. //! //! See the [`trace`] module docs for more information on creating and managing //! spans. @@ -173,7 +175,9 @@ //! //! //! The following feature flags enable APIs defined in OpenTelemetry specification that is in experimental phase: -//! * `otel_unstable`: Includes unstable APIs. There are no features behind this flag at the moment. +//! * `otel_unstable`: Includes unstable APIs such as +//! `opentelemetry::propagation::EnvVarExtractor` and +//! `opentelemetry::propagation::EnvVarInjector` for environment-variable context propagation. //! //! # Related Crates //! diff --git a/opentelemetry/src/propagation/env.rs b/opentelemetry/src/propagation/env.rs new file mode 100644 index 0000000000..d44924990b --- /dev/null +++ b/opentelemetry/src/propagation/env.rs @@ -0,0 +1,385 @@ +//! Experimental environment-variable propagation carriers. + +use crate::propagation::{Extractor, Injector}; +use std::{borrow::Cow, collections::HashMap, ffi::OsStr}; + +/// Experimental extractor for propagated context stored in environment variables. +/// +/// `EnvVarExtractor` captures a UTF-8 snapshot of the process environment and +/// implements [`Extractor`] with the normalization rules from the +/// OpenTelemetry environment-variable carrier specification. Keys are stored as +/// they appear in the source environment, `get()` normalizes the requested +/// propagation key before lookup, and `keys()` returns only names that are +/// already normalized. +/// +/// `from_env()` is intended for initialization-time extraction, such as child +/// process startup. Any environment variable name or value that is not valid +/// UTF-8 is ignored when building the snapshot. +/// +/// Environment variables are visible to other code running in the process and +/// may be visible to other users or processes with sufficient permissions, so +/// they are not suitable for sensitive data. +#[cfg_attr(docsrs, doc(cfg(feature = "otel_unstable")))] +#[derive(Clone, Debug, Default)] +pub struct EnvVarExtractor { + env: HashMap, +} + +impl EnvVarExtractor { + /// Creates an empty environment-variable extractor. + pub fn new() -> Self { + Self::default() + } + + /// Captures a UTF-8 snapshot of the current process environment. + /// + /// Non-UTF-8 environment variable names or values are ignored. + pub fn from_env() -> Self { + Self::from_os_iter(std::env::vars_os()) + } + + /// Builds an extractor from the provided UTF-8 environment entries. + /// + /// Entries are stored exactly as provided. `get()` still reads only the + /// normalized form of a propagation key, and `keys()` still returns only + /// already-normalized names. + pub fn from_entries(iter: I) -> Self + where + I: IntoIterator, + K: AsRef, + V: AsRef, + { + Self { + env: iter + .into_iter() + .map(|(key, value)| (key.as_ref().to_string(), value.as_ref().to_string())) + .collect(), + } + } + + fn from_os_iter(iter: I) -> Self + where + I: IntoIterator, + K: AsRef, + V: AsRef, + { + Self { + env: iter + .into_iter() + .filter_map(|(key, value)| { + Some(( + key.as_ref().to_str()?.to_string(), + value.as_ref().to_str()?.to_string(), + )) + }) + .collect(), + } + } +} + +impl Extractor for EnvVarExtractor { + fn get(&self, key: &str) -> Option<&str> { + let normalized = normalize_env_var_key(key); + self.env.get(normalized.as_ref()).map(String::as_str) + } + + fn keys(&self) -> Vec<&str> { + self.env + .keys() + .filter(|key| is_normalized_env_var_name(key)) + .map(String::as_str) + .collect() + } +} + +/// Experimental injector for child-process environment-variable propagation. +/// +/// `EnvVarInjector` collects UTF-8 environment entries and implements +/// [`Injector`] by normalizing each propagation key before storing it. This +/// makes it suitable for passing to process-spawning APIs such as +/// [`std::process::Command::envs`], while leaving the parent process +/// environment untouched. +/// +/// Most callers can start with [`EnvVarInjector::new`] and let +/// [`std::process::Command`] inherit the rest of the parent environment by +/// default. Use [`EnvVarInjector::from_env`] when you need an explicit UTF-8 +/// copy of the current process environment. +#[cfg_attr(docsrs, doc(cfg(feature = "otel_unstable")))] +#[derive(Clone, Debug, Default)] +pub struct EnvVarInjector { + env: HashMap, +} + +impl EnvVarInjector { + /// Creates an empty environment-variable injector. + pub fn new() -> Self { + Self::default() + } + + /// Captures a UTF-8 copy of the current process environment. + /// + /// Non-UTF-8 environment variable names or values are ignored. + pub fn from_env() -> Self { + Self::from_os_iter(std::env::vars_os()) + } + + /// Builds an injector from the provided UTF-8 environment entries. + /// + /// Existing entries are copied exactly as provided. Calls to [`Injector::set`] + /// normalize only the propagation keys added through this injector API. + pub fn from_entries(iter: I) -> Self + where + I: IntoIterator, + K: AsRef, + V: AsRef, + { + Self { + env: iter + .into_iter() + .map(|(key, value)| (key.as_ref().to_string(), value.as_ref().to_string())) + .collect(), + } + } + + /// Consumes the injector and returns the underlying environment-variable map. + pub fn into_inner(self) -> HashMap { + self.env + } + + fn from_os_iter(iter: I) -> Self + where + I: IntoIterator, + K: AsRef, + V: AsRef, + { + Self { + env: iter + .into_iter() + .filter_map(|(key, value)| { + Some(( + key.as_ref().to_str()?.to_string(), + value.as_ref().to_str()?.to_string(), + )) + }) + .collect(), + } + } +} + +impl Injector for EnvVarInjector { + fn set(&mut self, key: &str, value: String) { + self.env + .insert(normalize_env_var_key(key).into_owned(), value); + } + + fn reserve(&mut self, additional: usize) { + self.env.reserve(additional); + } +} + +impl IntoIterator for EnvVarInjector { + type Item = (String, String); + type IntoIter = std::collections::hash_map::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.env.into_iter() + } +} + +impl<'a> IntoIterator for &'a EnvVarInjector { + type Item = (&'a String, &'a String); + type IntoIter = std::collections::hash_map::Iter<'a, String, String>; + + fn into_iter(self) -> Self::IntoIter { + self.env.iter() + } +} + +fn normalize_env_var_key(key: &str) -> Cow<'_, str> { + if is_normalized_env_var_name(key) { + return Cow::Borrowed(key); + } + + if key.is_empty() { + return Cow::Borrowed("_"); + } + + let mut normalized = String::with_capacity( + key.len() + usize::from(key.as_bytes().first().is_some_and(u8::is_ascii_digit)), + ); + + if key.as_bytes().first().is_some_and(u8::is_ascii_digit) { + normalized.push('_'); + } + + for ch in key.chars() { + normalized.push(match ch { + 'a'..='z' => ch.to_ascii_uppercase(), + 'A'..='Z' | '0'..='9' | '_' => ch, + _ => '_', + }); + } + + Cow::Owned(normalized) +} + +fn is_normalized_env_var_name(name: &str) -> bool { + let mut chars = name.chars(); + let Some(first) = chars.next() else { + return false; + }; + + if !(first.is_ascii_uppercase() || first == '_') { + return false; + } + + chars.all(|ch| ch.is_ascii_uppercase() || ch.is_ascii_digit() || ch == '_') +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::propagation::{Extractor, Injector}; + use std::collections::HashSet; + use temp_env::with_vars; + + #[test] + fn normalize_env_var_key_matches_spec() { + assert_eq!(normalize_env_var_key("traceparent"), "TRACEPARENT"); + assert_eq!(normalize_env_var_key("tracestate"), "TRACESTATE"); + assert_eq!(normalize_env_var_key("x-b3-traceid"), "X_B3_TRACEID"); + assert_eq!(normalize_env_var_key("3trace"), "_3TRACE"); + assert_eq!(normalize_env_var_key(""), "_"); + assert_eq!(normalize_env_var_key("héllo.world"), "H_LLO_WORLD"); + } + + #[test] + fn normalized_env_var_detection_matches_spec() { + assert!(is_normalized_env_var_name("TRACEPARENT")); + assert!(is_normalized_env_var_name("_3TRACE")); + assert!(is_normalized_env_var_name("TRACE_STATE_2")); + assert!(!is_normalized_env_var_name("")); + assert!(!is_normalized_env_var_name("traceparent")); + assert!(!is_normalized_env_var_name("3TRACE")); + assert!(!is_normalized_env_var_name("TRACE-STATE")); + } + + #[test] + fn extractor_reads_only_normalized_names() { + let extractor = EnvVarExtractor::from_entries([ + ("TRACEPARENT", "normalized"), + ("traceparent", "ignored"), + ("x-b3-traceid", "ignored"), + ("X_B3_TRACEID", "normalized-b3"), + ]); + + assert_eq!( + Extractor::get(&extractor, "traceparent"), + Some("normalized") + ); + assert_eq!( + Extractor::get(&extractor, "x-b3-traceid"), + Some("normalized-b3") + ); + } + + #[test] + fn extractor_keys_return_only_normalized_names() { + let extractor = EnvVarExtractor::from_entries([ + ("TRACEPARENT", "value"), + ("traceparent", "ignored"), + ("TRACESTATE", "value"), + ("baggage", "ignored"), + ]); + + let keys = Extractor::keys(&extractor) + .into_iter() + .collect::>(); + + assert_eq!(keys, HashSet::from(["TRACEPARENT", "TRACESTATE"])); + } + + #[test] + fn extractor_from_env_ignores_non_normalized_entries() { + with_vars( + vec![ + ("OTEL_ENV_VAR_EXTRACTOR_TEST", Some("value")), + ("otel.env.var.extractor.other", Some("ignored")), + ], + || { + let extractor = EnvVarExtractor::from_env(); + + assert_eq!( + Extractor::get(&extractor, "OTEL_ENV_VAR_EXTRACTOR_TEST"), + Some("value") + ); + assert_eq!( + Extractor::get(&extractor, "otel.env.var.extractor.other"), + None + ); + assert!( + Extractor::keys(&extractor).contains(&"OTEL_ENV_VAR_EXTRACTOR_TEST"), + "normalized keys should be visible" + ); + assert!( + !Extractor::keys(&extractor).contains(&"otel.env.var.extractor.other"), + "non-normalized keys should be hidden" + ); + }, + ); + } + + #[test] + fn injector_normalizes_inserted_names() { + let mut injector = EnvVarInjector::from_entries([("PATH", "/bin"), ("traceparent", "old")]); + Injector::reserve(&mut injector, 2); + Injector::set(&mut injector, "x-b3-traceid", "trace-id".to_string()); + Injector::set(&mut injector, "3trace", "prefixed".to_string()); + + let env = injector.into_inner(); + + assert_eq!(env.get("PATH").map(String::as_str), Some("/bin")); + assert_eq!(env.get("traceparent").map(String::as_str), Some("old")); + assert_eq!( + env.get("X_B3_TRACEID").map(String::as_str), + Some("trace-id") + ); + assert_eq!(env.get("_3TRACE").map(String::as_str), Some("prefixed")); + } + + #[test] + fn injector_from_env_preserves_utf8_entries() { + with_vars(vec![("OTEL_ENV_VAR_INJECTOR_TEST", Some("value"))], || { + let env = EnvVarInjector::from_env().into_inner(); + + assert_eq!( + env.get("OTEL_ENV_VAR_INJECTOR_TEST").map(String::as_str), + Some("value") + ); + }); + } + + #[test] + fn injector_and_extractor_round_trip_propagation_keys() { + let mut injector = EnvVarInjector::new(); + + Injector::set( + &mut injector, + "traceparent", + "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01".to_string(), + ); + Injector::set(&mut injector, "tracestate", "foo=bar".to_string()); + + let env = injector.into_inner(); + assert!(env.contains_key("TRACEPARENT")); + assert!(env.contains_key("TRACESTATE")); + + let extractor = EnvVarExtractor::from_entries(env); + + assert_eq!( + Extractor::get(&extractor, "traceparent"), + Some("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01") + ); + assert_eq!(Extractor::get(&extractor, "tracestate"), Some("foo=bar")); + } +} diff --git a/opentelemetry/src/propagation/mod.rs b/opentelemetry/src/propagation/mod.rs index 7848ffe2a8..fcd09268d1 100644 --- a/opentelemetry/src/propagation/mod.rs +++ b/opentelemetry/src/propagation/mod.rs @@ -9,6 +9,7 @@ //! //! Currently, the following `Propagator` types are supported: //! - [`TextMapPropagator`], inject values into and extracts values from carriers as string key/value pairs +//! - Experimental environment-variable carriers behind the `otel_unstable` feature flag //! //! A binary Propagator type will be added in //! the future, See [tracking issues](https://github.com/open-telemetry/opentelemetry-specification/issues/437)). @@ -22,9 +23,15 @@ use std::collections::HashMap; pub mod composite; +#[cfg(feature = "otel_unstable")] +#[cfg_attr(docsrs, doc(cfg(feature = "otel_unstable")))] +pub mod env; pub mod text_map_propagator; pub use composite::TextMapCompositePropagator; +#[cfg(feature = "otel_unstable")] +#[cfg_attr(docsrs, doc(cfg(feature = "otel_unstable")))] +pub use env::{EnvVarExtractor, EnvVarInjector}; pub use text_map_propagator::TextMapPropagator; /// Injector provides an interface for adding fields from an underlying struct like `HashMap` From 718f9c20bcbeb8b43d6927f328c631c2aad67c03 Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Mon, 29 Jun 2026 19:45:07 +0200 Subject: [PATCH 02/13] fix(api): make env carrier snapshots explicit Align the experimental environment variable carriers with the latest spec guidance by removing hidden from_env snapshot helpers and requiring callers to provide environment entries explicitly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- examples/env-var-propagation/README.md | 7 +-- examples/env-var-propagation/src/main.rs | 2 +- opentelemetry/src/propagation/env.rs | 65 +++++++++++------------- 3 files changed, 36 insertions(+), 38 deletions(-) diff --git a/examples/env-var-propagation/README.md b/examples/env-var-propagation/README.md index be70492d72..d7e4a764b1 100644 --- a/examples/env-var-propagation/README.md +++ b/examples/env-var-propagation/README.md @@ -9,9 +9,10 @@ child process using environment variables as the propagation carrier. root span. 2. The parent injects that span context into an `EnvVarInjector`, then passes the resulting environment variables to a child process. -3. The child process captures its startup environment with `EnvVarExtractor`, - extracts the propagated context, and starts its own span as a child of the - parent's span. +3. The child process wraps its startup environment entries with + `EnvVarExtractor::from_os_entries(std::env::vars_os())`, extracts the + propagated context, and starts its own span as a child of the parent's + span. The example prints the parent's `TraceId` and `SpanId`, then prints the child's `TraceId` and remote parent `SpanId` so you can see that the trace was diff --git a/examples/env-var-propagation/src/main.rs b/examples/env-var-propagation/src/main.rs index 324c606d26..0aed8bc606 100644 --- a/examples/env-var-propagation/src/main.rs +++ b/examples/env-var-propagation/src/main.rs @@ -49,7 +49,7 @@ fn run_parent() -> Result<(), Box> { } fn run_child() -> Result<(), Box> { - let extractor = EnvVarExtractor::from_env(); + let extractor = EnvVarExtractor::from_os_entries(env::vars_os()); let parent_cx = global::get_text_map_propagator(|propagator| propagator.extract(&extractor)); let remote_parent = parent_cx.span().span_context().clone(); diff --git a/opentelemetry/src/propagation/env.rs b/opentelemetry/src/propagation/env.rs index d44924990b..3043ab0f69 100644 --- a/opentelemetry/src/propagation/env.rs +++ b/opentelemetry/src/propagation/env.rs @@ -5,16 +5,18 @@ use std::{borrow::Cow, collections::HashMap, ffi::OsStr}; /// Experimental extractor for propagated context stored in environment variables. /// -/// `EnvVarExtractor` captures a UTF-8 snapshot of the process environment and -/// implements [`Extractor`] with the normalization rules from the -/// OpenTelemetry environment-variable carrier specification. Keys are stored as -/// they appear in the source environment, `get()` normalizes the requested -/// propagation key before lookup, and `keys()` returns only names that are -/// already normalized. +/// `EnvVarExtractor` owns caller-provided environment entries and implements +/// [`Extractor`] with the normalization rules from the OpenTelemetry +/// environment-variable carrier specification. Keys are stored as they appear +/// in the source environment, `get()` normalizes the requested propagation key +/// before lookup, and `keys()` returns only names that are already normalized. /// -/// `from_env()` is intended for initialization-time extraction, such as child -/// process startup. Any environment variable name or value that is not valid -/// UTF-8 is ignored when building the snapshot. +/// Rust's [`Extractor`] trait returns borrowed values, so this adapter reads +/// from the environment entries supplied by the caller instead of performing +/// hidden process-environment lookups or internal caching. To adapt the +/// current process environment at child-process startup, pass +/// [`std::env::vars_os()`] to [`EnvVarExtractor::from_os_entries`] at the +/// extraction point. /// /// Environment variables are visible to other code running in the process and /// may be visible to other users or processes with sufficient permissions, so @@ -31,13 +33,6 @@ impl EnvVarExtractor { Self::default() } - /// Captures a UTF-8 snapshot of the current process environment. - /// - /// Non-UTF-8 environment variable names or values are ignored. - pub fn from_env() -> Self { - Self::from_os_iter(std::env::vars_os()) - } - /// Builds an extractor from the provided UTF-8 environment entries. /// /// Entries are stored exactly as provided. `get()` still reads only the @@ -57,7 +52,12 @@ impl EnvVarExtractor { } } - fn from_os_iter(iter: I) -> Self + /// Builds an extractor from OS-string environment entries. + /// + /// Any entry whose name or value is not valid UTF-8 is ignored. This is + /// useful when passing [`std::env::vars_os()`] explicitly at the extraction + /// point. + pub fn from_os_entries(iter: I) -> Self where I: IntoIterator, K: AsRef, @@ -94,16 +94,17 @@ impl Extractor for EnvVarExtractor { /// Experimental injector for child-process environment-variable propagation. /// -/// `EnvVarInjector` collects UTF-8 environment entries and implements -/// [`Injector`] by normalizing each propagation key before storing it. This -/// makes it suitable for passing to process-spawning APIs such as +/// `EnvVarInjector` owns environment entries and implements [`Injector`] by +/// normalizing each propagation key before storing it. This makes it suitable +/// for passing to process-spawning APIs such as /// [`std::process::Command::envs`], while leaving the parent process /// environment untouched. /// /// Most callers can start with [`EnvVarInjector::new`] and let /// [`std::process::Command`] inherit the rest of the parent environment by -/// default. Use [`EnvVarInjector::from_env`] when you need an explicit UTF-8 -/// copy of the current process environment. +/// default. If you need an explicit copy of existing environment entries, pass +/// them to [`EnvVarInjector::from_entries`] or +/// [`EnvVarInjector::from_os_entries`]. #[cfg_attr(docsrs, doc(cfg(feature = "otel_unstable")))] #[derive(Clone, Debug, Default)] pub struct EnvVarInjector { @@ -116,13 +117,6 @@ impl EnvVarInjector { Self::default() } - /// Captures a UTF-8 copy of the current process environment. - /// - /// Non-UTF-8 environment variable names or values are ignored. - pub fn from_env() -> Self { - Self::from_os_iter(std::env::vars_os()) - } - /// Builds an injector from the provided UTF-8 environment entries. /// /// Existing entries are copied exactly as provided. Calls to [`Injector::set`] @@ -146,7 +140,10 @@ impl EnvVarInjector { self.env } - fn from_os_iter(iter: I) -> Self + /// Builds an injector from OS-string environment entries. + /// + /// Any entry whose name or value is not valid UTF-8 is ignored. + pub fn from_os_entries(iter: I) -> Self where I: IntoIterator, K: AsRef, @@ -300,14 +297,14 @@ mod tests { } #[test] - fn extractor_from_env_ignores_non_normalized_entries() { + fn extractor_from_os_entries_ignores_non_normalized_entries() { with_vars( vec![ ("OTEL_ENV_VAR_EXTRACTOR_TEST", Some("value")), ("otel.env.var.extractor.other", Some("ignored")), ], || { - let extractor = EnvVarExtractor::from_env(); + let extractor = EnvVarExtractor::from_os_entries(std::env::vars_os()); assert_eq!( Extractor::get(&extractor, "OTEL_ENV_VAR_EXTRACTOR_TEST"), @@ -348,9 +345,9 @@ mod tests { } #[test] - fn injector_from_env_preserves_utf8_entries() { + fn injector_from_os_entries_preserves_utf8_entries() { with_vars(vec![("OTEL_ENV_VAR_INJECTOR_TEST", Some("value"))], || { - let env = EnvVarInjector::from_env().into_inner(); + let env = EnvVarInjector::from_os_entries(std::env::vars_os()).into_inner(); assert_eq!( env.get("OTEL_ENV_VAR_INJECTOR_TEST").map(String::as_str), From 084b34045da0422774d39b22494d4aa12ee3568d Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Mon, 29 Jun 2026 20:00:06 +0200 Subject: [PATCH 03/13] docs(example): clarify env var propagation flow Add targeted comments to the env-var-propagation example so the parent/child process flow and explicit environment extraction are easier to follow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- examples/env-var-propagation/src/main.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/env-var-propagation/src/main.rs b/examples/env-var-propagation/src/main.rs index 0aed8bc606..03daa9bace 100644 --- a/examples/env-var-propagation/src/main.rs +++ b/examples/env-var-propagation/src/main.rs @@ -9,6 +9,7 @@ use opentelemetry_stdout::SpanExporter; use std::{env, error::Error, io, process::Command}; fn init_tracer() -> SdkTracerProvider { + // Parent and child both use W3C Trace Context when encoding env vars. global::set_text_map_propagator(TraceContextPropagator::new()); let provider = SdkTracerProvider::builder() @@ -31,9 +32,12 @@ fn run_parent() -> Result<(), Box> { span_context.span_id() ); + // Inject into a fresh environment map so only the child process receives + // the propagated context variables. let mut child_env = EnvVarInjector::new(); global::get_text_map_propagator(|propagator| propagator.inject_context(&cx, &mut child_env)); + // Re-exec the current binary in child mode to keep the example self-contained. let status = Command::new(env::current_exe()?) .arg("--child") .envs(child_env.into_inner()) @@ -49,6 +53,8 @@ fn run_parent() -> Result<(), Box> { } fn run_child() -> Result<(), Box> { + // Make the environment snapshot explicit at the extraction point instead of + // hiding it inside the carrier implementation. let extractor = EnvVarExtractor::from_os_entries(env::vars_os()); let parent_cx = global::get_text_map_propagator(|propagator| propagator.extract(&extractor)); let remote_parent = parent_cx.span().span_context().clone(); @@ -74,6 +80,8 @@ fn run_child() -> Result<(), Box> { fn main() -> Result<(), Box> { let provider = init_tracer(); + // The parent launches the same executable with `--child` to demonstrate + // propagation across a real process boundary. let result = match env::args().nth(1).as_deref() { Some("--child") => run_child(), _ => run_parent(), From 99352a3e15cf6522798af04454f50b7798ee9e62 Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Mon, 29 Jun 2026 20:25:03 +0200 Subject: [PATCH 04/13] refactor(api): deduplicate env carrier entry collection --- opentelemetry/src/propagation/env.rs | 57 +++++++++++++++------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/opentelemetry/src/propagation/env.rs b/opentelemetry/src/propagation/env.rs index 3043ab0f69..19c8b17b43 100644 --- a/opentelemetry/src/propagation/env.rs +++ b/opentelemetry/src/propagation/env.rs @@ -45,10 +45,7 @@ impl EnvVarExtractor { V: AsRef, { Self { - env: iter - .into_iter() - .map(|(key, value)| (key.as_ref().to_string(), value.as_ref().to_string())) - .collect(), + env: collect_entries(iter), } } @@ -64,15 +61,7 @@ impl EnvVarExtractor { V: AsRef, { Self { - env: iter - .into_iter() - .filter_map(|(key, value)| { - Some(( - key.as_ref().to_str()?.to_string(), - value.as_ref().to_str()?.to_string(), - )) - }) - .collect(), + env: collect_os_entries(iter), } } } @@ -128,10 +117,7 @@ impl EnvVarInjector { V: AsRef, { Self { - env: iter - .into_iter() - .map(|(key, value)| (key.as_ref().to_string(), value.as_ref().to_string())) - .collect(), + env: collect_entries(iter), } } @@ -150,15 +136,7 @@ impl EnvVarInjector { V: AsRef, { Self { - env: iter - .into_iter() - .filter_map(|(key, value)| { - Some(( - key.as_ref().to_str()?.to_string(), - value.as_ref().to_str()?.to_string(), - )) - }) - .collect(), + env: collect_os_entries(iter), } } } @@ -192,6 +170,33 @@ impl<'a> IntoIterator for &'a EnvVarInjector { } } +fn collect_entries(iter: I) -> HashMap +where + I: IntoIterator, + K: AsRef, + V: AsRef, +{ + iter.into_iter() + .map(|(key, value)| (key.as_ref().to_string(), value.as_ref().to_string())) + .collect() +} + +fn collect_os_entries(iter: I) -> HashMap +where + I: IntoIterator, + K: AsRef, + V: AsRef, +{ + iter.into_iter() + .filter_map(|(key, value)| { + Some(( + key.as_ref().to_str()?.to_string(), + value.as_ref().to_str()?.to_string(), + )) + }) + .collect() +} + fn normalize_env_var_key(key: &str) -> Cow<'_, str> { if is_normalized_env_var_name(key) { return Cow::Borrowed(key); From eee36a9bba1ff53fc4b3257c6d6822e04e55a19a Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Mon, 29 Jun 2026 20:27:24 +0200 Subject: [PATCH 05/13] refactor(api): simplify env carrier entry collection --- opentelemetry/src/propagation/env.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/opentelemetry/src/propagation/env.rs b/opentelemetry/src/propagation/env.rs index 19c8b17b43..91204424ec 100644 --- a/opentelemetry/src/propagation/env.rs +++ b/opentelemetry/src/propagation/env.rs @@ -41,8 +41,8 @@ impl EnvVarExtractor { pub fn from_entries(iter: I) -> Self where I: IntoIterator, - K: AsRef, - V: AsRef, + K: Into, + V: Into, { Self { env: collect_entries(iter), @@ -108,13 +108,13 @@ impl EnvVarInjector { /// Builds an injector from the provided UTF-8 environment entries. /// - /// Existing entries are copied exactly as provided. Calls to [`Injector::set`] + /// Existing entries are stored exactly as provided. Calls to [`Injector::set`] /// normalize only the propagation keys added through this injector API. pub fn from_entries(iter: I) -> Self where I: IntoIterator, - K: AsRef, - V: AsRef, + K: Into, + V: Into, { Self { env: collect_entries(iter), @@ -173,11 +173,11 @@ impl<'a> IntoIterator for &'a EnvVarInjector { fn collect_entries(iter: I) -> HashMap where I: IntoIterator, - K: AsRef, - V: AsRef, + K: Into, + V: Into, { iter.into_iter() - .map(|(key, value)| (key.as_ref().to_string(), value.as_ref().to_string())) + .map(|(key, value)| (key.into(), value.into())) .collect() } From cc108934434c9130a5b09811ae1797698c3c8907 Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Mon, 29 Jun 2026 20:47:26 +0200 Subject: [PATCH 06/13] refactor(api): streamline env var injector API --- examples/env-var-propagation/README.md | 5 +- examples/env-var-propagation/src/main.rs | 8 +- opentelemetry/src/propagation/env.rs | 122 +++++++++++++---------- 3 files changed, 77 insertions(+), 58 deletions(-) diff --git a/examples/env-var-propagation/README.md b/examples/env-var-propagation/README.md index d7e4a764b1..702ce67830 100644 --- a/examples/env-var-propagation/README.md +++ b/examples/env-var-propagation/README.md @@ -7,8 +7,9 @@ child process using environment variables as the propagation carrier. 1. The parent process configures the W3C `TraceContextPropagator` and starts a root span. -2. The parent injects that span context into an `EnvVarInjector`, then passes - the resulting environment variables to a child process. +2. The parent copies its environment into an `EnvVarInjector`, injects that span + context into the copy, then passes the full environment copy to a child + process. 3. The child process wraps its startup environment entries with `EnvVarExtractor::from_os_entries(std::env::vars_os())`, extracts the propagated context, and starts its own span as a child of the parent's diff --git a/examples/env-var-propagation/src/main.rs b/examples/env-var-propagation/src/main.rs index 03daa9bace..09428648b1 100644 --- a/examples/env-var-propagation/src/main.rs +++ b/examples/env-var-propagation/src/main.rs @@ -32,15 +32,15 @@ fn run_parent() -> Result<(), Box> { span_context.span_id() ); - // Inject into a fresh environment map so only the child process receives - // the propagated context variables. - let mut child_env = EnvVarInjector::new(); + // Inject into an explicit copy of the parent environment so the child gets + // the normal environment plus the propagated context variables. + let mut child_env = EnvVarInjector::from_entries(env::vars_os()); global::get_text_map_propagator(|propagator| propagator.inject_context(&cx, &mut child_env)); // Re-exec the current binary in child mode to keep the example self-contained. let status = Command::new(env::current_exe()?) .arg("--child") - .envs(child_env.into_inner()) + .envs(child_env) .status()?; cx.span().end(); diff --git a/opentelemetry/src/propagation/env.rs b/opentelemetry/src/propagation/env.rs index 91204424ec..77e17175f0 100644 --- a/opentelemetry/src/propagation/env.rs +++ b/opentelemetry/src/propagation/env.rs @@ -1,7 +1,11 @@ //! Experimental environment-variable propagation carriers. use crate::propagation::{Extractor, Injector}; -use std::{borrow::Cow, collections::HashMap, ffi::OsStr}; +use std::{ + borrow::Cow, + collections::HashMap, + ffi::{OsStr, OsString}, +}; /// Experimental extractor for propagated context stored in environment variables. /// @@ -92,12 +96,11 @@ impl Extractor for EnvVarExtractor { /// Most callers can start with [`EnvVarInjector::new`] and let /// [`std::process::Command`] inherit the rest of the parent environment by /// default. If you need an explicit copy of existing environment entries, pass -/// them to [`EnvVarInjector::from_entries`] or -/// [`EnvVarInjector::from_os_entries`]. +/// them to [`EnvVarInjector::from_entries`]. #[cfg_attr(docsrs, doc(cfg(feature = "otel_unstable")))] #[derive(Clone, Debug, Default)] pub struct EnvVarInjector { - env: HashMap, + env: HashMap, } impl EnvVarInjector { @@ -106,37 +109,18 @@ impl EnvVarInjector { Self::default() } - /// Builds an injector from the provided UTF-8 environment entries. + /// Builds an injector from the provided environment entries. /// /// Existing entries are stored exactly as provided. Calls to [`Injector::set`] /// normalize only the propagation keys added through this injector API. pub fn from_entries(iter: I) -> Self where I: IntoIterator, - K: Into, - V: Into, + K: Into, + V: Into, { Self { - env: collect_entries(iter), - } - } - - /// Consumes the injector and returns the underlying environment-variable map. - pub fn into_inner(self) -> HashMap { - self.env - } - - /// Builds an injector from OS-string environment entries. - /// - /// Any entry whose name or value is not valid UTF-8 is ignored. - pub fn from_os_entries(iter: I) -> Self - where - I: IntoIterator, - K: AsRef, - V: AsRef, - { - Self { - env: collect_os_entries(iter), + env: collect_injector_entries(iter), } } } @@ -144,7 +128,7 @@ impl EnvVarInjector { impl Injector for EnvVarInjector { fn set(&mut self, key: &str, value: String) { self.env - .insert(normalize_env_var_key(key).into_owned(), value); + .insert(normalize_env_var_key(key).as_ref().into(), value.into()); } fn reserve(&mut self, additional: usize) { @@ -153,23 +137,14 @@ impl Injector for EnvVarInjector { } impl IntoIterator for EnvVarInjector { - type Item = (String, String); - type IntoIter = std::collections::hash_map::IntoIter; + type Item = (OsString, OsString); + type IntoIter = std::collections::hash_map::IntoIter; fn into_iter(self) -> Self::IntoIter { self.env.into_iter() } } -impl<'a> IntoIterator for &'a EnvVarInjector { - type Item = (&'a String, &'a String); - type IntoIter = std::collections::hash_map::Iter<'a, String, String>; - - fn into_iter(self) -> Self::IntoIter { - self.env.iter() - } -} - fn collect_entries(iter: I) -> HashMap where I: IntoIterator, @@ -181,6 +156,17 @@ where .collect() } +fn collect_injector_entries(iter: I) -> HashMap +where + I: IntoIterator, + K: Into, + V: Into, +{ + iter.into_iter() + .map(|(key, value)| (key.into(), value.into())) + .collect() +} + fn collect_os_entries(iter: I) -> HashMap where I: IntoIterator, @@ -242,7 +228,10 @@ fn is_normalized_env_var_name(name: &str) -> bool { mod tests { use super::*; use crate::propagation::{Extractor, Injector}; - use std::collections::HashSet; + use std::{ + collections::{HashMap, HashSet}, + ffi::OsStr, + }; use temp_env::with_vars; #[test] @@ -338,29 +327,58 @@ mod tests { Injector::set(&mut injector, "x-b3-traceid", "trace-id".to_string()); Injector::set(&mut injector, "3trace", "prefixed".to_string()); - let env = injector.into_inner(); + let env: HashMap<_, _> = injector.into_iter().collect(); - assert_eq!(env.get("PATH").map(String::as_str), Some("/bin")); - assert_eq!(env.get("traceparent").map(String::as_str), Some("old")); assert_eq!( - env.get("X_B3_TRACEID").map(String::as_str), + env.get(OsStr::new("PATH")).and_then(|value| value.to_str()), + Some("/bin") + ); + assert_eq!( + env.get(OsStr::new("traceparent")) + .and_then(|value| value.to_str()), + Some("old") + ); + assert_eq!( + env.get(OsStr::new("X_B3_TRACEID")) + .and_then(|value| value.to_str()), Some("trace-id") ); - assert_eq!(env.get("_3TRACE").map(String::as_str), Some("prefixed")); + assert_eq!( + env.get(OsStr::new("_3TRACE")) + .and_then(|value| value.to_str()), + Some("prefixed") + ); } #[test] - fn injector_from_os_entries_preserves_utf8_entries() { + fn injector_from_entries_preserves_os_entries() { with_vars(vec![("OTEL_ENV_VAR_INJECTOR_TEST", Some("value"))], || { - let env = EnvVarInjector::from_os_entries(std::env::vars_os()).into_inner(); + let env: HashMap<_, _> = EnvVarInjector::from_entries(std::env::vars_os()) + .into_iter() + .collect(); assert_eq!( - env.get("OTEL_ENV_VAR_INJECTOR_TEST").map(String::as_str), + env.get(OsStr::new("OTEL_ENV_VAR_INJECTOR_TEST")) + .and_then(|value| value.to_str()), Some("value") ); }); } + #[cfg(unix)] + #[test] + fn injector_from_entries_preserves_non_utf8_entries() { + use std::os::unix::ffi::OsStringExt; + + let key = OsString::from_vec(b"OTEL_ENV_VAR_NON_UTF8_\xFF".to_vec()); + let value = OsString::from_vec(b"value_\xFF".to_vec()); + let env: HashMap<_, _> = EnvVarInjector::from_entries([(key.clone(), value.clone())]) + .into_iter() + .collect(); + + assert_eq!(env.get(key.as_os_str()), Some(&value)); + } + #[test] fn injector_and_extractor_round_trip_propagation_keys() { let mut injector = EnvVarInjector::new(); @@ -372,11 +390,11 @@ mod tests { ); Injector::set(&mut injector, "tracestate", "foo=bar".to_string()); - let env = injector.into_inner(); - assert!(env.contains_key("TRACEPARENT")); - assert!(env.contains_key("TRACESTATE")); + let env: HashMap<_, _> = injector.into_iter().collect(); + assert!(env.contains_key(OsStr::new("TRACEPARENT"))); + assert!(env.contains_key(OsStr::new("TRACESTATE"))); - let extractor = EnvVarExtractor::from_entries(env); + let extractor = EnvVarExtractor::from_os_entries(env); assert_eq!( Extractor::get(&extractor, "traceparent"), From 16d2ec10e70a08ed03838cbe795d56dc256d72ab Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Mon, 29 Jun 2026 20:54:27 +0200 Subject: [PATCH 07/13] refactor(api): simplify env var injector --- examples/env-var-propagation/README.md | 6 +- examples/env-var-propagation/src/main.rs | 6 +- opentelemetry/src/propagation/env.rs | 103 +++-------------------- 3 files changed, 19 insertions(+), 96 deletions(-) diff --git a/examples/env-var-propagation/README.md b/examples/env-var-propagation/README.md index 702ce67830..e33ae97e1d 100644 --- a/examples/env-var-propagation/README.md +++ b/examples/env-var-propagation/README.md @@ -7,9 +7,9 @@ child process using environment variables as the propagation carrier. 1. The parent process configures the W3C `TraceContextPropagator` and starts a root span. -2. The parent copies its environment into an `EnvVarInjector`, injects that span - context into the copy, then passes the full environment copy to a child - process. +2. The parent injects that span context into an `EnvVarInjector`, then passes + the resulting environment variables to a child process. The child inherits + the rest of the parent's environment through `std::process::Command`. 3. The child process wraps its startup environment entries with `EnvVarExtractor::from_os_entries(std::env::vars_os())`, extracts the propagated context, and starts its own span as a child of the parent's diff --git a/examples/env-var-propagation/src/main.rs b/examples/env-var-propagation/src/main.rs index 09428648b1..8ed557e56d 100644 --- a/examples/env-var-propagation/src/main.rs +++ b/examples/env-var-propagation/src/main.rs @@ -32,9 +32,9 @@ fn run_parent() -> Result<(), Box> { span_context.span_id() ); - // Inject into an explicit copy of the parent environment so the child gets - // the normal environment plus the propagated context variables. - let mut child_env = EnvVarInjector::from_entries(env::vars_os()); + // Inject into a fresh environment map. `Command` inherits the parent + // environment by default, and `envs` adds the propagated context variables. + let mut child_env = EnvVarInjector::new(); global::get_text_map_propagator(|propagator| propagator.inject_context(&cx, &mut child_env)); // Re-exec the current binary in child mode to keep the example self-contained. diff --git a/opentelemetry/src/propagation/env.rs b/opentelemetry/src/propagation/env.rs index 77e17175f0..c3b0455612 100644 --- a/opentelemetry/src/propagation/env.rs +++ b/opentelemetry/src/propagation/env.rs @@ -1,11 +1,7 @@ //! Experimental environment-variable propagation carriers. use crate::propagation::{Extractor, Injector}; -use std::{ - borrow::Cow, - collections::HashMap, - ffi::{OsStr, OsString}, -}; +use std::{borrow::Cow, collections::HashMap, ffi::OsStr}; /// Experimental extractor for propagated context stored in environment variables. /// @@ -95,12 +91,11 @@ impl Extractor for EnvVarExtractor { /// /// Most callers can start with [`EnvVarInjector::new`] and let /// [`std::process::Command`] inherit the rest of the parent environment by -/// default. If you need an explicit copy of existing environment entries, pass -/// them to [`EnvVarInjector::from_entries`]. +/// default. #[cfg_attr(docsrs, doc(cfg(feature = "otel_unstable")))] #[derive(Clone, Debug, Default)] pub struct EnvVarInjector { - env: HashMap, + env: HashMap, } impl EnvVarInjector { @@ -108,27 +103,12 @@ impl EnvVarInjector { pub fn new() -> Self { Self::default() } - - /// Builds an injector from the provided environment entries. - /// - /// Existing entries are stored exactly as provided. Calls to [`Injector::set`] - /// normalize only the propagation keys added through this injector API. - pub fn from_entries(iter: I) -> Self - where - I: IntoIterator, - K: Into, - V: Into, - { - Self { - env: collect_injector_entries(iter), - } - } } impl Injector for EnvVarInjector { fn set(&mut self, key: &str, value: String) { self.env - .insert(normalize_env_var_key(key).as_ref().into(), value.into()); + .insert(normalize_env_var_key(key).into_owned(), value); } fn reserve(&mut self, additional: usize) { @@ -137,8 +117,8 @@ impl Injector for EnvVarInjector { } impl IntoIterator for EnvVarInjector { - type Item = (OsString, OsString); - type IntoIter = std::collections::hash_map::IntoIter; + type Item = (String, String); + type IntoIter = std::collections::hash_map::IntoIter; fn into_iter(self) -> Self::IntoIter { self.env.into_iter() @@ -156,17 +136,6 @@ where .collect() } -fn collect_injector_entries(iter: I) -> HashMap -where - I: IntoIterator, - K: Into, - V: Into, -{ - iter.into_iter() - .map(|(key, value)| (key.into(), value.into())) - .collect() -} - fn collect_os_entries(iter: I) -> HashMap where I: IntoIterator, @@ -228,10 +197,7 @@ fn is_normalized_env_var_name(name: &str) -> bool { mod tests { use super::*; use crate::propagation::{Extractor, Injector}; - use std::{ - collections::{HashMap, HashSet}, - ffi::OsStr, - }; + use std::collections::{HashMap, HashSet}; use temp_env::with_vars; #[test] @@ -322,7 +288,7 @@ mod tests { #[test] fn injector_normalizes_inserted_names() { - let mut injector = EnvVarInjector::from_entries([("PATH", "/bin"), ("traceparent", "old")]); + let mut injector = EnvVarInjector::new(); Injector::reserve(&mut injector, 2); Injector::set(&mut injector, "x-b3-traceid", "trace-id".to_string()); Injector::set(&mut injector, "3trace", "prefixed".to_string()); @@ -330,53 +296,10 @@ mod tests { let env: HashMap<_, _> = injector.into_iter().collect(); assert_eq!( - env.get(OsStr::new("PATH")).and_then(|value| value.to_str()), - Some("/bin") - ); - assert_eq!( - env.get(OsStr::new("traceparent")) - .and_then(|value| value.to_str()), - Some("old") - ); - assert_eq!( - env.get(OsStr::new("X_B3_TRACEID")) - .and_then(|value| value.to_str()), + env.get("X_B3_TRACEID").map(String::as_str), Some("trace-id") ); - assert_eq!( - env.get(OsStr::new("_3TRACE")) - .and_then(|value| value.to_str()), - Some("prefixed") - ); - } - - #[test] - fn injector_from_entries_preserves_os_entries() { - with_vars(vec![("OTEL_ENV_VAR_INJECTOR_TEST", Some("value"))], || { - let env: HashMap<_, _> = EnvVarInjector::from_entries(std::env::vars_os()) - .into_iter() - .collect(); - - assert_eq!( - env.get(OsStr::new("OTEL_ENV_VAR_INJECTOR_TEST")) - .and_then(|value| value.to_str()), - Some("value") - ); - }); - } - - #[cfg(unix)] - #[test] - fn injector_from_entries_preserves_non_utf8_entries() { - use std::os::unix::ffi::OsStringExt; - - let key = OsString::from_vec(b"OTEL_ENV_VAR_NON_UTF8_\xFF".to_vec()); - let value = OsString::from_vec(b"value_\xFF".to_vec()); - let env: HashMap<_, _> = EnvVarInjector::from_entries([(key.clone(), value.clone())]) - .into_iter() - .collect(); - - assert_eq!(env.get(key.as_os_str()), Some(&value)); + assert_eq!(env.get("_3TRACE").map(String::as_str), Some("prefixed")); } #[test] @@ -391,10 +314,10 @@ mod tests { Injector::set(&mut injector, "tracestate", "foo=bar".to_string()); let env: HashMap<_, _> = injector.into_iter().collect(); - assert!(env.contains_key(OsStr::new("TRACEPARENT"))); - assert!(env.contains_key(OsStr::new("TRACESTATE"))); + assert!(env.contains_key("TRACEPARENT")); + assert!(env.contains_key("TRACESTATE")); - let extractor = EnvVarExtractor::from_os_entries(env); + let extractor = EnvVarExtractor::from_entries(env); assert_eq!( Extractor::get(&extractor, "traceparent"), From 8a185f0e090619936b26b7fe48932e7766293378 Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Mon, 29 Jun 2026 21:03:46 +0200 Subject: [PATCH 08/13] docs(api): explain snapshoting in EnvVarExtractor --- opentelemetry/src/propagation/env.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/opentelemetry/src/propagation/env.rs b/opentelemetry/src/propagation/env.rs index c3b0455612..89d0ed9225 100644 --- a/opentelemetry/src/propagation/env.rs +++ b/opentelemetry/src/propagation/env.rs @@ -11,12 +11,13 @@ use std::{borrow::Cow, collections::HashMap, ffi::OsStr}; /// in the source environment, `get()` normalizes the requested propagation key /// before lookup, and `keys()` returns only names that are already normalized. /// -/// Rust's [`Extractor`] trait returns borrowed values, so this adapter reads -/// from the environment entries supplied by the caller instead of performing -/// hidden process-environment lookups or internal caching. To adapt the -/// current process environment at child-process startup, pass -/// [`std::env::vars_os()`] to [`EnvVarExtractor::from_os_entries`] at the -/// extraction point. +/// The extractor snapshots the environment because [`Extractor::get`] must +/// return `&str`, while [`std::env::var_os`] returns owned values. The snapshot +/// gives the carrier stable owned storage to borrow from and makes +/// [`Extractor::keys`] operate over a consistent environment view instead of +/// repeatedly reading process-global state. To adapt the current process +/// environment at child-process startup, pass [`std::env::vars_os()`] to +/// [`EnvVarExtractor::from_os_entries`] at the extraction point. /// /// Environment variables are visible to other code running in the process and /// may be visible to other users or processes with sufficient permissions, so From 55a5af6ff1aee17564afe27a34666f7d955cfc67 Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Mon, 29 Jun 2026 21:12:38 +0200 Subject: [PATCH 09/13] feat(api): add targeted env var extraction --- examples/env-var-propagation/README.md | 7 +- examples/env-var-propagation/src/main.rs | 9 +-- opentelemetry/src/propagation/env.rs | 84 +++++++++++++++++++++--- 3 files changed, 83 insertions(+), 17 deletions(-) diff --git a/examples/env-var-propagation/README.md b/examples/env-var-propagation/README.md index e33ae97e1d..3fc1927536 100644 --- a/examples/env-var-propagation/README.md +++ b/examples/env-var-propagation/README.md @@ -10,10 +10,9 @@ child process using environment variables as the propagation carrier. 2. The parent injects that span context into an `EnvVarInjector`, then passes the resulting environment variables to a child process. The child inherits the rest of the parent's environment through `std::process::Command`. -3. The child process wraps its startup environment entries with - `EnvVarExtractor::from_os_entries(std::env::vars_os())`, extracts the - propagated context, and starts its own span as a child of the parent's - span. +3. The child process builds an `EnvVarExtractor` from the active propagator's + fields, extracts the propagated context, and starts its own span as a child + of the parent's span. The example prints the parent's `TraceId` and `SpanId`, then prints the child's `TraceId` and remote parent `SpanId` so you can see that the trace was diff --git a/examples/env-var-propagation/src/main.rs b/examples/env-var-propagation/src/main.rs index 8ed557e56d..6cdda2c43f 100644 --- a/examples/env-var-propagation/src/main.rs +++ b/examples/env-var-propagation/src/main.rs @@ -53,10 +53,11 @@ fn run_parent() -> Result<(), Box> { } fn run_child() -> Result<(), Box> { - // Make the environment snapshot explicit at the extraction point instead of - // hiding it inside the carrier implementation. - let extractor = EnvVarExtractor::from_os_entries(env::vars_os()); - let parent_cx = global::get_text_map_propagator(|propagator| propagator.extract(&extractor)); + let parent_cx = global::get_text_map_propagator(|propagator| { + // Read only the environment variables used by the active propagator. + let extractor = EnvVarExtractor::from_fields(propagator.fields()); + propagator.extract(&extractor) + }); let remote_parent = parent_cx.span().span_context().clone(); if !remote_parent.is_valid() { diff --git a/opentelemetry/src/propagation/env.rs b/opentelemetry/src/propagation/env.rs index 89d0ed9225..83baa4ac41 100644 --- a/opentelemetry/src/propagation/env.rs +++ b/opentelemetry/src/propagation/env.rs @@ -11,13 +11,18 @@ use std::{borrow::Cow, collections::HashMap, ffi::OsStr}; /// in the source environment, `get()` normalizes the requested propagation key /// before lookup, and `keys()` returns only names that are already normalized. /// -/// The extractor snapshots the environment because [`Extractor::get`] must -/// return `&str`, while [`std::env::var_os`] returns owned values. The snapshot +/// The extractor stores environment values because [`Extractor::get`] must +/// return `&str`, while [`std::env::var_os`] returns owned values. Storing values /// gives the carrier stable owned storage to borrow from and makes -/// [`Extractor::keys`] operate over a consistent environment view instead of -/// repeatedly reading process-global state. To adapt the current process -/// environment at child-process startup, pass [`std::env::vars_os()`] to -/// [`EnvVarExtractor::from_os_entries`] at the extraction point. +/// [`Extractor::keys`] operate over a consistent view instead of repeatedly +/// reading process-global state. +/// +/// Most callers should pass the active propagator's fields to +/// [`EnvVarExtractor::from_fields`] at child-process startup so only known +/// propagation variables are read from the environment. Use +/// [`EnvVarExtractor::from_os_entries`] when a propagator needs +/// [`Extractor::keys`] to see the whole environment, such as legacy propagators +/// that scan carrier keys by prefix. /// /// Environment variables are visible to other code running in the process and /// may be visible to other users or processes with sufficient permissions, so @@ -34,6 +39,22 @@ impl EnvVarExtractor { Self::default() } + /// Builds an extractor by reading the normalized form of each provided field + /// from the current process environment. + /// + /// This is the recommended constructor when extracting with a known + /// [`crate::propagation::TextMapPropagator`]. It avoids enumerating the whole + /// environment for propagators that only call [`Extractor::get`] for their + /// advertised fields. Any value that is not valid UTF-8 is ignored. + pub fn from_fields<'a, I>(fields: I) -> Self + where + I: IntoIterator, + { + Self { + env: collect_fields(fields), + } + } + /// Builds an extractor from the provided UTF-8 environment entries. /// /// Entries are stored exactly as provided. `get()` still reads only the @@ -52,9 +73,9 @@ impl EnvVarExtractor { /// Builds an extractor from OS-string environment entries. /// - /// Any entry whose name or value is not valid UTF-8 is ignored. This is - /// useful when passing [`std::env::vars_os()`] explicitly at the extraction - /// point. + /// Any entry whose name or value is not valid UTF-8 is ignored. This scans + /// the provided entries, so prefer [`EnvVarExtractor::from_fields`] unless a + /// propagator needs [`Extractor::keys`] to see the whole environment. pub fn from_os_entries(iter: I) -> Self where I: IntoIterator, @@ -137,6 +158,24 @@ where .collect() } +fn collect_fields<'a, I>(fields: I) -> HashMap +where + I: IntoIterator, +{ + fields + .into_iter() + .filter_map(|field| { + let normalized = normalize_env_var_key(field); + std::env::var_os(normalized.as_ref()).and_then(|value| { + value + .into_string() + .ok() + .map(|value| (normalized.into_owned(), value)) + }) + }) + .collect() +} + fn collect_os_entries(iter: I) -> HashMap where I: IntoIterator, @@ -241,6 +280,33 @@ mod tests { ); } + #[test] + fn extractor_from_fields_reads_only_requested_environment_names() { + with_vars( + vec![ + ("TRACEPARENT", Some("normalized")), + ("TRACESTATE", Some("state")), + ("BAGGAGE", Some("not-requested")), + ], + || { + let extractor = + EnvVarExtractor::from_fields(["traceparent", "tracestate", "missing"]); + + assert_eq!( + Extractor::get(&extractor, "traceparent"), + Some("normalized") + ); + assert_eq!(Extractor::get(&extractor, "tracestate"), Some("state")); + assert_eq!(Extractor::get(&extractor, "baggage"), None); + + let keys = Extractor::keys(&extractor) + .into_iter() + .collect::>(); + assert_eq!(keys, HashSet::from(["TRACEPARENT", "TRACESTATE"])); + }, + ); + } + #[test] fn extractor_keys_return_only_normalized_names() { let extractor = EnvVarExtractor::from_entries([ From f1df492c49ea9b1ac15b3fbb0b6c05e7bed40b52 Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Mon, 29 Jun 2026 21:31:03 +0200 Subject: [PATCH 10/13] document case sensitivity --- opentelemetry/src/propagation/env.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/opentelemetry/src/propagation/env.rs b/opentelemetry/src/propagation/env.rs index 83baa4ac41..9c3c3a4250 100644 --- a/opentelemetry/src/propagation/env.rs +++ b/opentelemetry/src/propagation/env.rs @@ -60,6 +60,11 @@ impl EnvVarExtractor { /// Entries are stored exactly as provided. `get()` still reads only the /// normalized form of a propagation key, and `keys()` still returns only /// already-normalized names. + /// + /// Lookup in this snapshot is case-sensitive on all platforms. This can + /// differ from [`EnvVarExtractor::from_fields`] on platforms such as Windows, + /// where reading a normalized name from the process environment may match an + /// environment variable whose name differs only by case. pub fn from_entries(iter: I) -> Self where I: IntoIterator, @@ -76,6 +81,11 @@ impl EnvVarExtractor { /// Any entry whose name or value is not valid UTF-8 is ignored. This scans /// the provided entries, so prefer [`EnvVarExtractor::from_fields`] unless a /// propagator needs [`Extractor::keys`] to see the whole environment. + /// + /// Lookup in this snapshot is case-sensitive on all platforms. This can + /// differ from [`EnvVarExtractor::from_fields`] on platforms such as Windows, + /// where reading a normalized name from the process environment may match an + /// environment variable whose name differs only by case. pub fn from_os_entries(iter: I) -> Self where I: IntoIterator, From 05a39d5fd7fe6d9d2ea4ef42a3b1ff5b43244e03 Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Mon, 29 Jun 2026 21:56:45 +0200 Subject: [PATCH 11/13] fix(api): filter non-normalized env carrier entries --- opentelemetry/src/propagation/env.rs | 45 ++++++++++++++++++---------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/opentelemetry/src/propagation/env.rs b/opentelemetry/src/propagation/env.rs index 9c3c3a4250..fb3e24f04a 100644 --- a/opentelemetry/src/propagation/env.rs +++ b/opentelemetry/src/propagation/env.rs @@ -7,9 +7,9 @@ use std::{borrow::Cow, collections::HashMap, ffi::OsStr}; /// /// `EnvVarExtractor` owns caller-provided environment entries and implements /// [`Extractor`] with the normalization rules from the OpenTelemetry -/// environment-variable carrier specification. Keys are stored as they appear -/// in the source environment, `get()` normalizes the requested propagation key -/// before lookup, and `keys()` returns only names that are already normalized. +/// environment-variable carrier specification. Only keys that are already +/// normalized are retained, `get()` normalizes the requested propagation key +/// before lookup, and `keys()` returns the retained names. /// /// The extractor stores environment values because [`Extractor::get`] must /// return `&str`, while [`std::env::var_os`] returns owned values. Storing values @@ -57,9 +57,9 @@ impl EnvVarExtractor { /// Builds an extractor from the provided UTF-8 environment entries. /// - /// Entries are stored exactly as provided. `get()` still reads only the - /// normalized form of a propagation key, and `keys()` still returns only - /// already-normalized names. + /// Only entries whose names are already normalized are stored. `get()` still + /// reads only the normalized form of a propagation key, and `keys()` returns + /// the retained names. /// /// Lookup in this snapshot is case-sensitive on all platforms. This can /// differ from [`EnvVarExtractor::from_fields`] on platforms such as Windows, @@ -78,8 +78,9 @@ impl EnvVarExtractor { /// Builds an extractor from OS-string environment entries. /// - /// Any entry whose name or value is not valid UTF-8 is ignored. This scans - /// the provided entries, so prefer [`EnvVarExtractor::from_fields`] unless a + /// Any entry whose name or value is not valid UTF-8 is ignored. Entries + /// whose names are not already normalized are also ignored. This scans the + /// provided entries, so prefer [`EnvVarExtractor::from_fields`] unless a /// propagator needs [`Extractor::keys`] to see the whole environment. /// /// Lookup in this snapshot is case-sensitive on all platforms. This can @@ -164,7 +165,10 @@ where V: Into, { iter.into_iter() - .map(|(key, value)| (key.into(), value.into())) + .filter_map(|(key, value)| { + let key = key.into(); + is_normalized_env_var_name(&key).then(|| (key, value.into())) + }) .collect() } @@ -194,10 +198,12 @@ where { iter.into_iter() .filter_map(|(key, value)| { - Some(( - key.as_ref().to_str()?.to_string(), - value.as_ref().to_str()?.to_string(), - )) + let key = key.as_ref().to_str()?; + if is_normalized_env_var_name(key) { + Some((key.to_string(), value.as_ref().to_str()?.to_string())) + } else { + None + } }) .collect() } @@ -288,6 +294,11 @@ mod tests { Extractor::get(&extractor, "x-b3-traceid"), Some("normalized-b3") ); + + let keys = Extractor::keys(&extractor) + .into_iter() + .collect::>(); + assert_eq!(keys, HashSet::from(["TRACEPARENT", "X_B3_TRACEID"])); } #[test] @@ -351,12 +362,16 @@ mod tests { Extractor::get(&extractor, "otel.env.var.extractor.other"), None ); + + let keys = Extractor::keys(&extractor) + .into_iter() + .collect::>(); assert!( - Extractor::keys(&extractor).contains(&"OTEL_ENV_VAR_EXTRACTOR_TEST"), + keys.contains("OTEL_ENV_VAR_EXTRACTOR_TEST"), "normalized keys should be visible" ); assert!( - !Extractor::keys(&extractor).contains(&"otel.env.var.extractor.other"), + !keys.contains("otel.env.var.extractor.other"), "non-normalized keys should be hidden" ); }, From 06fc2ece594ebde90a48339610e7cc32dccbde98 Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Mon, 29 Jun 2026 22:07:14 +0200 Subject: [PATCH 12/13] remove redundant assertion --- opentelemetry/src/propagation/env.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/opentelemetry/src/propagation/env.rs b/opentelemetry/src/propagation/env.rs index fb3e24f04a..ce4d026776 100644 --- a/opentelemetry/src/propagation/env.rs +++ b/opentelemetry/src/propagation/env.rs @@ -294,11 +294,6 @@ mod tests { Extractor::get(&extractor, "x-b3-traceid"), Some("normalized-b3") ); - - let keys = Extractor::keys(&extractor) - .into_iter() - .collect::>(); - assert_eq!(keys, HashSet::from(["TRACEPARENT", "X_B3_TRACEID"])); } #[test] From 7fe831d27232a0e767e15cda6210c3a468aa832b Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Mon, 29 Jun 2026 22:23:22 +0200 Subject: [PATCH 13/13] then instead of if-else --- opentelemetry/src/propagation/env.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/opentelemetry/src/propagation/env.rs b/opentelemetry/src/propagation/env.rs index ce4d026776..4d8d5799ed 100644 --- a/opentelemetry/src/propagation/env.rs +++ b/opentelemetry/src/propagation/env.rs @@ -199,11 +199,8 @@ where iter.into_iter() .filter_map(|(key, value)| { let key = key.as_ref().to_str()?; - if is_normalized_env_var_name(key) { - Some((key.to_string(), value.as_ref().to_str()?.to_string())) - } else { - None - } + let value = value.as_ref().to_str()?; + is_normalized_env_var_name(key).then(|| (key.to_string(), value.to_string())) }) .collect() }