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