Skip to main content

dependency_injector/
logging.rs

1//! Logging configuration for dependency-injector
2//!
3//! This module provides easy setup for structured logging with support for
4//! both JSON (production) and pretty (development) output formats.
5//!
6//! # Features
7//!
8//! - `logging` - Enable debug logging (default)
9//! - `logging-json` - Use JSON structured output (recommended for production)
10//! - `logging-pretty` - Use colorful pretty output (recommended for development)
11//!
12//! # Example
13//!
14//! ```rust,ignore
15//! use dependency_injector::logging;
16//!
17//! // Initialize with default settings (JSON if logging-json, pretty if logging-pretty)
18//! logging::init();
19//!
20//! // Or initialize with specific format
21//! logging::init_json();
22//! logging::init_pretty();
23//!
24//! // Or use builder for custom configuration
25//! logging::builder()
26//!     .with_level(tracing::Level::DEBUG)
27//!     .with_target("dependency_injector")
28//!     .json()
29//!     .init();
30//! ```
31
32#[cfg(feature = "logging")]
33use tracing::Level;
34
35/// Logging format configuration
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
37pub enum LogFormat {
38    /// JSON structured logging (production default)
39    #[default]
40    Json,
41    /// Pretty colorful output (development)
42    Pretty,
43    /// Compact single-line output
44    Compact,
45}
46
47/// Builder for logging configuration
48#[cfg(feature = "logging")]
49#[derive(Debug, Clone)]
50pub struct LoggingBuilder {
51    level: Level,
52    format: LogFormat,
53    target: Option<&'static str>,
54    with_file: bool,
55    with_line_number: bool,
56    with_thread_ids: bool,
57    with_thread_names: bool,
58}
59
60#[cfg(feature = "logging")]
61impl Default for LoggingBuilder {
62    fn default() -> Self {
63        Self {
64            level: Level::DEBUG,
65            format: LogFormat::Json,
66            target: None,
67            with_file: false,
68            with_line_number: false,
69            with_thread_ids: false,
70            with_thread_names: false,
71        }
72    }
73}
74
75#[cfg(feature = "logging")]
76impl LoggingBuilder {
77    /// Create a new logging builder with default settings
78    pub fn new() -> Self {
79        Self::default()
80    }
81
82    /// Set the minimum log level
83    pub fn with_level(mut self, level: Level) -> Self {
84        self.level = level;
85        self
86    }
87
88    /// Set log level to TRACE (most verbose)
89    pub fn trace(mut self) -> Self {
90        self.level = Level::TRACE;
91        self
92    }
93
94    /// Set log level to DEBUG
95    pub fn debug(mut self) -> Self {
96        self.level = Level::DEBUG;
97        self
98    }
99
100    /// Set log level to INFO
101    pub fn info(mut self) -> Self {
102        self.level = Level::INFO;
103        self
104    }
105
106    /// Set log level to WARN
107    pub fn warn(mut self) -> Self {
108        self.level = Level::WARN;
109        self
110    }
111
112    /// Set log level to ERROR (least verbose)
113    pub fn error(mut self) -> Self {
114        self.level = Level::ERROR;
115        self
116    }
117
118    /// Filter to only show logs from a specific target
119    pub fn with_target_filter(mut self, target: &'static str) -> Self {
120        self.target = Some(target);
121        self
122    }
123
124    /// Only show dependency-injector logs
125    pub fn di_only(self) -> Self {
126        self.with_target_filter("dependency_injector")
127    }
128
129    /// Include file names in log output
130    pub fn with_file(mut self) -> Self {
131        self.with_file = true;
132        self
133    }
134
135    /// Include line numbers in log output
136    pub fn with_line_number(mut self) -> Self {
137        self.with_line_number = true;
138        self
139    }
140
141    /// Include thread IDs in log output
142    pub fn with_thread_ids(mut self) -> Self {
143        self.with_thread_ids = true;
144        self
145    }
146
147    /// Include thread names in log output
148    pub fn with_thread_names(mut self) -> Self {
149        self.with_thread_names = true;
150        self
151    }
152
153    /// Use JSON structured logging format
154    pub fn json(mut self) -> Self {
155        self.format = LogFormat::Json;
156        self
157    }
158
159    /// Use pretty colorful logging format
160    pub fn pretty(mut self) -> Self {
161        self.format = LogFormat::Pretty;
162        self
163    }
164
165    /// Use compact single-line logging format
166    pub fn compact(mut self) -> Self {
167        self.format = LogFormat::Compact;
168        self
169    }
170
171    /// Initialize the logging subscriber with the configured settings
172    ///
173    /// Requires either `logging-json` or `logging-pretty` feature to be enabled.
174    #[cfg(any(feature = "logging-json", feature = "logging-pretty"))]
175    pub fn init(self) {
176        use tracing_subscriber::{EnvFilter, fmt, prelude::*};
177
178        let filter = if let Some(target) = self.target {
179            EnvFilter::new(format!("{}={}", target, self.level))
180        } else {
181            EnvFilter::new(self.level.to_string())
182        };
183
184        match self.format {
185            LogFormat::Json => {
186                #[cfg(feature = "logging-json")]
187                {
188                    let subscriber = fmt::layer()
189                        .json()
190                        .with_file(self.with_file)
191                        .with_line_number(self.with_line_number)
192                        .with_thread_ids(self.with_thread_ids)
193                        .with_thread_names(self.with_thread_names)
194                        .with_target(true);
195
196                    tracing_subscriber::registry()
197                        .with(filter)
198                        .with(subscriber)
199                        .init();
200                }
201                #[cfg(not(feature = "logging-json"))]
202                {
203                    // Fall back to pretty if json not enabled
204                    let subscriber = fmt::layer()
205                        .with_file(self.with_file)
206                        .with_line_number(self.with_line_number)
207                        .with_thread_ids(self.with_thread_ids)
208                        .with_thread_names(self.with_thread_names)
209                        .with_target(true);
210
211                    tracing_subscriber::registry()
212                        .with(filter)
213                        .with(subscriber)
214                        .init();
215                }
216            }
217            LogFormat::Pretty => {
218                let subscriber = fmt::layer()
219                    .pretty()
220                    .with_file(self.with_file)
221                    .with_line_number(self.with_line_number)
222                    .with_thread_ids(self.with_thread_ids)
223                    .with_thread_names(self.with_thread_names)
224                    .with_target(true);
225
226                tracing_subscriber::registry()
227                    .with(filter)
228                    .with(subscriber)
229                    .init();
230            }
231            LogFormat::Compact => {
232                let subscriber = fmt::layer()
233                    .compact()
234                    .with_file(self.with_file)
235                    .with_line_number(self.with_line_number)
236                    .with_thread_ids(self.with_thread_ids)
237                    .with_thread_names(self.with_thread_names)
238                    .with_target(true);
239
240                tracing_subscriber::registry()
241                    .with(filter)
242                    .with(subscriber)
243                    .init();
244            }
245        }
246    }
247
248    /// Initialize (no-op when subscriber features not available)
249    #[cfg(not(any(feature = "logging-json", feature = "logging-pretty")))]
250    pub fn init(self) {
251        // No-op: tracing-subscriber not enabled
252        // Users should use logging-json or logging-pretty features
253    }
254}
255
256/// Create a new logging builder
257#[cfg(feature = "logging")]
258pub fn builder() -> LoggingBuilder {
259    LoggingBuilder::new()
260}
261
262/// Initialize logging with default settings
263///
264/// Uses JSON format if `logging-json` feature is enabled,
265/// otherwise uses pretty format if `logging-pretty` is enabled.
266#[cfg(any(feature = "logging-json", feature = "logging-pretty"))]
267pub fn init() {
268    #[cfg(feature = "logging-json")]
269    {
270        init_json();
271    }
272    #[cfg(all(feature = "logging-pretty", not(feature = "logging-json")))]
273    {
274        init_pretty();
275    }
276}
277
278/// Initialize logging (no-op when subscriber features not available)
279#[cfg(not(any(feature = "logging-json", feature = "logging-pretty")))]
280pub fn init() {
281    // No-op: requires logging-json or logging-pretty feature
282}
283
284/// Initialize JSON structured logging
285///
286/// Outputs logs in JSON format, ideal for production environments
287/// where logs are aggregated and parsed by tools like ELK or Datadog.
288///
289/// # Example output
290/// ```json
291/// {"timestamp":"2024-01-01T00:00:00.000Z","level":"DEBUG","target":"dependency_injector","message":"Creating new DI container"}
292/// ```
293#[cfg(any(feature = "logging-json", feature = "logging-pretty"))]
294pub fn init_json() {
295    builder().json().debug().init();
296}
297
298/// Initialize JSON logging (no-op when not available)
299#[cfg(not(any(feature = "logging-json", feature = "logging-pretty")))]
300pub fn init_json() {
301    // No-op: requires logging-json or logging-pretty feature
302}
303
304/// Initialize pretty colorful logging
305///
306/// Outputs logs in a human-readable format with colors,
307/// ideal for development and debugging.
308///
309/// # Example output
310/// ```text
311///   2024-01-01T00:00:00.000Z DEBUG dependency_injector: Creating new DI container
312/// ```
313#[cfg(any(feature = "logging-json", feature = "logging-pretty"))]
314pub fn init_pretty() {
315    builder().pretty().debug().init();
316}
317
318/// Initialize pretty logging (no-op when not available)
319#[cfg(not(any(feature = "logging-json", feature = "logging-pretty")))]
320pub fn init_pretty() {
321    // No-op: requires logging-json or logging-pretty feature
322}
323
324/// Initialize logging for dependency-injector only (filters other crates)
325#[cfg(any(feature = "logging-json", feature = "logging-pretty"))]
326pub fn init_di_only() {
327    builder().di_only().debug().init();
328}
329
330/// Initialize DI-only logging (no-op when not available)
331#[cfg(not(any(feature = "logging-json", feature = "logging-pretty")))]
332pub fn init_di_only() {
333    // No-op: requires logging-json or logging-pretty feature
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    #[test]
341    fn test_builder_defaults() {
342        let builder = LoggingBuilder::default();
343        assert_eq!(builder.level, Level::DEBUG);
344        assert_eq!(builder.format, LogFormat::Json);
345        assert!(builder.target.is_none());
346    }
347
348    #[test]
349    fn test_builder_chain() {
350        let builder = LoggingBuilder::new()
351            .trace()
352            .pretty()
353            .with_file()
354            .with_line_number()
355            .di_only();
356
357        assert_eq!(builder.level, Level::TRACE);
358        assert_eq!(builder.format, LogFormat::Pretty);
359        assert!(builder.with_file);
360        assert!(builder.with_line_number);
361        assert_eq!(builder.target, Some("dependency_injector"));
362    }
363}