Skip to main content

miden_node_utils/tracing/
attribute.rs

1use std::fmt::{self, Display, Formatter};
2use std::path::{Path, PathBuf};
3
4use miden_protocol::Word;
5use miden_protocol::account::{AccountId, AccountIdPrefix, StorageMapKey, StorageSlotName};
6use miden_protocol::batch::BatchId;
7use miden_protocol::block::BlockNumber;
8use miden_protocol::note::{NoteId, Nullifier};
9use miden_protocol::transaction::TransactionId;
10use tracing::Value;
11
12const BOOLEAN_FIELD_NAMES: &[&str] = &[
13    "account.updated",
14    "note.erased",
15    "note.id_resolved",
16    "panic",
17    "request.include_mmr_proof",
18    "request.include_proof",
19    "rpc.authentication.configured",
20];
21
22const NUMBER_FIELD_NAMES: &[&str] = &[
23    "account.id.length",
24    "account.index",
25    "asset.amount",
26    "batch.expiration_height",
27    "batch.expires_at",
28    "batch.reference_block.number",
29    "batch.size",
30    "block.from",
31    "block.number",
32    "block.protocol.version",
33    "block.size",
34    "block.timestamp",
35    "block_range.from",
36    "block_range.to",
37    "counter.failures.consecutive",
38    "counter.latency.timeout_ms",
39    "counter.value.expected",
40    "counter.value.observed",
41    "counter.value.target",
42    "current_client_block_height",
43    "cutoff_block",
44    "db.account_state_forest.size",
45    "db.account_tree.size",
46    "db.block_store.size",
47    "db.nullifier_tree.size",
48    "db.sqlite.connection_pool_size",
49    "db.sqlite.size",
50    "db.sqlite.wal.size",
51    "dice_roll",
52    "failure_rate",
53    "inputs_size",
54    "mempool.accounts",
55    "mempool.batches.proposed",
56    "mempool.batches.proven",
57    "mempool.nullifiers",
58    "mempool.output_notes",
59    "mempool.transactions.unbatched",
60    "mempool.transactions.uncommitted",
61    "note.tag",
62    "ntx_builder.max_cycles",
63    "ntx_builder.tx_expiration_delta",
64    "port",
65    "pow.hash",
66    "pow.nonce",
67    "pow.target",
68    "pow.target.leading_zero_bits",
69    "prefix_len",
70    "proof_size",
71    "prover.capacity",
72    "prover.port",
73    "prover.proof_type.raw",
74    "reference_block.number",
75    "retry.attempt",
76    "retry.delay_ms",
77    "shutdown.grace_period_ms",
78    "snapshot.block_num",
79    "snapshot.lifetime_ms",
80    "snapshot.superseded_for_ms",
81    "snapshots.live",
82    "subscription.idle_ms",
83    "subscription.stall_timeout_ms",
84    "sync.block_gap",
85    "sync.ready_threshold",
86    "sync.upstream_block",
87    "timeout.ms",
88    "tip.number",
89    "tip.stale_duration_secs",
90    "transaction.expiration_delta",
91    "transaction.expires_at",
92    "transaction.reference_block.number",
93    "transaction.submitted_at",
94    "worker.status.raw",
95    "workers.active",
96    "workers.capacity",
97];
98
99const STRING_FIELD_NAMES: &[&str] = &[
100    "account.id",
101    "account.storage.kind",
102    "account.storage.map.entry.operation",
103    "account.storage.operation",
104    "asset.symbol",
105    "batch.interval",
106    "block.interval",
107    "dependency.endpoint",
108    "dependency.name",
109    "genesis.source",
110    "genesis.source.kind",
111    "internal.listen",
112    "mempool.removal.reason",
113    "network_monitor.listen",
114    "node.role",
115    "note.execution_cycles",
116    "ntx_builder.endpoint",
117    "ntx_builder.idle_timeout",
118    "ntx_builder.listen",
119    "operation.name",
120    "path",
121    "pow.challenge.prefix",
122    "prover",
123    "prover.kind",
124    "prover.timeout",
125    "request.kind",
126    "rpc.endpoint",
127    "rpc.listen",
128    "sequencer.endpoint",
129    "service.name",
130    "service.version",
131    "shutdown.signal",
132    "sync.block_source.endpoint",
133    "task.name",
134    "transaction.id",
135    "transaction.input_notes",
136    "transaction.output_notes",
137    "tx_prover.endpoint",
138    "validator.admin_listen",
139    "validator.endpoints",
140    "validator.listen",
141    "validator.signer",
142    "worker.name",
143];
144
145/// Converts a value into its canonical tracing attribute representation.
146///
147/// Values passed to the Miden tracing span and event macros must implement this trait.
148/// Implementations decide the allowed scalar field names, the attribute's primitive type, and its
149/// formatting, allowing tracing macros to use one name and representation consistently at every
150/// recording site. Collection implementations derive their field names by appending `s` to these
151/// scalar names.
152pub trait RecordAttribute {
153    /// Scalar field names associated with this value's type.
154    const FIELD_NAMES: &'static [&'static str];
155
156    /// Whether the final component of each field name must have an `s` suffix.
157    const PLURALIZE_FIELD_NAMES: bool = false;
158
159    /// Returns the value that is passed to `tracing`.
160    fn record_attribute(&self) -> impl Value + '_;
161}
162
163/// Returns whether `field_name` occurs in `field_names`.
164///
165/// This is public because it is referenced by the tracing proc macros. Callers should use the
166/// macros rather than invoking it directly.
167#[doc(hidden)]
168pub const fn field_name_allowed(field_names: &[&str], field_name: &str, pluralize: bool) -> bool {
169    let mut index = 0;
170    while index < field_names.len() {
171        let allowed = if pluralize {
172            str_eq_with_s_suffix(field_names[index], field_name)
173        } else {
174            str_eq(field_names[index], field_name)
175        };
176        if allowed {
177            return true;
178        }
179        index += 1;
180    }
181    false
182}
183
184const fn str_eq_with_s_suffix(singular: &str, plural: &str) -> bool {
185    let singular = singular.as_bytes();
186    let plural = plural.as_bytes();
187    if plural.len() != singular.len() + 1 || plural[singular.len()] != b's' {
188        return false;
189    }
190
191    let mut index = 0;
192    while index < singular.len() {
193        if singular[index] != plural[index] {
194            return false;
195        }
196        index += 1;
197    }
198    true
199}
200
201const fn str_eq(left: &str, right: &str) -> bool {
202    let left = left.as_bytes();
203    let right = right.as_bytes();
204    if left.len() != right.len() {
205        return false;
206    }
207
208    let mut index = 0;
209    while index < left.len() {
210        if left[index] != right[index] {
211            return false;
212        }
213        index += 1;
214    }
215    true
216}
217
218/// Converts an approved attribute into a `tracing` value.
219///
220/// This is public because it is referenced by the tracing proc macros. Callers should use the
221/// macros rather than invoking it directly.
222#[doc(hidden)]
223pub fn record_attribute<T: RecordAttribute + ?Sized>(value: &T) -> impl Value + '_ {
224    value.record_attribute()
225}
226
227macro_rules! impl_scalar_attribute {
228    ($field_names:expr; $($ty:ty),* $(,)?) => {
229        $(
230            impl RecordAttribute for $ty {
231                const FIELD_NAMES: &'static [&'static str] = $field_names;
232
233                fn record_attribute(&self) -> impl Value + '_ {
234                    *self
235                }
236            }
237        )*
238    };
239}
240
241impl_scalar_attribute!(BOOLEAN_FIELD_NAMES; bool);
242impl_scalar_attribute!(
243    NUMBER_FIELD_NAMES;
244    f32,
245    f64,
246    i8,
247    i16,
248    i32,
249    i64,
250    i128,
251    isize,
252    u8,
253    u16,
254    u64,
255    u128,
256    usize,
257);
258impl_scalar_attribute!(NUMBER_FIELD_NAMES; u32);
259
260impl RecordAttribute for str {
261    const FIELD_NAMES: &'static [&'static str] = STRING_FIELD_NAMES;
262
263    fn record_attribute(&self) -> impl Value + '_ {
264        self
265    }
266}
267
268impl RecordAttribute for String {
269    const FIELD_NAMES: &'static [&'static str] = <str as RecordAttribute>::FIELD_NAMES;
270
271    fn record_attribute(&self) -> impl Value + '_ {
272        self.as_str()
273    }
274}
275
276impl<T: RecordAttribute + ?Sized> RecordAttribute for &T {
277    const FIELD_NAMES: &'static [&'static str] = T::FIELD_NAMES;
278    const PLURALIZE_FIELD_NAMES: bool = T::PLURALIZE_FIELD_NAMES;
279
280    fn record_attribute(&self) -> impl Value + '_ {
281        (*self).record_attribute()
282    }
283}
284
285impl<T: RecordAttribute> RecordAttribute for Option<T> {
286    const FIELD_NAMES: &'static [&'static str] = T::FIELD_NAMES;
287    const PLURALIZE_FIELD_NAMES: bool = T::PLURALIZE_FIELD_NAMES;
288
289    fn record_attribute(&self) -> impl Value + '_ {
290        self.as_ref().map(RecordAttribute::record_attribute)
291    }
292}
293
294impl RecordAttribute for Path {
295    const FIELD_NAMES: &'static [&'static str] = &["data.directory", "genesis.file", "path"];
296
297    fn record_attribute(&self) -> impl Value + '_ {
298        tracing::field::display(self.display())
299    }
300}
301
302impl RecordAttribute for PathBuf {
303    const FIELD_NAMES: &'static [&'static str] = <Path as RecordAttribute>::FIELD_NAMES;
304
305    fn record_attribute(&self) -> impl Value + '_ {
306        self.as_path().record_attribute()
307    }
308}
309
310impl RecordAttribute for BlockNumber {
311    const FIELD_NAMES: &'static [&'static str] = &[
312        "batch.expiration_height",
313        "batch.expires_at",
314        "batch.reference_block.number",
315        "block.from",
316        "block.number",
317        "block_range.from",
318        "block_range.to",
319        "cutoff_block",
320        "current_client_block_height",
321        "reference_block.number",
322        "snapshot.block_num",
323        "sync.upstream_block",
324        "tip.number",
325        "transaction.expires_at",
326        "transaction.reference_block.number",
327        "transaction.submitted_at",
328    ];
329
330    fn record_attribute(&self) -> impl Value + '_ {
331        self.as_u64()
332    }
333}
334
335macro_rules! impl_display_attribute {
336    ($ty:ty, $field_names:expr $(,)?) => {
337        impl RecordAttribute for $ty {
338            const FIELD_NAMES: &'static [&'static str] = $field_names;
339
340            fn record_attribute(&self) -> impl Value + '_ {
341                tracing::field::display(self)
342            }
343        }
344    };
345}
346
347impl_display_attribute!(
348    AccountId,
349    &[
350        "account.id",
351        "counter.account.id.new",
352        "counter.account.id.old",
353        "note.sender",
354        "wallet.account.id.new",
355        "wallet.account.id.old",
356    ],
357);
358impl_display_attribute!(AccountIdPrefix, &["account.id.network_prefix"]);
359impl_display_attribute!(StorageMapKey, &["account.storage.map.key"]);
360impl_display_attribute!(StorageSlotName, &["account.storage.slot"]);
361impl_display_attribute!(BatchId, &["batch.id", "block.batch.id"]);
362impl_display_attribute!(NoteId, &["note.id"]);
363impl_display_attribute!(Nullifier, &["note.nullifier"]);
364impl_display_attribute!(TransactionId, &["block.transaction.id", "transaction.id"]);
365impl_display_attribute!(
366    Word,
367    &[
368        "account.final_state.commitment",
369        "account.initial_state.commitment",
370        "account.storage.value",
371        "batch.reference_block.commitment",
372        "block.commitment",
373        "block.commitments.account",
374        "block.commitments.chain",
375        "block.commitments.kernel",
376        "block.commitments.note",
377        "block.commitments.nullifier",
378        "block.commitments.transaction",
379        "block.prev_block_commitment",
380        "block.sub_commitment",
381        "genesis.commitment",
382        "script.root",
383        "transaction.reference_block.commitment",
384    ],
385);
386
387/// Formats a slice as one string-valued tracing attribute.
388///
389/// This is not an OpenTelemetry array: `tracing::Value` has no array representation, so the `OTel`
390/// tracing layer receives the formatted list as a string.
391struct AttributeList<'a, T>(&'a [T]);
392
393impl<T: Display> Display for AttributeList<'_, T> {
394    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
395        let mut values = self.0.iter();
396        let Some(first) = values.next() else {
397            return f.write_str("None");
398        };
399
400        write!(f, "[{first}")?;
401        for value in values {
402            write!(f, ", {value}")?;
403        }
404        f.write_str("]")
405    }
406}
407
408impl<T: Display + RecordAttribute> RecordAttribute for [T] {
409    const FIELD_NAMES: &'static [&'static str] = T::FIELD_NAMES;
410    const PLURALIZE_FIELD_NAMES: bool = true;
411
412    fn record_attribute(&self) -> impl Value + '_ {
413        tracing::field::display(AttributeList(self))
414    }
415}
416
417impl<T: Display + RecordAttribute, const N: usize> RecordAttribute for [T; N] {
418    const FIELD_NAMES: &'static [&'static str] = T::FIELD_NAMES;
419    const PLURALIZE_FIELD_NAMES: bool = true;
420
421    fn record_attribute(&self) -> impl Value + '_ {
422        self.as_slice().record_attribute()
423    }
424}
425
426impl<T: Display + RecordAttribute> RecordAttribute for Vec<T> {
427    const FIELD_NAMES: &'static [&'static str] = T::FIELD_NAMES;
428    const PLURALIZE_FIELD_NAMES: bool = true;
429
430    fn record_attribute(&self) -> impl Value + '_ {
431        self.as_slice().record_attribute()
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use miden_protocol::account::AccountId;
438
439    use super::{AttributeList, RecordAttribute, field_name_allowed};
440
441    #[test]
442    fn lists_use_the_canonical_format() {
443        assert_eq!(AttributeList::<u32>(&[]).to_string(), "None");
444        assert_eq!(AttributeList(&[1, 2, 3]).to_string(), "[1, 2, 3]");
445    }
446
447    #[test]
448    fn references_are_approved_when_the_referenced_type_is_approved() {
449        fn assert_record_attribute(_: &impl RecordAttribute) {}
450
451        let value = "attribute";
452        assert_record_attribute(&value);
453        assert_record_attribute(&&value);
454        assert_record_attribute(&Some(value));
455        assert_record_attribute(&None::<&str>);
456    }
457
458    #[test]
459    fn field_names_are_specific_to_the_attribute_type() {
460        assert!(field_name_allowed(
461            AccountId::FIELD_NAMES,
462            "account.id",
463            AccountId::PLURALIZE_FIELD_NAMES,
464        ));
465        assert!(!field_name_allowed(
466            AccountId::FIELD_NAMES,
467            "account.ids",
468            AccountId::PLURALIZE_FIELD_NAMES,
469        ));
470        assert!(field_name_allowed(
471            <[AccountId]>::FIELD_NAMES,
472            "account.ids",
473            <[AccountId]>::PLURALIZE_FIELD_NAMES,
474        ));
475        assert!(!field_name_allowed(
476            <[AccountId]>::FIELD_NAMES,
477            "account.id",
478            <[AccountId]>::PLURALIZE_FIELD_NAMES,
479        ));
480    }
481}