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