rust_loguru/formatters/
text.rs

1use crate::formatters::FormatterTrait;
2use crate::record::Record;
3use chrono::Local;
4use serde_json;
5use std::fmt;
6use std::sync::Arc;
7
8/// A type alias for a format function
9pub type FormatFn = Arc<dyn Fn(&Record) -> String + Send + Sync>;
10
11/// Text formatter implementation
12#[derive(Clone)]
13pub struct TextFormatter {
14    use_colors: bool,
15    include_timestamp: bool,
16    include_level: bool,
17    include_module: bool,
18    include_location: bool,
19    pattern: String,
20    format_fn: Option<FormatFn>,
21}
22
23impl fmt::Debug for TextFormatter {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        f.debug_struct("TextFormatter")
26            .field("use_colors", &self.use_colors)
27            .field("include_timestamp", &self.include_timestamp)
28            .field("include_level", &self.include_level)
29            .field("include_module", &self.include_module)
30            .field("include_location", &self.include_location)
31            .field("pattern", &self.pattern)
32            .field("format_fn", &"<format_fn>")
33            .finish()
34    }
35}
36
37impl Default for TextFormatter {
38    fn default() -> Self {
39        Self {
40            use_colors: true,
41            include_timestamp: true,
42            include_level: true,
43            include_module: true,
44            include_location: true,
45            pattern: "{timestamp} {level} {module} {location} {message}".to_string(),
46            format_fn: None,
47        }
48    }
49}
50
51impl TextFormatter {
52    pub fn new() -> Self {
53        Self::default()
54    }
55}
56
57impl FormatterTrait for TextFormatter {
58    fn format(&self, record: &Record) -> String {
59        // If a custom format function is provided, use it
60        if let Some(format_fn) = &self.format_fn {
61            return format_fn(record);
62        }
63
64        let mut result = self.pattern.clone();
65
66        // Helper closure to replace placeholders only if value exists
67        let replace_if = |text: &mut String, placeholder: &str, value: Option<&str>| {
68            if let Some(val) = value {
69                if !val.is_empty() {
70                    *text = text.replace(placeholder, val);
71                } else {
72                    // Remove the placeholder and any surrounding whitespace
73                    *text = text
74                        .replace(&format!(" {}", placeholder), "")
75                        .replace(&format!("{} ", placeholder), "")
76                        .replace(placeholder, "");
77                }
78            } else {
79                // Remove the placeholder and any surrounding whitespace
80                *text = text
81                    .replace(&format!(" {}", placeholder), "")
82                    .replace(&format!("{} ", placeholder), "")
83                    .replace(placeholder, "");
84            }
85        };
86
87        // Replace message first
88        replace_if(&mut result, "{message}", Some(record.message()));
89
90        // Replace level if included
91        if self.include_level {
92            let level_str = if self.use_colors {
93                record.level().to_string_colored()
94            } else {
95                record.level().to_string()
96            };
97            replace_if(&mut result, "{level}", Some(&level_str));
98        } else {
99            replace_if(&mut result, "{level}", None);
100        }
101
102        // Replace module if included
103        if self.include_module {
104            replace_if(&mut result, "{module}", Some(record.module()));
105        } else {
106            replace_if(&mut result, "{module}", None);
107        }
108
109        // Replace location if included
110        if self.include_location {
111            let location = if !record.file().is_empty() {
112                Some(format!("{}:{}", record.file(), record.line()))
113            } else {
114                None
115            };
116            replace_if(&mut result, "{location}", location.as_deref());
117        } else {
118            replace_if(&mut result, "{location}", None);
119        }
120
121        // Replace timestamp if included
122        if self.include_timestamp {
123            let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S%.3f").to_string();
124            replace_if(&mut result, "{timestamp}", Some(&timestamp));
125        } else {
126            replace_if(&mut result, "{timestamp}", None);
127        }
128
129        // Add metadata to the output
130        if !record.metadata().is_empty() {
131            let metadata_str = record
132                .metadata()
133                .iter()
134                .map(|(k, v)| format!("{}={}", k, v))
135                .collect::<Vec<_>>()
136                .join(" ");
137            if !metadata_str.is_empty() {
138                result = format!("{} {}", result.trim_end(), metadata_str);
139            }
140        }
141
142        // Add structured data to the output
143        if !record.context().is_empty() {
144            let context_str = record
145                .context()
146                .iter()
147                .map(|(k, v)| format!("{}={}", k, serde_json::to_string(v).unwrap_or_default()))
148                .collect::<Vec<_>>()
149                .join(" ");
150            if !context_str.is_empty() {
151                result = format!("{} {}", result.trim_end(), context_str);
152            }
153        }
154
155        // Clean up whitespace while preserving newlines and indentation
156        result = result
157            .lines()
158            .map(|line| {
159                let trimmed = line.trim_end();
160                if trimmed.is_empty() {
161                    String::new()
162                } else {
163                    trimmed.to_string()
164                }
165            })
166            .collect::<Vec<_>>()
167            .join("\n");
168
169        // Ensure newline at end
170        if !result.ends_with('\n') {
171            result.push('\n');
172        }
173
174        result
175    }
176
177    fn with_colors(mut self, use_colors: bool) -> Self {
178        self.use_colors = use_colors;
179        self
180    }
181
182    fn with_timestamp(mut self, include_timestamp: bool) -> Self {
183        self.include_timestamp = include_timestamp;
184        self
185    }
186
187    fn with_level(mut self, include_level: bool) -> Self {
188        self.include_level = include_level;
189        self
190    }
191
192    fn with_module(mut self, include_module: bool) -> Self {
193        self.include_module = include_module;
194        self
195    }
196
197    fn with_location(mut self, include_location: bool) -> Self {
198        self.include_location = include_location;
199        self
200    }
201
202    fn with_pattern(mut self, pattern: impl Into<String>) -> Self {
203        self.pattern = pattern.into();
204        // Update the internal flags based on the pattern content
205        self.include_timestamp = self.pattern.contains("{timestamp}");
206        self.include_level = self.pattern.contains("{level}");
207        self.include_module = self.pattern.contains("{module}");
208        self.include_location = self.pattern.contains("{location}");
209        self
210    }
211
212    fn with_format<F>(mut self, format_fn: F) -> Self
213    where
214        F: Fn(&Record) -> String + Send + Sync + 'static,
215    {
216        self.format_fn = Some(Arc::new(format_fn));
217        self
218    }
219
220    fn box_clone(&self) -> Box<dyn FormatterTrait + Send + Sync> {
221        Box::new(Self {
222            use_colors: self.use_colors,
223            include_timestamp: self.include_timestamp,
224            include_level: self.include_level,
225            include_module: self.include_module,
226            include_location: self.include_location,
227            pattern: self.pattern.clone(),
228            format_fn: self.format_fn.clone(),
229        })
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use crate::level::LogLevel;
237
238    #[test]
239    fn test_text_formatter_default() {
240        let formatter = TextFormatter::default();
241        let record = Record::new(
242            LogLevel::Info,
243            "Test message",
244            Some("test".to_string()),
245            Some("test.rs".to_string()),
246            Some(42),
247        );
248
249        let formatted = formatter.format(&record);
250        assert!(formatted.contains("Test message"));
251        assert!(formatted.contains("INFO"));
252        assert!(formatted.contains("test"));
253        assert!(formatted.contains("test.rs:42"));
254    }
255
256    #[test]
257    fn test_text_formatter_no_colors() {
258        let formatter = TextFormatter::default().with_colors(false);
259        let record = Record::new(
260            LogLevel::Info,
261            "Test message",
262            Some("test".to_string()),
263            Some("test.rs".to_string()),
264            Some(42),
265        );
266
267        let formatted = formatter.format(&record);
268        assert!(formatted.contains("Test message"));
269        assert!(formatted.contains("INFO"));
270        assert!(formatted.contains("test"));
271        assert!(formatted.contains("test.rs:42"));
272        assert!(!formatted.contains("\x1b["));
273    }
274
275    #[test]
276    fn test_text_formatter_no_timestamp() {
277        let formatter = TextFormatter::default().with_timestamp(false);
278        let record = Record::new(
279            LogLevel::Info,
280            "Test message",
281            Some("test".to_string()),
282            Some("test.rs".to_string()),
283            Some(42),
284        );
285
286        let formatted = formatter.format(&record);
287        assert!(formatted.contains("Test message"));
288        assert!(formatted.contains("INFO"));
289        assert!(formatted.contains("test"));
290        assert!(formatted.contains("test.rs:42"));
291        assert!(!formatted.contains("2023")); // No year in timestamp
292    }
293
294    #[test]
295    fn test_text_formatter_no_level() {
296        let formatter = TextFormatter::default().with_level(false);
297        let record = Record::new(
298            LogLevel::Info,
299            "Test message",
300            Some("test".to_string()),
301            Some("test.rs".to_string()),
302            Some(42),
303        );
304
305        let formatted = formatter.format(&record);
306        assert!(formatted.contains("Test message"));
307        assert!(!formatted.contains("INFO"));
308        assert!(formatted.contains("test"));
309        assert!(formatted.contains("test.rs:42"));
310    }
311
312    #[test]
313    fn test_text_formatter_no_module() {
314        let formatter = TextFormatter::default().with_module(false);
315        let record = Record::new(
316            LogLevel::Info,
317            "Test message",
318            Some("test".to_string()),
319            Some("main.rs".to_string()),
320            Some(42),
321        );
322
323        let formatted = formatter.format(&record);
324        assert!(formatted.contains("Test message"));
325        assert!(formatted.contains("INFO"));
326        assert!(!formatted.contains("test"));
327        assert!(formatted.contains("main.rs:42"));
328    }
329
330    #[test]
331    fn test_text_formatter_no_location() {
332        let formatter = TextFormatter::default().with_location(false);
333        let record = Record::new(
334            LogLevel::Info,
335            "Test message",
336            Some("test".to_string()),
337            Some("test.rs".to_string()),
338            Some(42),
339        );
340
341        let formatted = formatter.format(&record);
342        assert!(formatted.contains("Test message"));
343        assert!(formatted.contains("INFO"));
344        assert!(formatted.contains("test"));
345        assert!(!formatted.contains("test.rs:42"));
346    }
347
348    #[test]
349    fn test_text_formatter_custom_format() {
350        let formatter =
351            TextFormatter::default().with_format(|record| format!("CUSTOM: {}", record.message()));
352        let record = Record::new(
353            LogLevel::Info,
354            "Test message",
355            Some("test".to_string()),
356            Some("test.rs".to_string()),
357            Some(42),
358        );
359
360        let formatted = formatter.format(&record);
361        assert_eq!(formatted, "CUSTOM: Test message");
362    }
363}