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}