Skip to main content

faucet_core/observability/
install.rs

1//! Idempotent global installer for the Prometheus recorder and a
2//! `tracing-subscriber`. Safe to call more than once; subsequent calls warn
3//! and continue rather than panicking. Port-in-use becomes a typed error.
4
5use thiserror::Error;
6
7/// Configuration for `install_observability`. Either or both sections may be
8/// `None`; unset sections install nothing.
9#[derive(Debug, Clone, Default)]
10pub struct ObservabilityConfig {
11    pub prometheus: Option<PrometheusConfig>,
12    pub tracing: Option<TracingConfig>,
13    pub otel: Option<crate::observability::otel::OtelConfig>,
14}
15
16/// Which metrics recorder to install given which exporters are requested.
17/// Only consulted by `install_observability`, which itself requires the recorder
18/// machinery, so it's gated on the same feature.
19#[cfg(feature = "observability-install")]
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub(crate) enum MetricsMode {
22    None,
23    PrometheusOnly,
24    OtelOnly,
25    Fanout,
26}
27
28#[cfg(feature = "observability-install")]
29impl MetricsMode {
30    pub(crate) fn select(prometheus: bool, otel_metrics: bool) -> Self {
31        match (prometheus, otel_metrics) {
32            (true, true) => MetricsMode::Fanout,
33            (true, false) => MetricsMode::PrometheusOnly,
34            (false, true) => MetricsMode::OtelOnly,
35            (false, false) => MetricsMode::None,
36        }
37    }
38}
39
40#[derive(Debug, Clone)]
41pub struct PrometheusConfig {
42    /// `host:port` to bind a `/metrics` HTTP endpoint. Recommended:
43    /// `127.0.0.1:9464`.
44    pub listen: String,
45    /// Histogram bucket overrides (in seconds). When `None`, sensible defaults
46    /// apply (0.001..300s spanning sub-ms through five-minute durations).
47    pub buckets: Option<Vec<f64>>,
48}
49
50#[derive(Debug, Clone)]
51pub struct TracingConfig {
52    /// `EnvFilter`-style directive, e.g. `"info"` or `"faucet_core=debug,info"`.
53    pub level: String,
54}
55
56/// Report from `install_observability` so callers can log what actually
57/// happened (recorder installed vs. already-installed vs. disabled).
58#[derive(Debug, Clone, Default)]
59pub struct InstallReport {
60    pub prometheus_listen: Option<String>,
61    pub prometheus_already_installed: bool,
62    pub tracing_already_installed: bool,
63    pub otel_installed: bool,
64    pub otel_signals: Vec<&'static str>,
65}
66
67#[derive(Debug, Error)]
68pub enum InstallError {
69    #[error("failed to bind Prometheus listener at {listen}: {source}")]
70    PrometheusBind {
71        listen: String,
72        #[source]
73        source: std::io::Error,
74    },
75    #[error("failed to install Prometheus recorder: {0}")]
76    PrometheusInstall(String),
77}
78
79/// Install observability if requested. Always returns; never panics.
80///
81/// Behavior:
82/// - If `prometheus` is set, builds a `PrometheusBuilder` and installs the
83///   recorder + HTTP `/metrics` endpoint at the configured listen address.
84///   Already-installed recorder (typed `BuildError::FailedToSetGlobalRecorder`)
85///   is logged via `tracing::warn!` and continues. Listen-address parse failures
86///   and HTTP-listener bind failures (e.g. port-in-use, typed
87///   `BuildError::FailedToCreateHTTPListener`) return `InstallError::PrometheusBind`.
88/// - If `tracing` is set, installs a `tracing-subscriber` registry with the
89///   given env-filter directive as the default subscriber. Already-set-default
90///   is logged via `tracing::warn!` and continues.
91/// - OTLP export (feature `otel`) needs a tokio runtime; called outside one it
92///   is skipped with a warning.
93#[cfg(feature = "observability-install")]
94pub fn install_observability(cfg: &ObservabilityConfig) -> Result<InstallReport, InstallError> {
95    let mut report = InstallReport::default();
96
97    // The OTLP exporters (and the fanout's `/metrics` task) need a tokio
98    // runtime; outside one, skip OTLP rather than panic.
99    #[cfg(feature = "otel")]
100    let without_otel;
101    #[cfg(feature = "otel")]
102    let cfg = if cfg.otel.is_some() && tokio::runtime::Handle::try_current().is_err() {
103        tracing::warn!("OTLP export needs a tokio runtime; install_observability skips it");
104        without_otel = ObservabilityConfig {
105            otel: None,
106            ..cfg.clone()
107        };
108        &without_otel
109    } else {
110        cfg
111    };
112
113    // Provider holders moved into the guard after both arms run; only populated
114    // (and only referenced) when the `otel` feature is enabled.
115    #[cfg(feature = "otel")]
116    let mut otel_tracer: Option<opentelemetry_sdk::trace::SdkTracerProvider> = None;
117
118    // --- Metrics ---
119    #[cfg(feature = "otel")]
120    let otel_metrics = cfg
121        .otel
122        .as_ref()
123        .map(|o| o.exports(crate::observability::otel::OtelSignal::Metrics))
124        .unwrap_or(false);
125    #[cfg(not(feature = "otel"))]
126    let otel_metrics = false;
127
128    let mode = MetricsMode::select(cfg.prometheus.is_some(), otel_metrics);
129
130    // Declared so the trace arm + guard step can move them; only used under otel.
131    #[cfg(feature = "otel")]
132    let mut otel_meter: Option<opentelemetry_sdk::metrics::SdkMeterProvider> = None;
133
134    match mode {
135        MetricsMode::None => {}
136        MetricsMode::PrometheusOnly => {
137            install_prometheus_only(cfg.prometheus.as_ref().unwrap(), &mut report)?;
138        }
139        #[cfg(feature = "otel")]
140        MetricsMode::OtelOnly => {
141            if let Some(otel) = cfg.otel.as_ref() {
142                match crate::observability::otel::build_meter_provider(otel) {
143                    Ok((mp, recorder)) => {
144                        if metrics::set_global_recorder(recorder).is_err() {
145                            tracing::warn!(
146                                "metrics recorder already installed; OTLP metrics are not exported"
147                            );
148                        } else {
149                            otel_meter = Some(mp);
150                            report.otel_signals.push("metrics");
151                        }
152                    }
153                    Err(e) => tracing::warn!("OTLP metrics exporter init failed; skipping: {e}"),
154                }
155            }
156        }
157        #[cfg(feature = "otel")]
158        MetricsMode::Fanout => {
159            install_fanout(
160                cfg.prometheus.as_ref().unwrap(),
161                cfg.otel.as_ref().unwrap(),
162                &mut report,
163                &mut otel_meter,
164            )?;
165        }
166        #[cfg(not(feature = "otel"))]
167        MetricsMode::OtelOnly | MetricsMode::Fanout => {
168            unreachable!("otel metrics mode selected without the otel feature")
169        }
170    }
171
172    // --- Traces ---
173    #[cfg(feature = "otel")]
174    let otel_traces = cfg
175        .otel
176        .as_ref()
177        .is_some_and(|o| o.exports(crate::observability::otel::OtelSignal::Traces));
178    #[cfg(not(feature = "otel"))]
179    let otel_traces = false;
180    if cfg.tracing.is_some() || otel_traces {
181        use tracing_subscriber::EnvFilter;
182        use tracing_subscriber::layer::SubscriberExt;
183        use tracing_subscriber::util::SubscriberInitExt;
184
185        let level = cfg.tracing.as_ref().map_or("info", |t| t.level.as_str());
186        let make_filter = || EnvFilter::try_new(level).unwrap_or_else(|_| EnvFilter::new("info"));
187
188        // Reassigned only in the `otel` branch below; without that feature the
189        // `mut` is genuinely unused.
190        #[cfg_attr(not(feature = "otel"), allow(unused_mut))]
191        let mut installed = false;
192
193        #[cfg(feature = "otel")]
194        if let (true, Some(otel)) = (otel_traces, cfg.otel.as_ref()) {
195            if crate::observability::otel::tracer_installed() {
196                installed = true;
197            } else {
198                match crate::observability::otel::build_trace_provider(otel) {
199                    Ok(tp) => {
200                        use opentelemetry::trace::TracerProvider as _;
201                        use tracing_subscriber::Layer as _;
202                        let tracer = tp.tracer("faucet");
203                        crate::observability::otel::install_propagator();
204                        let layer: crate::observability::otel::TraceLayer = Box::new(
205                            tracing_opentelemetry::layer()
206                                .with_tracer(tracer)
207                                .and_then(crate::observability::otel::OtelErrorCountLayer),
208                        );
209                        // A host that installed its own subscriber first (the CLI)
210                        // registered a slot for exactly this layer.
211                        match crate::observability::otel::fill_trace_slot(layer) {
212                            Ok(()) => {
213                                report.otel_signals.push("traces");
214                                otel_tracer = Some(tp);
215                            }
216                            Err(layer) => {
217                                let reg = tracing_subscriber::registry()
218                                    .with(layer)
219                                    .with(make_filter())
220                                    .with(
221                                        cfg.tracing.is_some().then(tracing_subscriber::fmt::layer),
222                                    );
223                                if reg.try_init().is_err() {
224                                    tracing::warn!(
225                                        "tracing subscriber already installed and no trace \
226                                         layer slot registered; OTLP traces are not exported"
227                                    );
228                                    report.tracing_already_installed = true;
229                                    // No otel layer was installed, so DON'T store the
230                                    // provider — let `tp` drop to shut its exporter down.
231                                } else {
232                                    report.otel_signals.push("traces");
233                                    otel_tracer = Some(tp);
234                                }
235                            }
236                        }
237                        installed = true;
238                    }
239                    Err(e) => tracing::warn!(
240                        "OTLP trace exporter init failed; traces are not exported: {e}"
241                    ),
242                }
243            }
244        }
245
246        if !installed {
247            // Install the OTLP export-error counter layer whenever ANY otel
248            // signal is exported — not only when a trace layer was installed —
249            // so `faucet_otel_export_failures_total` still fires under
250            // `otel.export: [metrics]` (audit #321 L7). `Option<Layer>` is a
251            // no-op when `None`, so this composes for the non-otel build too.
252            #[cfg(feature = "otel")]
253            let otel_error_layer = cfg
254                .otel
255                .as_ref()
256                .map(|o| {
257                    o.exports(crate::observability::otel::OtelSignal::Traces)
258                        || o.exports(crate::observability::otel::OtelSignal::Metrics)
259                })
260                .unwrap_or(false)
261                .then_some(crate::observability::otel::OtelErrorCountLayer);
262            #[cfg(not(feature = "otel"))]
263            let otel_error_layer: Option<tracing_subscriber::layer::Identity> = None;
264
265            let reg = tracing_subscriber::registry()
266                .with(make_filter())
267                .with(cfg.tracing.is_some().then(tracing_subscriber::fmt::layer))
268                .with(otel_error_layer);
269            if reg.try_init().is_err() {
270                // Some other code path has already set a global default. Log and
271                // continue — observability still works through the previously-
272                // installed subscriber.
273                tracing::warn!("tracing subscriber already installed; continuing");
274                report.tracing_already_installed = true;
275            }
276        }
277    }
278
279    // --- OTel guard + describe ---
280    #[cfg(feature = "otel")]
281    {
282        if otel_tracer.is_some() || otel_meter.is_some() {
283            crate::observability::otel::describe();
284            let _ = crate::observability::otel::set_guard(crate::observability::otel::OtelGuard {
285                tracer: otel_tracer,
286                meter: otel_meter,
287            });
288            report.otel_installed = true;
289        }
290    }
291
292    // Register metric HELP text + build_info after any Prometheus install
293    // attempt — describe!()/set!() into a not-yet-installed recorder is a no-op,
294    // so we order them last.
295    crate::observability::resilience::describe();
296    crate::observability::cleanup::describe();
297    crate::observability::drift::describe();
298    crate::observability::describe_roundtrip_metrics();
299    register_build_info();
300
301    Ok(report)
302}
303
304/// Install the Prometheus recorder + `/metrics` HTTP endpoint as the sole
305/// global recorder. Extracted verbatim from the original metrics arm so the
306/// fanout / otel-only paths can choose a different recorder.
307#[cfg(feature = "observability-install")]
308fn install_prometheus_only(
309    p: &PrometheusConfig,
310    report: &mut InstallReport,
311) -> Result<(), InstallError> {
312    use metrics_exporter_prometheus::{BuildError, PrometheusBuilder};
313
314    let listen: std::net::SocketAddr =
315        p.listen
316            .parse()
317            .map_err(|e: std::net::AddrParseError| InstallError::PrometheusBind {
318                listen: p.listen.clone(),
319                source: std::io::Error::new(std::io::ErrorKind::InvalidInput, e.to_string()),
320            })?;
321
322    const DEFAULT_BUCKETS: &[f64] = &[
323        0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0, 30.0, 60.0, 300.0,
324    ];
325    let buckets = p.buckets.as_deref().unwrap_or(DEFAULT_BUCKETS);
326
327    let builder = PrometheusBuilder::new()
328        .with_http_listener(listen)
329        .set_buckets(buckets)
330        .map_err(|e| InstallError::PrometheusInstall(e.to_string()))?;
331
332    match builder.install() {
333        Ok(()) => report.prometheus_listen = Some(p.listen.clone()),
334        // Match the TYPED `BuildError` variant rather than scraping its
335        // Display string — the latter breaks silently if the upstream
336        // wording changes.
337        Err(e) => match e {
338            // Recorder already installed (e.g. a prior `install` call or a
339            // test harness). Idempotent: warn and continue.
340            BuildError::FailedToSetGlobalRecorder(_) => {
341                tracing::warn!("Prometheus recorder already installed; continuing");
342                report.prometheus_already_installed = true;
343            }
344            // The HTTP `/metrics` listener could not bind. This is where a
345            // genuine bind failure (e.g. EADDRINUSE / port-in-use) lands,
346            // since the real `TcpListener::bind` happens inside `install()`,
347            // not in the address parse above. Surface it as the dedicated
348            // bind error so port-in-use is reported correctly.
349            BuildError::FailedToCreateHTTPListener(msg) => {
350                return Err(InstallError::PrometheusBind {
351                    listen: p.listen.clone(),
352                    source: std::io::Error::other(msg),
353                });
354            }
355            other => return Err(InstallError::PrometheusInstall(other.to_string())),
356        },
357    }
358    Ok(())
359}
360
361/// Install a `metrics-util` fanout recorder dispatching to BOTH a Prometheus
362/// recorder (with its `/metrics` HTTP endpoint) and an OTLP metrics recorder,
363/// so both coexist. If the OTLP exporter fails to build, falls back to a
364/// Prometheus-only recorder so `/metrics` still works — export never fails the
365/// process.
366#[cfg(all(feature = "observability-install", feature = "otel"))]
367fn install_fanout(
368    p: &PrometheusConfig,
369    otel: &crate::observability::otel::OtelConfig,
370    report: &mut InstallReport,
371    otel_meter: &mut Option<opentelemetry_sdk::metrics::SdkMeterProvider>,
372) -> Result<(), InstallError> {
373    use metrics_exporter_prometheus::{BuildError, PrometheusBuilder};
374    use metrics_util::layers::FanoutBuilder;
375
376    let listen: std::net::SocketAddr =
377        p.listen
378            .parse()
379            .map_err(|e: std::net::AddrParseError| InstallError::PrometheusBind {
380                listen: p.listen.clone(),
381                source: std::io::Error::new(std::io::ErrorKind::InvalidInput, e.to_string()),
382            })?;
383    const DEFAULT_BUCKETS: &[f64] = &[
384        0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0, 30.0, 60.0, 300.0,
385    ];
386    let buckets = p.buckets.as_deref().unwrap_or(DEFAULT_BUCKETS);
387
388    // build() returns (recorder, /metrics exporter future) WITHOUT setting the
389    // global recorder, so we can fan it out.
390    let (prom_recorder, prom_exporter) = PrometheusBuilder::new()
391        .with_http_listener(listen)
392        .set_buckets(buckets)
393        .map_err(|e| InstallError::PrometheusInstall(e.to_string()))?
394        .build()
395        .map_err(|e| match e {
396            BuildError::FailedToCreateHTTPListener(msg) => InstallError::PrometheusBind {
397                listen: p.listen.clone(),
398                source: std::io::Error::other(msg),
399            },
400            other => InstallError::PrometheusInstall(other.to_string()),
401        })?;
402
403    match crate::observability::otel::build_meter_provider(otel) {
404        Ok((mp, otel_recorder)) => {
405            let fanout = FanoutBuilder::default()
406                .add_recorder(prom_recorder)
407                .add_recorder(otel_recorder)
408                .build();
409            if metrics::set_global_recorder(fanout).is_err() {
410                tracing::warn!("metrics recorder already installed; continuing");
411                report.prometheus_already_installed = true;
412            } else {
413                report.prometheus_listen = Some(p.listen.clone());
414                report.otel_signals.push("metrics");
415                *otel_meter = Some(mp);
416                tokio::spawn(prom_exporter);
417            }
418        }
419        Err(e) => {
420            // OTLP metrics init failed — fall back to Prometheus-only so /metrics
421            // still works; export never fails the process.
422            tracing::warn!("OTLP metrics exporter init failed; Prometheus-only: {e}");
423            if metrics::set_global_recorder(prom_recorder).is_err() {
424                report.prometheus_already_installed = true;
425            } else {
426                report.prometheus_listen = Some(p.listen.clone());
427                tokio::spawn(prom_exporter);
428            }
429        }
430    }
431    Ok(())
432}
433
434/// Install, as the global metrics recorder, a fanout to `prometheus` — a
435/// recorder the host built (e.g. `PrometheusBuilder::build_recorder`) and
436/// renders through its own handle — and an OTLP metrics exporter built from
437/// `otel`. For a host such as an HTTP server that serves `/metrics` itself and
438/// so cannot use [`install_observability`]'s listener.
439///
440/// Returns `Ok(true)` when OTLP metrics are exported, `Ok(false)` when the
441/// exporter could not be built (the Prometheus recorder is installed alone, so
442/// `/metrics` keeps working), and an error when a global recorder is already
443/// installed. The meter provider lives for the process and is flushed by
444/// [`shutdown_otel`](crate::observability::otel::shutdown_otel).
445#[cfg(all(feature = "observability-install", feature = "otel"))]
446pub fn install_prometheus_with_otel_metrics(
447    prometheus: metrics_exporter_prometheus::PrometheusRecorder,
448    otel: &crate::observability::otel::OtelConfig,
449) -> Result<bool, InstallError> {
450    use metrics_util::layers::FanoutBuilder;
451    let already =
452        || InstallError::PrometheusInstall("a metrics recorder is already installed".into());
453    match crate::observability::otel::build_meter_provider(otel) {
454        Ok((mp, otel_recorder)) => {
455            let fanout = FanoutBuilder::default()
456                .add_recorder(prometheus)
457                .add_recorder(otel_recorder)
458                .build();
459            metrics::set_global_recorder(fanout).map_err(|_| already())?;
460            crate::observability::otel::keep_meter_provider(mp);
461            crate::observability::otel::describe();
462            Ok(true)
463        }
464        Err(e) => {
465            tracing::warn!("OTLP metrics exporter init failed; Prometheus-only: {e}");
466            metrics::set_global_recorder(prometheus).map_err(|_| already())?;
467            Ok(false)
468        }
469    }
470}
471
472/// Non-`observability-install` stub. Returns an empty report, never panics.
473#[cfg(not(feature = "observability-install"))]
474pub fn install_observability(_cfg: &ObservabilityConfig) -> Result<InstallReport, InstallError> {
475    crate::observability::resilience::describe();
476    crate::observability::cleanup::describe();
477    crate::observability::drift::describe();
478    crate::observability::describe_roundtrip_metrics();
479    crate::observability::otel::describe();
480    register_build_info();
481    Ok(InstallReport::default())
482}
483
484/// The version [`register_build_info`] reports, when an application set one.
485static BUILD_VERSION: std::sync::OnceLock<&'static str> = std::sync::OnceLock::new();
486
487/// Set the version the `faucet_build_info{version}` gauge reports — call it
488/// with the **binary's** own `env!("CARGO_PKG_VERSION")` before installing
489/// observability. Without it the label is `faucet-core`'s version, which
490/// differs from the binary's whenever only the binary was released (#832).
491///
492/// First call wins (a process has one build); returns `false` when a version
493/// was already set. Re-registers the gauge so a recorder installed before
494/// this call reports the new version too, and sets the series it reported
495/// until now to 0, so the recorder never holds two versions at 1.
496pub fn set_build_version(version: &'static str) -> bool {
497    let first = BUILD_VERSION.set(version).is_ok();
498    if first {
499        if version != env!("CARGO_PKG_VERSION") {
500            metrics::gauge!("faucet_build_info", "version" => env!("CARGO_PKG_VERSION")).set(0.0);
501        }
502        register_build_info();
503    }
504    first
505}
506
507/// The version `faucet_build_info` reports: the one passed to
508/// [`set_build_version`], else `faucet-core`'s `CARGO_PKG_VERSION`.
509pub fn build_version() -> &'static str {
510    BUILD_VERSION
511        .get()
512        .copied()
513        .unwrap_or(env!("CARGO_PKG_VERSION"))
514}
515
516/// Register the `faucet_build_info{version}` gauge (set to 1) under the
517/// currently-installed `metrics` recorder. Safe to call from any code path
518/// that wants to ensure the gauge is set; `install_observability` invokes
519/// this automatically. Gauges are naturally idempotent under the `metrics`
520/// model — repeat calls just re-set the same value.
521///
522/// The version label is [`build_version`]: the application's version when it
523/// called [`set_build_version`] (the `faucet` CLI does), else `faucet-core`'s.
524/// Dashboards `group_left` the gauge onto every other metric to annotate
525/// panels with the running version.
526pub fn register_build_info() {
527    metrics::gauge!(
528        "faucet_build_info",
529        "version" => build_version(),
530    )
531    .set(1.0);
532}
533
534#[cfg(all(test, feature = "observability-install"))]
535mod tests {
536    use super::*;
537    use std::sync::Mutex;
538
539    static LOCK: Mutex<()> = Mutex::new(());
540
541    #[cfg(feature = "otel")]
542    #[test]
543    fn otlp_outside_a_runtime_is_skipped_not_a_panic() {
544        let cfg = ObservabilityConfig {
545            otel: Some(crate::observability::otel::OtelConfig::default()),
546            ..Default::default()
547        };
548        let report = install_observability(&cfg).expect("no runtime is not an error");
549        assert!(report.otel_signals.is_empty());
550        assert!(!report.prometheus_already_installed);
551    }
552
553    #[test]
554    fn metrics_mode_selection() {
555        use super::MetricsMode;
556        assert_eq!(MetricsMode::select(true, true), MetricsMode::Fanout);
557        assert_eq!(
558            MetricsMode::select(true, false),
559            MetricsMode::PrometheusOnly
560        );
561        assert_eq!(MetricsMode::select(false, true), MetricsMode::OtelOnly);
562        assert_eq!(MetricsMode::select(false, false), MetricsMode::None);
563    }
564
565    #[test]
566    fn no_config_returns_empty_report() {
567        let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
568        let r = install_observability(&ObservabilityConfig::default()).unwrap();
569        assert!(r.prometheus_listen.is_none());
570        assert!(!r.prometheus_already_installed);
571        assert!(!r.tracing_already_installed);
572    }
573
574    #[test]
575    fn malformed_listen_returns_bind_error() {
576        let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
577        let cfg = ObservabilityConfig {
578            prometheus: Some(PrometheusConfig {
579                listen: "not-a-socket".into(),
580                buckets: None,
581            }),
582            tracing: None,
583            otel: None,
584        };
585        match install_observability(&cfg) {
586            Err(InstallError::PrometheusBind { .. }) => {}
587            other => panic!("expected PrometheusBind error, got {other:?}"),
588        }
589    }
590
591    #[test]
592    fn register_build_info_is_callable_and_idempotent() {
593        let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
594        // Gauges are idempotent under the metrics model; repeat calls must not
595        // panic regardless of which recorder (if any) is installed.
596        register_build_info();
597        register_build_info();
598    }
599
600    #[test]
601    fn install_prometheus_and_tracing_returns_ok() {
602        // Drive the full prometheus + tracing install path on an ephemeral
603        // port. The recorder + subscriber are process-global: depending on
604        // test ordering, the recorder either installs fresh (Ok path) or is
605        // already installed (idempotent warn path). Either way the call must
606        // return Ok and never panic, and the report must reflect what happened.
607        let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
608        let cfg = ObservabilityConfig {
609            prometheus: Some(PrometheusConfig {
610                listen: "127.0.0.1:0".into(),
611                // Exercise the explicit-buckets branch.
612                buckets: Some(vec![0.01, 0.1, 1.0]),
613            }),
614            tracing: Some(TracingConfig {
615                level: "info".into(),
616            }),
617            otel: None,
618        };
619        let report = install_observability(&cfg).expect("install must return Ok");
620        // Exactly one of: recorder installed (listen set) OR already installed.
621        assert!(
622            report.prometheus_listen.is_some() || report.prometheus_already_installed,
623            "prometheus install must either bind or report already-installed"
624        );
625    }
626
627    #[test]
628    fn install_tracing_with_invalid_directive_falls_back_to_info() {
629        // An unparseable EnvFilter directive must not error — it silently falls
630        // back to "info". The call returns Ok regardless of subscriber state.
631        let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
632        let cfg = ObservabilityConfig {
633            prometheus: None,
634            tracing: Some(TracingConfig {
635                // Garbage directive → try_new errors → fallback to info.
636                level: "this is !!! not a valid filter".into(),
637            }),
638            otel: None,
639        };
640        install_observability(&cfg).expect("invalid tracing directive must not fail install");
641    }
642}