rust_loguru/
formatter.rs

1use crate::formatters::FormatterTrait;
2use crate::level::LogLevel;
3use crate::record::Record;
4use chrono::Local;
5use colored::*;
6use std::fmt;
7use std::sync::Arc;
8
9/// A type alias for a format function
10pub type FormatFn = Arc<dyn Fn(&Record) -> String + Send + Sync>;
11
12/// A text formatter that formats log records as text
13#[derive(Clone)]
14pub struct TextFormatter {
15    /// Whether to use colors in the output
16    use_colors: bool,
17    /// Whether to include timestamps in the output
18    include_timestamp: bool,
19    /// Whether to include log levels in the output
20    include_level: bool,
21    /// Whether to include module names in the output
22    include_module: bool,
23    /// Whether to include file locations in the output
24    include_location: bool,
25    /// The format pattern to use
26    pattern: String,
27    /// A custom format function
28    format_fn: Option<FormatFn>,
29}
30
31impl fmt::Debug for TextFormatter {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        f.debug_struct("TextFormatter")
34            .field("use_colors", &self.use_colors)
35            .field("include_timestamp", &self.include_timestamp)
36            .field("include_level", &self.include_level)
37            .field("include_module", &self.include_module)
38            .field("include_location", &self.include_location)
39            .field("pattern", &self.pattern)
40            .field("format_fn", &"<format_fn>")
41            .finish()
42    }
43}
44
45impl Default for TextFormatter {
46    fn default() -> Self {
47        Self {
48            use_colors: true,
49            include_timestamp: true,
50            include_level: true,
51            include_module: true,
52            include_location: true,
53            pattern: "{timestamp} {level} {module} {location} {message}".to_string(),
54            format_fn: None,
55        }
56    }
57}
58
59impl FormatterTrait for TextFormatter {
60    fn format(&self, record: &Record) -> String {
61        if let Some(format_fn) = &self.format_fn {
62            return format_fn(record);
63        }
64
65        let mut output = self.pattern.clone();
66
67        if self.include_timestamp {
68            let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
69            output = output.replace("{timestamp}", &timestamp.to_string());
70        } else {
71            output = output.replace("{timestamp}", "");
72        }
73
74        if self.include_level {
75            let level = record.level().to_string();
76            if self.use_colors {
77                let colored_level = match record.level() {
78                    LogLevel::Error => level.red().to_string(),
79                    LogLevel::Warning => level.yellow().to_string(),
80                    LogLevel::Info => level.green().to_string(),
81                    LogLevel::Debug => level.blue().to_string(),
82                    LogLevel::Trace => level.cyan().to_string(),
83                    LogLevel::Success => level.green().to_string(),
84                    LogLevel::Critical => level.red().to_string(),
85                };
86                output = output.replace("{level}", &colored_level);
87            } else {
88                output = output.replace("{level}", &level);
89            }
90        } else {
91            output = output.replace("{level}", "");
92        }
93
94        if self.include_module {
95            output = output.replace("{module}", record.module());
96        } else {
97            output = output.replace("{module}", "");
98        }
99
100        if self.include_location {
101            let location = format!("{}:{}", record.file(), record.line());
102            output = output.replace("{location}", &location);
103        } else {
104            output = output.replace("{location}", "");
105        }
106
107        output = output.replace("{message}", record.message());
108
109        output.trim().to_string()
110    }
111
112    fn with_colors(mut self, use_colors: bool) -> Self {
113        self.use_colors = use_colors;
114        self
115    }
116
117    fn with_timestamp(mut self, include_timestamp: bool) -> Self {
118        self.include_timestamp = include_timestamp;
119        self
120    }
121
122    fn with_level(mut self, include_level: bool) -> Self {
123        self.include_level = include_level;
124        self
125    }
126
127    fn with_module(mut self, include_module: bool) -> Self {
128        self.include_module = include_module;
129        self
130    }
131
132    fn with_location(mut self, include_location: bool) -> Self {
133        self.include_location = include_location;
134        self
135    }
136
137    fn with_pattern(mut self, pattern: impl Into<String>) -> Self {
138        self.pattern = pattern.into();
139        self
140    }
141
142    fn with_format<F>(mut self, format_fn: F) -> Self
143    where
144        F: Fn(&Record) -> String + Send + Sync + 'static,
145    {
146        self.format_fn = Some(Arc::new(format_fn));
147        self
148    }
149
150    fn box_clone(&self) -> Box<dyn FormatterTrait + Send + Sync> {
151        Box::new(Self {
152            use_colors: self.use_colors,
153            include_timestamp: self.include_timestamp,
154            include_level: self.include_level,
155            include_module: self.include_module,
156            include_location: self.include_location,
157            pattern: self.pattern.clone(),
158            format_fn: self.format_fn.clone(),
159        })
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use crate::level::LogLevel;
167
168    #[test]
169    fn test_text_formatter_default() {
170        let formatter = TextFormatter::default();
171        let record = Record::new(
172            LogLevel::Info,
173            "Test message",
174            Some("test".to_string()),
175            Some("test.rs".to_string()),
176            Some(42),
177        );
178
179        let formatted = formatter.format(&record);
180        assert!(formatted.contains("Test message"));
181        assert!(formatted.contains("INFO"));
182        assert!(formatted.contains("test"));
183        assert!(formatted.contains("test.rs:42"));
184    }
185
186    #[test]
187    fn test_text_formatter_no_colors() {
188        let formatter = TextFormatter::default().with_colors(false);
189        let record = Record::new(
190            LogLevel::Info,
191            "Test message",
192            Some("test".to_string()),
193            Some("test.rs".to_string()),
194            Some(42),
195        );
196
197        let formatted = formatter.format(&record);
198        assert!(formatted.contains("Test message"));
199        assert!(formatted.contains("INFO"));
200        assert!(formatted.contains("test"));
201        assert!(formatted.contains("test.rs:42"));
202        assert!(!formatted.contains("\x1b["));
203    }
204
205    #[test]
206    fn test_text_formatter_no_timestamp() {
207        let formatter = TextFormatter::default().with_timestamp(false);
208        let record = Record::new(
209            LogLevel::Info,
210            "Test message",
211            Some("test".to_string()),
212            Some("test.rs".to_string()),
213            Some(42),
214        );
215
216        let formatted = formatter.format(&record);
217        assert!(formatted.contains("Test message"));
218        assert!(formatted.contains("INFO"));
219        assert!(formatted.contains("test"));
220        assert!(formatted.contains("test.rs:42"));
221        assert!(!formatted.contains("2023")); // No year in timestamp
222    }
223
224    #[test]
225    fn test_text_formatter_no_level() {
226        let formatter = TextFormatter::default().with_level(false);
227        let record = Record::new(
228            LogLevel::Info,
229            "Test message",
230            Some("test".to_string()),
231            Some("test.rs".to_string()),
232            Some(42),
233        );
234
235        let formatted = formatter.format(&record);
236        assert!(formatted.contains("Test message"));
237        assert!(!formatted.contains("INFO"));
238        assert!(formatted.contains("test"));
239        assert!(formatted.contains("test.rs:42"));
240    }
241
242    #[test]
243    fn test_text_formatter_no_module() {
244        let formatter = TextFormatter::default().with_module(false);
245        let record = Record::new(
246            LogLevel::Info,
247            "Test message",
248            Some("test".to_string()),
249            Some("main.rs".to_string()),
250            Some(42),
251        );
252
253        let formatted = formatter.format(&record);
254        assert!(formatted.contains("Test message"));
255        assert!(formatted.contains("INFO"));
256        assert!(!formatted.contains("test"));
257        assert!(formatted.contains("main.rs:42"));
258    }
259
260    #[test]
261    fn test_text_formatter_no_location() {
262        let formatter = TextFormatter::default().with_location(false);
263        let record = Record::new(
264            LogLevel::Info,
265            "Test message",
266            Some("test".to_string()),
267            Some("test.rs".to_string()),
268            Some(42),
269        );
270
271        let formatted = formatter.format(&record);
272        assert!(formatted.contains("Test message"));
273        assert!(formatted.contains("INFO"));
274        assert!(formatted.contains("test"));
275        assert!(!formatted.contains("test.rs:42"));
276    }
277
278    #[test]
279    fn test_text_formatter_custom_format() {
280        let formatter =
281            TextFormatter::default().with_format(|record| format!("CUSTOM: {}", record.message()));
282        let record = Record::new(
283            LogLevel::Info,
284            "Test message",
285            Some("test".to_string()),
286            Some("test.rs".to_string()),
287            Some(42),
288        );
289
290        let formatted = formatter.format(&record);
291        assert_eq!(formatted, "CUSTOM: Test message");
292    }
293}