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