rust_loguru/formatters/
text.rs

1use colored::Colorize;
2use std::fmt;
3
4use crate::formatters::FormatFn;
5use crate::formatters::FormatterTrait;
6use crate::level::LogLevel;
7use crate::record::Record;
8
9/// A text formatter that formats log records as text
10#[derive(Clone)]
11pub struct TextFormatter {
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    /// A custom format function
23    format_fn: Option<FormatFn>,
24}
25
26impl fmt::Debug for TextFormatter {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        f.debug_struct("TextFormatter")
29            .field("use_colors", &self.use_colors)
30            .field("include_timestamp", &self.include_timestamp)
31            .field("include_level", &self.include_level)
32            .field("include_module", &self.include_module)
33            .field("include_location", &self.include_location)
34            .field("format_fn", &"<format_fn>")
35            .finish()
36    }
37}
38
39impl Default for TextFormatter {
40    fn default() -> Self {
41        Self {
42            use_colors: true,
43            include_timestamp: true,
44            include_level: true,
45            include_module: true,
46            include_location: true,
47            format_fn: None,
48        }
49    }
50}
51
52impl FormatterTrait for TextFormatter {
53    fn fmt(&self, record: &Record) -> String {
54        // If a custom format function is provided, use it
55        if let Some(format_fn) = &self.format_fn {
56            return format_fn(record);
57        }
58
59        let mut result = String::new();
60
61        if self.include_timestamp {
62            result.push_str(&record.timestamp().to_rfc3339());
63            result.push(' ');
64        }
65
66        if self.include_level {
67            let level_str = record.level().to_string();
68            if self.use_colors {
69                result.push_str(&match record.level() {
70                    LogLevel::Trace => level_str.white().to_string(),
71                    LogLevel::Debug => level_str.blue().to_string(),
72                    LogLevel::Info => level_str.green().to_string(),
73                    LogLevel::Warning => level_str.yellow().to_string(),
74                    LogLevel::Error => level_str.red().to_string(),
75                    LogLevel::Critical => level_str.red().bold().to_string(),
76                    LogLevel::Success => level_str.green().bold().to_string(),
77                });
78            } else {
79                result.push_str(&level_str);
80            }
81            result.push_str(" - ");
82        }
83
84        if self.include_module {
85            let module = record.module();
86            if module != "unknown" {
87                result.push_str(module);
88                result.push(' ');
89            }
90        }
91
92        if self.include_location {
93            // Only include file and line, not module
94            let file = record.file();
95            let line = record.line();
96            if file != "unknown" {
97                result.push_str(&format!("{}:{}", file, line));
98                result.push(' ');
99            }
100        }
101
102        result.push_str(record.message());
103
104        if !result.ends_with('\n') {
105            result.push('\n');
106        }
107
108        result
109    }
110
111    fn with_colors(&mut self, use_colors: bool) {
112        self.use_colors = use_colors;
113    }
114
115    fn with_timestamp(&mut self, include_timestamp: bool) {
116        self.include_timestamp = include_timestamp;
117    }
118
119    fn with_level(&mut self, include_level: bool) {
120        self.include_level = include_level;
121    }
122
123    fn with_module(&mut self, include_module: bool) {
124        self.include_module = include_module;
125    }
126
127    fn with_location(&mut self, include_location: bool) {
128        self.include_location = include_location;
129    }
130
131    fn with_pattern(&mut self, _pattern: String) {
132        // Text formatter doesn't use patterns
133    }
134
135    fn with_format(&mut self, format_fn: FormatFn) {
136        self.format_fn = Some(format_fn);
137    }
138
139    fn box_clone(&self) -> Box<dyn FormatterTrait + Send + Sync> {
140        Box::new(self.clone())
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::level::LogLevel;
148
149    #[test]
150    fn test_text_formatter_default() {
151        let formatter = TextFormatter::default();
152        let record = Record::new(
153            LogLevel::Info,
154            "Test message",
155            Some("test".to_string()),
156            Some("test.rs".to_string()),
157            Some(42),
158        );
159
160        let formatted = FormatterTrait::fmt(&formatter, &record);
161        assert!(formatted.contains("Test message"));
162        assert!(formatted.contains("INFO"));
163        assert!(formatted.contains("test"));
164        assert!(formatted.contains("test.rs:42"));
165    }
166
167    #[test]
168    fn test_text_formatter_no_colors() {
169        let mut formatter = TextFormatter::default();
170        formatter.with_colors(false);
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 = FormatterTrait::fmt(&formatter, &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        assert!(!formatted.contains("\x1b["));
185    }
186
187    #[test]
188    fn test_text_formatter_no_timestamp() {
189        let mut formatter = TextFormatter::default();
190        formatter.with_timestamp(false);
191        let record = Record::new(
192            LogLevel::Info,
193            "Test message",
194            Some("test".to_string()),
195            Some("test.rs".to_string()),
196            Some(42),
197        );
198
199        let formatted = FormatterTrait::fmt(&formatter, &record);
200        assert!(formatted.contains("Test message"));
201        assert!(formatted.contains("INFO"));
202        assert!(formatted.contains("test"));
203        assert!(formatted.contains("test.rs:42"));
204        assert!(!formatted.contains("2023")); // No year in timestamp
205    }
206
207    #[test]
208    fn test_text_formatter_no_level() {
209        let mut formatter = TextFormatter::default();
210        formatter.with_level(false);
211        let record = Record::new(
212            LogLevel::Info,
213            "Test message",
214            Some("test".to_string()),
215            Some("test.rs".to_string()),
216            Some(42),
217        );
218
219        let formatted = FormatterTrait::fmt(&formatter, &record);
220        assert!(formatted.contains("Test message"));
221        assert!(!formatted.contains("INFO"));
222        assert!(formatted.contains("test"));
223        assert!(formatted.contains("test.rs:42"));
224    }
225
226    #[test]
227    fn test_text_formatter_no_module() {
228        let mut formatter = TextFormatter::default();
229        formatter.with_module(false);
230        let record = Record::new(
231            LogLevel::Info,
232            "Test message",
233            Some("test_module".to_string()),
234            Some("test.rs".to_string()),
235            Some(42),
236        );
237
238        let formatted = FormatterTrait::fmt(&formatter, &record);
239        assert!(formatted.contains("Test message"));
240        assert!(formatted.contains("INFO"));
241        assert!(!formatted.contains("test_module"));
242        assert!(formatted.contains("test.rs:42"));
243    }
244
245    #[test]
246
247    fn test_text_formatter_custom_format() {
248        use std::sync::Arc;
249        let mut formatter = TextFormatter::default();
250        formatter.with_format(Arc::new(|record: &Record| {
251            "CUSTOM: ".to_string() + record.message()
252        }));
253        let record = Record::new(
254            LogLevel::Info,
255            "Test message",
256            Some("test".to_string()),
257            Some("test.rs".to_string()),
258            Some(42),
259        );
260
261        let formatted = FormatterTrait::fmt(&formatter, &record);
262        assert_eq!(formatted, "CUSTOM: Test message");
263    }
264}