Skip to main content

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