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