rust_loguru/formatters/
json.rs

1use crate::formatters::FormatterTrait;
2use crate::record::Record;
3use chrono::Local;
4use serde_json::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/// A JSON formatter that formats log records as JSON
12#[derive(Clone)]
13pub struct JsonFormatter {
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    /// The format pattern to use
23    pattern: String,
24    /// A custom format function
25    format_fn: Option<FormatFn>,
26}
27
28impl fmt::Debug for JsonFormatter {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        f.debug_struct("JsonFormatter")
31            .field("include_timestamp", &self.include_timestamp)
32            .field("include_level", &self.include_level)
33            .field("include_module", &self.include_module)
34            .field("include_location", &self.include_location)
35            .field("pattern", &self.pattern)
36            .field("format_fn", &"<format_fn>")
37            .finish()
38    }
39}
40
41impl Default for JsonFormatter {
42    fn default() -> Self {
43        Self {
44            include_timestamp: true,
45            include_level: true,
46            include_module: true,
47            include_location: true,
48            pattern: "{timestamp} {level} {module} {location} {message}".to_string(),
49            format_fn: None,
50        }
51    }
52}
53
54impl JsonFormatter {
55    pub fn new() -> Self {
56        Self::default()
57    }
58}
59
60impl FormatterTrait for JsonFormatter {
61    fn format(&self, record: &Record) -> String {
62        if let Some(format_fn) = &self.format_fn {
63            return format_fn(record);
64        }
65
66        // If a custom pattern is provided and it's not the default pattern, use it
67        if self.pattern != "{timestamp} {level} {module} {location} {message}" {
68            let mut result = self.pattern.clone();
69
70            // Replace placeholders with JSON-formatted values
71            if self.include_level {
72                result = result.replace("{level}", &record.level().to_string());
73            } else {
74                result = result.replace("{level}", "");
75            }
76
77            if self.include_module {
78                result = result.replace("{module}", record.module());
79            } else {
80                result = result.replace("{module}", "");
81            }
82
83            if self.include_location {
84                result = result.replace(
85                    "{location}",
86                    &format!("{}:{}", record.file(), record.line()),
87                );
88            } else {
89                result = result.replace("{location}", "");
90            }
91
92            if self.include_timestamp {
93                let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
94                result = result.replace("{timestamp}", &timestamp.to_string());
95            } else {
96                result = result.replace("{timestamp}", "");
97            }
98
99            result = result.replace("{message}", record.message());
100
101            // Ensure newline at end
102            if result.ends_with('\n') {
103                result.to_string()
104            } else {
105                format!("{}\n", result)
106            }
107        } else {
108            // Use default JSON formatting
109            let mut json = serde_json::Map::new();
110
111            if self.include_timestamp {
112                let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
113                json.insert("timestamp".to_string(), json!(timestamp.to_string()));
114            }
115
116            if self.include_level {
117                json.insert("level".to_string(), json!(record.level().to_string()));
118            }
119
120            if self.include_module {
121                json.insert("module".to_string(), json!(record.module()));
122            }
123
124            if self.include_location {
125                json.insert(
126                    "location".to_string(),
127                    json!(format!("{}:{}", record.file(), record.line())),
128                );
129            }
130
131            json.insert("message".to_string(), json!(record.message()));
132
133            // Add any metadata
134            for (key, value) in record.metadata() {
135                json.insert(key.to_string(), json!(value));
136            }
137
138            // Add any structured data
139            for (key, value) in record.context() {
140                json.insert(key.to_string(), value.clone());
141            }
142
143            format!(
144                "{}\n",
145                serde_json::to_string(&json).unwrap_or_else(|_| "{}".to_string())
146            )
147        }
148    }
149
150    fn with_colors(self, _use_colors: bool) -> Self {
151        self
152    }
153
154    fn with_timestamp(mut self, include_timestamp: bool) -> Self {
155        self.include_timestamp = include_timestamp;
156        self
157    }
158
159    fn with_level(mut self, include_level: bool) -> Self {
160        self.include_level = include_level;
161        self
162    }
163
164    fn with_module(mut self, include_module: bool) -> Self {
165        self.include_module = include_module;
166        self
167    }
168
169    fn with_location(mut self, include_location: bool) -> Self {
170        self.include_location = include_location;
171        self
172    }
173
174    fn with_pattern(mut self, pattern: impl Into<String>) -> Self {
175        self.pattern = pattern.into();
176        self
177    }
178
179    fn with_format<F>(mut self, format_fn: F) -> Self
180    where
181        F: Fn(&Record) -> String + Send + Sync + 'static,
182    {
183        self.format_fn = Some(Arc::new(format_fn));
184        self
185    }
186
187    fn box_clone(&self) -> Box<dyn FormatterTrait + Send + Sync> {
188        Box::new(Self {
189            include_timestamp: self.include_timestamp,
190            include_level: self.include_level,
191            include_module: self.include_module,
192            include_location: self.include_location,
193            pattern: self.pattern.clone(),
194            format_fn: self.format_fn.clone(),
195        })
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use crate::level::LogLevel;
203
204    #[test]
205    fn test_json_formatter_default() {
206        let formatter = JsonFormatter::default();
207        let record = Record::new(
208            LogLevel::Info,
209            "Test message",
210            Some("test".to_string()),
211            Some("test.rs".to_string()),
212            Some(42),
213        );
214
215        let formatted = formatter.format(&record);
216        assert!(formatted.contains("Test message"));
217        assert!(formatted.contains("INFO"));
218        assert!(formatted.contains("test"));
219        assert!(formatted.contains("test.rs:42"));
220    }
221
222    #[test]
223    fn test_json_formatter_no_timestamp() {
224        let formatter = JsonFormatter::default().with_timestamp(false);
225        let record = Record::new(
226            LogLevel::Info,
227            "Test message",
228            Some("test".to_string()),
229            Some("test.rs".to_string()),
230            Some(42),
231        );
232
233        let formatted = formatter.format(&record);
234        assert!(formatted.contains("Test message"));
235        assert!(formatted.contains("INFO"));
236        assert!(formatted.contains("test"));
237        assert!(formatted.contains("test.rs:42"));
238        assert!(!formatted.contains("timestamp"));
239    }
240
241    #[test]
242    fn test_json_formatter_no_level() {
243        let formatter = JsonFormatter::default().with_level(false);
244        let record = Record::new(
245            LogLevel::Info,
246            "Test message",
247            Some("test".to_string()),
248            Some("test.rs".to_string()),
249            Some(42),
250        );
251
252        let formatted = formatter.format(&record);
253        assert!(formatted.contains("Test message"));
254        assert!(!formatted.contains("INFO"));
255        assert!(formatted.contains("test"));
256        assert!(formatted.contains("test.rs:42"));
257    }
258
259    #[test]
260    fn test_json_formatter_no_module() {
261        let formatter = JsonFormatter::default().with_module(false);
262        let record = Record::new(
263            LogLevel::Info,
264            "Test message",
265            Some("test".to_string()),
266            Some("main.rs".to_string()),
267            Some(42),
268        );
269
270        let formatted = formatter.format(&record);
271        assert!(formatted.contains("Test message"));
272        assert!(formatted.contains("INFO"));
273        assert!(!formatted.contains("test"));
274        assert!(formatted.contains("main.rs:42"));
275    }
276
277    #[test]
278    fn test_json_formatter_no_location() {
279        let formatter = JsonFormatter::default().with_location(false);
280        let record = Record::new(
281            LogLevel::Info,
282            "Test message",
283            Some("test".to_string()),
284            Some("test.rs".to_string()),
285            Some(42),
286        );
287
288        let formatted = formatter.format(&record);
289        assert!(formatted.contains("Test message"));
290        assert!(formatted.contains("INFO"));
291        assert!(formatted.contains("test"));
292        assert!(!formatted.contains("test.rs:42"));
293    }
294
295    #[test]
296    fn test_json_formatter_custom_format() {
297        let formatter =
298            JsonFormatter::default().with_format(|record| format!("CUSTOM: {}", record.message()));
299        let record = Record::new(
300            LogLevel::Info,
301            "Test message",
302            Some("test".to_string()),
303            Some("test.rs".to_string()),
304            Some(42),
305        );
306
307        let formatted = formatter.format(&record);
308        assert_eq!(formatted, "CUSTOM: Test message");
309    }
310}