rust_loguru/formatters/
template.rs

1use colored::*;
2use std::fmt;
3use std::sync::Arc;
4
5use crate::formatters::{FormatFn, FormatterTrait};
6use crate::level::LogLevel;
7use crate::record::Record;
8
9/// A template formatter that formats log records using a template
10#[derive(Clone)]
11pub struct TemplateFormatter {
12    /// Whether to use colors in the output
13    use_colors: bool,
14    /// Whether to include timestamps in the output
15    include_timestamp: bool,
16    /// Whether to include log levels in the output
17    include_level: bool,
18    /// Whether to include module names in the output
19    include_module: bool,
20    /// Whether to include file locations in the output
21    include_location: bool,
22    /// Whether to include metadata in the output
23    include_metadata: bool,
24    /// Whether to include structured data in the output
25    include_data: bool,
26    /// The format pattern to use
27    pattern: String,
28    /// A custom format function
29    format_fn: Option<FormatFn>,
30    /// Pre-computed placeholder positions for faster formatting
31    placeholders: Vec<(usize, usize, String)>,
32}
33
34impl fmt::Debug for TemplateFormatter {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        f.debug_struct("TemplateFormatter")
37            .field("use_colors", &self.use_colors)
38            .field("include_timestamp", &self.include_timestamp)
39            .field("include_level", &self.include_level)
40            .field("include_module", &self.include_module)
41            .field("include_location", &self.include_location)
42            .field("include_metadata", &self.include_metadata)
43            .field("include_data", &self.include_data)
44            .field("pattern", &self.pattern)
45            .field("format_fn", &"<format_fn>")
46            .finish()
47    }
48}
49
50impl Default for TemplateFormatter {
51    fn default() -> Self {
52        let pattern =
53            "{timestamp} {level} {module} {location} {message} {metadata} {data}".to_string();
54        let mut formatter = Self {
55            use_colors: true,
56            include_timestamp: true,
57            include_level: true,
58            include_module: true,
59            include_location: true,
60            include_metadata: true,
61            include_data: true,
62            pattern: pattern.clone(),
63            format_fn: None,
64            placeholders: Vec::new(),
65        };
66        formatter.update_placeholders();
67        formatter
68    }
69}
70
71impl TemplateFormatter {
72    pub fn new(pattern: impl Into<String>) -> Self {
73        let pattern = pattern.into();
74        let mut formatter = Self {
75            use_colors: true,
76            include_timestamp: pattern.contains("{timestamp}"),
77            include_level: pattern.contains("{level}"),
78            include_module: pattern.contains("{module}"),
79            include_location: pattern.contains("{location}"),
80            include_metadata: pattern.contains("{metadata}"),
81            include_data: pattern.contains("{data}"),
82            pattern,
83            format_fn: None,
84            placeholders: Vec::new(),
85        };
86        formatter.update_placeholders();
87        formatter
88    }
89
90    fn update_flags_from_pattern(&mut self) {
91        self.include_timestamp = self.pattern.contains("{timestamp}");
92        self.include_level = self.pattern.contains("{level}");
93        self.include_module = self.pattern.contains("{module}");
94        self.include_location = self.pattern.contains("{location}");
95        self.include_metadata = self.pattern.contains("{metadata}");
96        self.include_data = self.pattern.contains("{data}");
97    }
98
99    fn update_placeholders(&mut self) {
100        self.placeholders.clear();
101        let mut pos = 0;
102        while let Some(start) = self.pattern[pos..].find('{') {
103            if let Some(end) = self.pattern[pos + start..].find('}') {
104                let placeholder = self.pattern[pos + start + 1..pos + start + end].to_string();
105                self.placeholders
106                    .push((pos + start, pos + start + end + 1, placeholder));
107                pos += start + end + 1;
108            } else {
109                break;
110            }
111        }
112    }
113
114    pub fn with_colors(mut self, use_colors: bool) -> Self {
115        self.use_colors = use_colors;
116        self
117    }
118
119    pub fn with_timestamp(mut self, include_timestamp: bool) -> Self {
120        self.include_timestamp = include_timestamp;
121        self
122    }
123
124    pub fn with_level(mut self, include_level: bool) -> Self {
125        self.include_level = include_level;
126        self
127    }
128
129    pub fn with_module(mut self, include_module: bool) -> Self {
130        self.include_module = include_module;
131        self
132    }
133
134    pub fn with_location(mut self, include_location: bool) -> Self {
135        self.include_location = include_location;
136        self
137    }
138
139    pub fn with_pattern(mut self, pattern: impl Into<String>) -> Self {
140        self.pattern = pattern.into();
141        self.update_flags_from_pattern();
142        self.update_placeholders();
143        self
144    }
145
146    pub fn with_format<F>(mut self, format_fn: F) -> Self
147    where
148        F: Fn(&Record) -> String + Send + Sync + 'static,
149    {
150        self.format_fn = Some(Arc::new(format_fn));
151        self
152    }
153}
154
155impl FormatterTrait for TemplateFormatter {
156    fn fmt(&self, record: &Record) -> String {
157        if let Some(format_fn) = &self.format_fn {
158            // Return custom format as-is without modifications
159            let result = format_fn(record);
160            if result.ends_with('\n') {
161                result[..result.len() - 1].to_string()
162            } else {
163                result
164            }
165        } else {
166            // Pre-allocate with estimated capacity
167            let mut result = String::with_capacity(self.pattern.len() * 2);
168            let mut last_pos = 0;
169
170            // Use pre-computed placeholder positions for single-pass replacement
171            for &(start, end, ref placeholder) in &self.placeholders {
172                result.push_str(&self.pattern[last_pos..start]);
173
174                match placeholder.as_str() {
175                    "timestamp" => {
176                        if self.include_timestamp {
177                            result.push_str(&record.timestamp().to_rfc3339())
178                        }
179                    }
180                    "level" => {
181                        if self.include_level {
182                            let level_str = record.level().to_string();
183                            if self.use_colors {
184                                result.push_str(&match record.level() {
185                                    LogLevel::Trace => level_str.white().to_string(),
186                                    LogLevel::Debug => level_str.blue().to_string(),
187                                    LogLevel::Info => level_str.green().to_string(),
188                                    LogLevel::Warning => level_str.yellow().to_string(),
189                                    LogLevel::Error => level_str.red().to_string(),
190                                    LogLevel::Critical => level_str.red().bold().to_string(),
191                                    LogLevel::Success => level_str.green().bold().to_string(),
192                                });
193                            } else {
194                                result.push_str(&level_str);
195                            }
196                        }
197                    }
198                    "module" => {
199                        if self.include_module {
200                            let module = record.module();
201                            if module != "unknown" {
202                                result.push_str(module);
203                            }
204                        }
205                    }
206                    "location" => {
207                        if self.include_location {
208                            // Only include file and line, not module
209                            let file = record.file();
210                            let line = record.line();
211                            if file != "unknown" {
212                                result.push_str(&format!("{}:{}", file, line));
213                            }
214                        }
215                    }
216                    "message" => {
217                        result.push_str(record.message());
218                    }
219                    "metadata" => {
220                        if self.include_metadata {
221                            let metadata = record.metadata();
222                            if !metadata.is_empty() {
223                                let metadata_str = metadata
224                                    .iter()
225                                    .map(|(k, v)| format!("{}={}", k, v))
226                                    .collect::<Vec<_>>()
227                                    .join(" ");
228                                result.push_str(&metadata_str);
229                            }
230                        }
231                    }
232                    "data" => {
233                        if self.include_data {
234                            let metadata = record.metadata();
235                            if !metadata.is_empty() {
236                                let data_str = metadata
237                                    .iter()
238                                    .filter(|(k, _)| k.starts_with("data."))
239                                    .map(|(k, v)| format!("{}={}", k, v))
240                                    .collect::<Vec<_>>()
241                                    .join(" ");
242                                if !data_str.is_empty() {
243                                    result.push_str(&data_str);
244                                }
245                            }
246                        }
247                    }
248                    _ => {
249                        result.push_str(&self.pattern[start..end]);
250                    }
251                }
252                last_pos = end;
253            }
254
255            // Add remaining pattern content
256            result.push_str(&self.pattern[last_pos..]);
257
258            // Only add newline for default formatting
259            if !result.ends_with('\n') {
260                result.push('\n');
261            }
262
263            result
264        }
265    }
266
267    fn with_colors(&mut self, use_colors: bool) {
268        self.use_colors = use_colors;
269    }
270
271    fn with_timestamp(&mut self, include_timestamp: bool) {
272        self.include_timestamp = include_timestamp;
273    }
274
275    fn with_level(&mut self, include_level: bool) {
276        self.include_level = include_level;
277    }
278
279    fn with_module(&mut self, include_module: bool) {
280        self.include_module = include_module;
281    }
282
283    fn with_location(&mut self, include_location: bool) {
284        self.include_location = include_location;
285    }
286
287    fn with_pattern(&mut self, pattern: String) {
288        self.pattern = pattern;
289        self.update_flags_from_pattern();
290        self.update_placeholders();
291    }
292
293    fn with_format(&mut self, format_fn: FormatFn) {
294        self.format_fn = Some(format_fn);
295    }
296
297    fn box_clone(&self) -> Box<dyn FormatterTrait + Send + Sync> {
298        Box::new(self.clone())
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    #[test]
307    fn test_template_formatter_default() {
308        let formatter = TemplateFormatter::default();
309        let record = Record::new(
310            LogLevel::Info,
311            "Test message",
312            Some("test".to_string()),
313            Some("test.rs".to_string()),
314            Some(42),
315        );
316
317        let formatted = formatter.fmt(&record);
318        assert!(formatted.contains("Test message"));
319        assert!(formatted.contains("INFO"));
320        assert!(formatted.contains("test"));
321        assert!(formatted.contains("test.rs:42"));
322    }
323
324    #[test]
325    fn test_template_formatter_no_colors() {
326        let formatter = TemplateFormatter::default().with_colors(false);
327        let record = Record::new(
328            LogLevel::Info,
329            "Test message",
330            Some("test".to_string()),
331            Some("test.rs".to_string()),
332            Some(42),
333        );
334
335        let formatted = formatter.fmt(&record);
336        assert!(formatted.contains("Test message"));
337        assert!(formatted.contains("INFO"));
338        assert!(formatted.contains("test"));
339        assert!(formatted.contains("test.rs:42"));
340        assert!(!formatted.contains("\x1b["));
341    }
342
343    #[test]
344    fn test_template_formatter_no_timestamp() {
345        let formatter = TemplateFormatter::default().with_timestamp(false);
346        let record = Record::new(
347            LogLevel::Info,
348            "Test message",
349            Some("test".to_string()),
350            Some("test.rs".to_string()),
351            Some(42),
352        );
353
354        let formatted = formatter.fmt(&record);
355        assert!(formatted.contains("Test message"));
356        assert!(formatted.contains("INFO"));
357        assert!(formatted.contains("test"));
358        assert!(formatted.contains("test.rs:42"));
359        assert!(!formatted.contains("2023")); // No year in timestamp
360    }
361
362    #[test]
363    fn test_template_formatter_no_level() {
364        let formatter = TemplateFormatter::default().with_level(false);
365        let record = Record::new(
366            LogLevel::Info,
367            "Test message",
368            Some("test".to_string()),
369            Some("test.rs".to_string()),
370            Some(42),
371        );
372
373        let formatted = formatter.fmt(&record);
374        assert!(formatted.contains("Test message"));
375        assert!(!formatted.contains("INFO"));
376        assert!(formatted.contains("test"));
377        assert!(formatted.contains("test.rs:42"));
378    }
379
380    #[test]
381    fn test_template_formatter_no_module() {
382        let formatter = TemplateFormatter::default().with_module(false);
383        let record = Record::new(
384            LogLevel::Info,
385            "Test message",
386            Some("test_module".to_string()),
387            Some("test.rs".to_string()),
388            Some(42),
389        );
390
391        let formatted = formatter.fmt(&record);
392        assert!(formatted.contains("Test message"));
393        assert!(formatted.contains("INFO"));
394        assert!(!formatted.contains("test_module"));
395        assert!(formatted.contains("test.rs:42"));
396    }
397
398    #[test]
399    fn test_template_formatter_no_location() {
400        let formatter = TemplateFormatter::default().with_location(false);
401        let record = Record::new(
402            LogLevel::Info,
403            "Test message",
404            Some("test".to_string()),
405            Some("test.rs".to_string()),
406            Some(42),
407        );
408
409        let formatted = formatter.fmt(&record);
410        assert!(formatted.contains("Test message"));
411        assert!(formatted.contains("INFO"));
412        assert!(formatted.contains("test"));
413        assert!(!formatted.contains("test.rs:42"));
414    }
415
416    #[test]
417    fn test_template_formatter_custom_format() {
418        let formatter = TemplateFormatter::default()
419            .with_format(|record: &Record| format!("CUSTOM: {}", record.message()));
420        let record = Record::new(
421            LogLevel::Info,
422            "Test message",
423            Some("test_module".to_string()),
424            Some("test.rs".to_string()),
425            Some(42),
426        );
427
428        let formatted = formatter.fmt(&record);
429        assert_eq!(formatted, "CUSTOM: Test message");
430    }
431}