enya_client/tracing/mod.rs
1//! Tracing client abstraction for distributed trace backends.
2//!
3//! This module provides a unified interface for querying traces from different
4//! backends (Grafana Tempo, Jaeger, etc.) using the TracingClient trait.
5//!
6//! # Architecture
7//!
8//! The [`TracingClient`] trait defines a promise-based async interface that all
9//! backends implement. Methods return [`Promise`] objects that can be polled
10//! each frame in immediate mode GUIs like egui.
11//!
12//! # Example
13//!
14//! ```ignore
15//! use enya_client::tracing::{TracingClient, TraceSearchParams};
16//! use enya_client::tracing::tempo::TempoClient;
17//!
18//! // Create a client for your backend
19//! let client = TempoClient::new("http://localhost:3200");
20//!
21//! // Fire off a trace query - returns a promise
22//! let promise = client.get_trace("abc123def456", &ctx);
23//!
24//! // In your update loop, poll for results
25//! if let Some(result) = promise.ready() {
26//! match result {
27//! Ok(trace) => { /* render waterfall */ }
28//! Err(e) => { /* show error */ }
29//! }
30//! }
31//! ```
32
33pub mod tempo;
34
35// Re-export common types from tempo (these are backend-agnostic)
36pub use poll_promise::Promise;
37pub use tempo::types::{
38 Span, SpanLog, SpanStatus, Trace, TraceId, TraceSearchParams, TraceSummary, format_duration_us,
39};
40
41use crate::error::ClientError;
42
43/// Result type for trace fetch operations.
44pub type TraceResult = Result<Trace, ClientError>;
45
46/// Result type for trace search operations.
47pub type SearchResult = Result<Vec<TraceSummary>, ClientError>;
48
49/// Tracing client trait - promise-based async interface.
50///
51/// Implementations handle the HTTP communication with the backend. All async methods
52/// return [`Promise`] objects that can be polled each frame.
53pub trait TracingClient {
54 /// Fetch a trace by its ID (non-blocking).
55 ///
56 /// Returns a promise that resolves to the full trace with all spans.
57 /// The `egui::Context` is used to request a repaint when the response is ready.
58 fn get_trace(&self, trace_id: &str, ctx: &egui::Context) -> Promise<TraceResult>;
59
60 /// Search for traces matching the given parameters (non-blocking).
61 ///
62 /// Returns a promise that resolves to a list of trace summaries.
63 fn search_traces(
64 &self,
65 params: TraceSearchParams,
66 ctx: &egui::Context,
67 ) -> Promise<SearchResult>;
68
69 /// Get the backend type identifier (e.g., "tempo", "jaeger").
70 fn backend_type(&self) -> &'static str;
71}
72
73/// Manages in-flight trace fetch requests using promises.
74///
75/// Similar to [`QueryManager`](crate::QueryManager) for metrics, but for trace operations.
76pub struct TraceManager {
77 /// Pending trace fetch promise.
78 trace_promise: Option<Promise<TraceResult>>,
79 /// Pending search promise.
80 search_promise: Option<Promise<SearchResult>>,
81}
82
83impl Default for TraceManager {
84 fn default() -> Self {
85 Self::new()
86 }
87}
88
89impl TraceManager {
90 /// Create a new trace manager.
91 #[must_use]
92 pub fn new() -> Self {
93 Self {
94 trace_promise: None,
95 search_promise: None,
96 }
97 }
98
99 /// Check if a trace fetch is in flight.
100 #[must_use]
101 pub fn is_fetching_trace(&self) -> bool {
102 self.trace_promise.is_some()
103 }
104
105 /// Check if a search is in flight.
106 #[must_use]
107 pub fn is_searching(&self) -> bool {
108 self.search_promise.is_some()
109 }
110
111 /// Fetch a trace by ID.
112 ///
113 /// If a fetch is already in flight, it is cancelled and replaced.
114 pub fn fetch_trace<C: TracingClient + ?Sized>(
115 &mut self,
116 client: &C,
117 trace_id: &str,
118 ctx: &egui::Context,
119 ) {
120 self.trace_promise = Some(client.get_trace(trace_id, ctx));
121 }
122
123 /// Search for traces.
124 ///
125 /// If a search is already in flight, it is cancelled and replaced.
126 pub fn search<C: TracingClient + ?Sized>(
127 &mut self,
128 client: &C,
129 params: TraceSearchParams,
130 ctx: &egui::Context,
131 ) {
132 self.search_promise = Some(client.search_traces(params, ctx));
133 }
134
135 /// Poll for trace fetch result.
136 ///
137 /// Returns `Some(result)` if a fetch just completed, `None` otherwise.
138 pub fn poll_trace(&mut self) -> Option<TraceResult> {
139 let promise = self.trace_promise.as_ref()?;
140 if let Some(result) = promise.ready() {
141 let result = result.clone();
142 self.trace_promise = None;
143 Some(result)
144 } else {
145 None
146 }
147 }
148
149 /// Poll for search result.
150 ///
151 /// Returns `Some(result)` if a search just completed, `None` otherwise.
152 pub fn poll_search(&mut self) -> Option<SearchResult> {
153 let promise = self.search_promise.as_ref()?;
154 if let Some(result) = promise.ready() {
155 let result = result.clone();
156 self.search_promise = None;
157 Some(result)
158 } else {
159 None
160 }
161 }
162
163 /// Cancel any pending trace fetch.
164 pub fn cancel_trace(&mut self) {
165 self.trace_promise = None;
166 }
167
168 /// Cancel any pending search.
169 pub fn cancel_search(&mut self) {
170 self.search_promise = None;
171 }
172
173 /// Cancel all pending operations.
174 pub fn cancel_all(&mut self) {
175 self.trace_promise = None;
176 self.search_promise = None;
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn test_trace_manager_initial_state() {
186 let manager = TraceManager::new();
187 assert!(!manager.is_fetching_trace());
188 assert!(!manager.is_searching());
189 }
190}