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//! ```ignore
10//! use query_flow::{QueryRuntime, Tracer, SpanId};
11//!
12//! // Custom tracer implementation
13//! struct MyTracer;
14//!
15//! impl Tracer for MyTracer {
16//!     fn new_span_id(&self) -> SpanId {
17//!         SpanId(1)
18//!     }
19//!
20//!     fn on_query_start(&self, span_id: SpanId, query: TracerQueryKey) {
21//!         println!("Query started: {:?}", query);
22//!     }
23//! }
24//!
25//! let runtime = QueryRuntime::with_tracer(MyTracer);
26//! ```
27
28use serde::{Deserialize, Serialize};
29use std::sync::atomic::{AtomicU64, Ordering};
30use std::time::Duration;
31
32/// Unique identifier for a query execution span.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
34pub struct SpanId(pub u64);
35
36/// Represents a query key in a type-erased manner for tracing.
37#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38pub struct TracerQueryKey {
39    /// The query type name (e.g., "calc::ParseExpr")
40    pub query_type: &'static str,
41    /// Debug representation of the cache key (e.g., "(\"main.txt\",)")
42    pub cache_key_debug: String,
43}
44
45impl TracerQueryKey {
46    /// Create a new tracer query key.
47    #[inline]
48    pub fn new(query_type: &'static str, cache_key_debug: impl Into<String>) -> Self {
49        Self {
50            query_type,
51            cache_key_debug: cache_key_debug.into(),
52        }
53    }
54}
55
56/// Represents an asset key in a type-erased manner for tracing.
57#[derive(Debug, Clone, PartialEq, Eq, Hash)]
58pub struct TracerAssetKey {
59    /// The asset type name (e.g., "calc::SourceFile")
60    pub asset_type: &'static str,
61    /// Debug representation of the key (e.g., "SourceFile(\"main.txt\")")
62    pub key_debug: String,
63}
64
65impl TracerAssetKey {
66    /// Create a new tracer asset key.
67    #[inline]
68    pub fn new(asset_type: &'static str, key_debug: impl Into<String>) -> Self {
69        Self {
70            asset_type,
71            key_debug: key_debug.into(),
72        }
73    }
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: TracerQueryKey },
109    /// An asset dependency was updated.
110    AssetChanged { asset: TracerAssetKey },
111    /// Manual invalidation was triggered.
112    ManualInvalidation,
113    /// An asset was removed.
114    AssetRemoved { asset: TracerAssetKey },
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    /// This is the only required method. Called at the start of each query execution.
134    fn new_span_id(&self) -> SpanId;
135
136    /// Called when a query execution starts.
137    #[inline]
138    fn on_query_start(&self, _span_id: SpanId, _query: TracerQueryKey) {}
139
140    /// Called when cache validity is checked.
141    #[inline]
142    fn on_cache_check(&self, _span_id: SpanId, _query: TracerQueryKey, _valid: bool) {}
143
144    /// Called when a query execution ends.
145    #[inline]
146    fn on_query_end(
147        &self,
148        _span_id: SpanId,
149        _query: TracerQueryKey,
150        _result: ExecutionResult,
151        _duration: Duration,
152    ) {
153    }
154
155    /// Called when a query dependency is registered during execution.
156    #[inline]
157    fn on_dependency_registered(
158        &self,
159        _span_id: SpanId,
160        _parent: TracerQueryKey,
161        _dependency: TracerQueryKey,
162    ) {
163    }
164
165    /// Called when an asset dependency is registered during execution.
166    #[inline]
167    fn on_asset_dependency_registered(
168        &self,
169        _span_id: SpanId,
170        _parent: TracerQueryKey,
171        _asset: TracerAssetKey,
172    ) {
173    }
174
175    /// Called when early cutoff comparison is performed.
176    #[inline]
177    fn on_early_cutoff_check(
178        &self,
179        _span_id: SpanId,
180        _query: TracerQueryKey,
181        _output_changed: bool,
182    ) {
183    }
184
185    /// Called when an asset is requested.
186    #[inline]
187    fn on_asset_requested(&self, _asset: TracerAssetKey, _state: TracerAssetState) {}
188
189    /// Called when an asset is resolved with a value.
190    #[inline]
191    fn on_asset_resolved(&self, _asset: TracerAssetKey, _changed: bool) {}
192
193    /// Called when an asset is invalidated.
194    #[inline]
195    fn on_asset_invalidated(&self, _asset: TracerAssetKey) {}
196
197    /// Called when a query is invalidated.
198    #[inline]
199    fn on_query_invalidated(&self, _query: TracerQueryKey, _reason: InvalidationReason) {}
200
201    /// Called when a dependency cycle is detected.
202    #[inline]
203    fn on_cycle_detected(&self, _path: Vec<TracerQueryKey>) {}
204
205    /// Called when a missing dependency error occurs.
206    #[inline]
207    fn on_missing_dependency(&self, _query: TracerQueryKey, _dependency_description: String) {}
208}
209
210/// Zero-cost tracer that discards all events.
211///
212/// This is the default tracer for [`QueryRuntime`](crate::QueryRuntime).
213pub struct NoopTracer;
214
215/// Global span counter for NoopTracer.
216static NOOP_SPAN_COUNTER: AtomicU64 = AtomicU64::new(1);
217
218impl Tracer for NoopTracer {
219    #[inline(always)]
220    fn new_span_id(&self) -> SpanId {
221        SpanId(NOOP_SPAN_COUNTER.fetch_add(1, Ordering::Relaxed))
222    }
223    // All other methods use the default empty implementations
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use std::sync::atomic::AtomicUsize;
230    use std::sync::Arc;
231
232    struct CountingTracer {
233        start_count: AtomicUsize,
234        end_count: AtomicUsize,
235    }
236
237    impl CountingTracer {
238        fn new() -> Self {
239            Self {
240                start_count: AtomicUsize::new(0),
241                end_count: AtomicUsize::new(0),
242            }
243        }
244    }
245
246    impl Tracer for CountingTracer {
247        fn new_span_id(&self) -> SpanId {
248            SpanId(1)
249        }
250
251        fn on_query_start(&self, _span_id: SpanId, _query: TracerQueryKey) {
252            self.start_count.fetch_add(1, Ordering::Relaxed);
253        }
254
255        fn on_query_end(
256            &self,
257            _span_id: SpanId,
258            _query: TracerQueryKey,
259            _result: ExecutionResult,
260            _duration: Duration,
261        ) {
262            self.end_count.fetch_add(1, Ordering::Relaxed);
263        }
264    }
265
266    #[test]
267    fn test_noop_tracer_span_id() {
268        let tracer = NoopTracer;
269        let id1 = tracer.new_span_id();
270        let id2 = tracer.new_span_id();
271        assert_ne!(id1, id2);
272    }
273
274    #[test]
275    fn test_counting_tracer() {
276        let tracer = CountingTracer::new();
277        let key = TracerQueryKey::new("TestQuery", "()");
278
279        tracer.on_query_start(SpanId(1), key.clone());
280        tracer.on_query_start(SpanId(2), key.clone());
281        tracer.on_query_end(
282            SpanId(1),
283            key,
284            ExecutionResult::Changed,
285            Duration::from_millis(10),
286        );
287
288        assert_eq!(tracer.start_count.load(Ordering::Relaxed), 2);
289        assert_eq!(tracer.end_count.load(Ordering::Relaxed), 1);
290    }
291
292    #[test]
293    fn test_tracer_is_send_sync() {
294        fn assert_send_sync<T: Send + Sync>() {}
295        assert_send_sync::<NoopTracer>();
296        assert_send_sync::<Arc<CountingTracer>>();
297    }
298}