Skip to main content

dial9_trace_format_derive/
lib.rs

1//! Derive macro for `dial9_trace_format::TraceEvent`.
2//!
3//! See [`derive_trace_event`] for the supported `#[traceevent(...)]`
4//! attributes.
5
6use 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
11/// Unit values accepted by `#[traceevent(unit = "...")]`. Must stay in sync
12/// with the viewer's `formatFieldValue` (dial9-viewer/ui/format.js).
13const SUPPORTED_UNITS: &[&str] = &["ns", "us", "ms", "s", "bytes"];
14
15/// Metric interpretations accepted by `#[traceevent(kind = "...")]`. Must stay
16/// in sync with the viewer's `FieldChartKind`.
17const SUPPORTED_KINDS: &[&str] = &["gauge", "counter", "updown-counter"];
18
19/// Annotation key for `#[traceevent(role = "...")]`. Mirrors
20/// `dial9_core::schema_extensions::ROLE_KEY`, which this crate cannot depend on.
21const ROLE_ANNOTATION_KEY: &str = "dial9.role";
22
23/// Structural roles accepted by `#[traceevent(role = "...")]`. Mirrors the
24/// vocabulary in `dial9_core::schema_extensions::roles`, which this crate cannot
25/// depend on. An unrecognized role would silently decode as no role (turning a
26/// span schema into `NotSpan`), so a typo is rejected at compile time.
27const 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/// The `#[traceevent(...)]` keys a field may carry.
37#[derive(Default)]
38struct FieldAttrs {
39    /// `timestamp`: this field is the event timestamp (header, not a column).
40    timestamp: bool,
41    /// `name = "..."`: override this field's wire-schema name.
42    name: Option<syn::LitStr>,
43    /// `unit = "..."`: rendering unit for this field.
44    unit: Option<syn::LitStr>,
45    /// `role = "..."`: structural role for this field (`dial9.role`).
46    role: Option<syn::LitStr>,
47    /// `kind = "..."`: metric interpretation for this field.
48    kind: Option<syn::LitStr>,
49}
50
51/// Parse one field's `#[traceevent(...)]` keys. Malformed or unknown keys are
52/// compile errors rather than being silently ignored.
53fn 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
82/// Crates that can supply `dial9-trace-format`, and where it sits inside each.
83/// Checked in order, so the most direct dependency wins.
84const CANDIDATES: &[(&str, Option<&str>)] = &[
85    ("dial9-trace-format", None),
86    ("dial9", Some("__trace_format")),
87];
88
89/// Where the expansion should look for `dial9-trace-format`.
90///
91/// Since a bare `::dial9_trace_format` resolves against the caller's dependencies,
92/// it does not exist for anyone reaching the derive through a re-export.
93/// Instead, we read the caller's manifest to find the path.
94fn 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            // Every lookup reads the same manifest, so retrying is pointless.
100            Err(_) => break,
101        };
102        let root = match found {
103            // Compiling dial9-trace-format's own lib or examples,
104            // `extern crate self` in its lib makes this resolve.
105            FoundCrate::Itself => dep.replace('-', "_"),
106            // The name the caller declared it under, renames included.
107            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    // Fall back to the direct dependency
119    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    // Support borrowed event structs like `Event<'a> { data: &'a str }`. We
126    // allow at most one lifetime and no type/const parameters; generic event
127    // schemas are not currently supported.
128    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    // Parse struct-level attributes:
148    // - `wire_slot`: opt this type into the encoder's inline fast path (a global
149    //   slot doubling as wire id). Off by default.
150    // - `name = <expr>`: override the wire event name (defaults to the struct
151    //   name). Accepts any `&'static str` expression, not just a string literal,
152    //   so callers can build a per-call-site-unique name, e.g.
153    //   `concat!("SpanEnter:", file!(), ":", line!())`. Used to give generated
154    //   structs a name the viewer recognizes (e.g. `"SpanEnter:..."`).
155    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            // Propagated, not swallowed: a malformed attribute (e.g. `name`
160            // without a value) must be a compile error, not silently ignored.
161            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    // The wire event name expression returned by `event_name()`: either the
178    // `name = ...` override (evaluated at the override's call site, so builtins
179    // like `file!()`/`line!()` resolve there) or the struct name as a literal.
180    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    // Every key of a field's `#[traceevent(...)]` is parsed in one pass: the
189    // callback must consume each key's value, so a pass that recognized only
190    // some keys would choke on the ones it skipped.
191    let field_attrs = fields
192        .iter()
193        .map(parse_field_attrs)
194        .collect::<Result<Vec<_>, _>>()?;
195
196    // Find the field marked with #[traceevent(timestamp)]
197    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        // `unit = "..."` is emitted as a "unit" schema annotation so viewers can
214        // render the field in that unit.
215        let unit = attrs.unit.clone();
216
217        // Skip the timestamp field in schema/encode — it's in the event header
218        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            // field_index matches the position in field_defs(), which
272            // excludes the timestamp field.
273            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        // `kind = "..."` is emitted as a "kind" schema annotation telling the
299        // viewer how to chart the field (gauge / counter / updown-counter).
300        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    // `#[traceevent(wire_slot)]` types override `type_slot()`. Without it
337    // the trait default returns 0 and the encoder uses the dynamic path.
338    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    // Only override the trait-default schema_entry() when a field carries an
365    // annotation; the default builds the same entry with no annotations.
366    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/// Derives `dial9_trace_format::TraceEvent` for a struct with named fields.
398///
399/// Supported attributes:
400///
401/// - `#[traceevent(timestamp)]` (field, required on exactly one `u64` field):
402///   marks the event timestamp. It is encoded as a packed delta in the event
403///   header, not as a regular field.
404/// - `#[traceevent(wire_slot)]` (struct): opts the type into the encoder's
405///   inline fast path by claiming a static wire-ID slot.
406/// - `#[traceevent(name = <expr>)]` (struct): overrides the wire event name
407///   (defaults to the struct name). Accepts any `&'static str` expression, not
408///   just a string literal, so callers can build a per-call-site-unique name —
409///   e.g. `concat!("SpanEnter:", file!(), ":", line!())`. Useful for generated
410///   structs that need a name the viewer recognizes (e.g. `"SpanEnter:..."`),
411///   which cannot be a valid Rust identifier.
412/// - `#[traceevent(name = "...")]` (field): overrides the field's wire-schema
413///   name. This is useful for canonical names that are not valid Rust
414///   identifiers, such as `"dial9.tokio.task_id"`.
415/// - `#[traceevent(unit = "...")]` (field): attaches a `unit` schema
416///   annotation so viewers render the field in that unit. Supported values:
417///   `"ns"`, `"us"`, `"ms"`, `"s"`, `"bytes"`. Any other value is a compile
418///   error, as is placing `unit` on the timestamp field (the timestamp is
419///   encoded in the event header and is always nanoseconds).
420///
421/// - `#[traceevent(role = "...")]` (field): attaches a `dial9.role` schema
422///   annotation, telling consumers what the field *is* structurally (e.g.
423///   `"span.name"`). The vocabulary lives in
424///   `dial9_core::schema_extensions::roles`; an unrecognized role is a compile
425///   error (it would otherwise decode as no role).
426/// - `#[traceevent(kind = "...")]` (field): attaches a `kind` schema annotation
427///   telling the viewer how to chart the field. Supported values: `"gauge"`,
428///   `"counter"`, `"updown-counter"`. Any other value is a compile error, as is
429///   placing `kind` on the timestamp field.
430///
431/// A malformed or unrecognized `traceevent` key is a compile error. Only structs
432/// with named fields and at most one lifetime parameter are supported; type and
433/// const parameters are rejected.
434///
435/// # Example
436///
437/// ```ignore
438/// #[derive(TraceEvent)]
439/// struct RequestCompleted {
440///     #[traceevent(timestamp)]
441///     timestamp_ns: u64,
442///     #[traceevent(unit = "us")]
443///     latency_us: u64,
444///     status_code: u32,
445/// }
446/// ```
447#[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            /// Root documentation
523            struct DocEvent {
524                #[traceevent(timestamp)]
525                /// Event timestamp in nanoseconds.
526                timestamp_ns: u64,
527                /// The worker thread ID.
528                worker_id: u64,
529                /// Number of items in the local queue.
530                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    /// `name = <expr>` overrides `event_name()` with the given expression
577    /// (evaluated at the caller's site), so a generated struct can build a
578    /// per-call-site-unique name via `file!()`/`line!()`.
579    #[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}