1use std::sync::{OnceLock, RwLock};
7
8use crate::config::RuntimeOverrides;
9use crate::config::TelemetryConfig;
10use crate::errors::TelemetryError;
11#[cfg(feature = "otel")]
12use crate::otel::otel_installed;
13use crate::policies::apply_policies;
14pub use crate::runtime_facade::{
15 flush_result, provider_mode, reconfigure_result, runtime_state, runtime_status,
16 signal_flush_result, telemetry_config, telemetry_runtime, FlushResult, ProviderMode,
17 ReconfigureResult, RuntimeState, RuntimeStatus, SignalFlushResult, SignalStatus,
18 TelemetryRuntime,
19};
20
21static ACTIVE_CONFIG: OnceLock<RwLock<Option<TelemetryConfig>>> = OnceLock::new();
22#[cfg(feature = "otel")]
23const PROVIDER_CHANGE_RESTART_MESSAGE: &str =
24 "OpenTelemetry providers already installed; restart the process for provider-changing config";
25
26#[cfg_attr(test, mutants::skip)] fn empty_active_config() -> RwLock<Option<TelemetryConfig>> {
28 RwLock::new(None)
29}
30
31fn active_config() -> &'static RwLock<Option<TelemetryConfig>> {
32 ACTIVE_CONFIG.get_or_init(empty_active_config)
33}
34
35pub(crate) fn set_active_config(config: Option<TelemetryConfig>) {
36 *crate::_lock::rwlock_write(active_config()) = config;
37}
38
39#[cfg(any(feature = "otel", test))]
42fn identity_config_changed(current: &TelemetryConfig, target: &TelemetryConfig) -> bool {
43 current.service_name != target.service_name
44 || current.environment != target.environment
45 || current.version != target.version
46}
47
48#[cfg(any(feature = "otel", test))]
50fn logging_provider_config_changed(current: &TelemetryConfig, target: &TelemetryConfig) -> bool {
51 current.logging.otlp_endpoint != target.logging.otlp_endpoint
52 || current.logging.otlp_headers != target.logging.otlp_headers
53 || current.logging.otlp_protocol != target.logging.otlp_protocol
54 || current.exporter.logs_timeout_seconds != target.exporter.logs_timeout_seconds
55}
56
57#[cfg(any(feature = "otel", test))]
59fn tracing_provider_config_changed(current: &TelemetryConfig, target: &TelemetryConfig) -> bool {
60 current.tracing.enabled != target.tracing.enabled
61 || current.tracing.otlp_endpoint != target.tracing.otlp_endpoint
62 || current.tracing.otlp_headers != target.tracing.otlp_headers
63 || current.tracing.otlp_protocol != target.tracing.otlp_protocol
64 || current.exporter.traces_timeout_seconds != target.exporter.traces_timeout_seconds
65}
66
67#[cfg(any(feature = "otel", test))]
69fn metrics_provider_config_changed(current: &TelemetryConfig, target: &TelemetryConfig) -> bool {
70 current.metrics.enabled != target.metrics.enabled
71 || current.metrics.otlp_endpoint != target.metrics.otlp_endpoint
72 || current.metrics.otlp_headers != target.metrics.otlp_headers
73 || current.metrics.otlp_protocol != target.metrics.otlp_protocol
74 || current.metrics.metric_export_interval_ms != target.metrics.metric_export_interval_ms
75 || current.exporter.metrics_timeout_seconds != target.exporter.metrics_timeout_seconds
76}
77
78#[cfg(test)]
82pub(crate) fn provider_config_changed(current: &TelemetryConfig, target: &TelemetryConfig) -> bool {
83 identity_config_changed(current, target)
84 || logging_provider_config_changed(current, target)
85 || tracing_provider_config_changed(current, target)
86 || metrics_provider_config_changed(current, target)
87}
88
89pub fn get_runtime_config() -> Option<TelemetryConfig> {
90 crate::_lock::rwlock_read(active_config()).clone()
91}
92
93fn compute_cold_drift(current: &TelemetryConfig, fresh: &TelemetryConfig) -> Vec<&'static str> {
97 let mut drifted: Vec<&'static str> = Vec::new();
98 if current.service_name != fresh.service_name {
99 drifted.push("service_name");
100 }
101 if current.environment != fresh.environment {
102 drifted.push("environment");
103 }
104 if current.version != fresh.version {
105 drifted.push("version");
106 }
107 if current.tracing.enabled != fresh.tracing.enabled {
108 drifted.push("tracing.enabled");
109 }
110 if current.metrics.enabled != fresh.metrics.enabled {
111 drifted.push("metrics.enabled");
112 }
113 drifted
114}
115
116#[cfg(feature = "otel")]
125pub(crate) fn tracing_enabled_by_loaded_config() -> bool {
126 match crate::_lock::rwlock_read(active_config()).as_ref() {
128 Some(cfg) => cfg.tracing.enabled,
129 None => true,
130 }
131}
132
133#[cfg(feature = "otel")]
135pub(crate) fn metrics_enabled_by_loaded_config() -> bool {
136 match crate::_lock::rwlock_read(active_config()).as_ref() {
137 Some(cfg) => cfg.metrics.enabled,
138 None => true,
139 }
140}
141
142fn runtime_config_snapshot() -> (Option<TelemetryConfig>, bool) {
143 let guard = crate::_lock::rwlock_read(active_config());
144 let cfg = guard.clone();
145 (cfg.clone(), cfg.is_some())
146}
147
148pub fn get_runtime_status() -> RuntimeStatus {
149 let (cfg, setup_done) = runtime_config_snapshot();
150 let cfg = runtime_config_or_default(cfg);
151
152 #[cfg(feature = "otel")]
153 let providers = SignalStatus {
154 logs: crate::otel::logs::logger_provider_installed(),
155 traces: crate::otel::traces_provider_effective(),
156 metrics: crate::otel::metrics_provider_effective(),
157 };
158
159 #[cfg(not(feature = "otel"))]
160 let providers = SignalStatus {
161 logs: false,
162 traces: false,
163 metrics: false,
164 };
165
166 RuntimeStatus {
167 setup_done,
168 signals: SignalStatus {
169 logs: true,
170 traces: cfg.tracing.enabled,
171 metrics: cfg.metrics.enabled,
172 },
173 fallback: SignalStatus {
174 logs: !providers.logs,
175 traces: !providers.traces,
176 metrics: !providers.metrics,
177 },
178 providers,
179 setup_error: crate::health::get_health_snapshot().setup_error,
180 }
181}
182
183pub fn update_runtime_config(
184 overrides: RuntimeOverrides,
185) -> Result<TelemetryConfig, TelemetryError> {
186 let logging_override = overrides.logging.clone();
187 #[cfg(feature = "otel")]
190 let log_provider_live = crate::otel::logs::logger_provider_installed();
191 let next = {
192 let mut guard = crate::_lock::rwlock_write(active_config());
193 let current = match guard.as_ref().cloned() {
194 Some(current) => current,
195 None => {
196 return Err(TelemetryError::new(
197 "telemetry not set up: call setup_telemetry first",
198 ));
199 }
200 };
201 let next = apply_runtime_overrides(current.clone(), overrides);
202 #[cfg(feature = "otel")]
209 if log_provider_live && logging_provider_config_changed(¤t, &next) {
210 return Err(TelemetryError::from(
211 crate::errors::ProviderImmutableError::new(
212 "provider-changing logging reconfiguration is unsupported after \
213 OpenTelemetry log providers are installed. Restart the process and \
214 call setup_telemetry() with the new config.",
215 ),
216 ));
217 }
218 #[cfg(not(feature = "otel"))]
219 let _ = ¤t;
220 *guard = Some(next.clone());
221 next
222 }; apply_policies(&next);
224 if let Some(cfg) = logging_override {
230 crate::logger::configure_logging(cfg);
231 }
232 Ok(next)
233}
234
235pub fn reload_runtime_from_env() -> Result<TelemetryConfig, TelemetryError> {
236 let fresh = match TelemetryConfig::from_env() {
237 Ok(fresh) => fresh,
238 Err(err) => return Err(TelemetryError::new(err.message)),
239 };
240 let current = match get_runtime_config() {
241 Some(current) => current,
242 None => {
243 return Err(TelemetryError::new(
244 "telemetry not set up: call setup_telemetry first",
245 ))
246 }
247 };
248
249 let drifted = compute_cold_drift(¤t, &fresh);
251 if !drifted.is_empty() {
252 eprintln!(
253 "[provide-telemetry] runtime.cold_field_drift: {} — restart required to apply",
254 drifted.join(", ")
255 );
256 }
257
258 #[allow(unused_mut)] let mut hot_exporter = fresh.exporter;
265 #[cfg(feature = "otel")]
266 {
267 if crate::otel::logs::logger_provider_installed() {
268 hot_exporter.logs_timeout_seconds = current.exporter.logs_timeout_seconds;
269 }
270 if crate::otel::traces::tracer_provider_installed() {
271 hot_exporter.traces_timeout_seconds = current.exporter.traces_timeout_seconds;
272 }
273 if crate::otel::metrics::meter_provider_installed() {
274 hot_exporter.metrics_timeout_seconds = current.exporter.metrics_timeout_seconds;
275 }
276 }
277
278 #[allow(unused_mut)] let mut hot_logging = fresh.logging.clone();
285 #[cfg(feature = "otel")]
286 {
287 if crate::otel::logs::logger_provider_installed() {
288 hot_logging.otlp_endpoint = current.logging.otlp_endpoint.clone();
289 hot_logging.otlp_headers = current.logging.otlp_headers.clone();
290 hot_logging.otlp_protocol = current.logging.otlp_protocol.clone();
291 }
292 }
293
294 let overrides = RuntimeOverrides {
295 sampling: Some(fresh.sampling),
296 backpressure: Some(fresh.backpressure),
297 exporter: Some(hot_exporter),
298 security: Some(fresh.security),
299 slo: Some(fresh.slo),
300 pii_max_depth: Some(fresh.pii_max_depth),
301 strict_schema: Some(fresh.strict_schema),
302 event_schema: Some(fresh.event_schema),
303 logging: Some(hot_logging),
304 };
305
306 let mut next = apply_runtime_overrides(current.clone(), overrides);
307 set_active_config(Some(next.clone()));
308 apply_policies(&next);
309 crate::logger::configure_logging(next.logging.clone());
312 next.service_name = current.service_name;
313 next.environment = current.environment;
314 next.version = current.version;
315 next.tracing.enabled = current.tracing.enabled;
316 next.tracing.otlp_headers = current.tracing.otlp_headers;
317 next.metrics.enabled = current.metrics.enabled;
318 next.metrics.otlp_headers = current.metrics.otlp_headers;
319
320 set_active_config(Some(next.clone()));
321 Ok(next)
322}
323
324fn apply_runtime_overrides(
325 current: TelemetryConfig,
326 overrides: RuntimeOverrides,
327) -> TelemetryConfig {
328 let mut next = current;
329 next.sampling = overrides.sampling.unwrap_or(next.sampling);
330 next.backpressure = overrides.backpressure.unwrap_or(next.backpressure);
331 next.exporter = overrides.exporter.unwrap_or(next.exporter);
332 next.security = overrides.security.unwrap_or(next.security);
333 next.slo = overrides.slo.unwrap_or(next.slo);
334 next.pii_max_depth = overrides.pii_max_depth.unwrap_or(next.pii_max_depth);
335 next.strict_schema = overrides.strict_schema.unwrap_or(next.strict_schema);
336 next.event_schema = overrides.event_schema.unwrap_or(next.event_schema);
337 next.logging = overrides.logging.unwrap_or(next.logging);
338 next
339}
340
341fn runtime_config_or_default(config: Option<TelemetryConfig>) -> TelemetryConfig {
342 match config {
343 Some(config) => config,
344 None => TelemetryConfig::from_env().unwrap_or_default(),
345 }
346}
347
348pub fn reconfigure_telemetry(
349 config: Option<TelemetryConfig>,
350) -> Result<TelemetryConfig, TelemetryError> {
351 let target = match config {
352 Some(config) => config,
353 None => match TelemetryConfig::from_env() {
354 Ok(config) => config,
355 Err(err) => return Err(TelemetryError::new(err.message)),
356 },
357 };
358
359 #[cfg(feature = "otel")]
360 if let Some(current) = get_runtime_config() {
361 if otel_installed() {
362 let logs_live = crate::otel::logs::logger_provider_installed();
363 let traces_live = crate::otel::traces::tracer_provider_installed();
364 let metrics_live = crate::otel::metrics::meter_provider_installed();
365 let reject = identity_config_changed(¤t, &target)
368 || (logs_live && logging_provider_config_changed(¤t, &target))
369 || (traces_live && tracing_provider_config_changed(¤t, &target))
370 || (metrics_live && metrics_provider_config_changed(¤t, &target));
371 if reject {
372 Err(TelemetryError::from(
375 crate::errors::ProviderImmutableError::new(PROVIDER_CHANGE_RESTART_MESSAGE),
376 ))
377 } else {
378 Ok(())
379 }?;
380 }
381 }
382
383 set_active_config(Some(target.clone()));
384 apply_policies(&target);
385 Ok(target)
386}
387
388#[cfg(test)]
389#[path = "runtime_tests.rs"]
390mod tests;
391
392#[cfg(test)]
393#[path = "runtime_logging_tests.rs"]
394mod logging_tests;