tracing_cache/record.rs
1//! In-memory records for spans and events, plus the visitor that captures
2//! their fields.
3//!
4//! Field capture avoids the per-field `HashMap` + heap-allocated `String`
5//! cost the original layout paid. Each field is one entry in a
6//! `Vec<(&'static str, FieldValue)>` — a 24-byte header pointing at the
7//! field list on the heap, so `SpanRecord` itself stays small (the
8//! earlier `SmallVec<[..; 8]>` inlined ~330 bytes and made every
9//! pipeline transit of a `SpanRecord` proportionally expensive).
10//! `FieldValue` is a tagged union of the types `tracing::field::Visit`
11//! actually delivers, so primitive fields never touch the allocator and
12//! string variants pay only one heap-allocation per long field.
13
14use std::sync::{Arc, LazyLock};
15use std::time::Instant;
16
17use compact_str::CompactString;
18use tracing::Metadata;
19use tracing::callsite::{Callsite, DefaultCallsite, Identifier};
20use tracing::field::FieldSet;
21use tracing::metadata::Kind;
22
23/// Fallback metadata returned from accessors when a pooled record
24/// is queried before it has been filled in. Looks like an event
25/// at TRACE level with an empty field set, named `"unfilled"`.
26static EMPTY_CALLSITE: DefaultCallsite = {
27 static META: Metadata<'static> = Metadata::new(
28 "unfilled",
29 "tracing_cache::record",
30 tracing::Level::TRACE,
31 None,
32 None,
33 None,
34 FieldSet::new(&[], Identifier(&EMPTY_CALLSITE)),
35 Kind::EVENT,
36 );
37 DefaultCallsite::new(&META)
38};
39
40fn empty_metadata() -> &'static Metadata<'static> {
41 EMPTY_CALLSITE.metadata()
42}
43
44/// Stand-in `Instant` for unfilled records. Captured once at
45/// first access — there's no public `Instant::ZERO`, so the
46/// earliest moment we can name is "whenever this lazy first
47/// resolved." Consumers only see this on a fresh pool entry
48/// that hasn't been filled yet, which is a logic bug; the
49/// returned value is therefore monotonic and stable but not
50/// semantically meaningful.
51static UNFILLED_INSTANT: LazyLock<Instant> = LazyLock::new(Instant::now);
52
53/// Each captured field value. `Str` keeps a `&'static str` (zero-copy
54/// for literal field arguments), `SmallString` keeps the
55/// stack-inline-up-to-24-byte `CompactString` (no heap for short
56/// dynamic strings), `SharedString` keeps an `Arc<String>` for callers
57/// that want sharing, and `String` is the unrestricted owned fallback.
58#[derive(Debug, Clone)]
59pub enum FieldValue {
60 U64(u64),
61 I64(i64),
62 F64(f64),
63 Bool(bool),
64 Str(&'static str),
65 SmallString(CompactString),
66 SharedString(Arc<String>),
67 String(String),
68}
69
70impl FieldValue {
71 /// Return a `&str` view of the value. Numeric / bool variants
72 /// format into a fresh `CompactString` (cheap, usually inline).
73 /// Callers that want a stable borrow should match on the variant
74 /// directly.
75 pub fn to_display_string(&self) -> CompactString {
76 use std::fmt::Write;
77 match self {
78 FieldValue::U64(v) => {
79 let mut s = CompactString::default();
80 let _ = write!(s, "{}", v);
81 s
82 }
83 FieldValue::I64(v) => {
84 let mut s = CompactString::default();
85 let _ = write!(s, "{}", v);
86 s
87 }
88 FieldValue::F64(v) => {
89 let mut s = CompactString::default();
90 let _ = write!(s, "{}", v);
91 s
92 }
93 FieldValue::Bool(v) => CompactString::const_new(if *v { "true" } else { "false" }),
94 FieldValue::Str(s) => CompactString::const_new(s),
95 FieldValue::SmallString(s) => s.clone(),
96 FieldValue::SharedString(s) => CompactString::from(s.as_str()),
97 FieldValue::String(s) => CompactString::from(s.as_str()),
98 }
99 }
100
101 /// Substring-match the printed representation. Used by the server's
102 /// filter that matches against root-span field values.
103 pub fn contains(&self, needle: &str) -> bool {
104 match self {
105 FieldValue::Str(s) => s.contains(needle),
106 FieldValue::SmallString(s) => s.contains(needle),
107 FieldValue::SharedString(s) => s.contains(needle),
108 FieldValue::String(s) => s.contains(needle),
109 // Primitives: stringify on demand.
110 _ => self.to_display_string().contains(needle),
111 }
112 }
113}
114
115/// A field list small enough to keep inline for the typical span. Spans
116/// or events with > 8 fields spill to the heap.
117pub type FieldList = Vec<(&'static str, FieldValue)>;
118
119/// Look up a field by name; returns `None` if not present.
120#[inline]
121pub fn field_get<'a>(fields: &'a FieldList, name: &str) -> Option<&'a FieldValue> {
122 fields.iter().find(|(k, _)| *k == name).map(|(_, v)| v)
123}
124
125/// One captured event. `metadata` and `recorded_at` are `Option` purely
126/// so `EventRecord` can implement `Default` for the internal object
127/// pool — they are always `Some` once an event has been published
128/// through the subscriber. Helper accessors `metadata()` /
129/// `recorded_at()` fall back to safe defaults rather than panic.
130#[derive(Clone, Debug, Default)]
131pub struct EventRecord {
132 pub metadata: Option<&'static Metadata<'static>>,
133 pub fields: FieldList,
134 pub recorded_at: Option<Instant>,
135}
136
137impl EventRecord {
138 /// Metadata pointer. Always `Some` for events that have been
139 /// observed by the subscriber; freshly-acquired pool entries
140 /// that haven't been filled yet fall through to a static
141 /// `"unfilled"` metadata stand-in so consumers don't need to
142 /// guard against `None`.
143 #[inline]
144 pub fn metadata(&self) -> &'static Metadata<'static> {
145 self.metadata.unwrap_or_else(empty_metadata)
146 }
147
148 /// Captured `Instant` for the event. See [`Self::metadata`]
149 /// for the unfilled-entry behaviour — same shape: returns
150 /// the lazy `UNFILLED_INSTANT` stand-in instead of panicking.
151 #[inline]
152 pub fn recorded_at(&self) -> Instant {
153 self.recorded_at.unwrap_or(*UNFILLED_INSTANT)
154 }
155
156 pub fn field(&self, name: &str) -> Option<&FieldValue> {
157 field_get(&self.fields, name)
158 }
159}
160
161impl crate::object_pool::Resettable for EventRecord {
162 fn reset(&mut self) {
163 self.metadata = None;
164 self.fields.clear();
165 self.recorded_at = None;
166 }
167}
168
169#[derive(Clone, Debug)]
170pub struct SpanRecord {
171 pub id: u64,
172 pub parent_id: Option<u64>,
173 pub metadata: &'static Metadata<'static>,
174 pub fields: FieldList,
175 /// Events captured while this span was on the stack. Each entry is
176 /// a pooled `EventRecord` — pushing one moves a 16-byte pointer pair
177 /// rather than the full inline-vec body, and the underlying
178 /// `EventRecord` heap allocation is recycled when the SpanRecord
179 /// finally drops.
180 pub events: Vec<crate::object_pool::ReuseRef<EventRecord>>,
181 pub opened_at: Instant,
182 pub closed_at: Option<Instant>,
183}
184
185impl SpanRecord {
186 /// Convenience: linear-scan field lookup by name.
187 pub fn field(&self, name: &str) -> Option<&FieldValue> {
188 field_get(&self.fields, name)
189 }
190}
191
192/// Borrows a mutable reference to a field list and pushes every visited
193/// field onto it as a typed `FieldValue`. Reused for span attributes,
194/// span `record()` updates, and event fields.
195///
196/// `record_str` pays one allocation only if the string exceeds the
197/// `CompactString` inline budget (24 bytes on 64-bit). Numeric / bool
198/// variants are zero-allocation.
199pub(crate) struct FieldVisitor<'a> {
200 pub fields: &'a mut FieldList,
201}
202
203impl FieldVisitor<'_> {
204 /// Replace an existing entry by name or append a new one. Mirrors
205 /// `HashMap::insert` semantics — repeated `record(...)` calls
206 /// overwrite the prior value for that field name.
207 #[inline]
208 fn set(&mut self, name: &'static str, value: FieldValue) {
209 match self.fields.iter_mut().find(|(k, _)| *k == name) {
210 Some(slot) => slot.1 = value,
211 None => self.fields.push((name, value)),
212 }
213 }
214}
215
216impl tracing::field::Visit for FieldVisitor<'_> {
217 fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
218 use std::fmt::Write;
219 let mut s = CompactString::default();
220 let _ = write!(s, "{:?}", value);
221 self.set(field.name(), FieldValue::SmallString(s));
222 }
223
224 fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
225 // `tracing::field::Visit::record_str` erases lifetime, so we can't
226 // tell a `&'static str` literal from a stack-borrowed `&str` here
227 // — copy into a `CompactString` (inline for ≤ 24 bytes). The
228 // `Str(&'static str)` variant is reserved for synthetic
229 // constructions by tests / non-Visit callers that know the
230 // lifetime statically.
231 self.set(
232 field.name(),
233 FieldValue::SmallString(CompactString::from(value)),
234 );
235 }
236
237 fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
238 self.set(field.name(), FieldValue::I64(value));
239 }
240
241 fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
242 self.set(field.name(), FieldValue::U64(value));
243 }
244
245 fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
246 self.set(field.name(), FieldValue::Bool(value));
247 }
248
249 fn record_f64(&mut self, field: &tracing::field::Field, value: f64) {
250 self.set(field.name(), FieldValue::F64(value));
251 }
252
253 fn record_error(
254 &mut self,
255 field: &tracing::field::Field,
256 value: &(dyn std::error::Error + 'static),
257 ) {
258 use std::fmt::Write;
259 let mut s = CompactString::default();
260 let _ = write!(s, "{}", value);
261 self.set(field.name(), FieldValue::SmallString(s));
262 }
263}