1use crate::formatters::FormatterTrait;
2use crate::record::Record;
3use chrono::Local;
4use serde_json;
5use std::fmt;
6use std::sync::Arc;
7
8pub type FormatFn = Arc<dyn Fn(&Record) -> String + Send + Sync>;
10
11#[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 let Some(format_fn) = &self.format_fn {
61 return format_fn(record);
62 }
63
64 let mut result = self.pattern.clone();
65
66 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 *text = text
74 .replace(&format!(" {}", placeholder), "")
75 .replace(&format!("{} ", placeholder), "")
76 .replace(placeholder, "");
77 }
78 } else {
79 *text = text
81 .replace(&format!(" {}", placeholder), "")
82 .replace(&format!("{} ", placeholder), "")
83 .replace(placeholder, "");
84 }
85 };
86
87 replace_if(&mut result, "{message}", Some(record.message()));
89
90 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 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 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 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(×tamp));
125 } else {
126 replace_if(&mut result, "{timestamp}", None);
127 }
128
129 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 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 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 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 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")); }
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}