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