From 5ee15020deb536c056a1d3a55bc287f1e9e11ca9 Mon Sep 17 00:00:00 2001 From: Saurav Sharma Date: Tue, 5 Aug 2025 09:14:00 +0545 Subject: [PATCH] feat: add support for environment variable Signed-off-by: Saurav Sharma --- opentelemetry-sdk/CHANGELOG.md | 2 + opentelemetry-sdk/src/logs/logger.rs | 5 ++ opentelemetry-sdk/src/logs/logger_provider.rs | 72 ++++++++++++++-- .../src/metrics/meter_provider.rs | 32 +++++++- opentelemetry-sdk/src/trace/provider.rs | 82 +++++++++++++++++-- scripts/test.sh | 3 + 6 files changed, 182 insertions(+), 14 deletions(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 32fa12db65..400e2f417d 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -13,6 +13,8 @@ The logs functionality now operates independently, while automatic correlation between logs and traces continues to work when the "trace" feature is explicitly enabled. +- **Feature**: Add support for `OTEL_SDK_DISABLED` environment variable ([#3088](https://github.com/open-telemetry/opentelemetry-rust/pull/3088)) + ## 0.30.0 diff --git a/opentelemetry-sdk/src/logs/logger.rs b/opentelemetry-sdk/src/logs/logger.rs index 860922adf0..752137b4ad 100644 --- a/opentelemetry-sdk/src/logs/logger.rs +++ b/opentelemetry-sdk/src/logs/logger.rs @@ -22,6 +22,11 @@ impl SdkLogger { pub(crate) fn new(scope: InstrumentationScope, provider: SdkLoggerProvider) -> Self { SdkLogger { scope, provider } } + + #[cfg(test)] + pub(crate) fn provider(&self) -> &SdkLoggerProvider { + &self.provider + } } impl opentelemetry::logs::Logger for SdkLogger { diff --git a/opentelemetry-sdk/src/logs/logger_provider.rs b/opentelemetry-sdk/src/logs/logger_provider.rs index e7de152807..513a528e53 100644 --- a/opentelemetry-sdk/src/logs/logger_provider.rs +++ b/opentelemetry-sdk/src/logs/logger_provider.rs @@ -2,7 +2,7 @@ use super::{BatchLogProcessor, LogProcessor, SdkLogger, SimpleLogProcessor}; use crate::error::{OTelSdkError, OTelSdkResult}; use crate::logs::LogExporter; use crate::Resource; -use opentelemetry::{otel_debug, otel_info, InstrumentationScope}; +use opentelemetry::{otel_debug, otel_info, otel_warn, InstrumentationScope}; use std::time::Duration; use std::{ borrow::Cow, @@ -12,16 +12,32 @@ use std::{ }, }; -// a no nop logger provider used as placeholder when the provider is shutdown +// a no op logger provider used as placeholder when the provider is shutdown // TODO - replace it with LazyLock once it is stable -static NOOP_LOGGER_PROVIDER: OnceLock = OnceLock::new(); +static SHUTDOWN_LOGGER_PROVIDER: OnceLock = OnceLock::new(); #[inline] -fn noop_logger_provider() -> &'static SdkLoggerProvider { - NOOP_LOGGER_PROVIDER.get_or_init(|| SdkLoggerProvider { +fn shutdown_logger_provider() -> &'static SdkLoggerProvider { + SHUTDOWN_LOGGER_PROVIDER.get_or_init(|| SdkLoggerProvider { inner: Arc::new(LoggerProviderInner { processors: Vec::new(), is_shutdown: AtomicBool::new(true), + is_disabled: false, + }), + }) +} +// a no op logger provider used as placeholder when sdk is disabled with +// help of environment variable `OTEL_SDK_DISABLED` +// TODO - replace it with LazyLock once it is stable +static DISABLED_LOGGER_PROVIDER: OnceLock = OnceLock::new(); + +#[inline] +fn disabled_logger_provider() -> &'static SdkLoggerProvider { + DISABLED_LOGGER_PROVIDER.get_or_init(|| SdkLoggerProvider { + inner: Arc::new(LoggerProviderInner { + processors: Vec::new(), + is_shutdown: AtomicBool::new(false), + is_disabled: true, }), }) } @@ -53,13 +69,18 @@ impl opentelemetry::logs::LoggerProvider for SdkLoggerProvider { } fn logger_with_scope(&self, scope: InstrumentationScope) -> Self::Logger { - // If the provider is shutdown, new logger will refer a no-op logger provider. + // If the provider is shutdown, new logger will refer a shutdown no-op logger provider. if self.inner.is_shutdown.load(Ordering::Relaxed) { otel_debug!( name: "LoggerProvider.NoOpLoggerReturned", logger_name = scope.name(), ); - return SdkLogger::new(scope, noop_logger_provider().clone()); + return SdkLogger::new(scope, shutdown_logger_provider().clone()); + } + // If the provider is disabled, new logger will refer a disabled no-op logger provider. + if self.inner.is_disabled { + otel_warn!(name: "LoggerProvider.NoOpLoggerReturned", message = "Returned NoOpLogger. SDK is disabled"); + return SdkLogger::new(scope, disabled_logger_provider().clone()); } if scope.name().is_empty() { otel_info!(name: "LoggerNameEmpty", message = "Logger name is empty; consider providing a meaningful name. Logger will function normally and the provided name will be used as-is."); @@ -135,6 +156,7 @@ impl SdkLoggerProvider { struct LoggerProviderInner { processors: Vec>, is_shutdown: AtomicBool, + is_disabled: bool, } impl LoggerProviderInner { @@ -267,10 +289,18 @@ impl LoggerProviderBuilder { processor.set_resource(&resource); } + let is_disabled = + std::env::var("OTEL_SDK_DISABLED").is_ok_and(|var| var.to_lowercase() == "true"); + + if is_disabled { + otel_warn!(name: "LoggerProvider.Disabled", message = "SDK is disabled through environment variable"); + } + let logger_provider = SdkLoggerProvider { inner: Arc::new(LoggerProviderInner { processors, is_shutdown: AtomicBool::new(false), + is_disabled, }), }; @@ -749,6 +779,7 @@ mod tests { flush_called.clone(), ))], is_shutdown: AtomicBool::new(false), + is_disabled: false, }); { @@ -777,6 +808,32 @@ mod tests { assert!(!*flush_called.lock().unwrap()); } + #[test] + #[ignore = "modifies OTEL_SDK_DISABLED env var which can affect other test"] + fn otel_sdk_disabled_env() { + temp_env::with_var("OTEL_SDK_DISABLED", Some("true"), || { + let exporter = InMemoryLogExporter::default(); + let logger_provider = SdkLoggerProvider::builder() + .with_simple_exporter(exporter.clone()) + .build(); + let logger = logger_provider.logger("noop"); + let mut record = logger.create_log_record(); + record.set_body("Testing sdk disabled logger".into()); + logger.emit(record); + let mut record = logger.create_log_record(); + record.set_body("Testing sdk disabled logger".into()); + logger.emit(record); + let mut record = logger.create_log_record(); + record.set_body("Testing sdk disabled logger".into()); + logger.emit(record); + let emitted_logs = exporter.get_emitted_logs().unwrap(); + assert_eq!(emitted_logs.len(), 0); + + assert!(logger.provider().shutdown().is_ok()); + assert!(logger.provider().shutdown().is_err()); + }); + } + #[test] fn drop_after_shutdown_test_with_multiple_providers() { let shutdown_called = Arc::new(Mutex::new(0)); // Count the number of times shutdown is called @@ -789,6 +846,7 @@ mod tests { flush_called.clone(), ))], is_shutdown: AtomicBool::new(false), + is_disabled: false, }); // Create a scope to test behavior when providers are dropped diff --git a/opentelemetry-sdk/src/metrics/meter_provider.rs b/opentelemetry-sdk/src/metrics/meter_provider.rs index 5b5b4f8f16..66a11ad763 100644 --- a/opentelemetry-sdk/src/metrics/meter_provider.rs +++ b/opentelemetry-sdk/src/metrics/meter_provider.rs @@ -1,7 +1,7 @@ use core::fmt; use opentelemetry::{ metrics::{Meter, MeterProvider}, - otel_debug, otel_error, otel_info, InstrumentationScope, + otel_debug, otel_error, otel_info, otel_warn, InstrumentationScope, }; use std::time::Duration; use std::{ @@ -41,6 +41,7 @@ struct SdkMeterProviderInner { pipes: Arc, meters: Mutex>>, shutdown_invoked: AtomicBool, + is_disabled: bool, } impl Default for SdkMeterProvider { @@ -196,6 +197,10 @@ impl MeterProvider for SdkMeterProvider { ); return Meter::new(Arc::new(NoopMeter::new())); } + if self.inner.is_disabled { + otel_warn!(name: "MeterProvider.NoOpMeterReturned", message = "Returned NoOpMeter. SDK is disabled"); + return Meter::new(Arc::new(NoopMeter::new())); + } if scope.name().is_empty() { otel_info!(name: "MeterNameEmpty", message = "Meter name is empty; consider providing a meaningful name. Meter will function normally and the provided name will be used as-is."); @@ -379,6 +384,13 @@ impl MeterProviderBuilder { builder = format!("{:?}", &self), ); + let is_disabled = + std::env::var("OTEL_SDK_DISABLED").is_ok_and(|var| var.to_lowercase() == "true"); + + if is_disabled { + otel_warn!(name: "MeterProvider.Disabled", message = "SDK is disabled through environment variable"); + } + let meter_provider = SdkMeterProvider { inner: Arc::new(SdkMeterProviderInner { pipes: Arc::new(Pipelines::new( @@ -388,6 +400,7 @@ impl MeterProviderBuilder { )), meters: Default::default(), shutdown_invoked: AtomicBool::new(false), + is_disabled, }), }; @@ -729,4 +742,21 @@ mod tests { ); assert_eq!(resource.schema_url(), Some("http://example.com")); } + + #[test] + #[ignore = "modifies OTEL_SDK_DISABLED env var which can affect other test"] + fn otel_sdk_disabled_env() { + temp_env::with_var("OTEL_SDK_DISABLED", Some("true"), || { + let meter_provider = super::SdkMeterProvider::builder().build(); + let _ = meter_provider.meter("noop1"); + let _ = meter_provider.meter("noop2"); + let _ = meter_provider.meter("noop3"); + let _ = meter_provider.meter("noop4"); + let _ = meter_provider.meter("noop5"); + + assert_eq!(meter_provider.inner.meters.lock().unwrap().len(), 0); + assert!(meter_provider.shutdown().is_ok()); + assert!(meter_provider.shutdown().is_err()); + }); + } } diff --git a/opentelemetry-sdk/src/trace/provider.rs b/opentelemetry-sdk/src/trace/provider.rs index 2b05f89aea..51ce63bb9b 100644 --- a/opentelemetry-sdk/src/trace/provider.rs +++ b/opentelemetry-sdk/src/trace/provider.rs @@ -70,7 +70,7 @@ use crate::trace::{ }; use crate::Resource; use crate::{trace::SpanExporter, trace::SpanProcessor}; -use opentelemetry::otel_debug; +use opentelemetry::{otel_debug, otel_warn}; use opentelemetry::{otel_info, InstrumentationScope}; use std::borrow::Cow; use std::sync::atomic::{AtomicBool, Ordering}; @@ -79,12 +79,12 @@ use std::time::Duration; static PROVIDER_RESOURCE: OnceLock = OnceLock::new(); -// a no nop tracer provider used as placeholder when the provider is shutdown +// a no op tracer provider used as placeholder when the provider is shutdown // TODO Replace with LazyLock once it is stable -static NOOP_TRACER_PROVIDER: OnceLock = OnceLock::new(); +static SHUTDOWN_TRACER_PROVIDER: OnceLock = OnceLock::new(); #[inline] -fn noop_tracer_provider() -> &'static SdkTracerProvider { - NOOP_TRACER_PROVIDER.get_or_init(|| { +fn shutdown_tracer_provider() -> &'static SdkTracerProvider { + SHUTDOWN_TRACER_PROVIDER.get_or_init(|| { SdkTracerProvider { inner: Arc::new(TracerProviderInner { processors: Vec::new(), @@ -96,6 +96,31 @@ fn noop_tracer_provider() -> &'static SdkTracerProvider { resource: Cow::Owned(Resource::empty()), }, is_shutdown: AtomicBool::new(true), + is_disabled: false, + }), + } + }) +} + +// a no op tracer provider used as placeholder when sdk is disabled with +// help of environment variable `OTEL_SDK_DISABLED` +// TODO Replace with LazyLock once it is stable +static DISABLED_TRACER_PROVIDER: OnceLock = OnceLock::new(); +#[inline] +fn disabled_tracer_provider() -> &'static SdkTracerProvider { + DISABLED_TRACER_PROVIDER.get_or_init(|| { + SdkTracerProvider { + inner: Arc::new(TracerProviderInner { + processors: Vec::new(), + config: Config { + // cannot use default here as the default resource is not empty + sampler: Box::new(Sampler::ParentBased(Box::new(Sampler::AlwaysOn))), + id_generator: Box::::default(), + span_limits: SpanLimits::default(), + resource: Cow::Owned(Resource::empty()), + }, + is_shutdown: AtomicBool::new(false), + is_disabled: true, }), } }) @@ -107,6 +132,7 @@ pub(crate) struct TracerProviderInner { processors: Vec>, config: crate::trace::Config, is_shutdown: AtomicBool, + is_disabled: bool, } impl TracerProviderInner { @@ -286,7 +312,11 @@ impl opentelemetry::trace::TracerProvider for SdkTracerProvider { fn tracer_with_scope(&self, scope: InstrumentationScope) -> Self::Tracer { if self.inner.is_shutdown.load(Ordering::Relaxed) { - return SdkTracer::new(scope, noop_tracer_provider().clone()); + return SdkTracer::new(scope, shutdown_tracer_provider().clone()); + } + if self.inner.is_disabled { + otel_warn!(name: "TracerProvider.NoOpTracerReturned", message = "Returned NoOpTracer. SDK is disabled"); + return SdkTracer::new(scope, disabled_tracer_provider().clone()); } if scope.name().is_empty() { otel_info!(name: "TracerNameEmpty", message = "Tracer name is empty; consider providing a meaningful name. Tracer will function normally and the provided name will be used as-is."); @@ -455,11 +485,19 @@ impl TracerProviderBuilder { p.set_resource(config.resource.as_ref()); } + let is_disabled = + std::env::var("OTEL_SDK_DISABLED").is_ok_and(|var| var.to_lowercase() == "true"); + + if is_disabled { + otel_warn!(name: "TracerProvider.Disabled", message = "SDK is disabled through environment variable"); + } + let is_shutdown = AtomicBool::new(false); SdkTracerProvider::new(TracerProviderInner { processors, config, is_shutdown, + is_disabled, }) } } @@ -562,6 +600,7 @@ mod tests { ], config: Default::default(), is_shutdown: AtomicBool::new(false), + is_disabled: false, }); let results = tracer_provider.force_flush(); @@ -705,6 +744,7 @@ mod tests { processors: vec![Box::from(processor)], config: Default::default(), is_shutdown: AtomicBool::new(false), + is_disabled: false, }); let test_tracer_1 = tracer_provider.tracer("test1"); @@ -740,6 +780,34 @@ mod tests { assert!(test_tracer_1.provider().is_shutdown()); } + #[test] + #[ignore = "modifies OTEL_SDK_DISABLED env var which can affect other test"] + fn otel_sdk_disabled_env() { + temp_env::with_var("OTEL_SDK_DISABLED", Some("true"), || { + let processor = TestSpanProcessor::new(true); + let assert_handle = processor.assert_info(); + let tracer_provider = super::SdkTracerProvider::builder() + .with_span_processor(processor) + .build(); + + assert!(assert_handle.started_span_count(0)); + let noop_tracer = tracer_provider.tracer("noop"); + + // noop tracer cannot start anything + let _ = noop_tracer.start("test"); + let _ = noop_tracer.start("test2"); + let _ = noop_tracer.start("test3"); + assert!(assert_handle.started_span_count(0)); + + // noop tracer should have 0 processor + assert_eq!(noop_tracer.provider().span_processors().len(), 0); + + // shutdown noop tracer + assert!(noop_tracer.provider().shutdown().is_ok()); + assert!(noop_tracer.provider().shutdown().is_err()); + }); + } + #[test] fn with_resource_multiple_calls_ensure_additive() { let resource = SdkTracerProvider::builder() @@ -815,6 +883,7 @@ mod tests { ))], config: Config::default(), is_shutdown: AtomicBool::new(false), + is_disabled: false, }); { @@ -853,6 +922,7 @@ mod tests { ))], config: Config::default(), is_shutdown: AtomicBool::new(false), + is_disabled: false, }); // Create a scope to test behavior when providers are dropped diff --git a/scripts/test.sh b/scripts/test.sh index 3eb7e65b8f..01a2971ba3 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -20,6 +20,9 @@ cargo test --manifest-path=opentelemetry/Cargo.toml --no-default-features --lib echo "Running tests for opentelemetry-prometheus with --all-features" (cd opentelemetry-prometheus && cargo test --all-features --lib) +# Run test which set environment variable `OTEL_SDK_DISABLED` +cargo test --package opentelemetry_sdk --lib --all-features -- otel_sdk_disabled_env --ignored + # Run global tracer provider test in single thread # //TODO: This tests were not running for a while. Need to find out how to run # run them. Using --ignored will run other tests as well, so that cannot be used.