Skip to main content

provide_telemetry/
runtime.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::{OnceLock, RwLock};
7
8use crate::config::TelemetryConfig;
9use crate::errors::TelemetryError;
10use crate::otel::otel_installed;
11use crate::RuntimeOverrides;
12
13static ACTIVE_CONFIG: OnceLock<RwLock<Option<TelemetryConfig>>> = OnceLock::new();
14
15fn active_config() -> &'static RwLock<Option<TelemetryConfig>> {
16    ACTIVE_CONFIG.get_or_init(|| RwLock::new(None))
17}
18
19pub(crate) fn set_active_config(config: Option<TelemetryConfig>) {
20    *active_config()
21        .write()
22        .expect("runtime config lock poisoned") = config;
23}
24
25pub(crate) fn provider_config_changed(current: &TelemetryConfig, target: &TelemetryConfig) -> bool {
26    current.service_name != target.service_name
27        || current.environment != target.environment
28        || current.version != target.version
29        || current.logging.otlp_headers != target.logging.otlp_headers
30        || current.tracing != target.tracing
31        || current.metrics != target.metrics
32}
33
34pub fn get_runtime_config() -> Option<TelemetryConfig> {
35    active_config()
36        .read()
37        .expect("runtime config lock poisoned")
38        .clone()
39}
40
41pub fn update_runtime_config(
42    overrides: RuntimeOverrides,
43) -> Result<TelemetryConfig, TelemetryError> {
44    let mut guard = active_config()
45        .write()
46        .expect("runtime config lock poisoned");
47    let current = guard
48        .as_ref()
49        .cloned()
50        .ok_or_else(|| TelemetryError::new("telemetry not set up: call setup_telemetry first"))?;
51
52    let mut next = current;
53    if let Some(sampling) = overrides.sampling {
54        next.sampling = sampling;
55    }
56    if let Some(backpressure) = overrides.backpressure {
57        next.backpressure = backpressure;
58    }
59    if let Some(exporter) = overrides.exporter {
60        next.exporter = exporter;
61    }
62    if let Some(security) = overrides.security {
63        next.security = security;
64    }
65    if let Some(slo) = overrides.slo {
66        next.slo = slo;
67    }
68    if let Some(pii_max_depth) = overrides.pii_max_depth {
69        next.pii_max_depth = pii_max_depth;
70    }
71    if let Some(strict_schema) = overrides.strict_schema {
72        next.strict_schema = strict_schema;
73    }
74
75    *guard = Some(next.clone());
76    Ok(next)
77}
78
79pub fn reload_runtime_from_env() -> Result<TelemetryConfig, TelemetryError> {
80    let fresh = TelemetryConfig::from_env().map_err(|err| TelemetryError::new(err.message))?;
81    let current = get_runtime_config()
82        .ok_or_else(|| TelemetryError::new("telemetry not set up: call setup_telemetry first"))?;
83
84    let overrides = RuntimeOverrides {
85        sampling: Some(fresh.sampling),
86        backpressure: Some(fresh.backpressure),
87        exporter: Some(fresh.exporter),
88        security: Some(fresh.security),
89        slo: Some(fresh.slo),
90        pii_max_depth: Some(fresh.pii_max_depth),
91        strict_schema: Some(fresh.strict_schema),
92    };
93
94    let mut next = update_runtime_config(overrides)?;
95    next.service_name = current.service_name;
96    next.environment = current.environment;
97    next.version = current.version;
98    next.logging.level = current.logging.level;
99    next.logging.fmt = current.logging.fmt;
100    next.logging.otlp_headers = current.logging.otlp_headers;
101    next.tracing.enabled = current.tracing.enabled;
102    next.tracing.otlp_headers = current.tracing.otlp_headers;
103    next.metrics.enabled = current.metrics.enabled;
104    next.metrics.otlp_headers = current.metrics.otlp_headers;
105
106    set_active_config(Some(next.clone()));
107    Ok(next)
108}
109
110pub fn reconfigure_telemetry(
111    config: Option<TelemetryConfig>,
112) -> Result<TelemetryConfig, TelemetryError> {
113    let target = match config {
114        Some(config) => config,
115        None => TelemetryConfig::from_env().map_err(|err| TelemetryError::new(err.message))?,
116    };
117
118    if let Some(current) = get_runtime_config() {
119        if otel_installed() && provider_config_changed(&current, &target) {
120            return Err(TelemetryError::new(
121                "OpenTelemetry providers already installed; restart the process for provider-changing config",
122            ));
123        }
124    }
125
126    set_active_config(Some(target.clone()));
127    Ok(target)
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn runtime_test_provider_config_changed_detects_each_provider_field() {
136        let current = TelemetryConfig::default();
137
138        let mut changed = current.clone();
139        changed.service_name = "svc-2".to_string();
140        assert!(provider_config_changed(&current, &changed));
141
142        let mut changed = current.clone();
143        changed.environment = "prod".to_string();
144        assert!(provider_config_changed(&current, &changed));
145
146        let mut changed = current.clone();
147        changed.version = "1.2.3".to_string();
148        assert!(provider_config_changed(&current, &changed));
149
150        let mut changed = current.clone();
151        changed.logging.otlp_headers.insert("x".into(), "1".into());
152        assert!(provider_config_changed(&current, &changed));
153
154        let mut changed = current.clone();
155        changed.tracing.enabled = !changed.tracing.enabled;
156        assert!(provider_config_changed(&current, &changed));
157
158        let mut changed = current.clone();
159        changed.metrics.enabled = !changed.metrics.enabled;
160        assert!(provider_config_changed(&current, &changed));
161    }
162
163    #[test]
164    fn runtime_test_provider_config_changed_is_false_for_hot_only_changes() {
165        let current = TelemetryConfig::default();
166        let mut changed = current.clone();
167        changed.sampling.logs_rate = 0.25;
168        changed.backpressure.logs_maxsize = 9;
169        changed.exporter.logs_retries = 2;
170        changed.security.max_attr_count = 99;
171        changed.slo.enable_red_metrics = !changed.slo.enable_red_metrics;
172        changed.pii_max_depth = 3;
173        changed.strict_schema = true;
174
175        assert!(!provider_config_changed(&current, &changed));
176    }
177}