rust_loguru/formatters/
template.rs

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