Skip to main content

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}