use std::sync::{Mutex, OnceLock};
use crate::config::TelemetryConfig;
use crate::errors::TelemetryError;
use crate::otel::{flush_otel, setup_otel, shutdown_otel};
use crate::policies::apply_policies;
use crate::runtime::{get_runtime_config, set_active_config};
#[derive(Clone, Copy, Debug, Default)]
struct SetupState {
done: bool,
}
static SETUP_STATE: OnceLock<Mutex<SetupState>> = OnceLock::new();
#[cfg_attr(test, mutants::skip)] fn default_setup_state_mutex() -> Mutex<SetupState> {
Mutex::new(SetupState::default())
}
fn setup_state() -> &'static Mutex<SetupState> {
SETUP_STATE.get_or_init(default_setup_state_mutex)
}
pub fn setup_telemetry(config: Option<TelemetryConfig>) -> Result<TelemetryConfig, TelemetryError> {
let mut state = crate::_lock::lock(setup_state());
if state.done {
return get_runtime_config()
.ok_or_else(|| TelemetryError::new("telemetry setup state is inconsistent"));
}
let config = match config {
Some(explicit) => explicit,
None => TelemetryConfig::from_env().map_err(|err| TelemetryError::new(err.message))?,
};
config
.validate()
.map_err(|err| TelemetryError::new(err.message))?;
setup_otel(&config)?;
apply_policies(&config);
set_active_config(Some(config.clone()));
state.done = true;
Ok(config)
}
pub fn flush_telemetry(timeout_seconds: Option<f64>) -> Result<(), TelemetryError> {
match flush_otel(timeout_seconds) {
crate::otel::DrainOutcome::Drained => Ok(()),
crate::otel::DrainOutcome::Failed => Err(TelemetryError::new(
"telemetry flush failed: an exporter rejected the drain; records may not have been exported",
)),
crate::otel::DrainOutcome::TimedOut => Err(TelemetryError::new(
"telemetry flush exceeded its deadline; records may not have been exported",
)),
}
}
pub fn shutdown_telemetry(timeout_seconds: Option<f64>) -> Result<(), TelemetryError> {
{
let mut state = crate::_lock::lock(setup_state());
state.done = false;
}
shutdown_otel(timeout_seconds);
set_active_config(None);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::acquire_test_state_lock;
#[test]
fn an_explicit_config_is_installed_and_reported_back() {
let _guard = acquire_test_state_lock();
shutdown_telemetry(None).expect("pre-test shutdown should succeed");
let cfg = TelemetryConfig {
service_name: "explicit-setup".to_string(),
..Default::default()
};
let got = setup_telemetry(Some(cfg)).expect("a valid explicit config should install");
assert_eq!(got.service_name, "explicit-setup");
assert_eq!(
get_runtime_config()
.expect("an explicit setup should leave a runtime config")
.service_name,
"explicit-setup"
);
shutdown_telemetry(None).expect("shutdown should succeed");
}
#[test]
fn an_invalid_explicit_config_is_rejected_before_install() {
let _guard = acquire_test_state_lock();
shutdown_telemetry(None).expect("pre-test shutdown should succeed");
let mut cfg = TelemetryConfig::default();
cfg.sampling.logs_rate = 2.0;
let err = setup_telemetry(Some(cfg)).expect_err("a rate above one must be rejected");
assert!(
err.message.contains("PROVIDE_SAMPLING_LOGS_RATE"),
"unexpected message: {}",
err.message
);
assert!(
get_runtime_config().is_none(),
"a rejected config must not be installed"
);
shutdown_telemetry(None).expect("shutdown should succeed");
}
#[test]
fn flush_is_ok_when_nothing_is_installed() {
let _guard = acquire_test_state_lock();
shutdown_telemetry(None).expect("pre-test shutdown should succeed");
flush_telemetry(None).expect("flush with no providers should succeed");
}
#[test]
fn flush_leaves_telemetry_set_up_and_repeatable() {
let _guard = acquire_test_state_lock();
shutdown_telemetry(None).expect("pre-test shutdown should succeed");
let config = setup_telemetry(None).expect("setup should succeed");
flush_telemetry(None).expect("first flush should succeed");
flush_telemetry(None).expect("second flush should succeed");
assert_eq!(
get_runtime_config().expect("runtime config should survive a flush"),
config
);
shutdown_telemetry(None).expect("shutdown should succeed");
}
#[test]
fn setup_test_round_trip_sets_and_clears_runtime_state() {
let _guard = acquire_test_state_lock();
shutdown_telemetry(None).expect("pre-test shutdown should succeed");
let config = setup_telemetry(None).expect("setup should succeed");
assert_eq!(
get_runtime_config().expect("runtime config should exist"),
config
);
assert!(crate::_lock::lock(setup_state()).done);
shutdown_telemetry(None).expect("shutdown should succeed");
assert!(get_runtime_config().is_none());
assert!(!crate::_lock::lock(setup_state()).done);
}
#[test]
fn setup_test_repeated_setup_returns_existing_runtime_config() {
let _guard = acquire_test_state_lock();
shutdown_telemetry(None).expect("pre-test shutdown should succeed");
let first = setup_telemetry(None).expect("first setup should succeed");
let second = setup_telemetry(None).expect("second setup should return existing config");
assert_eq!(first, second);
shutdown_telemetry(None).expect("shutdown should succeed");
}
#[test]
fn setup_test_inconsistent_done_state_returns_error() {
let _guard = acquire_test_state_lock();
shutdown_telemetry(None).expect("pre-test shutdown should succeed");
set_active_config(None);
crate::_lock::lock(setup_state()).done = true;
let err = setup_telemetry(None).expect_err("inconsistent state must fail");
assert!(
err.message.contains("inconsistent"),
"unexpected error: {}",
err.message
);
crate::_lock::lock(setup_state()).done = false;
}
#[test]
fn setup_test_invalid_env_surfaces_parse_error() {
let _guard = acquire_test_state_lock();
shutdown_telemetry(None).expect("pre-test shutdown should succeed");
std::env::set_var("PROVIDE_LOG_INCLUDE_TIMESTAMP", "not-a-bool");
let err = setup_telemetry(None).expect_err("invalid env must fail setup");
assert!(err.message.contains("PROVIDE_LOG_INCLUDE_TIMESTAMP"));
std::env::remove_var("PROVIDE_LOG_INCLUDE_TIMESTAMP");
}
#[cfg(feature = "otel")]
#[test]
fn setup_test_invalid_otel_endpoint_surfaces_setup_error() {
let _guard = acquire_test_state_lock();
shutdown_telemetry(None).expect("pre-test shutdown should succeed");
std::env::set_var("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", "ftp://collector:4318");
std::env::set_var("PROVIDE_EXPORTER_LOGS_FAIL_OPEN", "false");
let err = setup_telemetry(None).expect_err("invalid OTEL endpoint must fail setup");
assert!(err.message.contains("scheme"));
std::env::remove_var("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT");
std::env::remove_var("PROVIDE_EXPORTER_LOGS_FAIL_OPEN");
}
}