rust_loguru/
record.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fmt;
5
6use crate::context;
7use crate::level::LogLevel;
8
9/// Type alias for the record formatting function.
10/// This function takes a reference to a Record and returns a formatted String.
11type RecordFormatter = Box<dyn Fn(&Record) -> String + Send + Sync>;
12
13/// A log record containing all information about a log message
14///
15/// This struct is designed to be thread-safe and can be safely shared between threads.
16/// All methods that modify the record take ownership and return a new instance,
17/// ensuring thread safety without the need for locks.
18pub struct Record {
19    /// The log level
20    level: LogLevel,
21    /// The log message
22    message: String,
23    /// The module path
24    module: String,
25    /// The file name
26    file: String,
27    /// The line number
28    line: u32,
29    /// The timestamp
30    timestamp: DateTime<Utc>,
31    /// Additional metadata
32    metadata: HashMap<String, String>,
33    /// Structured context data
34    context: HashMap<String, serde_json::Value>,
35    /// Deferred formatting function
36    format_fn: Option<RecordFormatter>,
37}
38
39impl Clone for Record {
40    fn clone(&self) -> Self {
41        Self {
42            level: self.level,
43            message: self.message.clone(),
44            module: self.module.clone(),
45            file: self.file.clone(),
46            line: self.line,
47            timestamp: self.timestamp,
48            metadata: self.metadata.clone(),
49            context: self.context.clone(),
50            format_fn: None, // We don't clone the format function
51        }
52    }
53}
54
55impl fmt::Debug for Record {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        f.debug_struct("Record")
58            .field("level", &self.level)
59            .field("message", &self.message)
60            .field("module", &self.module)
61            .field("file", &self.file)
62            .field("line", &self.line)
63            .field("timestamp", &self.timestamp)
64            .field("metadata", &self.metadata)
65            .field("context", &self.context)
66            .field("format_fn", &format_args!("<function>"))
67            .finish()
68    }
69}
70
71// Add static strings at the top of the file after imports
72static UNKNOWN: &str = "unknown";
73
74impl Record {
75    /// Create an empty record for pooling
76    pub(crate) fn empty() -> Self {
77        Self {
78            level: LogLevel::Info,
79            message: String::new(),
80            module: UNKNOWN.to_string(),
81            file: UNKNOWN.to_string(),
82            line: 0,
83            timestamp: Utc::now(),
84            metadata: HashMap::new(),
85            context: HashMap::new(),
86            format_fn: None,
87        }
88    }
89
90    /// Create a new log record with optimized allocations
91    pub fn new(
92        level: LogLevel,
93        message: impl Into<String>,
94        module: Option<String>,
95        file: Option<String>,
96        line: Option<u32>,
97    ) -> Self {
98        // Lazily build context only when needed
99        let context_map = if context::has_context() {
100            let mut map = HashMap::new();
101            for (k, v) in context::current_context().into_iter() {
102                map.insert(k, context_value_to_json(v));
103            }
104            map
105        } else {
106            HashMap::new()
107        };
108
109        Self {
110            level,
111            message: message.into(),
112            module: module.unwrap_or_else(|| UNKNOWN.to_string()),
113            file: file.unwrap_or_else(|| UNKNOWN.to_string()),
114            line: line.unwrap_or(0),
115            timestamp: Utc::now(),
116            metadata: HashMap::new(),
117            context: context_map,
118            format_fn: None,
119        }
120    }
121
122    /// Get the log level
123    pub fn level(&self) -> LogLevel {
124        self.level
125    }
126
127    /// Get the log message
128    pub fn message(&self) -> &str {
129        &self.message
130    }
131
132    /// Get the module path
133    pub fn module(&self) -> &str {
134        &self.module
135    }
136
137    /// Get the file name
138    pub fn file(&self) -> &str {
139        &self.file
140    }
141
142    /// Get the line number
143    pub fn line(&self) -> u32 {
144        self.line
145    }
146
147    /// Get the timestamp
148    pub fn timestamp(&self) -> DateTime<Utc> {
149        self.timestamp
150    }
151
152    /// Get the metadata
153    pub fn metadata(&self) -> &HashMap<String, String> {
154        &self.metadata
155    }
156
157    /// Get the context data
158    pub fn context(&self) -> &HashMap<String, serde_json::Value> {
159        &self.context
160    }
161
162    /// Add metadata to the record
163    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
164        self.metadata.insert(key.into(), value.into());
165        self
166    }
167
168    /// Adds structured data to the record's metadata.
169    ///
170    /// The data will be serialized to JSON and stored with the given key.
171    /// Returns a Result indicating success or failure of serialization.
172    ///
173    /// # Example
174    ///
175    /// ```rust
176    /// use rust_loguru::{Record, LogLevel};
177    /// use serde_json::json;
178    ///
179    /// let record = Record::new(LogLevel::Info, "test message", Some("test".to_string()), Some("test.rs".to_string()), Some(42));
180    /// let result = record.with_structured_data("user", &json!({
181    ///     "id": 123,
182    ///     "name": "test"
183    /// }));
184    /// assert!(result.is_ok());
185    /// ```
186    pub fn with_structured_data<T: serde::Serialize + ?Sized>(
187        mut self,
188        key: &str,
189        value: &T,
190    ) -> Result<Self, serde_json::Error> {
191        let json_value = serde_json::to_string(value)?;
192        self.metadata.insert(key.to_string(), json_value);
193        Ok(self)
194    }
195
196    /// Adds structured context data to the record.
197    ///
198    /// The data will be stored as a serde_json::Value and can be used for
199    /// structured logging and analysis.
200    ///
201    /// # Example
202    ///
203    /// ```rust
204    /// use rust_loguru::{Record, LogLevel};
205    /// use serde_json::json;
206    ///
207    /// let record = Record::new(LogLevel::Info, "test message", None, None, None);
208    /// let record = record.with_context("user", json!({
209    ///     "id": 123,
210    ///     "name": "test"
211    /// }));
212    /// ```
213    pub fn with_context(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
214        self.context.insert(key.into(), value);
215        self
216    }
217
218    /// Sets a deferred formatting function for the record.
219    ///
220    /// This allows for lazy evaluation of the record's string representation,
221    /// which can improve performance when the record is not actually displayed.
222    ///
223    /// # Example
224    ///
225    /// ```rust
226    /// use rust_loguru::{Record, LogLevel};
227    ///
228    /// let record = Record::new(LogLevel::Info, "test message", None, None, None);
229    /// let record = record.with_deferred_format(|r| {
230    ///     format!("[{}] {} - {}", r.timestamp(), r.level(), r.message())
231    /// });
232    /// ```
233    pub fn with_deferred_format<F>(mut self, format_fn: F) -> Self
234    where
235        F: Fn(&Record) -> String + Send + Sync + 'static,
236    {
237        self.format_fn = Some(Box::new(format_fn));
238        self
239    }
240
241    /// Returns the value associated with the given key, if any.
242    pub fn get_metadata(&self, key: &str) -> Option<&str> {
243        self.metadata.get(key).map(String::as_str)
244    }
245
246    /// Returns the context value associated with the given key, if any.
247    pub fn get_context(&self, key: &str) -> Option<&serde_json::Value> {
248        self.context.get(key)
249    }
250
251    /// Returns true if the record has any structured context data.
252    pub fn has_context(&self) -> bool {
253        !self.context.is_empty()
254    }
255
256    /// Returns true if the record has any metadata.
257    pub fn has_metadata(&self) -> bool {
258        !self.metadata.is_empty()
259    }
260
261    /// Returns true if the record has a deferred formatter.
262    pub fn has_formatter(&self) -> bool {
263        self.format_fn.is_some()
264    }
265
266    /// Returns the number of context entries in the record.
267    pub fn context_len(&self) -> usize {
268        self.context.len()
269    }
270
271    /// Returns the number of metadata entries in the record.
272    pub fn metadata_len(&self) -> usize {
273        self.metadata.len()
274    }
275
276    /// Get the location as a string in the format "file:line"
277    pub fn location(&self) -> String {
278        format!("{}:{}", self.file(), self.line())
279    }
280}
281
282impl fmt::Display for Record {
283    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284        if let Some(format_fn) = &self.format_fn {
285            write!(f, "{}", format_fn(self))
286        } else {
287            write!(
288                f,
289                "[{}] {} {}:{} - {}",
290                self.timestamp.format("%Y-%m-%d %H:%M:%S%.3f"),
291                self.level,
292                self.file,
293                self.line,
294                self.message
295            )
296        }
297    }
298}
299
300impl Serialize for Record {
301    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
302    where
303        S: serde::Serializer,
304    {
305        use serde::ser::SerializeStruct;
306        let mut state = serializer.serialize_struct("Record", 7)?;
307        state.serialize_field("level", &self.level)?;
308        state.serialize_field("message", &self.message)?;
309        state.serialize_field("module", &self.module)?;
310        state.serialize_field("file", &self.file)?;
311        state.serialize_field("line", &self.line)?;
312        state.serialize_field("timestamp", &self.timestamp.to_rfc3339())?;
313        state.serialize_field("metadata", &self.metadata)?;
314        state.end()
315    }
316}
317
318impl<'de> Deserialize<'de> for Record {
319    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
320    where
321        D: serde::Deserializer<'de>,
322    {
323        #[derive(Deserialize)]
324        #[serde(field_identifier, rename_all = "lowercase")]
325        enum Field {
326            Level,
327            Message,
328            Module,
329            File,
330            Line,
331            Timestamp,
332            Metadata,
333        }
334
335        struct RecordVisitor;
336
337        impl<'de> serde::de::Visitor<'de> for RecordVisitor {
338            type Value = Record;
339
340            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
341                formatter.write_str("struct Record")
342            }
343
344            fn visit_map<V>(self, mut map: V) -> Result<Record, V::Error>
345            where
346                V: serde::de::MapAccess<'de>,
347            {
348                let mut level = None;
349                let mut message = None;
350                let mut module = None;
351                let mut file = None;
352                let mut line = None;
353                let mut timestamp = None;
354                let mut metadata = None;
355
356                while let Some(key) = map.next_key()? {
357                    match key {
358                        Field::Level => {
359                            if level.is_some() {
360                                return Err(serde::de::Error::duplicate_field("level"));
361                            }
362                            level = Some(map.next_value()?);
363                        }
364                        Field::Message => {
365                            if message.is_some() {
366                                return Err(serde::de::Error::duplicate_field("message"));
367                            }
368                            message = Some(map.next_value()?);
369                        }
370                        Field::Module => {
371                            if module.is_some() {
372                                return Err(serde::de::Error::duplicate_field("module"));
373                            }
374                            module = Some(map.next_value()?);
375                        }
376                        Field::File => {
377                            if file.is_some() {
378                                return Err(serde::de::Error::duplicate_field("file"));
379                            }
380                            file = Some(map.next_value()?);
381                        }
382                        Field::Line => {
383                            if line.is_some() {
384                                return Err(serde::de::Error::duplicate_field("line"));
385                            }
386                            line = Some(map.next_value()?);
387                        }
388                        Field::Timestamp => {
389                            if timestamp.is_some() {
390                                return Err(serde::de::Error::duplicate_field("timestamp"));
391                            }
392                            let ts_str: String = map.next_value()?;
393                            timestamp = Some(
394                                DateTime::parse_from_rfc3339(&ts_str)
395                                    .map_err(serde::de::Error::custom)?
396                                    .with_timezone(&Utc),
397                            );
398                        }
399                        Field::Metadata => {
400                            if metadata.is_some() {
401                                return Err(serde::de::Error::duplicate_field("metadata"));
402                            }
403                            metadata = Some(map.next_value()?);
404                        }
405                    }
406                }
407
408                let level = level.ok_or_else(|| serde::de::Error::missing_field("level"))?;
409                let message = message.ok_or_else(|| serde::de::Error::missing_field("message"))?;
410                let module = module.ok_or_else(|| serde::de::Error::missing_field("module"))?;
411                let file = file.ok_or_else(|| serde::de::Error::missing_field("file"))?;
412                let line = line.ok_or_else(|| serde::de::Error::missing_field("line"))?;
413                let timestamp =
414                    timestamp.ok_or_else(|| serde::de::Error::missing_field("timestamp"))?;
415                let metadata = metadata.unwrap_or_default();
416
417                Ok(Record {
418                    level,
419                    message,
420                    module,
421                    file,
422                    line,
423                    timestamp,
424                    metadata,
425                    context: HashMap::new(),
426                    format_fn: None,
427                })
428            }
429        }
430
431        deserializer.deserialize_struct(
432            "Record",
433            &[
434                "level",
435                "message",
436                "module",
437                "file",
438                "line",
439                "timestamp",
440                "metadata",
441            ],
442            RecordVisitor,
443        )
444    }
445}
446
447fn context_value_to_json(val: context::ContextValue) -> serde_json::Value {
448    match val {
449        context::ContextValue::String(s) => serde_json::Value::String(s),
450        context::ContextValue::Integer(i) => serde_json::Value::Number(i.into()),
451        context::ContextValue::Float(f) => match serde_json::Number::from_f64(f) {
452            Some(num) => serde_json::Value::Number(num),
453            None => serde_json::Value::Null,
454        },
455        context::ContextValue::Bool(b) => serde_json::Value::Bool(b),
456    }
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462    use serde_json::json;
463
464    #[test]
465    fn test_record_creation() {
466        let record = Record::new(LogLevel::Info, "test message", None, None, None);
467        assert_eq!(record.level(), LogLevel::Info);
468        assert_eq!(record.message(), "test message");
469        assert_eq!(record.module(), "unknown");
470        assert_eq!(record.file(), "unknown");
471        assert_eq!(record.line(), 0);
472    }
473
474    #[test]
475    fn test_record_with_metadata() {
476        let record = Record::new(LogLevel::Info, "test message", None, None, None)
477            .with_metadata("key", "value");
478        assert_eq!(record.metadata().get("key").unwrap(), "value");
479    }
480
481    #[test]
482    fn test_record_with_all_fields() {
483        let record = Record::new(
484            LogLevel::Info,
485            "test message",
486            Some("test_module".to_string()),
487            Some("test_file.rs".to_string()),
488            Some(42),
489        );
490        assert_eq!(record.module(), "test_module");
491        assert_eq!(record.file(), "test_file.rs");
492        assert_eq!(record.line(), 42);
493    }
494
495    #[test]
496    fn test_record_display() {
497        let record = Record::new(LogLevel::Error, "Test error message", None, None, None);
498
499        let display = format!("{}", record);
500        assert!(display.contains("ERROR"));
501        assert!(display.contains("unknown:0"));
502        assert!(display.contains("Test error message"));
503    }
504
505    #[test]
506    fn test_record_metadata_overwrite() {
507        let record = Record::new(
508            LogLevel::Info,
509            "Test message",
510            Some("test_module".to_string()),
511            Some("test.rs".to_string()),
512            Some(42),
513        )
514        .with_metadata("key", "value1")
515        .with_metadata("key", "value2");
516        assert_eq!(record.metadata().get("key"), Some(&"value2".to_string()));
517    }
518
519    #[test]
520    fn test_record_structured_data() {
521        let record = Record::new(
522            LogLevel::Info,
523            "test message",
524            Some("test_module".to_string()),
525            Some("test.rs".to_string()),
526            Some(42),
527        );
528        let record = record.with_structured_data("key", &"value").unwrap();
529        assert_eq!(record.metadata().get("key"), Some(&"\"value\"".to_string()));
530    }
531
532    #[test]
533    fn test_record_timestamp() {
534        let record = Record::new(
535            LogLevel::Info,
536            "Test message",
537            Some("test_module".to_string()),
538            Some("test.rs".to_string()),
539            Some(42),
540        );
541        let now = chrono::Utc::now();
542        assert!(record.timestamp() <= now);
543    }
544
545    #[test]
546    fn test_record_context() {
547        let record = Record::new(LogLevel::Info, "test message", None, None, None).with_context(
548            "user",
549            json!({
550                "id": 123,
551                "name": "test"
552            }),
553        );
554
555        let user = record.get_context("user").unwrap();
556        assert_eq!(user["id"], 123);
557        assert_eq!(user["name"], "test");
558    }
559
560    #[test]
561    fn test_record_deferred_format() {
562        let record = Record::new(LogLevel::Info, "test message", None, None, None)
563            .with_deferred_format(|r| {
564                format!("[{}] {} - {}", r.timestamp(), r.level(), r.message())
565            });
566
567        let display = format!("{}", record);
568        assert!(display.contains("INFO"));
569        assert!(display.contains("test message"));
570    }
571
572    #[test]
573    fn test_record_serialization() {
574        let record = Record::new(
575            LogLevel::Info,
576            "test message",
577            Some("test_module".to_string()),
578            Some("test.rs".to_string()),
579            Some(42),
580        );
581
582        let serialized = serde_json::to_string(&record).unwrap();
583        let deserialized: Record = serde_json::from_str(&serialized).unwrap();
584
585        assert_eq!(deserialized.level(), record.level());
586        assert_eq!(deserialized.message(), record.message());
587        assert_eq!(deserialized.module(), record.module());
588        assert_eq!(deserialized.file(), record.file());
589        assert_eq!(deserialized.line(), record.line());
590    }
591
592    #[test]
593    fn test_record_state_checks() {
594        let record = Record::new(LogLevel::Info, "test message", None, None, None);
595        assert!(!record.has_context());
596        assert!(!record.has_metadata());
597        assert!(!record.has_formatter());
598        assert_eq!(record.context_len(), 0);
599        assert_eq!(record.metadata_len(), 0);
600
601        let record = record
602            .with_metadata("key", "value")
603            .with_context("user", json!({"id": 1}))
604            .with_deferred_format(|r| format!("{}", r.message()));
605
606        assert!(record.has_context());
607        assert!(record.has_metadata());
608        assert!(record.has_formatter());
609        assert_eq!(record.context_len(), 1);
610        assert_eq!(record.metadata_len(), 1);
611    }
612}