Skip to main content

fraiseql_core/federation/
logging.rs

1//! Structured logging for federation operations.
2//!
3//! Provides a context struct for federation logs that includes:
4//! - Operation metadata (type, query ID, entity count)
5//! - Resolution details (strategy, typename, subgraph)
6//! - Timing and status information
7
8use std::time::Instant;
9
10use serde::Serialize;
11
12/// Federation operation types for logging.
13#[derive(Debug, Clone, Copy, Serialize)]
14pub enum FederationOperationType {
15    /// Entity resolution (_entities query)
16    #[serde(rename = "entity_resolution")]
17    EntityResolution,
18    /// Service schema resolution (_service query)
19    #[serde(rename = "service_schema")]
20    ServiceSchema,
21    /// Entity resolution via database
22    #[serde(rename = "resolve_db")]
23    ResolveDb,
24    /// Entity resolution via HTTP subgraph
25    #[serde(rename = "resolve_http")]
26    ResolveHttp,
27    /// Mutation execution
28    #[serde(rename = "mutation_execute")]
29    MutationExecute,
30}
31
32/// Federation resolution strategy for logging.
33#[derive(Debug, Clone, Copy, Serialize)]
34pub enum ResolutionStrategy {
35    /// Local resolution (in-memory cache)
36    #[serde(rename = "local")]
37    Local,
38    /// Direct database query
39    #[serde(rename = "db")]
40    Db,
41    /// HTTP request to subgraph
42    #[serde(rename = "http")]
43    Http,
44}
45
46/// Structured log context for federation operations.
47#[derive(Debug, Clone, Serialize)]
48pub struct FederationLogContext {
49    /// Type of federation operation
50    pub operation_type: FederationOperationType,
51
52    /// Unique query identifier for correlation
53    pub query_id: String,
54
55    /// Total number of entities in request
56    pub entity_count: usize,
57
58    /// Number of unique entities (after deduplication)
59    pub entity_count_unique: Option<usize>,
60
61    /// Resolution strategy used
62    pub strategy: Option<ResolutionStrategy>,
63
64    /// GraphQL typename being resolved
65    pub typename: Option<String>,
66
67    /// Subgraph name (for HTTP resolution)
68    pub subgraph_name: Option<String>,
69
70    /// Operation duration in milliseconds
71    pub duration_ms: f64,
72
73    /// Operation status
74    pub status: OperationStatus,
75
76    /// Error message if operation failed
77    pub error_message: Option<String>,
78
79    /// HTTP status code (for subgraph requests)
80    pub http_status: Option<u16>,
81
82    /// Number of entities resolved
83    pub resolved_count: Option<usize>,
84
85    /// Trace ID for distributed tracing correlation
86    pub trace_id: Option<String>,
87
88    /// Request ID for end-to-end request correlation
89    pub request_id: Option<String>,
90}
91
92/// Operation status for federation logs.
93#[derive(Debug, Clone, Copy, Serialize)]
94pub enum OperationStatus {
95    /// Operation started (but not completed)
96    #[serde(rename = "started")]
97    Started,
98    /// Operation completed successfully
99    #[serde(rename = "success")]
100    Success,
101    /// Operation failed with error
102    #[serde(rename = "error")]
103    Error,
104    /// Operation timed out
105    #[serde(rename = "timeout")]
106    Timeout,
107}
108
109impl FederationLogContext {
110    /// Create new federation log context.
111    pub fn new(
112        operation_type: FederationOperationType,
113        query_id: String,
114        entity_count: usize,
115    ) -> Self {
116        Self {
117            operation_type,
118            query_id,
119            entity_count,
120            entity_count_unique: None,
121            strategy: None,
122            typename: None,
123            subgraph_name: None,
124            duration_ms: 0.0,
125            status: OperationStatus::Started,
126            error_message: None,
127            http_status: None,
128            resolved_count: None,
129            trace_id: None,
130            request_id: None,
131        }
132    }
133
134    /// Set resolution strategy.
135    pub fn with_strategy(mut self, strategy: ResolutionStrategy) -> Self {
136        self.strategy = Some(strategy);
137        self
138    }
139
140    /// Set typename.
141    pub fn with_typename(mut self, typename: String) -> Self {
142        self.typename = Some(typename);
143        self
144    }
145
146    /// Set subgraph name.
147    pub fn with_subgraph_name(mut self, subgraph_name: String) -> Self {
148        self.subgraph_name = Some(subgraph_name);
149        self
150    }
151
152    /// Set entity count after deduplication.
153    pub fn with_entity_count_unique(mut self, count: usize) -> Self {
154        self.entity_count_unique = Some(count);
155        self
156    }
157
158    /// Set resolved entity count.
159    pub fn with_resolved_count(mut self, count: usize) -> Self {
160        self.resolved_count = Some(count);
161        self
162    }
163
164    /// Set HTTP status code.
165    pub fn with_http_status(mut self, status: u16) -> Self {
166        self.http_status = Some(status);
167        self
168    }
169
170    /// Set trace ID for correlation.
171    pub fn with_trace_id(mut self, trace_id: String) -> Self {
172        self.trace_id = Some(trace_id);
173        self
174    }
175
176    /// Set request ID for correlation.
177    pub fn with_request_id(mut self, request_id: String) -> Self {
178        self.request_id = Some(request_id);
179        self
180    }
181
182    /// Mark operation as completed successfully.
183    pub fn complete(mut self, duration_ms: f64) -> Self {
184        self.status = OperationStatus::Success;
185        self.duration_ms = duration_ms;
186        self
187    }
188
189    /// Mark operation as failed.
190    pub fn fail(mut self, duration_ms: f64, error_message: String) -> Self {
191        self.status = OperationStatus::Error;
192        self.duration_ms = duration_ms;
193        self.error_message = Some(error_message);
194        self
195    }
196
197    /// Mark operation as timed out.
198    pub fn timeout(mut self, duration_ms: f64) -> Self {
199        self.status = OperationStatus::Timeout;
200        self.duration_ms = duration_ms;
201        self
202    }
203}
204
205/// Timer for measuring operation duration.
206pub struct LogTimer {
207    start: Instant,
208}
209
210impl LogTimer {
211    /// Create new timer.
212    pub fn new() -> Self {
213        Self {
214            start: Instant::now(),
215        }
216    }
217
218    /// Get elapsed time in milliseconds.
219    pub fn elapsed_ms(&self) -> f64 {
220        self.start.elapsed().as_secs_f64() * 1000.0
221    }
222}
223
224impl Default for LogTimer {
225    fn default() -> Self {
226        Self::new()
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_federation_log_context_creation() {
236        let ctx = FederationLogContext::new(
237            FederationOperationType::EntityResolution,
238            "query-123".to_string(),
239            10,
240        );
241
242        assert_eq!(ctx.entity_count, 10);
243        assert_eq!(ctx.query_id, "query-123");
244        assert!(ctx.typename.is_none());
245        assert!(ctx.error_message.is_none());
246    }
247
248    #[test]
249    fn test_federation_log_context_builder() {
250        let ctx = FederationLogContext::new(
251            FederationOperationType::ResolveDb,
252            "query-456".to_string(),
253            20,
254        )
255        .with_strategy(ResolutionStrategy::Db)
256        .with_typename("User".to_string())
257        .with_entity_count_unique(15)
258        .with_resolved_count(15)
259        .complete(25.5);
260
261        assert_eq!(ctx.entity_count, 20);
262        assert_eq!(ctx.entity_count_unique, Some(15));
263        assert_eq!(ctx.resolved_count, Some(15));
264        assert_eq!(ctx.duration_ms, 25.5);
265        assert!(matches!(ctx.status, OperationStatus::Success));
266    }
267
268    #[test]
269    fn test_federation_log_context_error() {
270        let ctx = FederationLogContext::new(
271            FederationOperationType::ResolveHttp,
272            "query-789".to_string(),
273            5,
274        )
275        .fail(15.2, "Connection refused".to_string());
276
277        assert!(matches!(ctx.status, OperationStatus::Error));
278        assert_eq!(ctx.error_message, Some("Connection refused".to_string()));
279        assert_eq!(ctx.duration_ms, 15.2);
280    }
281
282    #[test]
283    fn test_log_timer_elapsed() {
284        let timer = LogTimer::new();
285        std::thread::sleep(std::time::Duration::from_millis(10));
286        let elapsed = timer.elapsed_ms();
287        assert!(elapsed >= 10.0);
288        assert!(elapsed < 100.0); // Should be much less than 100ms
289    }
290
291    #[test]
292    fn test_federation_log_context_serialization() {
293        let ctx = FederationLogContext::new(
294            FederationOperationType::EntityResolution,
295            "query-123".to_string(),
296            10,
297        )
298        .with_strategy(ResolutionStrategy::Db)
299        .with_typename("User".to_string())
300        .complete(25.5);
301
302        let json = serde_json::to_string(&ctx).expect("JSON serialization failed");
303        assert!(json.contains("\"entity_count\":10"));
304        assert!(json.contains("\"duration_ms\":25.5"));
305        assert!(json.contains("\"status\":\"success\""));
306    }
307}