Skip to main content

provide_telemetry/
setup.rs

1// SPDX-FileCopyrightText: Copyright (C) 2026 provide.io llc
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-Comment: Part of provide-telemetry.
4//
5
6use 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)] // Equivalent mutants only swap in Mutex::default().
22fn 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() -> Result<TelemetryConfig, TelemetryError> {
31    let mut state = crate::_lock::lock(setup_state());
32    if state.done {
33        return get_runtime_config()
34            .ok_or_else(|| TelemetryError::new("telemetry setup state is inconsistent"));
35    }
36
37    let config = TelemetryConfig::from_env().map_err(|err| TelemetryError::new(err.message))?;
38    setup_otel(&config)?;
39    apply_policies(&config);
40    set_active_config(Some(config.clone()));
41    state.done = true;
42    Ok(config)
43}
44
45/// Force-flush installed providers without tearing them down.
46///
47/// The drain half of [`shutdown_telemetry`]: every provider we installed is
48/// force-flushed under the bounded-shutdown deadline
49/// (`PROVIDE_EXPORTER_LOGS_SHUTDOWN_TIMEOUT_SECONDS`) and stays installed and
50/// usable. Use it where records must be out before control returns — a request
51/// boundary, a checkpoint, a serverless freeze — rather than shutting telemetry
52/// down and paying to set it up again.
53///
54/// Returns `Ok(())` when every signal drained within the deadline (including
55/// when nothing is installed) and `Err` when any was abandoned, so a caller
56/// flushing to be sure its records are out learns when they are not.
57pub fn flush_telemetry() -> Result<(), TelemetryError> {
58    if flush_otel() {
59        return Ok(());
60    }
61    Err(TelemetryError::new(
62        "telemetry flush exceeded its deadline; records may not have been exported",
63    ))
64}
65
66pub fn shutdown_telemetry() -> Result<(), TelemetryError> {
67    {
68        let mut state = crate::_lock::lock(setup_state());
69        state.done = false;
70    }
71    shutdown_otel();
72    set_active_config(None);
73    Ok(())
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    use crate::testing::acquire_test_state_lock;
81
82    #[test]
83    fn flush_is_ok_when_nothing_is_installed() {
84        let _guard = acquire_test_state_lock();
85        shutdown_telemetry().expect("pre-test shutdown should succeed");
86
87        // Nothing installed means nothing to drain — a successful no-op, not
88        // an error, so callers can flush unconditionally.
89        flush_telemetry().expect("flush with no providers should succeed");
90    }
91
92    #[test]
93    fn flush_leaves_telemetry_set_up_and_repeatable() {
94        let _guard = acquire_test_state_lock();
95        shutdown_telemetry().expect("pre-test shutdown should succeed");
96        let config = setup_telemetry().expect("setup should succeed");
97
98        flush_telemetry().expect("first flush should succeed");
99        flush_telemetry().expect("second flush should succeed");
100
101        // Unlike shutdown, flush must leave the active runtime config in place.
102        assert_eq!(
103            get_runtime_config().expect("runtime config should survive a flush"),
104            config
105        );
106        shutdown_telemetry().expect("shutdown should succeed");
107    }
108
109    #[test]
110    fn setup_test_round_trip_sets_and_clears_runtime_state() {
111        let _guard = acquire_test_state_lock();
112        shutdown_telemetry().expect("pre-test shutdown should succeed");
113
114        let config = setup_telemetry().expect("setup should succeed");
115        assert_eq!(
116            get_runtime_config().expect("runtime config should exist"),
117            config
118        );
119        assert!(crate::_lock::lock(setup_state()).done);
120
121        shutdown_telemetry().expect("shutdown should succeed");
122        assert!(get_runtime_config().is_none());
123        assert!(!crate::_lock::lock(setup_state()).done);
124    }
125
126    #[test]
127    fn setup_test_repeated_setup_returns_existing_runtime_config() {
128        let _guard = acquire_test_state_lock();
129        shutdown_telemetry().expect("pre-test shutdown should succeed");
130
131        let first = setup_telemetry().expect("first setup should succeed");
132        let second = setup_telemetry().expect("second setup should return existing config");
133
134        assert_eq!(first, second);
135        shutdown_telemetry().expect("shutdown should succeed");
136    }
137
138    #[test]
139    fn setup_test_inconsistent_done_state_returns_error() {
140        let _guard = acquire_test_state_lock();
141        shutdown_telemetry().expect("pre-test shutdown should succeed");
142        set_active_config(None);
143        crate::_lock::lock(setup_state()).done = true;
144
145        let err = setup_telemetry().expect_err("inconsistent state must fail");
146        assert!(
147            err.message.contains("inconsistent"),
148            "unexpected error: {}",
149            err.message
150        );
151
152        crate::_lock::lock(setup_state()).done = false;
153    }
154
155    #[test]
156    fn setup_test_invalid_env_surfaces_parse_error() {
157        let _guard = acquire_test_state_lock();
158        shutdown_telemetry().expect("pre-test shutdown should succeed");
159        std::env::set_var("PROVIDE_LOG_INCLUDE_TIMESTAMP", "not-a-bool");
160
161        let err = setup_telemetry().expect_err("invalid env must fail setup");
162        assert!(err.message.contains("PROVIDE_LOG_INCLUDE_TIMESTAMP"));
163
164        std::env::remove_var("PROVIDE_LOG_INCLUDE_TIMESTAMP");
165    }
166
167    #[cfg(feature = "otel")]
168    #[test]
169    fn setup_test_invalid_otel_endpoint_surfaces_setup_error() {
170        let _guard = acquire_test_state_lock();
171        shutdown_telemetry().expect("pre-test shutdown should succeed");
172        std::env::set_var("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", "ftp://collector:4318");
173        std::env::set_var("PROVIDE_EXPORTER_LOGS_FAIL_OPEN", "false");
174
175        let err = setup_telemetry().expect_err("invalid OTEL endpoint must fail setup");
176        assert!(err.message.contains("scheme"));
177
178        std::env::remove_var("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT");
179        std::env::remove_var("PROVIDE_EXPORTER_LOGS_FAIL_OPEN");
180    }
181}