Skip to main content

log_io/
record.rs

1//! Log records and their constituent parts.
2//!
3//! The data model is intentionally small. A [`Record`] is the
4//! immutable unit that flows from logger to filter to formatter to
5//! sink. All its parts borrow, so a record can be constructed on the
6//! stack without touching the allocator.
7
8use crate::level::Level;
9use crate::value::Value;
10
11/// A single key-value pair attached to a [`Record`].
12///
13/// Keys are borrowed strings. Values are borrowed via [`Value`]. A
14/// builder that needs to attach owned data must keep that data alive
15/// for the duration of the [`Record`].
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct Field<'a> {
18    /// The field name.
19    pub key: &'a str,
20    /// The field value.
21    pub value: Value<'a>,
22}
23
24impl<'a> Field<'a> {
25    /// Construct a field from a key and a value.
26    ///
27    /// `value` is `impl Into<Value>` so callers can write
28    /// `Field::new("port", 8080_u32)` directly.
29    pub fn new<V: Into<Value<'a>>>(key: &'a str, value: V) -> Self {
30        Self {
31            key,
32            value: value.into(),
33        }
34    }
35}
36
37/// Metadata describing the origin and severity of a record.
38///
39/// Most metadata is optional. `level` and `target` are required; the
40/// rest is best-effort context that a caller may supply. Macros
41/// populate `file` and `line` automatically.
42#[derive(Debug, Clone, Copy, PartialEq)]
43pub struct Metadata<'a> {
44    /// Severity of the record.
45    pub level: Level,
46    /// Logical target. Typically the module path, but callers are free
47    /// to use a domain-specific identifier for filtering.
48    pub target: &'a str,
49    /// Source file path, if known.
50    pub file: Option<&'a str>,
51    /// Source line number, if known.
52    pub line: Option<u32>,
53    /// Unix nanoseconds since the epoch. Populated by the logger when
54    /// std is available. `None` indicates the consumer should supply
55    /// its own clock (typical in `no_std` builds).
56    pub timestamp_unix_nanos: Option<u128>,
57}
58
59impl<'a> Metadata<'a> {
60    /// Build minimal metadata containing only `level` and `target`.
61    ///
62    /// All optional fields are `None`. Useful for handcrafted records
63    /// where a richer context isn't needed.
64    pub const fn new(level: Level, target: &'a str) -> Self {
65        Self {
66            level,
67            target,
68            file: None,
69            line: None,
70            timestamp_unix_nanos: None,
71        }
72    }
73
74    /// Replace the source location.
75    pub const fn with_location(mut self, file: &'a str, line: u32) -> Self {
76        self.file = Some(file);
77        self.line = Some(line);
78        self
79    }
80
81    /// Replace the timestamp.
82    pub const fn with_timestamp(mut self, ts_unix_nanos: u128) -> Self {
83        self.timestamp_unix_nanos = Some(ts_unix_nanos);
84        self
85    }
86}
87
88/// A complete, immutable log record.
89///
90/// Records borrow their data; the lifetime parameter is the shortest
91/// of the borrows. A [`crate::Sink`] receives a `&Record` and must not
92/// outlive it.
93#[derive(Debug, Clone, Copy)]
94pub struct Record<'a> {
95    /// Static origin and severity information.
96    pub metadata: Metadata<'a>,
97    /// Free-form message.
98    pub message: &'a str,
99    /// Borrowed structured fields. May be empty.
100    pub fields: &'a [Field<'a>],
101    /// Borrowed context fields. May be empty. Distinct from `fields`
102    /// so formatters can label them (for example, prefixing context
103    /// keys in human format) and filters can skip them.
104    pub context: &'a [Field<'a>],
105}
106
107impl<'a> Record<'a> {
108    /// Build a record with no context fields.
109    pub const fn new(metadata: Metadata<'a>, message: &'a str, fields: &'a [Field<'a>]) -> Self {
110        Self {
111            metadata,
112            message,
113            fields,
114            context: &[],
115        }
116    }
117
118    /// Build a record with explicit context fields.
119    pub const fn with_context(
120        metadata: Metadata<'a>,
121        message: &'a str,
122        fields: &'a [Field<'a>],
123        context: &'a [Field<'a>],
124    ) -> Self {
125        Self {
126            metadata,
127            message,
128            fields,
129            context,
130        }
131    }
132
133    /// Iterate over context fields followed by structured fields.
134    ///
135    /// Most formatters render them in this order so context appears
136    /// before the per-call data.
137    pub fn all_fields(&self) -> impl Iterator<Item = &Field<'a>> {
138        self.context.iter().chain(self.fields.iter())
139    }
140}
141
142#[cfg(all(test, feature = "std"))]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn field_new_accepts_typed_values() {
148        let f = Field::new("port", 8080_u32);
149        assert_eq!(f.key, "port");
150        assert_eq!(f.value, Value::U64(8080));
151    }
152
153    #[test]
154    fn metadata_chain_setters() {
155        let m = Metadata::new(Level::Info, "tgt")
156            .with_location("file.rs", 12)
157            .with_timestamp(1_700_000_000_000_000_000);
158        assert_eq!(m.file, Some("file.rs"));
159        assert_eq!(m.line, Some(12));
160        assert_eq!(m.timestamp_unix_nanos, Some(1_700_000_000_000_000_000));
161    }
162
163    #[test]
164    fn record_all_fields_orders_context_first() {
165        let ctx = [Field::new("trace_id", "abc")];
166        let fields = [Field::new("port", 80_u32)];
167        let record = Record::with_context(Metadata::new(Level::Info, "tgt"), "msg", &fields, &ctx);
168        let keys: Vec<_> = record.all_fields().map(|f| f.key).collect();
169        assert_eq!(keys, vec!["trace_id", "port"]);
170    }
171}