Skip to main content

oxibonsai_runtime/
tracing_setup.rs

1//! Tracing initialization with configurable output format.
2//!
3//! Provides a structured way to initialize the `tracing` subscriber
4//! with either human-readable or JSON output, driven by configuration.
5//!
6//! On WASM targets, `tracing_subscriber` is not available. The
7//! `init_tracing` function is a no-op stub that always succeeds.
8
9#[cfg(not(target_arch = "wasm32"))]
10use tracing_subscriber::{fmt, prelude::*, EnvFilter};
11
12/// Configuration for the tracing/logging subsystem.
13#[derive(Debug, Clone)]
14pub struct TracingConfig {
15    /// Log level filter string (e.g. "info", "debug", "oxibonsai=trace").
16    pub log_level: String,
17    /// Whether to emit JSON-formatted log lines.
18    pub json_output: bool,
19    /// Whether to include the source file path in log output.
20    pub with_file: bool,
21    /// Whether to include line numbers in log output.
22    pub with_line_number: bool,
23    /// Whether to include the tracing target in log output.
24    pub with_target: bool,
25}
26
27impl Default for TracingConfig {
28    fn default() -> Self {
29        Self {
30            log_level: "info".to_string(),
31            json_output: false,
32            with_file: false,
33            with_line_number: false,
34            with_target: true,
35        }
36    }
37}
38
39impl TracingConfig {
40    /// Create a `TracingConfig` from an `ObservabilityConfig`.
41    pub fn from_observability(obs: &crate::config::ObservabilityConfig) -> Self {
42        Self {
43            log_level: obs.log_level.clone(),
44            json_output: obs.json_logs,
45            ..Self::default()
46        }
47    }
48}
49
50/// Initialize tracing with the given configuration.
51///
52/// On non-WASM targets, sets up a `tracing_subscriber` with either
53/// human-readable or JSON output format.
54///
55/// On WASM targets, this is a no-op (tracing_subscriber is unavailable).
56///
57/// # Errors
58///
59/// On non-WASM: returns an error if the tracing subscriber cannot be
60/// initialized (e.g. if a global subscriber is already set).
61///
62/// On WASM: always returns `Ok(())`.
63#[cfg(not(target_arch = "wasm32"))]
64pub fn init_tracing(config: &TracingConfig) -> Result<(), Box<dyn std::error::Error>> {
65    let env_filter =
66        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.log_level));
67
68    if config.json_output {
69        tracing_subscriber::registry()
70            .with(env_filter)
71            .with(fmt::layer().json())
72            .try_init()
73            .map_err(|e| -> Box<dyn std::error::Error> {
74                Box::new(std::io::Error::other(format!(
75                    "failed to init tracing: {e}"
76                )))
77            })?;
78    } else {
79        tracing_subscriber::registry()
80            .with(env_filter)
81            .with(
82                fmt::layer()
83                    .with_file(config.with_file)
84                    .with_line_number(config.with_line_number)
85                    .with_target(config.with_target),
86            )
87            .try_init()
88            .map_err(|e| -> Box<dyn std::error::Error> {
89                Box::new(std::io::Error::other(format!(
90                    "failed to init tracing: {e}"
91                )))
92            })?;
93    }
94
95    Ok(())
96}
97
98/// Initialize tracing (no-op on WASM targets).
99#[cfg(target_arch = "wasm32")]
100pub fn init_tracing(_config: &TracingConfig) -> Result<(), Box<dyn std::error::Error>> {
101    Ok(())
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn default_config_values() {
110        let cfg = TracingConfig::default();
111        assert_eq!(cfg.log_level, "info");
112        assert!(!cfg.json_output);
113        assert!(!cfg.with_file);
114        assert!(!cfg.with_line_number);
115        assert!(cfg.with_target);
116    }
117
118    #[test]
119    fn from_observability_config() {
120        let obs = crate::config::ObservabilityConfig {
121            log_level: "debug".to_string(),
122            json_logs: true,
123        };
124        let cfg = TracingConfig::from_observability(&obs);
125        assert_eq!(cfg.log_level, "debug");
126        assert!(cfg.json_output);
127    }
128
129    #[test]
130    fn tracing_config_clone() {
131        let cfg = TracingConfig {
132            log_level: "warn".to_string(),
133            json_output: true,
134            with_file: true,
135            with_line_number: true,
136            with_target: false,
137        };
138        let cloned = cfg.clone();
139        assert_eq!(cloned.log_level, "warn");
140        assert!(cloned.json_output);
141        assert!(cloned.with_file);
142        assert!(cloned.with_line_number);
143        assert!(!cloned.with_target);
144    }
145
146    #[test]
147    fn tracing_config_debug() {
148        let cfg = TracingConfig::default();
149        let debug_str = format!("{cfg:?}");
150        assert!(debug_str.contains("TracingConfig"));
151        assert!(debug_str.contains("info"));
152    }
153}