1use proc_macro::TokenStream;
7use proc_macro_crate::{Error as CrateNameError, FoundCrate, crate_name};
8use quote::quote;
9use syn::{Data, DeriveInput, Fields, parse_macro_input};
10
11const SUPPORTED_UNITS: &[&str] = &["ns", "us", "ms", "s", "bytes"];
14
15const SUPPORTED_KINDS: &[&str] = &["gauge", "counter", "updown-counter"];
18
19const ROLE_ANNOTATION_KEY: &str = "dial9.role";
22
23const SUPPORTED_ROLES: &[&str] = &[
28 "span.start",
29 "span.duration",
30 "span.name",
31 "thread_id",
32 "tokio.task_id",
33 "tokio.worker_id",
34];
35
36#[derive(Default)]
38struct FieldAttrs {
39 timestamp: bool,
41 name: Option<syn::LitStr>,
43 unit: Option<syn::LitStr>,
45 role: Option<syn::LitStr>,
47 kind: Option<syn::LitStr>,
49}
50
51fn parse_field_attrs(field: &syn::Field) -> Result<FieldAttrs, syn::Error> {
54 let mut parsed = FieldAttrs::default();
55 for attr in &field.attrs {
56 if !attr.path().is_ident("traceevent") {
57 continue;
58 }
59 attr.parse_nested_meta(|meta| {
60 if meta.path.is_ident("timestamp") {
61 parsed.timestamp = true;
62 } else if meta.path.is_ident("name") {
63 parsed.name = Some(meta.value()?.parse::<syn::LitStr>()?);
64 } else if meta.path.is_ident("unit") {
65 parsed.unit = Some(meta.value()?.parse::<syn::LitStr>()?);
66 } else if meta.path.is_ident("role") {
67 parsed.role = Some(meta.value()?.parse::<syn::LitStr>()?);
68 } else if meta.path.is_ident("kind") {
69 parsed.kind = Some(meta.value()?.parse::<syn::LitStr>()?);
70 } else {
71 return Err(meta.error(
72 "unrecognized `traceevent` field attribute; expected `timestamp`, \
73 `name = \"...\"`, `unit = \"...\"`, `role = \"...\"` or `kind = \"...\"`",
74 ));
75 }
76 Ok(())
77 })?;
78 }
79 Ok(parsed)
80}
81
82const CANDIDATES: &[(&str, Option<&str>)] = &[
85 ("dial9-trace-format", None),
86 ("dial9", Some("__trace_format")),
87];
88
89fn resolve_crate_path() -> proc_macro2::TokenStream {
95 for (dep, suffix) in CANDIDATES {
96 let found = match crate_name(dep) {
97 Ok(found) => found,
98 Err(CrateNameError::CrateNotFound { .. }) => continue,
99 Err(_) => break,
101 };
102 let root = match found {
103 FoundCrate::Itself => dep.replace('-', "_"),
106 FoundCrate::Name(name) => name,
108 };
109 let root = proc_macro2::Ident::new(&root, proc_macro2::Span::call_site());
110 return match suffix {
111 None => quote!(::#root),
112 Some(module) => {
113 let module = proc_macro2::Ident::new(module, proc_macro2::Span::call_site());
114 quote!(::#root::#module)
115 }
116 };
117 }
118 quote!(::dial9_trace_format)
120}
121
122fn derive_trace_event_impl(input: DeriveInput) -> Result<proc_macro2::TokenStream, syn::Error> {
123 let name = &input.ident;
124
125 if input.generics.type_params().next().is_some()
129 || input.generics.const_params().next().is_some()
130 || input.generics.lifetimes().count() > 1
131 {
132 return Err(syn::Error::new_spanned(
133 &input.generics,
134 "TraceEvent supports at most one lifetime parameter and no type or const parameters",
135 ));
136 }
137 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
138
139 let fields = match &input.data {
140 Data::Struct(data) => match &data.fields {
141 Fields::Named(f) => &f.named,
142 _ => panic!("TraceEvent only supports named fields"),
143 },
144 _ => panic!("TraceEvent can only be derived for structs"),
145 };
146
147 let mut wire_slot = false;
156 let mut name_override: Option<syn::Expr> = None;
157 for attr in &input.attrs {
158 if attr.path().is_ident("traceevent") {
159 attr.parse_nested_meta(|meta| {
162 if meta.path.is_ident("wire_slot") {
163 wire_slot = true;
164 } else if meta.path.is_ident("name") {
165 name_override = Some(meta.value()?.parse::<syn::Expr>()?);
166 } else {
167 return Err(meta.error(
168 "unrecognized `traceevent` attribute; expected `wire_slot` or `name = ...`",
169 ));
170 }
171 Ok(())
172 })?;
173 }
174 }
175 let krate = resolve_crate_path();
176
177 let event_name_expr = match &name_override {
181 Some(expr) => quote! { #expr },
182 None => {
183 let name_str = name.to_string();
184 quote! { #name_str }
185 }
186 };
187
188 let field_attrs = fields
192 .iter()
193 .map(parse_field_attrs)
194 .collect::<Result<Vec<_>, _>>()?;
195
196 let mut timestamp_field_name = None;
198 for (field, attrs) in fields.iter().zip(&field_attrs) {
199 if attrs.timestamp {
200 timestamp_field_name = Some(field.ident.as_ref().unwrap().clone());
201 }
202 }
203
204 let mut field_def_tokens = Vec::new();
205 let mut field_def_names = Vec::new();
206 let mut encode_tokens = Vec::new();
207 let mut annotation_tokens = Vec::new();
208
209 for (field, attrs) in fields.iter().zip(&field_attrs) {
210 let field_name = field.ident.as_ref().unwrap();
211 let ty = &field.ty;
212
213 let unit = attrs.unit.clone();
216
217 if timestamp_field_name.as_ref() == Some(field_name) {
219 if let Some(name) = &attrs.name {
220 return Err(syn::Error::new_spanned(
221 name,
222 "the timestamp field cannot have a wire name: it is encoded in the event \
223 header, not as a schema field",
224 ));
225 }
226 if let Some(unit) = unit {
227 return Err(syn::Error::new_spanned(
228 &unit,
229 "the timestamp field cannot carry a unit annotation: it is encoded in the \
230 event header (always nanoseconds), not as a schema field",
231 ));
232 }
233 if let Some(role) = &attrs.role {
234 return Err(syn::Error::new_spanned(
235 role,
236 "the timestamp field cannot carry a role annotation: it is encoded in the \
237 event header, not as a schema field",
238 ));
239 }
240 if let Some(kind) = &attrs.kind {
241 return Err(syn::Error::new_spanned(
242 kind,
243 "the timestamp field cannot carry a kind annotation: it is encoded in the \
244 event header, not as a schema field",
245 ));
246 }
247 continue;
248 }
249 let field_name_lit = attrs.name.clone().unwrap_or_else(|| {
250 syn::LitStr::new(&field_name.to_string(), proc_macro2::Span::call_site())
251 });
252 let field_name_value = field_name_lit.value();
253 if field_def_names.contains(&field_name_value) {
254 return Err(syn::Error::new_spanned(
255 &field_name_lit,
256 format!("duplicate trace event field name \"{field_name_value}\""),
257 ));
258 }
259 field_def_names.push(field_name_value);
260 if let Some(unit) = unit {
261 if !SUPPORTED_UNITS.contains(&unit.value().as_str()) {
262 return Err(syn::Error::new_spanned(
263 &unit,
264 format!(
265 "unsupported unit \"{}\"; supported units: {}",
266 unit.value(),
267 SUPPORTED_UNITS.join(", ")
268 ),
269 ));
270 }
271 let idx = field_def_tokens.len() as u16;
274 annotation_tokens.push(quote! {
275 #krate::schema::FieldAnnotation::new(#idx, "unit", #unit)
276 });
277 }
278 if let Some(role) = &attrs.role {
279 if !SUPPORTED_ROLES.contains(&role.value().as_str()) {
280 return Err(syn::Error::new_spanned(
281 role,
282 format!(
283 "unsupported role \"{}\"; supported roles: {}",
284 role.value(),
285 SUPPORTED_ROLES.join(", ")
286 ),
287 ));
288 }
289 let idx = field_def_tokens.len() as u16;
290 annotation_tokens.push(quote! {
291 #krate::schema::FieldAnnotation::new(
292 #idx,
293 #ROLE_ANNOTATION_KEY,
294 #role,
295 )
296 });
297 }
298 if let Some(kind) = &attrs.kind {
301 if !SUPPORTED_KINDS.contains(&kind.value().as_str()) {
302 return Err(syn::Error::new_spanned(
303 kind,
304 format!(
305 "unsupported kind \"{}\"; supported kinds: {}",
306 kind.value(),
307 SUPPORTED_KINDS.join(", ")
308 ),
309 ));
310 }
311 let idx = field_def_tokens.len() as u16;
312 annotation_tokens.push(quote! {
313 #krate::schema::FieldAnnotation::new(#idx, "kind", #kind)
314 });
315 }
316
317 field_def_tokens.push(quote! {
318 #krate::schema::FieldDef::new(
319 #field_name_lit,
320 <#ty as #krate::TraceField>::field_type(),
321 )
322 });
323 encode_tokens.push(quote! {
324 <#ty as #krate::TraceField>::encode(&self.#field_name, enc)?;
325 });
326 }
327
328 let timestamp_impl = if let Some(ref ts_field) = timestamp_field_name {
329 quote! {
330 fn timestamp(&self) -> u64 { self.#ts_field }
331 }
332 } else {
333 panic!("TraceEvent requires a field marked with #[traceevent(timestamp)]");
334 };
335
336 let type_slot_impl = if wire_slot {
339 quote! {
340 fn type_slot() -> u16 {
341 static SLOT: ::std::sync::atomic::AtomicU16 =
342 ::std::sync::atomic::AtomicU16::new(0);
343 let cached = SLOT.load(::std::sync::atomic::Ordering::Relaxed);
344 if cached != 0 {
345 return cached;
346 }
347 let new = #krate::__NEXT_TYPE_SLOT
348 .fetch_add(1, ::std::sync::atomic::Ordering::Relaxed);
349 match SLOT.compare_exchange(
350 0,
351 new,
352 ::std::sync::atomic::Ordering::Relaxed,
353 ::std::sync::atomic::Ordering::Relaxed,
354 ) {
355 Ok(_) => new,
356 Err(existing) => existing,
357 }
358 }
359 }
360 } else {
361 quote! {}
362 };
363
364 let schema_entry_impl = if annotation_tokens.is_empty() {
367 quote! {}
368 } else {
369 quote! {
370 fn schema_entry() -> #krate::schema::SchemaEntry {
371 #krate::schema::SchemaEntry::with_annotations(
372 Self::event_name(),
373 Self::field_defs(),
374 vec![#(#annotation_tokens),*],
375 )
376 }
377 }
378 };
379
380 Ok(quote! {
381 impl #impl_generics #krate::TraceEvent for #name #ty_generics #where_clause {
382 fn event_name() -> &'static str { #event_name_expr }
383 #type_slot_impl
384 fn field_defs() -> Vec<#krate::schema::FieldDef> {
385 vec![#(#field_def_tokens),*]
386 }
387 #schema_entry_impl
388 #timestamp_impl
389 fn encode_fields<W: ::std::io::Write>(&self, enc: &mut #krate::EventEncoder<'_, W>) -> ::std::io::Result<()> {
390 #(#encode_tokens)*
391 Ok(())
392 }
393 }
394 })
395}
396
397#[proc_macro_derive(TraceEvent, attributes(traceevent))]
448pub fn derive_trace_event(input: TokenStream) -> TokenStream {
449 let input = parse_macro_input!(input as DeriveInput);
450 match derive_trace_event_impl(input) {
451 Ok(tokens) => tokens.into(),
452 Err(err) => err.to_compile_error().into(),
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459 use insta::assert_snapshot;
460 use quote::quote;
461
462 fn expand_to_string(input: proc_macro2::TokenStream) -> String {
463 let input: DeriveInput = syn::parse2(input).unwrap();
464 let output = derive_trace_event_impl(input).expect("expansion failed");
465 match syn::parse2::<syn::File>(output.clone()) {
466 Ok(file) => prettyplease::unparse(&file),
467 Err(_) => output.to_string(),
468 }
469 }
470
471 fn expand_err(input: proc_macro2::TokenStream) -> syn::Error {
472 let input: DeriveInput = syn::parse2(input).unwrap();
473 derive_trace_event_impl(input).expect_err("expansion should fail")
474 }
475
476 #[test]
477 fn simple_event() {
478 assert_snapshot!(expand_to_string(quote! {
479 struct SimpleEvent {
480 #[traceevent(timestamp)]
481 timestamp_ns: u64,
482 value: u32,
483 }
484 }));
485 }
486
487 #[test]
488 fn empty_event() {
489 assert_snapshot!(expand_to_string(quote! {
490 struct EmptyEvent {
491 #[traceevent(timestamp)]
492 timestamp_ns: u64,
493 }
494 }));
495 }
496
497 #[test]
498 fn all_field_types() {
499 assert_snapshot!(expand_to_string(quote! {
500 struct AllFieldTypes {
501 #[traceevent(timestamp)]
502 timestamp_ns: u64,
503 a_u8: u8,
504 b_u16: u16,
505 c_u32: u32,
506 d_u64: u64,
507 e_i64: i64,
508 f_f64: f64,
509 g_bool: bool,
510 h_string: String,
511 i_bytes: Vec<u8>,
512 j_interned: InternedString,
513 k_frames: StackFrames,
514 l_map: Vec<(String, String)>,
515 }
516 }));
517 }
518
519 #[test]
520 fn doc_comments_copied_to_ref_fields() {
521 assert_snapshot!(expand_to_string(quote! {
522 struct DocEvent {
524 #[traceevent(timestamp)]
525 timestamp_ns: u64,
527 worker_id: u64,
529 local_queue: u8,
531 }
532 }));
533 }
534
535 #[test]
536 fn wire_slot_event() {
537 assert_snapshot!(expand_to_string(quote! {
538 #[traceevent(wire_slot)]
539 struct WireSlotEvent {
540 #[traceevent(timestamp)]
541 timestamp_ns: u64,
542 value: u32,
543 }
544 }));
545 }
546
547 #[test]
548 fn unit_attribute() {
549 assert_snapshot!(expand_to_string(quote! {
550 struct ResourceUsage {
551 #[traceevent(timestamp)]
552 timestamp_ns: u64,
553 #[traceevent(unit = "ns")]
554 user_cpu_ns: u64,
555 minor_faults: u64,
556 #[traceevent(unit = "bytes")]
557 max_rss_bytes: u64,
558 }
559 }));
560 }
561
562 #[test]
563 fn role_attribute() {
564 assert_snapshot!(expand_to_string(quote! {
565 struct SpanEnter {
566 #[traceevent(timestamp)]
567 timestamp_ns: u64,
568 #[traceevent(role = "span.name")]
569 span_name: InternedString,
570 #[traceevent(unit = "ns")]
571 active_ns: u64,
572 }
573 }));
574 }
575
576 #[test]
580 fn name_attribute() {
581 assert_snapshot!(expand_to_string(quote! {
582 #[traceevent(name = concat!("SpanEnter:", file!(), ":", line!()))]
583 struct Renamed {
584 #[traceevent(timestamp)]
585 timestamp_ns: u64,
586 value: u64,
587 }
588 }));
589 }
590
591 #[test]
592 fn field_name_attribute() {
593 let expanded = expand_to_string(quote! {
594 struct TaskEvent {
595 #[traceevent(timestamp)]
596 timestamp_ns: u64,
597 #[traceevent(name = "dial9.tokio.task_id", role = "tokio.task_id")]
598 task_id: Option<u64>,
599 }
600 });
601 let compact: String = expanded.split_whitespace().collect();
602 assert!(
603 compact.contains("FieldDef::new(\"dial9.tokio.task_id\","),
604 "wire field override missing from expansion:\n{expanded}"
605 );
606 assert!(
607 compact.contains("TraceField>::encode(&self.task_id,enc)?"),
608 "encoding must still read the Rust field:\n{expanded}"
609 );
610 }
611
612 #[test]
613 fn duplicate_field_name_rejected() {
614 let err = expand_err(quote! {
615 struct DuplicateName {
616 #[traceevent(timestamp)]
617 timestamp_ns: u64,
618 #[traceevent(name = "value")]
619 first: u64,
620 value: u64,
621 }
622 });
623 assert_eq!(
624 err.to_string(),
625 "duplicate trace event field name \"value\""
626 );
627 }
628
629 #[test]
630 fn field_name_on_timestamp_rejected() {
631 let err = expand_err(quote! {
632 struct NamedTimestamp {
633 #[traceevent(timestamp, name = "timestamp")]
634 timestamp_ns: u64,
635 }
636 });
637 assert_eq!(
638 err.to_string(),
639 "the timestamp field cannot have a wire name: it is encoded in the event header, \
640 not as a schema field"
641 );
642 }
643
644 #[test]
645 fn kind_attribute() {
646 assert_snapshot!(expand_to_string(quote! {
647 struct Metrics {
648 #[traceevent(timestamp)]
649 timestamp_ns: u64,
650 #[traceevent(unit = "ns", kind = "counter")]
651 cpu_time_ns: u64,
652 #[traceevent(kind = "gauge")]
653 queue_depth: u64,
654 #[traceevent(kind = "updown-counter")]
655 active_requests: i64,
656 }
657 }));
658 }
659
660 #[test]
661 fn invalid_kind_rejected() {
662 let err = expand_err(quote! {
663 struct BadKind {
664 #[traceevent(timestamp)]
665 timestamp_ns: u64,
666 #[traceevent(kind = "histogram")]
667 value: u64,
668 }
669 });
670 assert_eq!(
671 err.to_string(),
672 "unsupported kind \"histogram\"; supported kinds: gauge, counter, updown-counter"
673 );
674 }
675
676 #[test]
677 fn invalid_role_rejected() {
678 let err = expand_err(quote! {
679 struct BadRole {
680 #[traceevent(timestamp)]
681 timestamp_ns: u64,
682 #[traceevent(role = "span.naem")]
683 span_name: InternedString,
684 }
685 });
686 assert_eq!(
687 err.to_string(),
688 "unsupported role \"span.naem\"; supported roles: span.start, span.duration, \
689 span.name, thread_id, tokio.task_id, tokio.worker_id"
690 );
691 }
692
693 #[test]
694 fn kind_on_timestamp_rejected() {
695 let err = expand_err(quote! {
696 struct TimestampKind {
697 #[traceevent(timestamp)]
698 #[traceevent(kind = "counter")]
699 timestamp_ns: u64,
700 value: u64,
701 }
702 });
703 assert_eq!(
704 err.to_string(),
705 "the timestamp field cannot carry a kind annotation: it is encoded in the \
706 event header, not as a schema field"
707 );
708 }
709
710 #[test]
711 fn invalid_unit_rejected() {
712 let err = expand_err(quote! {
713 struct BadUnit {
714 #[traceevent(timestamp)]
715 timestamp_ns: u64,
716 #[traceevent(unit = "nss")]
717 value: u64,
718 }
719 });
720 assert_eq!(
721 err.to_string(),
722 "unsupported unit \"nss\"; supported units: ns, us, ms, s, bytes"
723 );
724 }
725
726 #[test]
727 fn unit_on_timestamp_rejected() {
728 let err = expand_err(quote! {
729 struct TimestampUnit {
730 #[traceevent(timestamp)]
731 #[traceevent(unit = "ns")]
732 timestamp_ns: u64,
733 value: u64,
734 }
735 });
736 assert_eq!(
737 err.to_string(),
738 "the timestamp field cannot carry a unit annotation: it is encoded in the \
739 event header (always nanoseconds), not as a schema field"
740 );
741 }
742
743 #[test]
744 fn mu_char_unit_rejected() {
745 let err = expand_err(quote! {
746 struct MuUnit {
747 #[traceevent(timestamp)]
748 timestamp_ns: u64,
749 #[traceevent(unit = "µs")]
750 latency: u64,
751 }
752 });
753 assert!(err.to_string().contains("unsupported unit \"µs\""));
754 }
755
756 #[test]
757 fn malformed_name_rejected() {
758 let err = expand_err(quote! {
759 #[traceevent(name)]
760 struct MalformedName {
761 #[traceevent(timestamp)]
762 timestamp_ns: u64,
763 }
764 });
765 assert!(
766 err.to_string().contains("expected `=`"),
767 "unexpected error: {err}"
768 );
769 }
770
771 #[test]
772 fn borrowed_str_event() {
773 assert_snapshot!(expand_to_string(quote! {
774 struct BorrowedStr<'a> {
775 #[traceevent(timestamp)]
776 timestamp_ns: u64,
777 path: &'a str,
778 }
779 }));
780 }
781
782 #[test]
783 fn borrowed_bytes_event() {
784 assert_snapshot!(expand_to_string(quote! {
785 struct BorrowedBytes<'a> {
786 #[traceevent(timestamp)]
787 timestamp_ns: u64,
788 body: &'a [u8],
789 }
790 }));
791 }
792
793 #[test]
794 fn mixed_owned_and_borrowed() {
795 assert_snapshot!(expand_to_string(quote! {
796 struct Mixed<'a> {
797 #[traceevent(timestamp)]
798 timestamp_ns: u64,
799 owned: String,
800 borrowed: &'a str,
801 count: u32,
802 }
803 }));
804 }
805
806 #[test]
807 fn wire_slot_with_lifetime() {
808 assert_snapshot!(expand_to_string(quote! {
809 #[traceevent(wire_slot)]
810 struct WireSlotBorrowed<'a> {
811 #[traceevent(timestamp)]
812 timestamp_ns: u64,
813 data: &'a str,
814 }
815 }));
816 }
817
818 #[test]
819 fn two_lifetimes_rejected() {
820 let err = expand_err(quote! {
821 struct TwoLifetimes<'a, 'b> {
822 #[traceevent(timestamp)]
823 timestamp_ns: u64,
824 a: &'a str,
825 b: &'b str,
826 }
827 });
828 assert!(
829 err.to_string().contains("at most one lifetime"),
830 "unexpected error: {err}"
831 );
832 }
833
834 #[test]
835 fn type_param_rejected() {
836 let err = expand_err(quote! {
837 struct Generic<T> {
838 #[traceevent(timestamp)]
839 timestamp_ns: u64,
840 value: T,
841 }
842 });
843 assert!(
844 err.to_string().contains("no type or const parameters"),
845 "unexpected error: {err}"
846 );
847 }
848
849 #[test]
850 fn unknown_struct_attribute_rejected() {
851 let err = expand_err(quote! {
852 #[traceevent(wire_slots)]
853 struct Typo {
854 #[traceevent(timestamp)]
855 timestamp_ns: u64,
856 }
857 });
858 assert_eq!(
859 err.to_string(),
860 "unrecognized `traceevent` attribute; expected `wire_slot` or `name = ...`"
861 );
862 }
863
864 #[test]
865 fn unknown_field_attribute_rejected() {
866 let err = expand_err(quote! {
867 struct Typo {
868 #[traceevent(timestamp)]
869 timestamp_ns: u64,
870 #[traceevent(units = "ns")]
871 value: u64,
872 }
873 });
874 assert_eq!(
875 err.to_string(),
876 "unrecognized `traceevent` field attribute; expected `timestamp`, `name = \"...\"`, \
877 `unit = \"...\"`, `role = \"...\"` or `kind = \"...\"`"
878 );
879 }
880
881 #[test]
882 fn timestamp_attribute() {
883 assert_snapshot!(expand_to_string(quote! {
884 struct PollStart {
885 #[traceevent(timestamp)]
886 timestamp_ns: u64,
887 worker_id: u64,
888 task_id: u64,
889 }
890 }));
891 }
892}