indexbus_platform_obs/
tracing_init.rs

1//! Tracing initialization helpers.
2//!
3//! **Feature requirements**
4//! - `tracing`
5//!
6//! **Contracts**
7//! - `tracing-subscriber` installation is global; initializing twice will return an error.
8//! - Output is written to stderr.
9
10#[cfg(feature = "tracing")]
11use std::{
12    io,
13    sync::{
14        atomic::{AtomicBool, Ordering},
15        Arc,
16    },
17};
18
19#[cfg(feature = "tracing")]
20use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
21
22#[cfg(feature = "tracing")]
23use crate::errors::{Error, Result};
24
25/// Basic tracing configuration for std processes.
26#[cfg(feature = "tracing")]
27#[derive(Clone, Debug)]
28pub struct TracingConfig {
29    /// Set via `RUST_LOG` if present; otherwise this default is used.
30    pub default_filter: String,
31    /// Output format.
32    pub format: TracingFormat,
33    /// Include the event target (Rust module path).
34    pub with_target: bool,
35    /// Include numeric thread ids.
36    pub with_thread_ids: bool,
37    /// Include thread names.
38    pub with_thread_names: bool,
39}
40
41/// Tracing output format.
42#[cfg(feature = "tracing")]
43#[derive(Clone, Copy, Debug, Default)]
44pub enum TracingFormat {
45    /// Human-friendly logs (default).
46    #[default]
47    Pretty,
48
49    /// Structured JSON logs.
50    ///
51    /// Requires the `tracing-json` crate feature.
52    #[cfg(feature = "tracing-json")]
53    Json,
54}
55
56#[cfg(feature = "tracing")]
57impl Default for TracingConfig {
58    fn default() -> Self {
59        Self {
60            default_filter: "info".to_string(),
61            format: TracingFormat::Pretty,
62            with_target: true,
63            with_thread_ids: false,
64            with_thread_names: true,
65        }
66    }
67}
68
69/// A handle that can be used to request shutdown of observability background tasks.
70///
71/// Currently this is a minimal primitive; it can later be extended to stop exporters.
72#[cfg(feature = "tracing")]
73#[derive(Clone, Debug)]
74pub struct TracingHandle {
75    shutdown: Arc<AtomicBool>,
76}
77
78#[cfg(feature = "tracing")]
79impl TracingHandle {
80    /// Request shutdown of background observability tasks.
81    pub fn request_shutdown(&self) {
82        self.shutdown.store(true, Ordering::Relaxed);
83    }
84
85    /// Returns `true` if shutdown was requested.
86    pub fn is_shutdown_requested(&self) -> bool {
87        self.shutdown.load(Ordering::Relaxed)
88    }
89}
90
91/// Initialize a reasonable default `tracing` subscriber for CLI/services.
92///
93/// - Uses `RUST_LOG` if set, else `TracingConfig::default_filter`.
94/// - Logs to stderr.
95///
96/// # Errors
97/// Returns an error if the filter cannot be parsed or if a global subscriber is already set.
98#[cfg(feature = "tracing")]
99pub fn init_tracing(config: &TracingConfig) -> Result<TracingHandle> {
100    let env_filter = EnvFilter::try_from_default_env()
101        .or_else(|_| EnvFilter::try_new(&config.default_filter))
102        .map_err(|e| Error::TracingInit(e.to_string()))?;
103
104    let base_layer = fmt::layer()
105        .with_writer(io::stderr)
106        .with_target(config.with_target)
107        .with_thread_ids(config.with_thread_ids)
108        .with_thread_names(config.with_thread_names);
109
110    #[cfg(feature = "tracing-json")]
111    {
112        match config.format {
113            TracingFormat::Pretty => tracing_subscriber::registry()
114                .with(env_filter)
115                .with(base_layer)
116                .try_init()
117                .map_err(|e| Error::TracingInit(e.to_string()))?,
118            TracingFormat::Json => tracing_subscriber::registry()
119                .with(env_filter)
120                .with(base_layer.json())
121                .try_init()
122                .map_err(|e| Error::TracingInit(e.to_string()))?,
123        }
124    }
125
126    #[cfg(not(feature = "tracing-json"))]
127    {
128        tracing_subscriber::registry()
129            .with(env_filter)
130            .with(base_layer)
131            .try_init()
132            .map_err(|e| Error::TracingInit(e.to_string()))?;
133    }
134
135    Ok(TracingHandle {
136        shutdown: Arc::new(AtomicBool::new(false)),
137    })
138}
139
140#[cfg(all(test, feature = "tracing"))]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn tracing_handle_shutdown_flag_roundtrips() {
146        let h = TracingHandle {
147            shutdown: Arc::new(AtomicBool::new(false)),
148        };
149
150        assert!(!h.is_shutdown_requested());
151        h.request_shutdown();
152        assert!(h.is_shutdown_requested());
153    }
154
155    #[test]
156    fn init_tracing_fails_if_called_twice() {
157        let cfg = TracingConfig::default();
158        let _ = init_tracing(&cfg).expect("first init");
159
160        let second = init_tracing(&cfg);
161        assert!(matches!(second, Err(Error::TracingInit(_))));
162    }
163}