From 5924a55931cc7d709be013125033602450917f8f Mon Sep 17 00:00:00 2001 From: Rafael Kansy <16208524+Pirat83@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:00:24 +0200 Subject: [PATCH] fix(obs): honor RUSTFS_OBS_USE_STDOUT with per-signal endpoints (#8476) use_stdout was forced to true whenever the root RUSTFS_OBS_ENDPOINT was empty, so a deployment that sets only a per-signal endpoint such as RUSTFS_OBS_TRACE_ENDPOINT could not turn the stdout exporters off: every exported span was also printed to stdout as a multi-line text block. Count a non-empty trace, metric or log endpoint as a configured endpoint. With no endpoint at all the stdout default is still forced on. Add a test covering each per-signal endpoint and an empty value. --- crates/obs/src/config.rs | 57 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/crates/obs/src/config.rs b/crates/obs/src/config.rs index c637719ff..4bc7c7705 100644 --- a/crates/obs/src/config.rs +++ b/crates/obs/src/config.rs @@ -238,8 +238,10 @@ impl OtelConfig { /// that know their display/build version should use /// [`OtelConfig::extract_otel_config_from_env_with_version`]. /// - /// When no endpoint is configured at all, `use_stdout` is forced to `true` - /// so that logs are still visible during development. + /// When no endpoint is configured at all (neither the root endpoint nor any + /// per-signal one), `use_stdout` is forced to `true` so that logs are still + /// visible during development. With any endpoint set, `RUSTFS_OBS_USE_STDOUT` + /// is honoured. /// /// # Example /// ```no_run @@ -268,9 +270,15 @@ impl OtelConfig { _ => env::var(ENV_OBS_ENDPOINT).unwrap_or_default(), }; - // Force stdout when there is no remote endpoint so that operators - // always have *some* log output in the default configuration. - let use_stdout = if endpoint.is_empty() { + // Force stdout only when no OTLP endpoint is configured at all, so that operators + // always have *some* log output in the default configuration. A per-signal endpoint + // (e.g. `RUSTFS_OBS_TRACE_ENDPOINT`) counts as configured: otherwise setting only that + // one forces the stdout exporters on and silently ignores `RUSTFS_OBS_USE_STDOUT=false`, + // printing every exported span to stdout. + let has_signal_endpoint = [ENV_OBS_TRACE_ENDPOINT, ENV_OBS_METRIC_ENDPOINT, ENV_OBS_LOG_ENDPOINT] + .into_iter() + .any(|key| get_env_opt_str(key).is_some_and(|value| !value.is_empty())); + let use_stdout = if endpoint.is_empty() && !has_signal_endpoint { true } else { get_env_bool(ENV_OBS_USE_STDOUT, USE_STDOUT) @@ -538,6 +546,45 @@ mod tests { }); } + #[test] + fn use_stdout_is_forced_only_when_no_endpoint_is_configured() { + with_profiling_env_lock(|| { + temp_env::with_vars_unset( + [ + ENV_OBS_ENDPOINT, + ENV_OBS_TRACE_ENDPOINT, + ENV_OBS_METRIC_ENDPOINT, + ENV_OBS_LOG_ENDPOINT, + ], + || { + temp_env::with_var(ENV_OBS_USE_STDOUT, Some("false"), || { + // No endpoint at all: forced on, the explicit false is ignored. + assert_eq!(OtelConfig::extract_otel_config_from_env(None).use_stdout, Some(true)); + }); + for key in [ENV_OBS_TRACE_ENDPOINT, ENV_OBS_METRIC_ENDPOINT, ENV_OBS_LOG_ENDPOINT] { + temp_env::with_var(key, Some("http://localhost:4318/v1/signal"), || { + temp_env::with_var(ENV_OBS_USE_STDOUT, Some("false"), || { + assert_eq!(OtelConfig::extract_otel_config_from_env(None).use_stdout, Some(false), "{key}"); + }); + temp_env::with_var(ENV_OBS_USE_STDOUT, Some("true"), || { + assert_eq!(OtelConfig::extract_otel_config_from_env(None).use_stdout, Some(true), "{key}"); + }); + temp_env::with_var_unset(ENV_OBS_USE_STDOUT, || { + assert_eq!(OtelConfig::extract_otel_config_from_env(None).use_stdout, Some(USE_STDOUT), "{key}"); + }); + }); + } + temp_env::with_var(ENV_OBS_TRACE_ENDPOINT, Some(""), || { + temp_env::with_var(ENV_OBS_USE_STDOUT, Some("false"), || { + // An empty per-signal endpoint is not a configured endpoint. + assert_eq!(OtelConfig::extract_otel_config_from_env(None).use_stdout, Some(true)); + }); + }); + }, + ); + }); + } + #[test] fn stdout_mirror_environment_preserves_unset_true_and_false() { with_profiling_env_lock(|| {