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