Skip to main content

turbo_cdn/
logging.rs

1// Licensed under the MIT License
2// Copyright (c) 2025 Hal <hal.long@outlook.com>
3
4//! Professional logging configuration for turbo-cdn
5//!
6//! This module provides structured logging with different levels for CLI and API usage.
7
8use std::io;
9use tracing_subscriber::{
10    fmt::{self, format::FmtSpan},
11    layer::SubscriberExt,
12    util::SubscriberInitExt,
13    EnvFilter,
14};
15
16/// Logging mode for different usage contexts
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum LoggingMode {
19    /// CLI mode - user-friendly output
20    Cli,
21    /// API mode - structured logging for applications
22    Api,
23    /// Debug mode - verbose logging for development
24    Debug,
25    /// Silent mode - minimal logging
26    Silent,
27}
28
29/// Logging configuration
30#[derive(Debug, Clone)]
31pub struct LoggingConfig {
32    pub mode: LoggingMode,
33    pub level: String,
34    pub show_target: bool,
35    pub show_thread_ids: bool,
36    pub show_file_line: bool,
37    pub use_ansi_colors: bool,
38    pub log_to_file: Option<String>,
39}
40
41impl Default for LoggingConfig {
42    fn default() -> Self {
43        Self {
44            mode: LoggingMode::Api,
45            level: "warn".to_string(),
46            show_target: false,
47            show_thread_ids: false,
48            show_file_line: false,
49            use_ansi_colors: true,
50            log_to_file: None,
51        }
52    }
53}
54
55impl LoggingConfig {
56    /// Create CLI logging configuration
57    pub fn cli(verbose: bool) -> Self {
58        Self {
59            mode: LoggingMode::Cli,
60            level: if verbose {
61                "info".to_string()
62            } else {
63                "warn".to_string()
64            },
65            show_target: verbose,
66            show_thread_ids: false,
67            show_file_line: verbose,
68            use_ansi_colors: true,
69            log_to_file: None,
70        }
71    }
72
73    /// Create API logging configuration
74    pub fn api() -> Self {
75        Self {
76            mode: LoggingMode::Api,
77            level: "warn".to_string(),
78            show_target: false,
79            show_thread_ids: false,
80            show_file_line: false,
81            use_ansi_colors: false,
82            log_to_file: None,
83        }
84    }
85
86    /// Create debug logging configuration
87    pub fn debug() -> Self {
88        Self {
89            mode: LoggingMode::Debug,
90            level: "debug".to_string(),
91            show_target: true,
92            show_thread_ids: true,
93            show_file_line: true,
94            use_ansi_colors: true,
95            log_to_file: Some("turbo-cdn-debug.log".to_string()),
96        }
97    }
98
99    /// Create silent logging configuration
100    pub fn silent() -> Self {
101        Self {
102            mode: LoggingMode::Silent,
103            level: "error".to_string(),
104            show_target: false,
105            show_thread_ids: false,
106            show_file_line: false,
107            use_ansi_colors: false,
108            log_to_file: None,
109        }
110    }
111}
112
113/// Initialize logging with the given configuration
114pub fn init_logging(config: LoggingConfig) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
115    // Create environment filter
116    let env_filter = EnvFilter::try_from_default_env()
117        .unwrap_or_else(|_| EnvFilter::new(format!("turbo_cdn={}", config.level)));
118
119    let registry = tracing_subscriber::registry().with(env_filter);
120
121    match config.mode {
122        LoggingMode::Cli => {
123            // CLI mode: clean, user-friendly output
124            let fmt_layer = fmt::layer()
125                .with_target(config.show_target)
126                .with_thread_ids(config.show_thread_ids)
127                .with_file(config.show_file_line)
128                .with_line_number(config.show_file_line)
129                .with_ansi(config.use_ansi_colors)
130                .with_span_events(FmtSpan::NONE)
131                .compact();
132
133            registry.with(fmt_layer).init();
134        }
135        LoggingMode::Api => {
136            // API mode: structured, machine-readable output
137            let fmt_layer = fmt::layer()
138                .with_target(false)
139                .with_thread_ids(false)
140                .with_file(false)
141                .with_line_number(false)
142                .with_ansi(false)
143                .with_span_events(FmtSpan::NONE)
144                .compact()
145                .with_writer(io::stderr);
146
147            registry.with(fmt_layer).init();
148        }
149        LoggingMode::Debug => {
150            // Debug mode: verbose output with optional file logging
151            let fmt_layer = fmt::layer()
152                .with_target(true)
153                .with_thread_ids(true)
154                .with_file(true)
155                .with_line_number(true)
156                .with_ansi(config.use_ansi_colors)
157                .with_span_events(FmtSpan::ENTER | FmtSpan::CLOSE)
158                .pretty();
159
160            if let Some(log_file) = config.log_to_file {
161                let file_appender = tracing_appender::rolling::daily("./logs", log_file);
162                let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
163
164                let file_layer = fmt::layer()
165                    .with_target(true)
166                    .with_thread_ids(true)
167                    .with_file(true)
168                    .with_line_number(true)
169                    .with_ansi(false)
170                    .with_writer(non_blocking)
171                    .json();
172
173                registry.with(fmt_layer).with(file_layer).init();
174            } else {
175                registry.with(fmt_layer).init();
176            }
177        }
178        LoggingMode::Silent => {
179            // Silent mode: minimal output
180            let fmt_layer = fmt::layer()
181                .with_target(false)
182                .with_thread_ids(false)
183                .with_file(false)
184                .with_line_number(false)
185                .with_ansi(false)
186                .with_span_events(FmtSpan::NONE)
187                .compact()
188                .with_writer(io::stderr);
189
190            registry.with(fmt_layer).init();
191        }
192    }
193
194    Ok(())
195}
196
197/// Initialize CLI logging (convenience function)
198pub fn init_cli_logging(verbose: bool) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
199    init_logging(LoggingConfig::cli(verbose))
200}
201
202/// Initialize API logging (convenience function)
203pub fn init_api_logging() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
204    init_logging(LoggingConfig::api())
205}
206
207/// Initialize debug logging (convenience function)
208pub fn init_debug_logging() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
209    init_logging(LoggingConfig::debug())
210}
211
212/// Initialize silent logging (convenience function)
213pub fn init_silent_logging() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
214    init_logging(LoggingConfig::silent())
215}
216
217/// Logging macros for different contexts
218#[macro_export]
219macro_rules! cli_info {
220    ($($arg:tt)*) => {
221        if tracing::enabled!(tracing::Level::INFO) {
222            tracing::info!($($arg)*);
223        }
224    };
225}
226
227#[macro_export]
228macro_rules! cli_warn {
229    ($($arg:tt)*) => {
230        tracing::warn!($($arg)*);
231    };
232}
233
234#[macro_export]
235macro_rules! cli_error {
236    ($($arg:tt)*) => {
237        tracing::error!($($arg)*);
238    };
239}
240
241#[macro_export]
242macro_rules! api_debug {
243    ($($arg:tt)*) => {
244        if tracing::enabled!(tracing::Level::DEBUG) {
245            tracing::debug!($($arg)*);
246        }
247    };
248}
249
250#[macro_export]
251macro_rules! api_info {
252    ($($arg:tt)*) => {
253        if tracing::enabled!(tracing::Level::INFO) {
254            tracing::info!($($arg)*);
255        }
256    };
257}
258
259/// Check if we're in verbose mode
260pub fn is_verbose() -> bool {
261    tracing::enabled!(tracing::Level::INFO)
262}
263
264/// Check if we're in debug mode
265pub fn is_debug() -> bool {
266    tracing::enabled!(tracing::Level::DEBUG)
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn test_logging_config_creation() {
275        let cli_config = LoggingConfig::cli(true);
276        assert_eq!(cli_config.mode, LoggingMode::Cli);
277        assert_eq!(cli_config.level, "info");
278        assert!(cli_config.show_target);
279
280        let api_config = LoggingConfig::api();
281        assert_eq!(api_config.mode, LoggingMode::Api);
282        assert_eq!(api_config.level, "warn");
283        assert!(!api_config.show_target);
284    }
285
286    #[test]
287    fn test_debug_config() {
288        let debug_config = LoggingConfig::debug();
289        assert_eq!(debug_config.mode, LoggingMode::Debug);
290        assert_eq!(debug_config.level, "debug");
291        assert!(debug_config.show_target);
292        assert!(debug_config.show_thread_ids);
293        assert!(debug_config.show_file_line);
294    }
295}