miden_node_tracing_macro/
lib.rs1use std::collections::BTreeSet;
2
3use proc_macro::TokenStream;
4use proc_macro2::{Delimiter, Group, TokenStream as TokenStream2, TokenTree};
5use quote::{ToTokens, quote};
6use syn::parse::{Parse, ParseStream};
7use syn::punctuated::Punctuated;
8use syn::token::Dot;
9use syn::visit::Visit;
10use syn::{
11 Attribute,
12 Block,
13 Expr,
14 Ident,
15 ItemFn,
16 LitStr,
17 Macro,
18 Meta,
19 Result,
20 Token,
21 parse_macro_input,
22 parse_quote,
23};
24
25#[proc_macro_attribute]
32pub fn miden_instrument(attr: TokenStream, item: TokenStream) -> TokenStream {
33 let attr = match rewrite_explicit_fields(TokenStream2::from(attr)) {
34 Ok(attr) => attr,
35 Err(error) => return error.into_compile_error().into(),
36 };
37 let mut function = parse_macro_input!(item as ItemFn);
38 let fields = collect_recorded_fields(&function);
39 let args = match merge_inferred_fields(attr, &fields) {
40 Ok(args) => args,
41 Err(error) => return error.into_compile_error().into(),
42 };
43 let statements = &function.block.stmts;
44 let block: Block = parse_quote! {{
45 #[allow(unused_macros)]
46 macro_rules! __miden_span_record_must_be_used_within_miden_instrument {
47 () => {};
48 }
49
50 #(#statements)*
51 }};
52 *function.block = block;
53
54 let expanded = quote! {
55 #[::tracing::instrument(#args)]
56 #function
57 };
58
59 expanded.into()
60}
61
62#[proc_macro]
87pub fn trace(input: TokenStream) -> TokenStream {
88 expand_event(input, "trace", false)
89}
90
91#[proc_macro]
116pub fn debug(input: TokenStream) -> TokenStream {
117 expand_event(input, "debug", false)
118}
119
120#[proc_macro]
146pub fn info(input: TokenStream) -> TokenStream {
147 expand_event(input, "info", false)
148}
149
150#[proc_macro]
175pub fn warn(input: TokenStream) -> TokenStream {
176 expand_event(input, "warn", false)
177}
178
179#[proc_macro]
200pub fn error(input: TokenStream) -> TokenStream {
201 expand_event(input, "error", true)
202}
203
204fn expand_event(input: TokenStream, level: &str, error_required: bool) -> TokenStream {
205 let event = if error_required {
206 syn::parse::<ErrorEvent>(input).map(|event| event.0)
207 } else {
208 syn::parse::<OptionalErrorEvent>(input).map(|event| event.0)
209 };
210 let event = match event {
211 Ok(event) => event,
212 Err(error) => return error.into_compile_error().into(),
213 };
214
215 event.tokens(&Ident::new(level, proc_macro2::Span::call_site())).into()
216}
217
218struct ErrorEvent(Event);
219
220impl Parse for ErrorEvent {
221 fn parse(input: ParseStream<'_>) -> Result<Self> {
222 let event = parse_error_event(input)?;
223 event.reject_exception_message()?;
224
225 Ok(Self(event))
226 }
227}
228
229struct OptionalErrorEvent(Event);
230
231impl Parse for OptionalErrorEvent {
232 fn parse(input: ParseStream<'_>) -> Result<Self> {
233 let event = if starts_without_error(input) {
234 Event::parse_after_error(input, None)?
235 } else {
236 parse_error_event(input)?
237 };
238 event.reject_exception_message()?;
239
240 Ok(Self(event))
241 }
242}
243
244fn parse_error_event(input: ParseStream<'_>) -> Result<Event> {
245 if input.is_empty() {
246 return Err(input.error("expected an error expression"));
247 }
248
249 let error = input.parse()?;
250 if input.is_empty() {
251 return Err(syn::Error::new_spanned(error, "expected a static event name string literal"));
252 }
253 input.parse::<Token![,]>()?;
254
255 Event::parse_after_error(input, Some(error))
256}
257
258struct Event {
259 error: Option<Expr>,
260 target: Option<Expr>,
261 parent: Option<Expr>,
262 name: LitStr,
263 fields: Vec<RecordField>,
264}
265
266impl Event {
267 fn parse_after_error(input: ParseStream<'_>, error: Option<Expr>) -> Result<Self> {
268 let target = if input.peek(event_kw::target) {
269 input.parse::<event_kw::target>()?;
270 input.parse::<Token![:]>()?;
271 let target = input.parse()?;
272 input.parse::<Token![,]>()?;
273 Some(target)
274 } else {
275 None
276 };
277
278 let parent = if input.peek(event_kw::parent) {
279 input.parse::<event_kw::parent>()?;
280 input.parse::<Token![:]>()?;
281 let parent = input.parse()?;
282 input.parse::<Token![,]>()?;
283 Some(parent)
284 } else {
285 None
286 };
287
288 let name = input
289 .parse::<LitStr>()
290 .map_err(|_| input.error("expected a static event name string literal"))?;
291 let mut fields = Vec::new();
292
293 if !input.is_empty() {
294 let comma = input.parse::<Token![,]>()?;
295 if input.is_empty() {
296 return Err(syn::Error::new_spanned(comma, "trailing commas are not supported"));
297 }
298
299 loop {
300 fields.push(RecordField::parse(input, true)?);
301 if input.is_empty() {
302 break;
303 }
304
305 let comma = input.parse::<Token![,]>()?;
306 if input.is_empty() {
307 return Err(syn::Error::new_spanned(
308 comma,
309 "trailing commas are not supported",
310 ));
311 }
312 }
313 }
314
315 Ok(Self { error, target, parent, name, fields })
316 }
317
318 fn reject_exception_message(&self) -> Result<()> {
319 if let Some(field) =
320 self.fields.iter().find(|field| field.path.name() == "exception.message")
321 {
322 Err(syn::Error::new_spanned(
323 &field.path,
324 "pass the error as the first argument instead of recording `exception.message`",
325 ))
326 } else {
327 Ok(())
328 }
329 }
330
331 fn tokens(&self, level: &Ident) -> TokenStream2 {
332 let target = self.target.as_ref().map(|target| quote! { target: #target, });
333 let parent = self.parent.as_ref().map(|parent| quote! { parent: #parent, });
334 let name = &self.name;
335 let error = self.error.as_ref().map(|error| {
336 quote! {
337 , exception.message = ::miden_node_utils::tracing::record_attribute(
338 &({
339 use ::miden_node_utils::ErrorReport as _;
340 (#error).as_report()
341 })
342 )
343 }
344 });
345 let fields = self.fields.iter().map(RecordField::instrument_tokens);
346
347 quote! {
348 ::tracing::#level!(
349 #target
350 #parent
351 message = #name
352 #error
353 #(, #fields)*
354 )
355 }
356 }
357}
358
359mod event_kw {
360 syn::custom_keyword!(parent);
361 syn::custom_keyword!(target);
362}
363
364fn starts_without_error(input: ParseStream<'_>) -> bool {
365 if input.peek(LitStr) {
366 return true;
367 }
368
369 let ahead = input.fork();
370 let starts_with_target =
371 ahead.parse::<event_kw::target>().is_ok() && ahead.parse::<Token![:]>().is_ok();
372 if starts_with_target {
373 return true;
374 }
375
376 let ahead = input.fork();
377 ahead.parse::<event_kw::parent>().is_ok() && ahead.parse::<Token![:]>().is_ok()
378}
379
380fn merge_inferred_fields(attr: TokenStream2, fields: &[FieldPath]) -> Result<TokenStream2> {
381 let mut args = split_top_level_args(attr);
382 reject_skip_directives(&args)?;
383
384 args.push(quote! { skip_all });
387
388 if fields.is_empty() {
389 return Ok(quote! { #(#args),* });
390 }
391
392 let inferred_fields = quote! { #(#fields = ::tracing::field::Empty),* };
393 let mut merged_existing_fields = false;
394 let args = args
395 .into_iter()
396 .map(|arg| {
397 if let Some(group) = fields_group(&arg) {
398 merged_existing_fields = true;
399 let existing_fields = group.stream();
400 let merged_fields = if existing_fields.is_empty() {
401 inferred_fields.clone()
402 } else if ends_with_comma(&existing_fields) {
403 quote! { #existing_fields #inferred_fields }
404 } else {
405 quote! { #existing_fields, #inferred_fields }
406 };
407 let mut merged_group = Group::new(Delimiter::Parenthesis, merged_fields);
408 merged_group.set_span(group.span());
409 quote! { fields #merged_group }
410 } else {
411 arg
412 }
413 })
414 .collect::<Vec<_>>();
415
416 if merged_existing_fields {
417 Ok(quote! { #(#args),* })
418 } else {
419 Ok(quote! { #(#args,)* fields(#inferred_fields) })
420 }
421}
422
423fn reject_skip_directives(args: &[TokenStream2]) -> Result<()> {
424 for arg in args {
425 let Some(TokenTree::Ident(ident)) = arg.clone().into_iter().next() else {
426 continue;
427 };
428 if ident == "skip" || ident == "skip_all" {
429 return Err(syn::Error::new_spanned(
430 arg,
431 format!(
432 "`{ident}` is not supported by `miden_instrument`; function arguments are \
433 always skipped, record fields explicitly with `fields(...)`"
434 ),
435 ));
436 }
437 }
438
439 Ok(())
440}
441
442fn rewrite_explicit_fields(attr: TokenStream2) -> Result<TokenStream2> {
443 let args = split_top_level_args(attr)
444 .into_iter()
445 .map(|arg| {
446 if let Some(group) = fields_group(&arg) {
447 let fields = syn::parse2::<InstrumentFields>(group.stream())?;
448 let fields = fields.fields.iter().map(RecordField::instrument_tokens);
449 let mut rewritten = Group::new(Delimiter::Parenthesis, quote! { #(#fields),* });
450 rewritten.set_span(group.span());
451 Ok(quote! { fields #rewritten })
452 } else {
453 Ok(arg)
454 }
455 })
456 .collect::<Result<Vec<_>>>()?;
457
458 Ok(quote! { #(#args),* })
459}
460
461fn reject_formatter(input: ParseStream<'_>) -> Result<()> {
462 let formatter = if input.peek(Token![%]) {
463 Some(input.parse::<Token![%]>()?.span)
464 } else if input.peek(Token![?]) {
465 Some(input.parse::<Token![?]>()?.span)
466 } else {
467 None
468 };
469
470 if let Some(span) = formatter {
471 Err(syn::Error::new(
472 span,
473 "tracing format specifiers are not supported; implement `RecordAttribute` to define \
474 the type's canonical encoding",
475 ))
476 } else {
477 Ok(())
478 }
479}
480
481impl RecordField {
482 fn instrument_tokens(&self) -> TokenStream2 {
483 let path = &self.path;
484 if let Some(value) = &self.value {
485 let value = value.value_tokens(&self.path.name(), self.path.is_count());
486 quote! { #path = #value }
487 } else {
488 quote! { #path }
489 }
490 }
491}
492
493fn split_top_level_args(tokens: TokenStream2) -> Vec<TokenStream2> {
494 let mut args = Vec::new();
495 let mut current = TokenStream2::new();
496
497 for token in tokens {
498 match &token {
499 TokenTree::Punct(punct) if punct.as_char() == ',' => {
500 args.push(current);
501 current = TokenStream2::new();
502 },
503 _ => current.extend([token]),
504 }
505 }
506
507 if !current.is_empty() {
508 args.push(current);
509 }
510
511 args
512}
513
514fn fields_group(arg: &TokenStream2) -> Option<Group> {
515 let mut tokens = arg.clone().into_iter();
516 let Some(TokenTree::Ident(ident)) = tokens.next() else {
517 return None;
518 };
519 if ident != "fields" {
520 return None;
521 }
522
523 let Some(TokenTree::Group(group)) = tokens.next() else {
524 return None;
525 };
526 if group.delimiter() != Delimiter::Parenthesis || tokens.next().is_some() {
527 return None;
528 }
529
530 Some(group)
531}
532
533fn ends_with_comma(tokens: &TokenStream2) -> bool {
534 matches!(
535 tokens.clone().into_iter().last(),
536 Some(TokenTree::Punct(punct)) if punct.as_char() == ','
537 )
538}
539
540#[proc_macro]
547pub fn miden_span_record(input: TokenStream) -> TokenStream {
548 let records = parse_macro_input!(input as RecordFields);
549 let records = records.fields.into_iter().map(|field| {
550 let name = field.path.name();
551 let value = field
552 .value
553 .expect("record fields are parsed with required values")
554 .value_tokens(&name, field.path.is_count());
555
556 quote! {
557 ::tracing::Span::current().record(#name, #value);
558 }
559 });
560
561 quote! {
562 __miden_span_record_must_be_used_within_miden_instrument!();
563 #(#records)*
564 }
565 .into()
566}
567
568fn collect_recorded_fields(function: &ItemFn) -> Vec<FieldPath> {
569 let mut visitor = MacroVisitor::default();
570 visitor.visit_block(&function.block);
571
572 let mut names = BTreeSet::new();
573 visitor.fields.into_iter().filter(|field| names.insert(field.name())).collect()
574}
575
576#[derive(Default)]
577struct MacroVisitor {
578 fields: Vec<FieldPath>,
579}
580
581impl<'ast> Visit<'ast> for MacroVisitor {
582 fn visit_macro(&mut self, mac: &'ast Macro) {
583 if mac
584 .path
585 .segments
586 .last()
587 .is_some_and(|segment| segment.ident == "miden_span_record")
588 {
589 if let Ok(records) = syn::parse2::<RecordFields>(mac.tokens.clone()) {
590 self.fields.extend(records.fields.into_iter().map(|field| field.path));
591 }
592 }
593
594 syn::visit::visit_macro(self, mac);
595 }
596}
597
598type InstrumentFields = Fields<false>;
599type RecordFields = Fields<true>;
600
601struct Fields<const VALUE_REQUIRED: bool> {
602 fields: Punctuated<RecordField, Token![,]>,
603}
604
605impl<const VALUE_REQUIRED: bool> Parse for Fields<VALUE_REQUIRED> {
606 fn parse(input: ParseStream<'_>) -> Result<Self> {
607 Ok(Self {
608 fields: Punctuated::parse_terminated_with(input, |input| {
609 RecordField::parse(input, VALUE_REQUIRED)
610 })?,
611 })
612 }
613}
614
615struct RecordField {
616 path: FieldPath,
617 value: Option<RecordValue>,
618}
619
620impl RecordField {
621 fn parse(input: ParseStream<'_>, value_required: bool) -> Result<Self> {
622 reject_formatter(input)?;
623 let path = input.parse()?;
624 let value = if value_required || input.peek(Token![=]) {
625 input.parse::<Token![=]>()?;
626 reject_formatter(input)?;
627 Some(input.parse()?)
628 } else {
629 None
630 };
631
632 Ok(Self { path, value })
633 }
634}
635
636struct FieldPath {
637 first: Ident,
638 rest: Vec<(Dot, Ident)>,
639}
640
641impl FieldPath {
642 fn name(&self) -> String {
643 std::iter::once(&self.first)
644 .chain(self.rest.iter().map(|(_, ident)| ident))
645 .map(ToString::to_string)
646 .collect::<Vec<_>>()
647 .join(".")
648 }
649
650 fn is_count(&self) -> bool {
651 self.rest.last().is_some_and(|(_, ident)| ident == "count")
652 }
653}
654
655impl Parse for FieldPath {
656 fn parse(input: ParseStream<'_>) -> Result<Self> {
657 let first = input.parse()?;
658 let mut rest = Vec::new();
659
660 while input.peek(Token![.]) {
661 rest.push((input.parse()?, input.parse()?));
662 }
663
664 Ok(Self { first, rest })
665 }
666}
667
668impl ToTokens for FieldPath {
669 fn to_tokens(&self, tokens: &mut TokenStream2) {
670 self.first.to_tokens(tokens);
671 for (dot, ident) in &self.rest {
672 dot.to_tokens(tokens);
673 ident.to_tokens(tokens);
674 }
675 }
676}
677
678struct RecordValue {
679 expr: Expr,
680 nonstandard: bool,
681}
682
683impl RecordValue {
684 fn value_tokens(&self, field_name: &str, is_count: bool) -> TokenStream2 {
685 let expr = &self.expr;
686 let assert_field_name = (!self.nonstandard && !is_count).then(|| {
687 quote! {
688 fn __miden_assert_field_name<T>(_: &T)
689 where
690 T: ::miden_node_utils::tracing::RecordAttribute + ?Sized,
691 {
692 const {
693 assert!(
694 ::miden_node_utils::tracing::field_name_allowed(
695 T::FIELD_NAMES,
696 #field_name,
697 T::PLURALIZE_FIELD_NAMES,
698 ),
699 concat!(
700 "tracing field `",
701 #field_name,
702 "` is not allowed for this attribute type",
703 ),
704 );
705 }
706 }
707
708 __miden_assert_field_name(value);
709 }
710 });
711 let assert_count = is_count.then(|| {
712 quote! {
713 fn __miden_assert_count(_: &usize) {}
714
715 __miden_assert_count(value);
716 }
717 });
718
719 quote! {
720 match &(#expr) {
721 value => {
722 #assert_field_name
723 #assert_count
724 ::miden_node_utils::tracing::record_attribute(value)
725 }
726 }
727 }
728 }
729}
730
731impl Parse for RecordValue {
732 fn parse(input: ParseStream<'_>) -> Result<Self> {
733 let expr = input.parse()?;
734 let attributes = input.call(Attribute::parse_outer)?;
735 let nonstandard = match attributes.as_slice() {
736 [] => false,
737 [attribute] if matches!(&attribute.meta, Meta::Path(path) if path.is_ident("nonstandard")) => {
738 true
739 },
740 [attribute, ..] => {
741 return Err(syn::Error::new_spanned(
742 attribute,
743 "only `#[nonstandard]` is supported after a tracing field value",
744 ));
745 },
746 };
747
748 Ok(Self { expr, nonstandard })
749 }
750}