diff --git a/Cargo.lock b/Cargo.lock index d9f5511d9..26ff9c94b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2657,6 +2657,7 @@ dependencies = [ name = "quent-events" version = "0.1.0" dependencies = [ + "quent-dynamic-attributes", "quent-time", "serde", "uuid", diff --git a/Cargo.toml b/Cargo.toml index d447dd3c8..b2e91672c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -135,6 +135,7 @@ petgraph = "0.8.3" postcard = { version = "1", default-features = false, features = ["alloc"] } prost = "0.14.1" pyo3 = "0.29" +quent-dynamic-attributes = { path = "crates/dynamic-attributes" } quent-resource = { path = "crates/resource" } rmp-serde = "1" rustc-hash = "2" diff --git a/crates/events/Cargo.toml b/crates/events/Cargo.toml index 6c8e4d0ca..b5aee3195 100644 --- a/crates/events/Cargo.toml +++ b/crates/events/Cargo.toml @@ -6,9 +6,10 @@ publish.workspace = true [features] default = [] -serde = ["dep:serde"] +serde = ["dep:serde", "quent-dynamic-attributes/serde", "uuid/serde"] [dependencies] +quent-dynamic-attributes.workspace = true quent-time = { path = "../time" } -uuid = { workspace = true, features = ["serde"] } +uuid.workspace = true serde = { workspace = true, optional = true } diff --git a/crates/instrumentation/src/entity_ref.rs b/crates/events/src/entity_ref.rs similarity index 100% rename from crates/instrumentation/src/entity_ref.rs rename to crates/events/src/entity_ref.rs diff --git a/crates/events/src/lib.rs b/crates/events/src/lib.rs index ff4964493..aaedd4225 100644 --- a/crates/events/src/lib.rs +++ b/crates/events/src/lib.rs @@ -3,10 +3,15 @@ //! Type definitions of entity events. +mod entity_ref; + use quent_time::{TimeUnixNanoSec, Timestamp, timestamp}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use uuid::Uuid; + +pub use entity_ref::{AnyEntity, EntityRef}; +pub use quent_dynamic_attributes::DynamicAttributes; +pub use uuid::Uuid; /// Trait for the event type of an entity. pub trait EntityEvent { @@ -14,6 +19,12 @@ pub trait EntityEvent { const NAME: &'static str; } +/// Associates an entity marker with the events emitted for that entity. +pub trait Entity: Sized { + /// Events emitted for this entity. + type Event: EntityEvent; +} + #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] #[derive(Debug)] pub struct Event { diff --git a/crates/instrumentation-build/example/Cargo.lock b/crates/instrumentation-build/example/Cargo.lock index 4579e3c4d..d9cbfc749 100644 --- a/crates/instrumentation-build/example/Cargo.lock +++ b/crates/instrumentation-build/example/Cargo.lock @@ -352,6 +352,7 @@ dependencies = [ name = "quent-events" version = "0.1.0" dependencies = [ + "quent-dynamic-attributes", "quent-time", "uuid", ] diff --git a/crates/instrumentation-build/example/build.rs b/crates/instrumentation-build/example/build.rs index c5700ebe1..8495b3a3c 100644 --- a/crates/instrumentation-build/example/build.rs +++ b/crates/instrumentation-build/example/build.rs @@ -21,8 +21,6 @@ fn main() -> Result<(), Box> { // Schema -> generated Rust instrumentation source. let opts = Options { - event_derives: &["Debug"], - record_derives: &["Debug"], // To just print the events in this example, we'll be using the callback // exporter. This exporter takes a type-erased event, so in order to // simplify downcasting back to a statically-typed event, this features diff --git a/crates/instrumentation-build/src/any_event.rs b/crates/instrumentation-build/src/any_event.rs index 5641e80ee..54b800645 100644 --- a/crates/instrumentation-build/src/any_event.rs +++ b/crates/instrumentation-build/src/any_event.rs @@ -15,8 +15,7 @@ use crate::common::{ use crate::namespace::Namespace; use crate::{GenerateError, Options}; -/// Generate `AnyEvent` and its `from_any` decoder, carrying the event enums' -/// derives ([`Options::event_derives`]). +/// Generate `AnyEvent` and its `from_any` decoder. /// /// # Errors /// @@ -51,16 +50,17 @@ pub(crate) fn generate_any_event( return Ok(quote! {}); } - let derives = derive_attr(opts.event_derives)?; + let derives = derive_attr(opts.event_derives, opts.debug, opts.serde, false)?; + let runtime = opts.event_runtime(); let decls = variants.iter().map(|(variant, event)| { - quote! { #variant(&'a ::quent_instrumentation::Event<#event>) } + quote! { #variant(&'a #runtime::Event<#event>) } }); let child_decls = children.iter().map(|(variant, module)| { quote! { #variant(#module::AnyEvent<'a>) } }); let direct_arms = variants.iter().map(|(variant, event)| { quote! { - if let Some(event) = any.downcast_ref::<::quent_instrumentation::Event<#event>>() { + if let Some(event) = any.downcast_ref::<#runtime::Event<#event>>() { return Some(Self::#variant(event)); } } @@ -103,10 +103,7 @@ mod tests { .with_entity(entity("Server", [event("booted", [])])) .build() .unwrap(); - let opts = Options { - event_derives: &["Debug"], - ..Options::default() - }; + let opts = Options::default(); let namespaces = Namespace::root(&schema); let expected = quote! { #[derive(Debug)] diff --git a/crates/instrumentation-build/src/common.rs b/crates/instrumentation-build/src/common.rs index 4f9930ec9..b0cbd6332 100644 --- a/crates/instrumentation-build/src/common.rs +++ b/crates/instrumentation-build/src/common.rs @@ -6,29 +6,84 @@ use convert_case::{Boundary, Case, Casing}; use proc_macro2::{Span, TokenStream}; use quent_schema::{Identifier, Path}; -use quote::quote; +use quote::{ToTokens, quote}; +use std::collections::HashSet; use syn::Ident; use crate::GenerateError; -/// Build a `#[derive(..)]` attribute from `derives`. -pub(crate) fn derive_attr(derives: &[&str]) -> Result { - if derives.is_empty() { - return Ok(quote! {}); +/// Build a deduplicated `#[derive(..)]` attribute. +pub(crate) fn derive_attr( + derives: &[&str], + debug: bool, + serialize: bool, + deserialize: bool, +) -> Result { + let mut paths = Vec::new(); + if debug { + paths.push(syn::parse_quote!(Debug)); } - let paths = derives - .iter() - .copied() - .map(|d| { - syn::parse_str::(d).map_err(|source| GenerateError::InvalidDerive { - derive: d.to_owned(), - source, + if serialize { + paths.push(syn::parse_quote!(::serde::Serialize)); + } + if deserialize { + paths.push(syn::parse_quote!(::serde::Deserialize)); + } + paths.extend( + derives + .iter() + .copied() + .map(|derive| { + syn::parse_str::(derive).map_err(|source| GenerateError::InvalidDerive { + derive: derive.to_owned(), + source, + }) }) - }) - .collect::, _>>()?; + .collect::, _>>()?, + ); + for path in &mut paths { + canonicalize_known_derive_path(path); + } + let mut seen = HashSet::new(); + paths.retain(|path| seen.insert(path.to_token_stream().to_string())); + if paths.is_empty() { + return Ok(quote! {}); + } Ok(quote! { #[derive(#(#paths),*)] }) } +fn canonicalize_known_derive_path(path: &mut syn::Path) { + canonicalize_external_derive_path(path, "serde", &["Serialize", "Deserialize"]); + if path_has_segments(path, &["std", "fmt", "Debug"]) + || path_has_segments(path, &["core", "fmt", "Debug"]) + { + *path = syn::parse_quote!(Debug); + } +} + +fn path_has_segments(path: &syn::Path, names: &[&str]) -> bool { + path.segments.len() == names.len() + && path.segments.iter().zip(names).all(|(segment, name)| { + segment.ident == *name && matches!(segment.arguments, syn::PathArguments::None) + }) +} + +fn canonicalize_external_derive_path( + path: &mut syn::Path, + crate_name: &str, + derive_names: &[&str], +) { + if path.leading_colon.is_none() + && path.segments.len() == 2 + && path.segments[0].ident == crate_name + && derive_names + .iter() + .any(|derive| path.segments[1].ident == derive) + { + path.leading_colon = Some(Default::default()); + } +} + /// Build a `#[doc = ..]` attribute from `docs`. pub(crate) fn doc_attr(docs: Option<&str>) -> TokenStream { match docs { diff --git a/crates/instrumentation-build/src/data_type.rs b/crates/instrumentation-build/src/data_type.rs index ff3163446..4de78ab6a 100644 --- a/crates/instrumentation-build/src/data_type.rs +++ b/crates/instrumentation-build/src/data_type.rs @@ -9,31 +9,31 @@ use quent_schema::{Annotations, DataType}; use quote::quote; use crate::common::{relative_root_type, relative_type_path}; +use crate::{GenerateError, Options}; /// Maximum nesting depth of `Option`/`List`/`EntityRef` wrappers a single field /// type may have, far above any realistic schema. Self-referential records are -/// already ruled out by base validation, but even if somehow schemas are -/// produced with great nesting depth, this will produce a friendlier panic -/// instead of a stack overflow. +/// already ruled out by base validation. pub(crate) const MAX_TYPE_DEPTH: usize = 64; /// Map a [`DataType`] to its Rust type tokens. -/// -/// # Panics -/// -/// Panics if `ty` nests deeper than [`MAX_TYPE_DEPTH`]. pub(crate) fn map_data_type( ty: &DataType, depth: usize, source_namespace: &[quent_schema::Identifier], -) -> TokenStream { - assert!( - depth <= MAX_TYPE_DEPTH, - "field type nesting exceeds the maximum depth of {MAX_TYPE_DEPTH}" - ); - match ty { + opts: &Options, +) -> Result { + if depth > MAX_TYPE_DEPTH { + return Err(GenerateError::TypeNestingTooDeep { + max: MAX_TYPE_DEPTH, + }); + } + Ok(match ty { DataType::Bool => quote! { bool }, - DataType::Uuid => quote! { ::quent_instrumentation::Uuid }, + DataType::Uuid => { + let runtime = opts.event_runtime(); + quote! { #runtime::Uuid } + } DataType::String => quote! { String }, DataType::U8 => quote! { u8 }, DataType::U16 => quote! { u16 }, @@ -46,26 +46,30 @@ pub(crate) fn map_data_type( DataType::F32 => quote! { f32 }, DataType::F64 => quote! { f64 }, DataType::Option(inner) => { - let inner = map_data_type(inner, depth + 1, source_namespace); + let inner = map_data_type(inner, depth + 1, source_namespace, opts)?; quote! { Option<#inner> } } DataType::List(inner) => { - let inner = map_data_type(inner, depth + 1, source_namespace); + let inner = map_data_type(inner, depth + 1, source_namespace, opts)?; quote! { Vec<#inner> } } DataType::Record(path) => relative_type_path(path, source_namespace, ""), - DataType::DynamicRecord => quote! { ::quent_instrumentation::DynamicAttributes }, + DataType::DynamicRecord => { + let runtime = opts.event_runtime(); + quote! { #runtime::DynamicAttributes } + } DataType::EntityRef { data, annotations } => { let target = ref_target_marker(annotations, source_namespace); + let runtime = opts.event_runtime(); match data { Some(inner) => { - let inner = map_data_type(inner, depth + 1, source_namespace); - quote! { ::quent_instrumentation::EntityRef<#target, #inner> } + let inner = map_data_type(inner, depth + 1, source_namespace, opts)?; + quote! { #runtime::EntityRef<#target, #inner> } } - None => quote! { ::quent_instrumentation::EntityRef<#target> }, + None => quote! { #runtime::EntityRef<#target> }, } } - } + }) } /// The target-entity marker type for an entity reference, taken from its @@ -89,13 +93,17 @@ mod tests { use quent_schema::DataType; #[test] - #[should_panic(expected = "maximum depth")] - fn excessive_type_nesting_panics() { + fn excessive_type_nesting_returns_error() { let mut ty = DataType::U8; for _ in 0..(MAX_TYPE_DEPTH + 5) { ty = DataType::Option(Box::new(ty)); } - let _ = map_data_type(&ty, 0, &[]); + assert!(matches!( + map_data_type(&ty, 0, &[], &Options::default()), + Err(GenerateError::TypeNestingTooDeep { + max: MAX_TYPE_DEPTH + }) + )); } #[test] @@ -108,7 +116,9 @@ mod tests { data: Some(Box::new(DataType::U64)), annotations: annotations.build().unwrap(), }; - let tokens = map_data_type(&ty, 0, &[]).to_string(); + let tokens = map_data_type(&ty, 0, &[], &Options::default()) + .unwrap() + .to_string(); assert!(tokens.contains("EntityRef < Cluster , u64 >"), "{tokens}"); } } diff --git a/crates/instrumentation-build/src/events.rs b/crates/instrumentation-build/src/events.rs index 4eded0a48..1a9d346f9 100644 --- a/crates/instrumentation-build/src/events.rs +++ b/crates/instrumentation-build/src/events.rs @@ -12,6 +12,42 @@ use crate::common::{derive_attr, doc_attr, doc_attr_or, path_name_pascal, raw_id use crate::data_type::map_data_type; use crate::{GenerateError, Options}; +/// Re-export the runtime types used by event-only generated source. +pub(crate) fn reexports() -> TokenStream { + quote! { + pub use ::quent_events::{ + AnyEntity, DynamicAttributes, EntityRef, Event, Uuid, + }; + } +} + +/// Generate the schema entity marker and its event metadata implementation. +pub(crate) fn entity_types(entity: &Entity, opts: &Options) -> TokenStream { + let marker = raw_ident(path_name_pascal(entity.path())); + let event = raw_ident(format!("{}Event", path_name_pascal(entity.path()))); + let marker_doc = format!("Marker type for the `{}` entity.", entity.path()); + let stream_name = entity.path().to_string(); + let runtime = opts.event_runtime(); + let events_runtime = if opts.instrumentation { + quote! { ::quent_instrumentation::events } + } else { + quote! { ::quent_events } + }; + quote! { + #[doc = #marker_doc] + #[derive(Debug, Clone, Copy)] + pub struct #marker; + + impl #runtime::EntityEvent for #event { + const NAME: &'static str = #stream_name; + } + + impl #events_runtime::Entity for #marker { + type Event = #event; + } + } +} + pub(crate) fn entity_event_enum( entity: &Entity, opts: &Options, @@ -22,8 +58,8 @@ pub(crate) fn entity_event_enum( entity.annotations().docs(), &format!("Events emitted by `{}` entities.", entity.path()), ); - let derives = derive_attr(opts.event_derives)?; - let variants: Vec = entity + let derives = derive_attr(opts.event_derives, opts.debug, opts.serde, opts.serde)?; + let variants = entity .events() .map(|event| { let variant = raw_ident(to_case(event.name(), Case::Pascal)); @@ -31,22 +67,22 @@ pub(crate) fn entity_event_enum( event.annotations().docs(), &format!("The `{}` event.", event.name()), ); - let fields: Vec = event + let fields = event .fields() .map(|field| { let name = raw_ident(to_case(field.name(), Case::Snake)); - let ty = map_data_type(field.ty(), 0, entity.path().namespace()); + let ty = map_data_type(field.ty(), 0, entity.path().namespace(), opts)?; let field_docs = doc_attr(field.annotations().docs()); - quote! { #field_docs #name: #ty } + Ok::<_, GenerateError>(quote! { #field_docs #name: #ty }) }) - .collect(); + .collect::, _>>()?; if fields.is_empty() { - quote! { #variant_docs #variant } + Ok(quote! { #variant_docs #variant }) } else { - quote! { #variant_docs #variant { #(#fields),* } } + Ok(quote! { #variant_docs #variant { #(#fields),* } }) } }) - .collect(); + .collect::, GenerateError>>()?; Ok(quote! { #docs #derives @@ -65,7 +101,11 @@ mod tests { use quent_schema::{Annotations, Cardinality, DataType, Field}; fn event_src(entity: &Entity) -> String { - pretty(entity_event_enum(entity, &Options::default()).unwrap()) + let opts = Options { + debug: false, + ..Options::default() + }; + pretty(entity_event_enum(entity, &opts).unwrap()) } #[test] diff --git a/crates/instrumentation-build/src/lib.rs b/crates/instrumentation-build/src/lib.rs index a00514c33..c421684ef 100644 --- a/crates/instrumentation-build/src/lib.rs +++ b/crates/instrumentation-build/src/lib.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! Generates a Rust instrumentation library source from a +//! Generates Rust event types and an optional instrumentation surface from a //! [`quent_schema::Schema`]. //! //! The usual workflow is build-time generation: @@ -20,13 +20,7 @@ //! use quent_instrumentation_build::{Options, generate}; //! //! let schema = todo!(); -//! let opts = Options { -//! // Exporters serialize events, so a `Serialize` derive is required. -//! event_derives: &["Debug", "::serde::Serialize"], -//! record_derives: &["Debug", "::serde::Serialize"], -//! out_dir: std::env::var("OUT_DIR")?.into(), -//! file_name: None, // defaults to `.rs` -//! }; +//! let opts = Options::default(); //! generate(&schema, &opts)?; //! ``` //! @@ -40,16 +34,15 @@ //! //! # Restrictions //! -//! The schema does not limit how many events an entity declares, but this -//! generator caps once-cardinality +//! The schema does not limit how many events an entity declares, but the +//! instrumentation surface caps once-cardinality //! ([`Cardinality::Once`](quent_schema::Cardinality::Once)) events at 64 per //! entity; beyond that, generation fails with //! [`GenerateError::TooManyOnceEvents`]. //! -//! Building an exporter requires the event type to be `Serialize`, so -//! [`Options::event_derives`] (and [`Options::record_derives`], for events -//! carrying records or entity refs) must include a `Serialize`-providing -//! derive; otherwise the generated code will not compile. +//! Serde derives are opt-in through [`Options::serde`]. The generated crate +//! must also depend on `serde` with its derive feature and enable the matching +//! runtime crate's `serde` feature. mod any_event; mod common; @@ -65,21 +58,29 @@ use quent_constraints::{BaseConstraintsError, Report, validate}; use quent_schema::{Path, Schema}; use quote::quote; -/// Options controlling instrumentation library generation. +/// Options controlling event and instrumentation source generation. pub struct Options { + /// Add handles, observers, a context, and model integration to the event + /// types. + pub instrumentation: bool, + + /// Derive [`Debug`](std::fmt::Debug) on generated event and record types. + pub debug: bool, + + /// Derive `serde::Serialize` and `serde::Deserialize` on generated event + /// and record types. + /// + /// `AnyEvent` is borrowed, so it derives only `serde::Serialize`. + pub serde: bool, + /// Derives applied to every generated event payload enum. /// - /// Must include a `Serialize`-providing derive (e.g. `"::serde::Serialize"`): - /// the generated context builds exporters, which require it. - // TODO(johanpel): derives are kept as simple as possible for now, but - // eventually some built-in options for built-in exporters (e.g. serde-based - // or Narrow) will surface here as simpler type-safe options. + /// Use [`Self::debug`] and [`Self::serde`] for the built-in derives. pub event_derives: &'static [&'static str], /// Derives applied to every generated record struct. /// - /// Records embedded in events must also be `Serialize`, so include a - /// `Serialize`-providing derive (e.g. `"::serde::Serialize"`). + /// Use [`Self::debug`] and [`Self::serde`] for the built-in derives. pub record_derives: &'static [&'static str], /// Directory the generated file is written into. @@ -90,7 +91,8 @@ pub struct Options { pub file_name: Option, /// Emit root and namespace-local `AnyEvent` enums that decode type-erased - /// events. Each enum carries [`Self::event_derives`]. + /// events. Each enum carries [`Self::debug`], compatible serde derives, and + /// [`Self::event_derives`]. /// /// No aggregate is emitted for a namespace without events. pub any_event: bool, @@ -99,6 +101,9 @@ pub struct Options { impl Default for Options { fn default() -> Self { Self { + instrumentation: true, + debug: true, + serde: false, event_derives: Default::default(), record_derives: Default::default(), out_dir: PathBuf::from(std::env::var("OUT_DIR").unwrap_or_default()), @@ -108,7 +113,17 @@ impl Default for Options { } } -/// An error from generating instrumentation source. +impl Options { + pub(crate) fn event_runtime(&self) -> proc_macro2::TokenStream { + if self.instrumentation { + quote! { ::quent_instrumentation } + } else { + quote! { ::quent_events } + } + } +} + +/// An error from generating event or instrumentation source. #[derive(Debug, thiserror::Error)] pub enum GenerateError { #[error("base schema validation failed: {0}")] @@ -139,6 +154,8 @@ pub enum GenerateError { /// The schema type whose generated name conflicts. schema_path: Path, }, + #[error("field type nesting exceeds the maximum depth of {max}")] + TypeNestingTooDeep { max: usize }, #[error("failed to write generated file")] Io(#[from] std::io::Error), } @@ -148,7 +165,7 @@ pub struct GenerateInfo { pub warnings: Vec, } -/// Generate the full instrumentation source for `schema` with `opts`. +/// Generate event source and, when enabled, instrumentation source for `schema`. pub fn generate(schema: &Schema, opts: &Options) -> Result { let Report { base_constraints, @@ -171,20 +188,26 @@ pub fn generate(schema: &Schema, opts: &Options) -> Result Result { let namespaces = namespace::Namespace::root(schema); - let reexports = runtime::reexports(); - let entity_types = runtime::entity_types(schema); + let reexports = if opts.instrumentation { + runtime::reexports() + } else { + events::reexports() + }; + let entity_types = opts.instrumentation.then(|| runtime::entity_types(schema)); let types = generate_namespace(schema, opts, &namespaces, false)?; - let model = runtime::generate_model(schema, &namespaces); + let model = opts + .instrumentation + .then(|| runtime::generate_model(schema, &namespaces)); let any_event = if opts.any_event { any_event::generate_any_event(&namespaces, opts)? } else { @@ -217,11 +240,20 @@ fn generate_namespace( .iter() .map(|entity| events::entity_event_enum(entity, opts)) .collect::, _>>()?; - let runtime = namespace + let entity_types = namespace .entities() .iter() - .map(|entity| runtime::entity_runtime_types(schema, entity)) - .collect::, _>>()?; + .map(|entity| events::entity_types(entity, opts)) + .collect::>(); + let runtime = if opts.instrumentation { + namespace + .entities() + .iter() + .map(|entity| runtime::entity_runtime_types(schema, entity, opts)) + .collect::, _>>()? + } else { + Vec::new() + }; let children = namespace .children() .iter() @@ -244,10 +276,15 @@ fn generate_namespace( } else { quote! {} }; - let observer_storage = runtime::observer_storage(schema, namespace)?; + let observer_storage = if opts.instrumentation { + runtime::observer_storage(schema, namespace)? + } else { + quote! {} + }; Ok(quote! { #(#records)* #(#events)* + #(#entity_types)* #(#runtime)* #(#children)* #observer_storage @@ -265,6 +302,41 @@ mod path_tests { use quent_schema::test_utils::{entity, event, field, path, record, record_type}; use quent_schema::{Annotations, DataType}; + #[test] + fn built_in_derive_path_spellings_are_deduplicated() { + let schema = SchemaBuilder::try_new("Demo") + .unwrap() + .with_record(record("Meta", [])) + .with_entity(entity("Query", [event("created", [])])) + .build() + .unwrap(); + let opts = Options { + instrumentation: false, + debug: true, + serde: true, + event_derives: &[ + "Debug", + "std::fmt::Debug", + "::core::fmt::Debug", + "serde::Serialize", + "::serde::Serialize", + ], + record_derives: &[ + "Debug", + "core::fmt::Debug", + "::std::fmt::Debug", + "serde::Deserialize", + "::serde::Deserialize", + ], + ..Options::default() + }; + + let source = generate_str(&schema, &opts).unwrap(); + + let derives = "#[derive(Debug, ::serde::Serialize, ::serde::Deserialize)]"; + assert_eq!(source.matches(derives).count(), 2); + } + #[test] fn places_entity_types_in_path_modules() { let schema = SchemaBuilder::try_new("Demo") @@ -277,11 +349,15 @@ mod path_tests { assert!(source.contains("pub mod foo")); assert!(source.contains("pub enum QueryEvent")); assert!(!source.contains("pub type Observer")); - assert!(source.contains( - "pub struct Handle>>" - )); + assert!(source.contains("pub struct Handle<")); + assert!( + source.contains( + "E: ::quent_instrumentation::InstrumentedEntity>" + ) + ); assert!(source.contains("impl super::Handle")); - assert!(source.contains("impl ::quent_instrumentation::Entity for Query")); + assert!(source.contains("impl ::quent_instrumentation::InstrumentedEntity for Query")); + assert!(source.contains("impl ::quent_instrumentation::events::Entity for Query")); assert!(source.contains("type Context = super::Context")); assert!(source.contains("pub struct DemoObservers")); assert!(source.contains("struct FooObservers")); @@ -424,4 +500,25 @@ mod path_tests { source.rfind("pub enum AnyEvent") > source.rfind("impl ::quent_instrumentation::Model") ); } + + #[test] + fn generates_any_event_without_instrumentation() { + let schema = SchemaBuilder::try_new("Demo") + .unwrap() + .with_entity(entity("Query", [event("created", [])])) + .build() + .unwrap(); + let opts = Options { + instrumentation: false, + any_event: true, + ..Options::default() + }; + + let source = generate_str(&schema, &opts).unwrap(); + assert!(source.contains("Query(&'a ::quent_events::Event)")); + assert!(!source.contains("quent_instrumentation")); + assert!(!source.contains("pub struct Handle")); + assert!(!source.contains("Observers")); + assert!(!source.contains("impl ::quent_instrumentation::Model")); + } } diff --git a/crates/instrumentation-build/src/records.rs b/crates/instrumentation-build/src/records.rs index 306a3160b..54043f7bb 100644 --- a/crates/instrumentation-build/src/records.rs +++ b/crates/instrumentation-build/src/records.rs @@ -19,16 +19,16 @@ pub(crate) fn record_struct(record: &Record, opts: &Options) -> Result = record + let derives = derive_attr(opts.record_derives, opts.debug, opts.serde, opts.serde)?; + let fields = record .fields() .map(|field| { let name = raw_ident(to_case(field.name(), Case::Snake)); - let ty = map_data_type(field.ty(), 0, record.path().namespace()); + let ty = map_data_type(field.ty(), 0, record.path().namespace(), opts)?; let field_docs = doc_attr(field.annotations().docs()); - quote! { #field_docs pub #name: #ty } + Ok::<_, GenerateError>(quote! { #field_docs pub #name: #ty }) }) - .collect(); + .collect::, _>>()?; if fields.is_empty() { Ok(quote! { #docs #derives pub struct #ident; }) } else { @@ -66,7 +66,16 @@ mod tests { } }; assert_eq!( - pretty(record_struct(&record, &Options::default()).unwrap()), + pretty( + record_struct( + &record, + &Options { + debug: false, + ..Options::default() + }, + ) + .unwrap(), + ), pretty(expected) ); } diff --git a/crates/instrumentation-build/src/runtime/handle.rs b/crates/instrumentation-build/src/runtime/handle.rs index 9acbbc79e..b8b9c1556 100644 --- a/crates/instrumentation-build/src/runtime/handle.rs +++ b/crates/instrumentation-build/src/runtime/handle.rs @@ -9,9 +9,9 @@ use quent_schema::{Cardinality, Entity}; use quote::quote; use super::{event_ident, marker_ident}; -use crate::GenerateError; use crate::common::{doc_attr_or, raw_ident, relative_root_type, to_case}; use crate::data_type::map_data_type; +use crate::{GenerateError, Options}; /// The maximum once-events an entity may declare: one bit per event in the /// handle's `u64` once-flag word. @@ -23,7 +23,7 @@ pub(crate) const MAX_ONCE_EVENTS: usize = u64::BITS as usize; /// /// Returns [`GenerateError::TooManyOnceEvents`] if the entity declares more /// once-cardinality events than fit the once-flag word. -pub(super) fn entity_handle(entity: &Entity) -> Result { +pub(super) fn entity_handle(entity: &Entity, opts: &Options) -> Result { let event_ty = event_ident(entity); let marker_ty = marker_ident(entity); let handle_ty = relative_root_type("Handle", entity.path().namespace()); @@ -42,7 +42,7 @@ pub(super) fn entity_handle(entity: &Entity) -> Result = entity + let methods = entity .events() .map(|event| { let method = raw_ident(to_case(event.name(), Case::Snake)); @@ -58,14 +58,14 @@ pub(super) fn entity_handle(entity: &Entity) -> Result = event + let params = event .fields() .map(|f| { let name = raw_ident(to_case(f.name(), Case::Snake)); - let ty = map_data_type(f.ty(), 0, entity.path().namespace()); - quote! { #name: #ty } + let ty = map_data_type(f.ty(), 0, entity.path().namespace(), opts)?; + Ok::<_, GenerateError>(quote! { #name: #ty }) }) - .collect(); + .collect::, _>>()?; let field_names: Vec = event .fields() .map(|f| { @@ -79,7 +79,7 @@ pub(super) fn entity_handle(entity: &Entity) -> Result { let bit = Literal::u32_unsuffixed(once_bit); once_bit += 1; @@ -116,9 +116,9 @@ pub(super) fn entity_handle(entity: &Entity) -> Result, GenerateError>>()?; Ok(quote! { impl #handle_ty<#marker_ty> { diff --git a/crates/instrumentation-build/src/runtime/mod.rs b/crates/instrumentation-build/src/runtime/mod.rs index 2b1229e7d..f99f54759 100644 --- a/crates/instrumentation-build/src/runtime/mod.rs +++ b/crates/instrumentation-build/src/runtime/mod.rs @@ -11,6 +11,7 @@ use quote::quote; use syn::Ident; use crate::GenerateError; +use crate::Options; use crate::common::{path_name_pascal, raw_ident, relative_root_type, to_case}; mod context; @@ -21,14 +22,11 @@ pub(crate) use handle::MAX_ONCE_EVENTS; pub(crate) fn entity_runtime_types( schema: &Schema, entity: &Entity, + opts: &Options, ) -> Result { - let marker = entity_marker(entity); - let event_impl = entity_event_impl(entity); - let handle = handle::entity_handle(entity)?; + let handle = handle::entity_handle(entity, opts)?; let entity_impl = entity_impl(schema, entity); Ok(quote! { - #marker - #event_impl #handle #entity_impl }) @@ -55,11 +53,11 @@ pub(crate) fn entity_types(schema: &Schema) -> TokenStream { format!("Handle to one entity instance in the `{model_name}` instrumentation model."); quote! { #[doc = #handle_docs] - pub struct Handle>> { + pub struct Handle>> { inner: ::quent_instrumentation::HandleInner, } - impl>> + impl>> ::core::convert::From<::quent_instrumentation::HandleInner> for Handle { fn from(inner: ::quent_instrumentation::HandleInner) -> Self { @@ -67,7 +65,7 @@ pub(crate) fn entity_types(schema: &Schema) -> TokenStream { } } - impl>> ::core::ops::Deref + impl>> ::core::ops::Deref for Handle { type Target = ::quent_instrumentation::HandleInner; @@ -91,29 +89,6 @@ pub(crate) fn reexports() -> TokenStream { } } -/// `{Entity}` — the zero-size marker naming the entity, used as the target -/// type of [`EntityRef`](quent_instrumentation::EntityRef) fields that point at it. -fn entity_marker(entity: &Entity) -> TokenStream { - let marker = marker_ident(entity); - let doc = format!("Marker type for the `{}` entity.", entity.path()); - quote! { - #[doc = #doc] - #[derive(Debug, Clone, Copy)] - pub struct #marker; - } -} - -/// Tie an entity's event enum to its canonical schema path. -fn entity_event_impl(entity: &Entity) -> TokenStream { - let event_ty = event_ident(entity); - let stream_name = entity.path().to_string(); - quote! { - impl ::quent_instrumentation::EntityEvent for #event_ty { - const NAME: &'static str = #stream_name; - } - } -} - /// `{Entity}Event` — the entity's event enum. fn event_ident(entity: &Entity) -> Ident { raw_ident(format!("{}Event", path_name_pascal(entity.path()))) @@ -131,14 +106,12 @@ pub(super) fn model_ident(schema: &Schema) -> Ident { fn entity_impl(schema: &Schema, entity: &Entity) -> TokenStream { let namespace = entity.path().namespace(); let marker = marker_ident(entity); - let event = event_ident(entity); let context = relative_root_type("Context", namespace); let model_name = model_ident(schema).to_string(); let model = relative_root_type(&model_name, namespace); let handle = relative_root_type("Handle", namespace); quote! { - impl ::quent_instrumentation::Entity for #marker { - type Event = #event; + impl ::quent_instrumentation::InstrumentedEntity for #marker { type Context = #context<#model>; type Handle = #handle; } @@ -170,15 +143,15 @@ mod tests { .build() .unwrap(); let entity = s.entities().next().unwrap(); - let entity_types = entity_runtime_types(&s, entity).unwrap(); + let event_types = crate::events::entity_types(entity, &Options::default()); + let entity_types = entity_runtime_types(&s, entity, &Options::default()).unwrap(); let namespaces = crate::namespace::Namespace::root(&s); let model = generate_model(&s, &namespaces); let src = pretty(quote! { + #event_types #entity_types #model }); - assert!(src.contains("impl ::quent_instrumentation::EntityEvent for ConnectionEvent")); - assert!(src.contains(r#"const NAME: &'static str = "Connection""#)); assert!(src.contains("type Event = ConnectionEvent")); assert!(src.contains("impl Handle")); assert!(src.contains("pub struct Demo")); diff --git a/crates/instrumentation/src/entity.rs b/crates/instrumentation/src/entity.rs index 2bb15af19..9cbed0be7 100644 --- a/crates/instrumentation/src/entity.rs +++ b/crates/instrumentation/src/entity.rs @@ -1,19 +1,14 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! Entity markers used by generated instrumentation libraries. +//! Instrumented entity markers used by generated instrumentation libraries. use std::sync::Arc; -use quent_events::EntityEvent; - use crate::{HandleInner, ObserverInner}; -/// Associates a generated entity marker with its event type and context. -pub trait Entity: Sized { - /// Events emitted for this entity. - type Event: EntityEvent; - +/// Adds instrumentation context and handle types to an entity marker. +pub trait InstrumentedEntity: quent_events::Entity { /// Instrumentation context containing this entity. type Context; @@ -22,11 +17,11 @@ pub trait Entity: Sized { } /// Provides handles for an entity type through its shared event observer. -pub struct Observer { +pub struct Observer { inner: Arc>, } -impl Clone for Observer { +impl Clone for Observer { fn clone(&self) -> Self { Self { inner: Arc::clone(&self.inner), @@ -34,7 +29,7 @@ impl Clone for Observer { } } -impl Observer { +impl Observer { /// Creates an observer backed by `inner`. /// /// Hidden because generated models construct observers; callers obtain them diff --git a/crates/instrumentation/src/handle.rs b/crates/instrumentation/src/handle.rs index 6eb71b293..2a78651b1 100644 --- a/crates/instrumentation/src/handle.rs +++ b/crates/instrumentation/src/handle.rs @@ -5,7 +5,7 @@ use std::sync::Arc; -use crate::{Entity, ObserverInner}; +use crate::{InstrumentedEntity, ObserverInner}; /// An error from emitting through a generated entity handle. #[derive(Debug, thiserror::Error)] @@ -26,14 +26,14 @@ pub enum HandleError { /// /// Hidden because generated handle newtypes are the application-facing API. #[doc(hidden)] -pub struct HandleInner { +pub struct HandleInner { id: crate::Uuid, /// One bit per once-cardinality event, set once that event is emitted. once_flags: u64, observer: Arc>, } -impl HandleInner { +impl HandleInner { pub(crate) fn new(observer: Arc>) -> Self { Self::with_id(crate::Uuid::now_v7(), observer) } diff --git a/crates/instrumentation/src/lib.rs b/crates/instrumentation/src/lib.rs index 733e59064..55ebc0dcd 100644 --- a/crates/instrumentation/src/lib.rs +++ b/crates/instrumentation/src/lib.rs @@ -9,15 +9,13 @@ mod context; mod entity; -mod entity_ref; mod handle; mod model; mod observer; mod sidecar; pub use context::ContextInner; -pub use entity::{Entity, Observer}; -pub use entity_ref::{AnyEntity, EntityRef}; +pub use entity::{InstrumentedEntity, Observer}; pub use handle::{HandleError, HandleInner}; pub use model::{Context, Model, ObserverProvider}; pub use observer::{EventSender, ObserverInner}; @@ -28,7 +26,9 @@ pub use sidecar::write_sidecar; // exporter backend through its `io-*` features. pub use quent_build_info as build_info; pub use quent_dynamic_attributes::DynamicAttributes; -pub use quent_events::{EntityEvent, Event}; +#[doc(hidden)] +pub use quent_events as events; +pub use quent_events::{AnyEntity, EntityEvent, EntityRef, Event}; pub use quent_io::ExporterOptions; pub use uuid::Uuid; diff --git a/crates/instrumentation/src/model.rs b/crates/instrumentation/src/model.rs index 90a6ef79c..32f56e9e8 100644 --- a/crates/instrumentation/src/model.rs +++ b/crates/instrumentation/src/model.rs @@ -3,14 +3,16 @@ //! Instrumentation models and their contexts. -use crate::{ContextInner, Entity, ExporterOptions, Observer, Uuid, build_info, write_sidecar}; +use crate::{ + ContextInner, ExporterOptions, InstrumentedEntity, Observer, Uuid, build_info, write_sidecar, +}; /// Provides typed access to an entity observer in a generated model. /// /// Hidden because generated observer collections implement it; callers use /// [`Context::observer`]. #[doc(hidden)] -pub trait ObserverProvider { +pub trait ObserverProvider { /// Returns the observer stored for `E`. fn observer(&self) -> Observer; } @@ -81,7 +83,7 @@ impl Context { /// Returns the observer associated with entity marker `E`. pub fn observer(&self) -> Observer where - E: Entity, + E: InstrumentedEntity, M::Observers: ObserverProvider, { self.observers.observer() diff --git a/integrations/nvtx/example/tests/thread_id.rs b/integrations/nvtx/example/tests/thread_id.rs index 447bd5fbd..519f02d01 100644 --- a/integrations/nvtx/example/tests/thread_id.rs +++ b/integrations/nvtx/example/tests/thread_id.rs @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 //! End-to-end proof that captured Push/Pop ranges carry real, per-thread OS ids.