provide_telemetry/
setup.rs1use std::sync::{Mutex, OnceLock};
7
8use crate::config::TelemetryConfig;
9use crate::errors::TelemetryError;
10use crate::otel::{flush_otel, setup_otel, shutdown_otel};
11use crate::policies::apply_policies;
12use crate::runtime::{get_runtime_config, set_active_config};
13
14#[derive(Clone, Copy, Debug, Default)]
15struct SetupState {
16 done: bool,
17}
18
19static SETUP_STATE: OnceLock<Mutex<SetupState>> = OnceLock::new();
20
21#[cfg_attr(test, mutants::skip)] fn default_setup_state_mutex() -> Mutex<SetupState> {
23 Mutex::new(SetupState::default())
24}
25
26fn setup_state() -> &'static Mutex<SetupState> {
27 SETUP_STATE.get_or_init(default_setup_state_mutex)
28}
29
30pub fn setup_telemetry(config: Option<TelemetryConfig>) -> Result<TelemetryConfig, TelemetryError> {
38 let mut state = crate::_lock::lock(setup_state());
39 if state.done {
40 return get_runtime_config()
41 .ok_or_else(|| TelemetryError::new("telemetry setup state is inconsistent"));
42 }
43
44 let config = match config {
45 Some(explicit) => explicit,
46 None => TelemetryConfig::from_env().map_err(|err| TelemetryError::new(err.message))?,
47 };
48 config
53 .validate()
54 .map_err(|err| TelemetryError::new(err.message))?;
55 setup_otel(&config)?;
56 apply_policies(&config);
57 set_active_config(Some(config.clone()));
58 state.done = true;
59 Ok(config)
60}
61
62pub fn flush_telemetry(timeout_seconds: Option<f64>) -> Result<(), TelemetryError> {
79 match flush_otel(timeout_seconds) {
80 crate::otel::DrainOutcome::Drained => Ok(()),
81 crate::otel::DrainOutcome::Failed => Err(TelemetryError::new(
85 "telemetry flush failed: an exporter rejected the drain; records may not have been exported",
86 )),
87 crate::otel::DrainOutcome::TimedOut => Err(TelemetryError::new(
88 "telemetry flush exceeded its deadline; records may not have been exported",
89 )),
90 }
91}
92
93pub fn shutdown_telemetry(timeout_seconds: Option<f64>) -> Result<(), TelemetryError> {
103 {
104 let mut state = crate::_lock::lock(setup_state());
105 state.done = false;
106 }
107 shutdown_otel(timeout_seconds);
108 set_active_config(None);
109 Ok(())
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 use crate::testing::acquire_test_state_lock;
117
118 #[test]
122 fn an_explicit_config_is_installed_and_reported_back() {
123 let _guard = acquire_test_state_lock();
124 shutdown_telemetry(None).expect("pre-test shutdown should succeed");
125
126 let cfg = TelemetryConfig {
127 service_name: "explicit-setup".to_string(),
128 ..Default::default()
129 };
130
131 let got = setup_telemetry(Some(cfg)).expect("a valid explicit config should install");
132
133 assert_eq!(got.service_name, "explicit-setup");
134 assert_eq!(
137 get_runtime_config()
138 .expect("an explicit setup should leave a runtime config")
139 .service_name,
140 "explicit-setup"
141 );
142
143 shutdown_telemetry(None).expect("shutdown should succeed");
144 }
145
146 #[test]
150 fn an_invalid_explicit_config_is_rejected_before_install() {
151 let _guard = acquire_test_state_lock();
152 shutdown_telemetry(None).expect("pre-test shutdown should succeed");
153
154 let mut cfg = TelemetryConfig::default();
155 cfg.sampling.logs_rate = 2.0;
156
157 let err = setup_telemetry(Some(cfg)).expect_err("a rate above one must be rejected");
158
159 assert!(
160 err.message.contains("PROVIDE_SAMPLING_LOGS_RATE"),
161 "unexpected message: {}",
162 err.message
163 );
164 assert!(
165 get_runtime_config().is_none(),
166 "a rejected config must not be installed"
167 );
168
169 shutdown_telemetry(None).expect("shutdown should succeed");
170 }
171
172 #[test]
173 fn flush_is_ok_when_nothing_is_installed() {
174 let _guard = acquire_test_state_lock();
175 shutdown_telemetry(None).expect("pre-test shutdown should succeed");
176
177 flush_telemetry(None).expect("flush with no providers should succeed");
180 }
181
182 #[test]
183 fn flush_leaves_telemetry_set_up_and_repeatable() {
184 let _guard = acquire_test_state_lock();
185 shutdown_telemetry(None).expect("pre-test shutdown should succeed");
186 let config = setup_telemetry(None).expect("setup should succeed");
187
188 flush_telemetry(None).expect("first flush should succeed");
189 flush_telemetry(None).expect("second flush should succeed");
190
191 assert_eq!(
193 get_runtime_config().expect("runtime config should survive a flush"),
194 config
195 );
196 shutdown_telemetry(None).expect("shutdown should succeed");
197 }
198
199 #[test]
200 fn setup_test_round_trip_sets_and_clears_runtime_state() {
201 let _guard = acquire_test_state_lock();
202 shutdown_telemetry(None).expect("pre-test shutdown should succeed");
203
204 let config = setup_telemetry(None).expect("setup should succeed");
205 assert_eq!(
206 get_runtime_config().expect("runtime config should exist"),
207 config
208 );
209 assert!(crate::_lock::lock(setup_state()).done);
210
211 shutdown_telemetry(None).expect("shutdown should succeed");
212 assert!(get_runtime_config().is_none());
213 assert!(!crate::_lock::lock(setup_state()).done);
214 }
215
216 #[test]
217 fn setup_test_repeated_setup_returns_existing_runtime_config() {
218 let _guard = acquire_test_state_lock();
219 shutdown_telemetry(None).expect("pre-test shutdown should succeed");
220
221 let first = setup_telemetry(None).expect("first setup should succeed");
222 let second = setup_telemetry(None).expect("second setup should return existing config");
223
224 assert_eq!(first, second);
225 shutdown_telemetry(None).expect("shutdown should succeed");
226 }
227
228 #[test]
229 fn setup_test_inconsistent_done_state_returns_error() {
230 let _guard = acquire_test_state_lock();
231 shutdown_telemetry(None).expect("pre-test shutdown should succeed");
232 set_active_config(None);
233 crate::_lock::lock(setup_state()).done = true;
234
235 let err = setup_telemetry(None).expect_err("inconsistent state must fail");
236 assert!(
237 err.message.contains("inconsistent"),
238 "unexpected error: {}",
239 err.message
240 );
241
242 crate::_lock::lock(setup_state()).done = false;
243 }
244
245 #[test]
246 fn setup_test_invalid_env_surfaces_parse_error() {
247 let _guard = acquire_test_state_lock();
248 shutdown_telemetry(None).expect("pre-test shutdown should succeed");
249 std::env::set_var("PROVIDE_LOG_INCLUDE_TIMESTAMP", "not-a-bool");
250
251 let err = setup_telemetry(None).expect_err("invalid env must fail setup");
252 assert!(err.message.contains("PROVIDE_LOG_INCLUDE_TIMESTAMP"));
253
254 std::env::remove_var("PROVIDE_LOG_INCLUDE_TIMESTAMP");
255 }
256
257 #[cfg(feature = "otel")]
258 #[test]
259 fn setup_test_invalid_otel_endpoint_surfaces_setup_error() {
260 let _guard = acquire_test_state_lock();
261 shutdown_telemetry(None).expect("pre-test shutdown should succeed");
262 std::env::set_var("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", "ftp://collector:4318");
263 std::env::set_var("PROVIDE_EXPORTER_LOGS_FAIL_OPEN", "false");
264
265 let err = setup_telemetry(None).expect_err("invalid OTEL endpoint must fail setup");
266 assert!(err.message.contains("scheme"));
267
268 std::env::remove_var("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT");
269 std::env::remove_var("PROVIDE_EXPORTER_LOGS_FAIL_OPEN");
270 }
271}