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
9pub type FormatFn = Arc<dyn Fn(&Record) -> String + Send + Sync>;
11
12#[derive(Clone)]
14pub struct TextFormatter {
15 use_colors: bool,
17 include_timestamp: bool,
19 include_level: bool,
21 include_module: bool,
23 include_location: bool,
25 pattern: String,
27 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 format(&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}", ×tamp.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) -> Self {
113 self.use_colors = use_colors;
114 self
115 }
116
117 fn with_timestamp(mut self, include_timestamp: bool) -> Self {
118 self.include_timestamp = include_timestamp;
119 self
120 }
121
122 fn with_level(mut self, include_level: bool) -> Self {
123 self.include_level = include_level;
124 self
125 }
126
127 fn with_module(mut self, include_module: bool) -> Self {
128 self.include_module = include_module;
129 self
130 }
131
132 fn with_location(mut self, include_location: bool) -> Self {
133 self.include_location = include_location;
134 self
135 }
136
137 fn with_pattern(mut self, pattern: impl Into<String>) -> Self {
138 self.pattern = pattern.into();
139 self
140 }
141
142 fn with_format<F>(mut self, format_fn: F) -> Self
143 where
144 F: Fn(&Record) -> String + Send + Sync + 'static,
145 {
146 self.format_fn = Some(Arc::new(format_fn));
147 self
148 }
149
150 fn box_clone(&self) -> Box<dyn FormatterTrait + Send + Sync> {
151 Box::new(Self {
152 use_colors: self.use_colors,
153 include_timestamp: self.include_timestamp,
154 include_level: self.include_level,
155 include_module: self.include_module,
156 include_location: self.include_location,
157 pattern: self.pattern.clone(),
158 format_fn: self.format_fn.clone(),
159 })
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use crate::level::LogLevel;
167
168 #[test]
169 fn test_text_formatter_default() {
170 let formatter = TextFormatter::default();
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 = formatter.format(&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 }
185
186 #[test]
187 fn test_text_formatter_no_colors() {
188 let formatter = TextFormatter::default().with_colors(false);
189 let record = Record::new(
190 LogLevel::Info,
191 "Test message",
192 Some("test".to_string()),
193 Some("test.rs".to_string()),
194 Some(42),
195 );
196
197 let formatted = formatter.format(&record);
198 assert!(formatted.contains("Test message"));
199 assert!(formatted.contains("INFO"));
200 assert!(formatted.contains("test"));
201 assert!(formatted.contains("test.rs:42"));
202 assert!(!formatted.contains("\x1b["));
203 }
204
205 #[test]
206 fn test_text_formatter_no_timestamp() {
207 let formatter = TextFormatter::default().with_timestamp(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("2023")); }
223
224 #[test]
225 fn test_text_formatter_no_level() {
226 let formatter = TextFormatter::default().with_level(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 }
241
242 #[test]
243 fn test_text_formatter_no_module() {
244 let formatter = TextFormatter::default().with_module(false);
245 let record = Record::new(
246 LogLevel::Info,
247 "Test message",
248 Some("test".to_string()),
249 Some("main.rs".to_string()),
250 Some(42),
251 );
252
253 let formatted = formatter.format(&record);
254 assert!(formatted.contains("Test message"));
255 assert!(formatted.contains("INFO"));
256 assert!(!formatted.contains("test"));
257 assert!(formatted.contains("main.rs:42"));
258 }
259
260 #[test]
261 fn test_text_formatter_no_location() {
262 let formatter = TextFormatter::default().with_location(false);
263 let record = Record::new(
264 LogLevel::Info,
265 "Test message",
266 Some("test".to_string()),
267 Some("test.rs".to_string()),
268 Some(42),
269 );
270
271 let formatted = formatter.format(&record);
272 assert!(formatted.contains("Test message"));
273 assert!(formatted.contains("INFO"));
274 assert!(formatted.contains("test"));
275 assert!(!formatted.contains("test.rs:42"));
276 }
277
278 #[test]
279 fn test_text_formatter_custom_format() {
280 let formatter =
281 TextFormatter::default().with_format(|record| format!("CUSTOM: {}", record.message()));
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_eq!(formatted, "CUSTOM: Test message");
292 }
293}