Skip to main content

miden_node_tracing/
attribute.rs

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