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
127#[cfg(feature = "otel")]
136pub(crate) fn tracing_enabled_by_loaded_config() -> bool {
137 match crate::_lock::rwlock_read(active_config()).as_ref() {
139 Some(cfg) => cfg.tracing.enabled,
140 None => true,
141 }
142}
143
144#[cfg(feature = "otel")]
146pub(crate) fn metrics_enabled_by_loaded_config() -> bool {
147 match crate::_lock::rwlock_read(active_config()).as_ref() {
148 Some(cfg) => cfg.metrics.enabled,
149 None => true,
150 }
151}
152
153fn runtime_config_snapshot() -> (Option<TelemetryConfig>, bool) {
154 let guard = crate::_lock::rwlock_read(active_config());
155 let cfg = guard.clone();
156 (cfg.clone(), cfg.is_some())
157}
158
159pub fn get_runtime_status() -> RuntimeStatus {
160 let (cfg, setup_done) = runtime_config_snapshot();
161 let cfg = runtime_config_or_default(cfg);
162
163 #[cfg(feature = "otel")]
164 let providers = SignalStatus {
165 logs: crate::otel::logs::logger_provider_installed(),
166 traces: crate::otel::traces_provider_effective(),
167 metrics: crate::otel::metrics_provider_effective(),
168 };
169
170 #[cfg(not(feature = "otel"))]
171 let providers = SignalStatus {
172 logs: false,
173 traces: false,
174 metrics: false,
175 };
176
177 RuntimeStatus {
178 setup_done,
179 signals: SignalStatus {
180 logs: true,
181 traces: cfg.tracing.enabled,
182 metrics: cfg.metrics.enabled,
183 },
184 fallback: SignalStatus {
185 logs: !providers.logs,
186 traces: !providers.traces,
187 metrics: !providers.metrics,
188 },
189 providers,
190 setup_error: crate::health::get_health_snapshot().setup_error,
191 }
192}
193
194pub fn update_runtime_config(
195 overrides: RuntimeOverrides,
196) -> Result<TelemetryConfig, TelemetryError> {
197 let logging_override = overrides.logging.clone();
198 let next = {
199 let mut guard = crate::_lock::rwlock_write(active_config());
200 let current = match guard.as_ref().cloned() {
201 Some(current) => current,
202 None => {
203 return Err(TelemetryError::new(
204 "telemetry not set up: call setup_telemetry first",
205 ));
206 }
207 };
208 let next = apply_runtime_overrides(current, overrides);
209 *guard = Some(next.clone());
210 next
211 }; apply_policies(&next);
213 if let Some(cfg) = logging_override {
219 crate::logger::configure_logging(cfg);
220 }
221 Ok(next)
222}
223
224pub fn reload_runtime_from_env() -> Result<TelemetryConfig, TelemetryError> {
225 let fresh = match TelemetryConfig::from_env() {
226 Ok(fresh) => fresh,
227 Err(err) => return Err(TelemetryError::new(err.message)),
228 };
229 let current = match get_runtime_config() {
230 Some(current) => current,
231 None => {
232 return Err(TelemetryError::new(
233 "telemetry not set up: call setup_telemetry first",
234 ))
235 }
236 };
237
238 let drifted = compute_cold_drift(¤t, &fresh);
240 if !drifted.is_empty() {
241 eprintln!(
242 "[provide-telemetry] runtime.cold_field_drift: {} — restart required to apply",
243 drifted.join(", ")
244 );
245 }
246
247 #[allow(unused_mut)] let mut hot_exporter = fresh.exporter;
254 #[cfg(feature = "otel")]
255 {
256 if crate::otel::logs::logger_provider_installed() {
257 hot_exporter.logs_timeout_seconds = current.exporter.logs_timeout_seconds;
258 }
259 if crate::otel::traces::tracer_provider_installed() {
260 hot_exporter.traces_timeout_seconds = current.exporter.traces_timeout_seconds;
261 }
262 if crate::otel::metrics::meter_provider_installed() {
263 hot_exporter.metrics_timeout_seconds = current.exporter.metrics_timeout_seconds;
264 }
265 }
266
267 #[allow(unused_mut)] let mut hot_logging = fresh.logging.clone();
274 #[cfg(feature = "otel")]
275 {
276 if crate::otel::logs::logger_provider_installed() {
277 hot_logging.otlp_endpoint = current.logging.otlp_endpoint.clone();
278 hot_logging.otlp_headers = current.logging.otlp_headers.clone();
279 hot_logging.otlp_protocol = current.logging.otlp_protocol.clone();
280 }
281 }
282
283 let overrides = RuntimeOverrides {
284 sampling: Some(fresh.sampling),
285 backpressure: Some(fresh.backpressure),
286 exporter: Some(hot_exporter),
287 security: Some(fresh.security),
288 slo: Some(fresh.slo),
289 pii_max_depth: Some(fresh.pii_max_depth),
290 strict_schema: Some(fresh.strict_schema),
291 event_schema: Some(fresh.event_schema),
292 logging: Some(hot_logging),
293 };
294
295 let mut next = apply_runtime_overrides(current.clone(), overrides);
296 set_active_config(Some(next.clone()));
297 apply_policies(&next);
298 crate::logger::configure_logging(next.logging.clone());
301 next.service_name = current.service_name;
302 next.environment = current.environment;
303 next.version = current.version;
304 next.tracing.enabled = current.tracing.enabled;
305 next.tracing.otlp_headers = current.tracing.otlp_headers;
306 next.metrics.enabled = current.metrics.enabled;
307 next.metrics.otlp_headers = current.metrics.otlp_headers;
308
309 set_active_config(Some(next.clone()));
310 Ok(next)
311}
312
313fn apply_runtime_overrides(
314 current: TelemetryConfig,
315 overrides: RuntimeOverrides,
316) -> TelemetryConfig {
317 let mut next = current;
318 next.sampling = overrides.sampling.unwrap_or(next.sampling);
319 next.backpressure = overrides.backpressure.unwrap_or(next.backpressure);
320 next.exporter = overrides.exporter.unwrap_or(next.exporter);
321 next.security = overrides.security.unwrap_or(next.security);
322 next.slo = overrides.slo.unwrap_or(next.slo);
323 next.pii_max_depth = overrides.pii_max_depth.unwrap_or(next.pii_max_depth);
324 next.strict_schema = overrides.strict_schema.unwrap_or(next.strict_schema);
325 next.event_schema = overrides.event_schema.unwrap_or(next.event_schema);
326 next.logging = overrides.logging.unwrap_or(next.logging);
327 next
328}
329
330fn runtime_config_or_default(config: Option<TelemetryConfig>) -> TelemetryConfig {
331 match config {
332 Some(config) => config,
333 None => TelemetryConfig::from_env().unwrap_or_default(),
334 }
335}
336
337pub fn reconfigure_telemetry(
338 config: Option<TelemetryConfig>,
339) -> Result<TelemetryConfig, TelemetryError> {
340 let target = match config {
341 Some(config) => config,
342 None => match TelemetryConfig::from_env() {
343 Ok(config) => config,
344 Err(err) => return Err(TelemetryError::new(err.message)),
345 },
346 };
347
348 #[cfg(feature = "otel")]
349 if let Some(current) = get_runtime_config() {
350 if otel_installed() {
351 let logs_live = crate::otel::logs::logger_provider_installed();
352 let traces_live = crate::otel::traces::tracer_provider_installed();
353 let metrics_live = crate::otel::metrics::meter_provider_installed();
354 let reject = identity_config_changed(¤t, &target)
357 || (logs_live && logging_provider_config_changed(¤t, &target))
358 || (traces_live && tracing_provider_config_changed(¤t, &target))
359 || (metrics_live && metrics_provider_config_changed(¤t, &target));
360 if reject {
361 Err(TelemetryError::new(PROVIDER_CHANGE_RESTART_MESSAGE))
362 } else {
363 Ok(())
364 }?;
365 }
366 }
367
368 set_active_config(Some(target.clone()));
369 apply_policies(&target);
370 Ok(target)
371}
372
373#[cfg(test)]
374#[path = "runtime_tests.rs"]
375mod tests;
376
377#[cfg(test)]
378#[path = "runtime_logging_tests.rs"]
379mod logging_tests;