1use std::sync::{OnceLock, RwLock};
7
8use serde::{Deserialize, Serialize};
9
10use crate::config::TelemetryConfig;
11use crate::errors::TelemetryError;
12#[cfg(feature = "otel")]
13use crate::otel::otel_installed;
14use crate::policies::apply_policies;
15use crate::RuntimeOverrides;
16
17static ACTIVE_CONFIG: OnceLock<RwLock<Option<TelemetryConfig>>> = OnceLock::new();
18#[cfg(feature = "otel")]
19const PROVIDER_CHANGE_RESTART_MESSAGE: &str =
20 "OpenTelemetry providers already installed; restart the process for provider-changing config";
21
22fn empty_active_config() -> RwLock<Option<TelemetryConfig>> {
23 RwLock::new(None)
24}
25
26fn active_config() -> &'static RwLock<Option<TelemetryConfig>> {
27 ACTIVE_CONFIG.get_or_init(empty_active_config)
28}
29
30#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
31pub struct SignalStatus {
32 pub logs: bool,
33 pub traces: bool,
34 pub metrics: bool,
35}
36
37#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
38pub struct RuntimeStatus {
39 pub setup_done: bool,
40 pub signals: SignalStatus,
41 pub providers: SignalStatus,
42 pub fallback: SignalStatus,
43 pub setup_error: Option<String>,
44}
45
46pub(crate) fn set_active_config(config: Option<TelemetryConfig>) {
47 *crate::_lock::rwlock_write(active_config()) = config;
48}
49
50#[cfg(any(feature = "otel", test))]
53fn identity_config_changed(current: &TelemetryConfig, target: &TelemetryConfig) -> bool {
54 current.service_name != target.service_name
55 || current.environment != target.environment
56 || current.version != target.version
57}
58
59#[cfg(any(feature = "otel", test))]
61fn logging_provider_config_changed(current: &TelemetryConfig, target: &TelemetryConfig) -> bool {
62 current.logging.otlp_endpoint != target.logging.otlp_endpoint
63 || current.logging.otlp_headers != target.logging.otlp_headers
64 || current.logging.otlp_protocol != target.logging.otlp_protocol
65 || current.exporter.logs_timeout_seconds != target.exporter.logs_timeout_seconds
66}
67
68#[cfg(any(feature = "otel", test))]
70fn tracing_provider_config_changed(current: &TelemetryConfig, target: &TelemetryConfig) -> bool {
71 current.tracing.enabled != target.tracing.enabled
72 || current.tracing.otlp_endpoint != target.tracing.otlp_endpoint
73 || current.tracing.otlp_headers != target.tracing.otlp_headers
74 || current.tracing.otlp_protocol != target.tracing.otlp_protocol
75 || current.exporter.traces_timeout_seconds != target.exporter.traces_timeout_seconds
76}
77
78#[cfg(any(feature = "otel", test))]
80fn metrics_provider_config_changed(current: &TelemetryConfig, target: &TelemetryConfig) -> bool {
81 current.metrics.enabled != target.metrics.enabled
82 || current.metrics.otlp_endpoint != target.metrics.otlp_endpoint
83 || current.metrics.otlp_headers != target.metrics.otlp_headers
84 || current.metrics.otlp_protocol != target.metrics.otlp_protocol
85 || current.metrics.metric_export_interval_ms != target.metrics.metric_export_interval_ms
86 || current.exporter.metrics_timeout_seconds != target.exporter.metrics_timeout_seconds
87}
88
89#[cfg(test)]
93pub(crate) fn provider_config_changed(current: &TelemetryConfig, target: &TelemetryConfig) -> bool {
94 identity_config_changed(current, target)
95 || logging_provider_config_changed(current, target)
96 || tracing_provider_config_changed(current, target)
97 || metrics_provider_config_changed(current, target)
98}
99
100pub fn get_runtime_config() -> Option<TelemetryConfig> {
101 crate::_lock::rwlock_read(active_config()).clone()
102}
103
104fn compute_cold_drift(current: &TelemetryConfig, fresh: &TelemetryConfig) -> Vec<&'static str> {
108 let mut drifted: Vec<&'static str> = Vec::new();
109 if current.service_name != fresh.service_name {
110 drifted.push("service_name");
111 }
112 if current.environment != fresh.environment {
113 drifted.push("environment");
114 }
115 if current.version != fresh.version {
116 drifted.push("version");
117 }
118 if current.tracing.enabled != fresh.tracing.enabled {
119 drifted.push("tracing.enabled");
120 }
121 if current.metrics.enabled != fresh.metrics.enabled {
122 drifted.push("metrics.enabled");
123 }
124 drifted
125}
126
127fn runtime_config_snapshot() -> (Option<TelemetryConfig>, bool) {
128 let guard = crate::_lock::rwlock_read(active_config());
129 let cfg = guard.clone();
130 (cfg.clone(), cfg.is_some())
131}
132
133pub fn get_runtime_status() -> RuntimeStatus {
134 let (cfg, setup_done) = runtime_config_snapshot();
135 let cfg = runtime_config_or_default(cfg);
136
137 #[cfg(feature = "otel")]
138 let providers = SignalStatus {
139 logs: crate::otel::logs::logger_provider_installed(),
140 traces: crate::otel::traces::tracer_provider_installed(),
141 metrics: crate::otel::metrics::meter_provider_installed(),
142 };
143
144 #[cfg(not(feature = "otel"))]
145 let providers = SignalStatus {
146 logs: false,
147 traces: false,
148 metrics: false,
149 };
150
151 RuntimeStatus {
152 setup_done,
153 signals: SignalStatus {
154 logs: true,
155 traces: cfg.tracing.enabled,
156 metrics: cfg.metrics.enabled,
157 },
158 fallback: SignalStatus {
159 logs: !providers.logs,
160 traces: !providers.traces,
161 metrics: !providers.metrics,
162 },
163 providers,
164 setup_error: crate::health::get_health_snapshot().setup_error,
165 }
166}
167
168pub fn update_runtime_config(
169 overrides: RuntimeOverrides,
170) -> Result<TelemetryConfig, TelemetryError> {
171 let logging_override = overrides.logging.clone();
172 let next = {
173 let mut guard = crate::_lock::rwlock_write(active_config());
174 let current = match guard.as_ref().cloned() {
175 Some(current) => current,
176 None => {
177 return Err(TelemetryError::new(
178 "telemetry not set up: call setup_telemetry first",
179 ));
180 }
181 };
182 let next = apply_runtime_overrides(current, overrides);
183 *guard = Some(next.clone());
184 next
185 }; apply_policies(&next);
187 if let Some(cfg) = logging_override {
193 crate::logger::configure_logging(cfg);
194 }
195 Ok(next)
196}
197
198pub fn reload_runtime_from_env() -> Result<TelemetryConfig, TelemetryError> {
199 let fresh = match TelemetryConfig::from_env() {
200 Ok(fresh) => fresh,
201 Err(err) => return Err(TelemetryError::new(err.message)),
202 };
203 let current = match get_runtime_config() {
204 Some(current) => current,
205 None => {
206 return Err(TelemetryError::new(
207 "telemetry not set up: call setup_telemetry first",
208 ))
209 }
210 };
211
212 let drifted = compute_cold_drift(¤t, &fresh);
214 if !drifted.is_empty() {
215 eprintln!(
216 "[provide-telemetry] runtime.cold_field_drift: {} — restart required to apply",
217 drifted.join(", ")
218 );
219 }
220
221 #[allow(unused_mut)] let mut hot_exporter = fresh.exporter;
228 #[cfg(feature = "otel")]
229 {
230 if crate::otel::logs::logger_provider_installed() {
231 hot_exporter.logs_timeout_seconds = current.exporter.logs_timeout_seconds;
232 }
233 if crate::otel::traces::tracer_provider_installed() {
234 hot_exporter.traces_timeout_seconds = current.exporter.traces_timeout_seconds;
235 }
236 if crate::otel::metrics::meter_provider_installed() {
237 hot_exporter.metrics_timeout_seconds = current.exporter.metrics_timeout_seconds;
238 }
239 }
240
241 #[allow(unused_mut)] let mut hot_logging = fresh.logging.clone();
248 #[cfg(feature = "otel")]
249 {
250 if crate::otel::logs::logger_provider_installed() {
251 hot_logging.otlp_endpoint = current.logging.otlp_endpoint.clone();
252 hot_logging.otlp_headers = current.logging.otlp_headers.clone();
253 hot_logging.otlp_protocol = current.logging.otlp_protocol.clone();
254 }
255 }
256
257 let overrides = RuntimeOverrides {
258 sampling: Some(fresh.sampling),
259 backpressure: Some(fresh.backpressure),
260 exporter: Some(hot_exporter),
261 security: Some(fresh.security),
262 slo: Some(fresh.slo),
263 pii_max_depth: Some(fresh.pii_max_depth),
264 strict_schema: Some(fresh.strict_schema),
265 event_schema: Some(fresh.event_schema),
266 logging: Some(hot_logging),
267 };
268
269 let mut next = apply_runtime_overrides(current.clone(), overrides);
270 set_active_config(Some(next.clone()));
271 apply_policies(&next);
272 crate::logger::configure_logging(next.logging.clone());
275 next.service_name = current.service_name;
276 next.environment = current.environment;
277 next.version = current.version;
278 next.tracing.enabled = current.tracing.enabled;
279 next.tracing.otlp_headers = current.tracing.otlp_headers;
280 next.metrics.enabled = current.metrics.enabled;
281 next.metrics.otlp_headers = current.metrics.otlp_headers;
282
283 set_active_config(Some(next.clone()));
284 Ok(next)
285}
286
287fn apply_runtime_overrides(
288 current: TelemetryConfig,
289 overrides: RuntimeOverrides,
290) -> TelemetryConfig {
291 let mut next = current;
292 next.sampling = overrides.sampling.unwrap_or(next.sampling);
293 next.backpressure = overrides.backpressure.unwrap_or(next.backpressure);
294 next.exporter = overrides.exporter.unwrap_or(next.exporter);
295 next.security = overrides.security.unwrap_or(next.security);
296 next.slo = overrides.slo.unwrap_or(next.slo);
297 next.pii_max_depth = overrides.pii_max_depth.unwrap_or(next.pii_max_depth);
298 next.strict_schema = overrides.strict_schema.unwrap_or(next.strict_schema);
299 next.event_schema = overrides.event_schema.unwrap_or(next.event_schema);
300 next.logging = overrides.logging.unwrap_or(next.logging);
301 next
302}
303
304fn runtime_config_or_default(config: Option<TelemetryConfig>) -> TelemetryConfig {
305 match config {
306 Some(config) => config,
307 None => TelemetryConfig::from_env().unwrap_or_default(),
308 }
309}
310
311pub fn reconfigure_telemetry(
312 config: Option<TelemetryConfig>,
313) -> Result<TelemetryConfig, TelemetryError> {
314 let target = match config {
315 Some(config) => config,
316 None => match TelemetryConfig::from_env() {
317 Ok(config) => config,
318 Err(err) => return Err(TelemetryError::new(err.message)),
319 },
320 };
321
322 #[cfg(feature = "otel")]
323 if let Some(current) = get_runtime_config() {
324 if otel_installed() {
325 let logs_live = crate::otel::logs::logger_provider_installed();
326 let traces_live = crate::otel::traces::tracer_provider_installed();
327 let metrics_live = crate::otel::metrics::meter_provider_installed();
328 let reject = identity_config_changed(¤t, &target)
331 || (logs_live && logging_provider_config_changed(¤t, &target))
332 || (traces_live && tracing_provider_config_changed(¤t, &target))
333 || (metrics_live && metrics_provider_config_changed(¤t, &target));
334 if reject {
335 Err(TelemetryError::new(PROVIDER_CHANGE_RESTART_MESSAGE))
336 } else {
337 Ok(())
338 }?;
339 }
340 }
341
342 set_active_config(Some(target.clone()));
343 apply_policies(&target);
344 Ok(target)
345}
346
347#[cfg(test)]
348#[path = "runtime_tests.rs"]
349mod tests;
350
351#[cfg(test)]
352#[path = "runtime_logging_tests.rs"]
353mod logging_tests;