Skip to main content

fresh/services/
tracing_setup.rs

1//! Tracing subscriber setup
2//!
3//! This module provides shared tracing configuration used by both
4//! the main application and tests.
5
6use std::fs::File;
7use std::path::Path;
8use std::sync::Arc;
9use tracing_subscriber::prelude::*;
10use tracing_subscriber::{fmt, EnvFilter};
11
12use super::status_log::{StatusLogHandle, StatusLogLayer};
13use super::warning_log::{WarningLogHandle, WarningLogLayer};
14
15/// Combined handles for all log layers
16pub struct TracingHandles {
17    pub warning: WarningLogHandle,
18    pub status: StatusLogHandle,
19}
20
21/// Initialize the global tracing subscriber with file logging and warning/status capture.
22///
23/// This sets up:
24/// - File-based logging with the given log file
25/// - Environment-based filtering (RUST_LOG) with DEBUG default
26/// - Warning log layer that captures WARN+ to a separate file
27/// - Status log layer that captures status messages to a separate file
28///
29/// Returns the tracing handles if successful, None if setup failed.
30pub fn init_global(log_file_path: &Path) -> Option<TracingHandles> {
31    let (warning_layer, warning_handle) = super::warning_log::create().ok()?;
32    let (status_layer, status_handle) = super::status_log::create().ok()?;
33    let log_file = File::create(log_file_path).ok()?;
34
35    let subscriber = build_subscriber(log_file, Some(warning_layer), Some(status_layer));
36    subscriber.init();
37
38    Some(TracingHandles {
39        warning: warning_handle,
40        status: status_handle,
41    })
42}
43
44/// Build a subscriber with file logging and optional warning/status layers.
45///
46/// This is the core subscriber configuration shared between production and tests.
47pub fn build_subscriber(
48    log_file: File,
49    warning_layer: Option<WarningLogLayer>,
50    status_layer: Option<StatusLogLayer>,
51) -> impl tracing::Subscriber + Send + Sync {
52    let env_filter = EnvFilter::from_default_env()
53        .add_directive(tracing::Level::DEBUG.into())
54        // Suppress noisy SWC debug logs
55        .add_directive("swc_ecma_transforms_base=info".parse().unwrap())
56        .add_directive("swc_common=info".parse().unwrap());
57
58    let span_events = if std::env::var("FRESH_LOG_SPANS").is_ok() {
59        fmt::format::FmtSpan::CLOSE
60    } else {
61        fmt::format::FmtSpan::NONE
62    };
63
64    let fmt_layer = fmt::layer()
65        .with_writer(Arc::new(log_file))
66        .with_span_events(span_events);
67
68    tracing_subscriber::registry()
69        .with(fmt_layer)
70        .with(env_filter)
71        .with(warning_layer)
72        .with(status_layer)
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use std::time::Duration;
79    use tempfile::{NamedTempFile, TempPath};
80
81    struct TestSubscriber {
82        subscriber: Box<dyn tracing::Subscriber + Send + Sync>,
83        warning_handle: WarningLogHandle,
84        // Keep tempfiles alive so they don't get deleted
85        _log_file: NamedTempFile,
86        _warning_log_path: TempPath,
87        _status_log_path: TempPath,
88    }
89
90    fn create_test_subscriber() -> TestSubscriber {
91        let log_file = NamedTempFile::new().unwrap();
92        let warning_log_file = NamedTempFile::new().unwrap();
93        let warning_log_path = warning_log_file.into_temp_path();
94        let status_log_file = NamedTempFile::new().unwrap();
95        let status_log_path = status_log_file.into_temp_path();
96
97        let (warning_layer, warning_handle) =
98            super::super::warning_log::create_with_path(warning_log_path.to_path_buf()).unwrap();
99        let (status_layer, _status_handle) =
100            super::super::status_log::create_with_path(status_log_path.to_path_buf()).unwrap();
101
102        let subscriber = build_subscriber(
103            log_file.reopen().unwrap(),
104            Some(warning_layer),
105            Some(status_layer),
106        );
107
108        TestSubscriber {
109            subscriber: Box::new(subscriber),
110            warning_handle,
111            _log_file: log_file,
112            _warning_log_path: warning_log_path,
113            _status_log_path: status_log_path,
114        }
115    }
116
117    #[test]
118    fn test_warning_log_captures_warn_level() {
119        let test = create_test_subscriber();
120        let path = test.warning_handle.path.clone();
121
122        tracing::subscriber::with_default(test.subscriber, || {
123            tracing::warn!("Test warning message");
124        });
125
126        let result = test
127            .warning_handle
128            .receiver
129            .recv_timeout(Duration::from_secs(1));
130        assert!(result.is_ok(), "Should receive notification for WARN");
131
132        let contents = std::fs::read_to_string(&path).expect("Failed to read log");
133        assert!(contents.contains("WARN"), "Log should contain WARN level");
134        assert!(
135            contents.contains("Test warning message"),
136            "Log should contain message"
137        );
138    }
139
140    #[test]
141    fn test_warning_log_captures_error_level() {
142        let test = create_test_subscriber();
143        let path = test.warning_handle.path.clone();
144
145        tracing::subscriber::with_default(test.subscriber, || {
146            tracing::error!("Test error message");
147        });
148
149        let result = test
150            .warning_handle
151            .receiver
152            .recv_timeout(Duration::from_secs(1));
153        assert!(result.is_ok(), "Should receive notification for ERROR");
154
155        let contents = std::fs::read_to_string(&path).expect("Failed to read log");
156        assert!(contents.contains("ERROR"), "Log should contain ERROR level");
157        assert!(
158            contents.contains("Test error message"),
159            "Log should contain message"
160        );
161    }
162
163    #[test]
164    fn test_warning_log_ignores_info_level() {
165        let test = create_test_subscriber();
166        let path = test.warning_handle.path.clone();
167
168        tracing::subscriber::with_default(test.subscriber, || {
169            tracing::info!("Test info message");
170        });
171
172        let result = test
173            .warning_handle
174            .receiver
175            .recv_timeout(Duration::from_millis(100));
176        assert!(result.is_err(), "Should NOT receive notification for INFO");
177
178        let contents = std::fs::read_to_string(&path).unwrap_or_default();
179        assert!(
180            !contents.contains("Test info message"),
181            "Log should NOT contain INFO message"
182        );
183    }
184
185    #[test]
186    fn test_warning_log_ignores_debug_level() {
187        let test = create_test_subscriber();
188
189        tracing::subscriber::with_default(test.subscriber, || {
190            tracing::debug!("Test debug message");
191        });
192
193        let result = test
194            .warning_handle
195            .receiver
196            .recv_timeout(Duration::from_millis(100));
197        assert!(result.is_err(), "Should NOT receive notification for DEBUG");
198    }
199
200    #[test]
201    fn test_warning_log_multiple_warnings() {
202        let test = create_test_subscriber();
203        let path = test.warning_handle.path.clone();
204
205        tracing::subscriber::with_default(test.subscriber, || {
206            tracing::warn!("First warning");
207            tracing::error!("An error");
208            tracing::warn!("Second warning");
209        });
210
211        for i in 0..3 {
212            let result = test
213                .warning_handle
214                .receiver
215                .recv_timeout(Duration::from_secs(1));
216            assert!(result.is_ok(), "Should receive notification {}", i + 1);
217        }
218
219        let contents = std::fs::read_to_string(&path).expect("Failed to read log");
220        assert!(
221            contents.contains("First warning"),
222            "Log should contain first warning"
223        );
224        assert!(contents.contains("An error"), "Log should contain error");
225        assert!(
226            contents.contains("Second warning"),
227            "Log should contain second warning"
228        );
229    }
230}