query_flow/tracer.rs
1//! Tracer trait for observing query-flow execution.
2//!
3//! This module defines the [`Tracer`] trait and related types for observing
4//! query execution. The default [`NoopTracer`] provides zero-cost when tracing
5//! is not needed.
6//!
7//! # Example
8//!
9//! ```
10//! use query_flow::{
11//! query, Db, QueryCacheKey, QueryError, QueryRuntime, SpanContext, SpanId, TraceId, Tracer,
12//! };
13//!
14//! // Custom tracer implementation
15//! struct MyTracer;
16//!
17//! impl Tracer for MyTracer {
18//! fn new_span_id(&self) -> SpanId {
19//! SpanId(1)
20//! }
21//!
22//! fn new_trace_id(&self) -> TraceId {
23//! TraceId(1)
24//! }
25//!
26//! fn on_query_start(&self, ctx: &SpanContext, query: &QueryCacheKey) {
27//! println!("Query started: {:?} (trace={:?})", query, ctx.trace_id);
28//! }
29//! }
30//!
31//! #[query]
32//! fn double(db: &impl Db, x: i32) -> Result<i32, QueryError> {
33//! let _ = db;
34//! Ok(x * 2)
35//! }
36//!
37//! let runtime = QueryRuntime::with_tracer(MyTracer);
38//! assert_eq!(*runtime.query(Double::new(21)).unwrap(), 42);
39//! ```
40
41use serde::{Deserialize, Serialize};
42
43use crate::key::{AssetCacheKey, FullCacheKey, QueryCacheKey};
44
45/// Unique identifier for a query execution span.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
47pub struct SpanId(pub u64);
48
49impl SpanId {
50 /// Zero value, used by NoopTracer.
51 pub const ZERO: Self = Self(0);
52}
53
54/// Unique identifier for a trace (a complete query execution tree).
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
56pub struct TraceId(pub u64);
57
58impl TraceId {
59 /// Zero value, used by NoopTracer.
60 pub const ZERO: Self = Self(0);
61}
62
63/// Context for a span within a trace, providing parent-child relationships.
64///
65/// This enables reconstructing the full dependency tree of query executions.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
67pub struct SpanContext {
68 /// Unique identifier for this span.
69 pub span_id: SpanId,
70 /// Identifier for the trace this span belongs to.
71 pub trace_id: TraceId,
72 /// The parent span's ID, if this is a nested query.
73 pub parent_span_id: Option<SpanId>,
74}
75
76/// Query execution result classification.
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub enum ExecutionResult {
79 /// Query computed a new value (output changed).
80 Changed,
81 /// Query completed but output unchanged (early cutoff applied).
82 Unchanged,
83 /// Query returned cached value without execution.
84 CacheHit,
85 /// Query suspended waiting for async loading.
86 Suspended,
87 /// Query detected a dependency cycle.
88 CycleDetected,
89 /// Query failed with an error.
90 Error { message: String },
91}
92
93/// Asset loading state for tracing.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum TracerAssetState {
96 /// Asset is currently loading.
97 Loading,
98 /// Asset is ready with a value.
99 Ready,
100 /// Asset was not found.
101 NotFound,
102}
103
104/// Reason for cache invalidation.
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub enum InvalidationReason {
107 /// A dependency query changed its output.
108 DependencyChanged { dep: FullCacheKey },
109 /// An asset dependency was updated.
110 AssetChanged { asset: FullCacheKey },
111 /// Manual invalidation was triggered.
112 ManualInvalidation,
113 /// An asset was removed.
114 AssetRemoved { asset: FullCacheKey },
115}
116
117/// Tracer trait for observing query-flow execution.
118///
119/// Implementations can collect events for testing, forward to the `tracing` crate,
120/// or provide custom observability.
121///
122/// All methods have default empty implementations, so you only need to override
123/// the events you're interested in. The [`NoopTracer`] uses all defaults for
124/// zero-cost when tracing is disabled.
125///
126/// # Thread Safety
127///
128/// Implementations must be `Send + Sync` as the tracer may be called from
129/// multiple threads concurrently.
130pub trait Tracer: Send + Sync + 'static {
131 /// Generate a new unique span ID.
132 ///
133 /// Called at the start of each query execution.
134 fn new_span_id(&self) -> SpanId;
135
136 /// Generate a new unique trace ID.
137 ///
138 /// Called when starting a root query (one with no parent in the span stack).
139 fn new_trace_id(&self) -> TraceId;
140
141 /// Called when a query execution starts.
142 ///
143 /// Use `query.type_name()` to get the query type and `query.debug_repr()` for the key.
144 #[inline]
145 fn on_query_start(&self, _ctx: &SpanContext, _query: &QueryCacheKey) {}
146
147 /// Called when cache validity is checked.
148 #[inline]
149 fn on_cache_check(&self, _ctx: &SpanContext, _query: &QueryCacheKey, _valid: bool) {}
150
151 /// Called when a query execution ends.
152 #[inline]
153 fn on_query_end(&self, _ctx: &SpanContext, _query: &QueryCacheKey, _result: ExecutionResult) {}
154
155 /// Called when a query dependency is registered during execution.
156 #[inline]
157 fn on_dependency_registered(
158 &self,
159 _ctx: &SpanContext,
160 _parent: &FullCacheKey,
161 _dependency: &FullCacheKey,
162 ) {
163 }
164
165 /// Called when an asset dependency is registered during execution.
166 #[inline]
167 fn on_asset_dependency_registered(
168 &self,
169 _ctx: &SpanContext,
170 _parent: &FullCacheKey,
171 _asset: &FullCacheKey,
172 ) {
173 }
174
175 /// Called when early cutoff comparison is performed.
176 #[inline]
177 fn on_early_cutoff_check(
178 &self,
179 _ctx: &SpanContext,
180 _query: &QueryCacheKey,
181 _output_changed: bool,
182 ) {
183 }
184
185 /// Called when an asset is requested (START event).
186 ///
187 /// This is called BEFORE the locator executes. Child queries called by
188 /// the locator will appear as children of this asset in the trace tree.
189 #[inline]
190 fn on_asset_requested(&self, _ctx: &SpanContext, _asset: &AssetCacheKey) {}
191
192 /// Called when an asset locator finishes execution.
193 ///
194 /// This is the "end" event for assets, corresponding to `on_query_end` for queries.
195 /// Called after the locator executes with the final state.
196 #[inline]
197 fn on_asset_located(
198 &self,
199 _ctx: &SpanContext,
200 _asset: &AssetCacheKey,
201 _state: TracerAssetState,
202 ) {
203 }
204
205 /// Called when an asset is resolved with a value.
206 #[inline]
207 fn on_asset_resolved(&self, _asset: &AssetCacheKey, _changed: bool) {}
208
209 /// Called when an asset is invalidated.
210 #[inline]
211 fn on_asset_invalidated(&self, _asset: &AssetCacheKey) {}
212
213 /// Called when a query is invalidated.
214 #[inline]
215 fn on_query_invalidated(&self, _query: &QueryCacheKey, _reason: InvalidationReason) {}
216
217 /// Called when a dependency cycle is detected.
218 ///
219 /// The path can contain both queries and assets since cycles may involve asset locators.
220 #[inline]
221 fn on_cycle_detected(&self, _path: &[FullCacheKey]) {}
222
223 /// Called when a query is accessed, providing the [`FullCacheKey`] for GC tracking.
224 ///
225 /// This is called at the start of each query execution, before `on_query_start`.
226 /// Use this to track access times or reference counts for garbage collection.
227 ///
228 /// # Example
229 ///
230 /// ```
231 /// use query_flow::{query, Db, FullCacheKey, QueryError, QueryRuntime, SpanId, TraceId, Tracer};
232 /// use std::collections::HashMap;
233 /// use std::sync::Mutex;
234 /// use std::time::Instant;
235 ///
236 /// struct GcTracer {
237 /// access_times: Mutex<HashMap<FullCacheKey, Instant>>,
238 /// }
239 ///
240 /// impl Tracer for GcTracer {
241 /// fn new_span_id(&self) -> SpanId { SpanId(0) }
242 ///
243 /// fn new_trace_id(&self) -> TraceId { TraceId(0) }
244 ///
245 /// fn on_query_key(&self, full_key: &FullCacheKey) {
246 /// self.access_times.lock().unwrap()
247 /// .insert(full_key.clone(), Instant::now());
248 /// }
249 /// }
250 ///
251 /// #[query]
252 /// fn double(db: &impl Db, x: i32) -> Result<i32, QueryError> {
253 /// let _ = db;
254 /// Ok(x * 2)
255 /// }
256 ///
257 /// let runtime = QueryRuntime::with_tracer(GcTracer {
258 /// access_times: Mutex::new(HashMap::new()),
259 /// });
260 /// runtime.query(Double::new(21)).unwrap();
261 ///
262 /// assert_eq!(runtime.tracer().access_times.lock().unwrap().len(), 1);
263 /// ```
264 #[inline]
265 fn on_query_key(&self, _full_key: &FullCacheKey) {}
266}
267
268/// Zero-cost tracer that discards all events.
269///
270/// This is the default tracer for [`QueryRuntime`](crate::QueryRuntime).
271pub struct NoopTracer;
272
273impl Tracer for NoopTracer {
274 #[inline(always)]
275 fn new_span_id(&self) -> SpanId {
276 // ZERO is valid because all callbacks are no-ops, so no one observes these IDs.
277 // This avoids atomic counter overhead.
278 SpanId::ZERO
279 }
280
281 #[inline(always)]
282 fn new_trace_id(&self) -> TraceId {
283 TraceId::ZERO
284 }
285 // All other methods use the default empty implementations
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use crate::key::QueryCacheKey;
292 use std::sync::atomic::AtomicUsize;
293 use std::sync::atomic::Ordering;
294 use std::sync::Arc;
295
296 struct CountingTracer {
297 start_count: AtomicUsize,
298 end_count: AtomicUsize,
299 }
300
301 impl CountingTracer {
302 fn new() -> Self {
303 Self {
304 start_count: AtomicUsize::new(0),
305 end_count: AtomicUsize::new(0),
306 }
307 }
308 }
309
310 impl Tracer for CountingTracer {
311 fn new_span_id(&self) -> SpanId {
312 SpanId(1)
313 }
314
315 fn new_trace_id(&self) -> TraceId {
316 TraceId(1)
317 }
318
319 fn on_query_start(&self, _ctx: &SpanContext, _query: &QueryCacheKey) {
320 self.start_count.fetch_add(1, Ordering::Relaxed);
321 }
322
323 fn on_query_end(
324 &self,
325 _ctx: &SpanContext,
326 _query: &QueryCacheKey,
327 _result: ExecutionResult,
328 ) {
329 self.end_count.fetch_add(1, Ordering::Relaxed);
330 }
331 }
332
333 #[test]
334 fn test_noop_tracer_returns_zero() {
335 let tracer = NoopTracer;
336 assert_eq!(tracer.new_span_id(), SpanId::ZERO);
337 assert_eq!(tracer.new_trace_id(), TraceId::ZERO);
338 }
339
340 #[test]
341 fn test_counting_tracer() {
342 let tracer = CountingTracer::new();
343 let key = QueryCacheKey::new(("TestQuery",));
344
345 let ctx1 = SpanContext {
346 span_id: SpanId(1),
347 trace_id: TraceId(1),
348 parent_span_id: None,
349 };
350 let ctx2 = SpanContext {
351 span_id: SpanId(2),
352 trace_id: TraceId(1),
353 parent_span_id: Some(SpanId(1)),
354 };
355
356 tracer.on_query_start(&ctx1, &key);
357 tracer.on_query_start(&ctx2, &key);
358 tracer.on_query_end(&ctx1, &key, ExecutionResult::Changed);
359
360 assert_eq!(tracer.start_count.load(Ordering::Relaxed), 2);
361 assert_eq!(tracer.end_count.load(Ordering::Relaxed), 1);
362 }
363
364 #[test]
365 fn test_tracer_is_send_sync() {
366 fn assert_send_sync<T: Send + Sync>() {}
367 assert_send_sync::<NoopTracer>();
368 assert_send_sync::<Arc<CountingTracer>>();
369 }
370}