ccxt_core/
logging.rs

1//! Structured logging system.
2//!
3//! Provides tracing-based structured logging with support for:
4//! - Multi-level logging (TRACE, DEBUG, INFO, WARN, ERROR)
5//! - Structured fields
6//! - Environment variable configuration
7//! - JSON and formatted output
8
9use tracing::Level;
10use tracing_subscriber::{
11    EnvFilter, Layer,
12    fmt::{self, format::FmtSpan},
13    layer::SubscriberExt,
14    util::SubscriberInitExt,
15};
16
17/// Log level.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum LogLevel {
20    /// Trace level: most detailed debugging information.
21    Trace,
22    /// Debug level: detailed debugging information.
23    Debug,
24    /// Info level: important business events.
25    Info,
26    /// Warn level: potential issues.
27    Warn,
28    /// Error level: error information.
29    Error,
30}
31
32impl From<LogLevel> for Level {
33    fn from(level: LogLevel) -> Self {
34        match level {
35            LogLevel::Trace => Level::TRACE,
36            LogLevel::Debug => Level::DEBUG,
37            LogLevel::Info => Level::INFO,
38            LogLevel::Warn => Level::WARN,
39            LogLevel::Error => Level::ERROR,
40        }
41    }
42}
43
44impl std::fmt::Display for LogLevel {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        match self {
47            LogLevel::Trace => write!(f, "trace"),
48            LogLevel::Debug => write!(f, "debug"),
49            LogLevel::Info => write!(f, "info"),
50            LogLevel::Warn => write!(f, "warn"),
51            LogLevel::Error => write!(f, "error"),
52        }
53    }
54}
55
56/// Log format.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum LogFormat {
59    /// Human-readable formatted output.
60    Pretty,
61    /// Compact format.
62    Compact,
63    /// JSON format for production environments.
64    Json,
65}
66
67/// Log configuration.
68#[derive(Debug, Clone)]
69pub struct LogConfig {
70    /// Log level.
71    pub level: LogLevel,
72    /// Log format.
73    pub format: LogFormat,
74    /// Whether to show timestamps.
75    pub show_time: bool,
76    /// Whether to show thread IDs.
77    pub show_thread_ids: bool,
78    /// Whether to show target module.
79    pub show_target: bool,
80    /// Whether to show span events (function enter/exit).
81    pub show_span_events: bool,
82}
83
84impl Default for LogConfig {
85    fn default() -> Self {
86        Self {
87            level: LogLevel::Info,
88            format: LogFormat::Pretty,
89            show_time: true,
90            show_thread_ids: false,
91            show_target: true,
92            show_span_events: false,
93        }
94    }
95}
96
97impl LogConfig {
98    /// Creates a log configuration for development environments.
99    pub fn development() -> Self {
100        Self {
101            level: LogLevel::Debug,
102            format: LogFormat::Pretty,
103            show_time: true,
104            show_thread_ids: false,
105            show_target: true,
106            show_span_events: true,
107        }
108    }
109
110    /// Creates a log configuration for production environments.
111    pub fn production() -> Self {
112        Self {
113            level: LogLevel::Info,
114            format: LogFormat::Json,
115            show_time: true,
116            show_thread_ids: true,
117            show_target: true,
118            show_span_events: false,
119        }
120    }
121
122    /// Creates a log configuration for test environments.
123    pub fn test() -> Self {
124        Self {
125            level: LogLevel::Warn,
126            format: LogFormat::Compact,
127            show_time: false,
128            show_thread_ids: false,
129            show_target: false,
130            show_span_events: false,
131        }
132    }
133}
134
135/// Initializes the logging system.
136///
137/// # Arguments
138///
139/// * `config` - The logging configuration.
140///
141/// # Examples
142///
143/// ```no_run
144/// use ccxt_core::logging::{init_logging, LogConfig};
145///
146/// // Use default configuration
147/// init_logging(LogConfig::default());
148///
149/// // Or use development configuration
150/// init_logging(LogConfig::development());
151/// ```
152pub fn init_logging(config: LogConfig) {
153    let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
154        EnvFilter::new(format!(
155            "ccxt_core={},ccxt_exchanges={}",
156            config.level, config.level
157        ))
158    });
159
160    match config.format {
161        LogFormat::Pretty => {
162            let fmt_layer = fmt::layer()
163                .pretty()
164                .with_timer(fmt::time::time())
165                .with_thread_ids(config.show_thread_ids)
166                .with_target(config.show_target)
167                .with_span_events(if config.show_span_events {
168                    FmtSpan::ENTER | FmtSpan::CLOSE
169                } else {
170                    FmtSpan::NONE
171                })
172                .with_filter(env_filter);
173
174            tracing_subscriber::registry().with(fmt_layer).init();
175        }
176        LogFormat::Compact => {
177            let fmt_layer = fmt::layer()
178                .compact()
179                .with_timer(fmt::time::time())
180                .with_thread_ids(config.show_thread_ids)
181                .with_target(config.show_target)
182                .with_span_events(if config.show_span_events {
183                    FmtSpan::ENTER | FmtSpan::CLOSE
184                } else {
185                    FmtSpan::NONE
186                })
187                .with_filter(env_filter);
188
189            tracing_subscriber::registry().with(fmt_layer).init();
190        }
191        LogFormat::Json => {
192            let fmt_layer = fmt::layer()
193                .json()
194                .with_timer(fmt::time::time())
195                .with_thread_ids(config.show_thread_ids)
196                .with_target(config.show_target)
197                .with_span_events(if config.show_span_events {
198                    FmtSpan::ENTER | FmtSpan::CLOSE
199                } else {
200                    FmtSpan::NONE
201                })
202                .with_filter(env_filter);
203
204            tracing_subscriber::registry().with(fmt_layer).init();
205        }
206    }
207}
208
209/// Attempts to initialize the logging system, ignoring duplicate initialization errors.
210///
211/// Suitable for test environments where multiple calls should not panic.
212pub fn try_init_logging(config: LogConfig) {
213    let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
214        EnvFilter::new(format!(
215            "ccxt_core={},ccxt_exchanges={}",
216            config.level, config.level
217        ))
218    });
219
220    let result = match config.format {
221        LogFormat::Pretty => {
222            let fmt_layer = fmt::layer()
223                .pretty()
224                .with_timer(fmt::time::time())
225                .with_thread_ids(config.show_thread_ids)
226                .with_target(config.show_target)
227                .with_span_events(if config.show_span_events {
228                    FmtSpan::ENTER | FmtSpan::CLOSE
229                } else {
230                    FmtSpan::NONE
231                })
232                .with_filter(env_filter);
233
234            tracing_subscriber::registry().with(fmt_layer).try_init()
235        }
236        LogFormat::Compact => {
237            let fmt_layer = fmt::layer()
238                .compact()
239                .with_timer(fmt::time::time())
240                .with_thread_ids(config.show_thread_ids)
241                .with_target(config.show_target)
242                .with_span_events(if config.show_span_events {
243                    FmtSpan::ENTER | FmtSpan::CLOSE
244                } else {
245                    FmtSpan::NONE
246                })
247                .with_filter(env_filter);
248
249            tracing_subscriber::registry().with(fmt_layer).try_init()
250        }
251        LogFormat::Json => {
252            let fmt_layer = fmt::layer()
253                .json()
254                .with_timer(fmt::time::time())
255                .with_thread_ids(config.show_thread_ids)
256                .with_target(config.show_target)
257                .with_span_events(if config.show_span_events {
258                    FmtSpan::ENTER | FmtSpan::CLOSE
259                } else {
260                    FmtSpan::NONE
261                })
262                .with_filter(env_filter);
263
264            tracing_subscriber::registry().with(fmt_layer).try_init()
265        }
266    };
267
268    let _ = result;
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn test_log_level_conversion() {
277        assert_eq!(Level::from(LogLevel::Trace), Level::TRACE);
278        assert_eq!(Level::from(LogLevel::Debug), Level::DEBUG);
279        assert_eq!(Level::from(LogLevel::Info), Level::INFO);
280        assert_eq!(Level::from(LogLevel::Warn), Level::WARN);
281        assert_eq!(Level::from(LogLevel::Error), Level::ERROR);
282    }
283
284    #[test]
285    fn test_log_level_display() {
286        assert_eq!(LogLevel::Trace.to_string(), "trace");
287        assert_eq!(LogLevel::Debug.to_string(), "debug");
288        assert_eq!(LogLevel::Info.to_string(), "info");
289        assert_eq!(LogLevel::Warn.to_string(), "warn");
290        assert_eq!(LogLevel::Error.to_string(), "error");
291    }
292
293    #[test]
294    fn test_log_config_default() {
295        let config = LogConfig::default();
296        assert_eq!(config.level, LogLevel::Info);
297        assert_eq!(config.format, LogFormat::Pretty);
298        assert!(config.show_time);
299        assert!(!config.show_thread_ids);
300        assert!(config.show_target);
301        assert!(!config.show_span_events);
302    }
303
304    #[test]
305    fn test_log_config_development() {
306        let config = LogConfig::development();
307        assert_eq!(config.level, LogLevel::Debug);
308        assert_eq!(config.format, LogFormat::Pretty);
309        assert!(config.show_span_events);
310    }
311
312    #[test]
313    fn test_log_config_production() {
314        let config = LogConfig::production();
315        assert_eq!(config.level, LogLevel::Info);
316        assert_eq!(config.format, LogFormat::Json);
317        assert!(config.show_thread_ids);
318    }
319
320    #[test]
321    fn test_log_config_test() {
322        let config = LogConfig::test();
323        assert_eq!(config.level, LogLevel::Warn);
324        assert_eq!(config.format, LogFormat::Compact);
325        assert!(!config.show_time);
326    }
327
328    #[test]
329    fn test_try_init_logging() {
330        try_init_logging(LogConfig::test());
331        try_init_logging(LogConfig::test());
332    }
333}