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 fmt(&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) {
113        self.use_colors = use_colors;
114    }
115
116    fn with_timestamp(&mut self, include_timestamp: bool) {
117        self.include_timestamp = include_timestamp;
118    }
119
120    fn with_level(&mut self, include_level: bool) {
121        self.include_level = include_level;
122    }
123
124    fn with_module(&mut self, include_module: bool) {
125        self.include_module = include_module;
126    }
127
128    fn with_location(&mut self, include_location: bool) {
129        self.include_location = include_location;
130    }
131
132    fn with_pattern(&mut self, pattern: String) {
133        self.pattern = pattern;
134    }
135
136    fn with_format(&mut self, format_fn: FormatFn) {
137        self.format_fn = Some(format_fn);
138    }
139
140    fn box_clone(&self) -> Box<dyn FormatterTrait + Send + Sync> {
141        Box::new(Self {
142            use_colors: self.use_colors,
143            include_timestamp: self.include_timestamp,
144            include_level: self.include_level,
145            include_module: self.include_module,
146            include_location: self.include_location,
147            pattern: self.pattern.clone(),
148            format_fn: self.format_fn.clone(),
149        })
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use crate::level::LogLevel;
157
158    #[test]
159    fn test_text_formatter_default() {
160        let formatter = TextFormatter::default();
161        let record = Record::new(
162            LogLevel::Info,
163            "Test message",
164            Some("test".to_string()),
165            Some("test.rs".to_string()),
166            Some(42),
167        );
168
169        let formatted = FormatterTrait::fmt(&formatter, &record);
170        assert!(formatted.contains("Test message"));
171        assert!(formatted.contains("INFO"));
172        assert!(formatted.contains("test"));
173        assert!(formatted.contains("test.rs:42"));
174    }
175
176    #[test]
177    fn test_text_formatter_no_colors() {
178        let mut formatter = TextFormatter::default();
179        formatter.with_colors(false);
180        let record = Record::new(
181            LogLevel::Info,
182            "Test message",
183            Some("test".to_string()),
184            Some("test.rs".to_string()),
185            Some(42),
186        );
187
188        let formatted = FormatterTrait::fmt(&formatter, &record);
189        assert!(formatted.contains("Test message"));
190        assert!(formatted.contains("INFO"));
191        assert!(formatted.contains("test"));
192        assert!(formatted.contains("test.rs:42"));
193        assert!(!formatted.contains("\x1b["));
194    }
195
196    #[test]
197    fn test_text_formatter_no_timestamp() {
198        let mut formatter = TextFormatter::default();
199        formatter.with_timestamp(false);
200        let record = Record::new(
201            LogLevel::Info,
202            "Test message",
203            Some("test".to_string()),
204            Some("test.rs".to_string()),
205            Some(42),
206        );
207
208        let formatted = FormatterTrait::fmt(&formatter, &record);
209        assert!(formatted.contains("Test message"));
210        assert!(formatted.contains("INFO"));
211        assert!(formatted.contains("test"));
212        assert!(formatted.contains("test.rs:42"));
213        assert!(!formatted.contains("2023")); // No year in timestamp
214    }
215
216    #[test]
217    fn test_text_formatter_no_level() {
218        let mut formatter = TextFormatter::default();
219        formatter.with_level(false);
220        let record = Record::new(
221            LogLevel::Info,
222            "Test message",
223            Some("test".to_string()),
224            Some("test.rs".to_string()),
225            Some(42),
226        );
227
228        let formatted = FormatterTrait::fmt(&formatter, &record);
229        assert!(formatted.contains("Test message"));
230        assert!(!formatted.contains("INFO"));
231        assert!(formatted.contains("test"));
232        assert!(formatted.contains("test.rs:42"));
233    }
234
235    #[test]
236    fn test_text_formatter_no_module() {
237        let mut formatter = TextFormatter::default();
238        formatter.with_module(false);
239        let record = Record::new(
240            LogLevel::Info,
241            "Test message",
242            Some("test_module".to_string()),
243            Some("test.rs".to_string()),
244            Some(42),
245        );
246
247        let formatted = FormatterTrait::fmt(&formatter, &record);
248        assert!(formatted.contains("Test message"));
249        assert!(formatted.contains("INFO"));
250        assert!(!formatted.contains("test_module"));
251        assert!(formatted.contains("test.rs:42"));
252    }
253
254    #[test]
255    fn test_text_formatter_custom_format() {
256        let mut formatter = TextFormatter::default();
257        formatter.with_format(Arc::new(|record: &Record| {
258            format!("CUSTOM: {}", record.message())
259        }));
260        let record = Record::new(
261            LogLevel::Info,
262            "Test message",
263            Some("test".to_string()),
264            Some("test.rs".to_string()),
265            Some(42),
266        );
267
268        let formatted = FormatterTrait::fmt(&formatter, &record);
269        assert_eq!(formatted, "CUSTOM: Test message");
270    }
271}