1use crate::error::{Error, Result};
2use crate::models::{
3 LogEntry, LogsQuery, LogsResponse, MetricResponse, Trace, TracesQuery, TracesResponse,
4};
5use reqwest::Client;
6use std::time::Duration;
7
8pub struct ApiClient {
9 client: Client,
10 base_url: String,
11}
12
13impl ApiClient {
14 pub fn new(endpoint: String, timeout: Duration) -> Result<Self> {
15 let client = Client::builder()
16 .timeout(timeout)
17 .build()
18 .map_err(|e| Error::ConnectionError(format!("Failed to create HTTP client: {}", e)))?;
19
20 Ok(Self {
21 client,
22 base_url: endpoint,
23 })
24 }
25
26 pub async fn fetch_logs(&self, params: Vec<(&str, String)>) -> Result<LogsResponse> {
27 let url = format!("{}/api/logs", self.base_url);
28 let response = self.client.get(&url).query(¶ms).send().await?;
29
30 if !response.status().is_success() {
31 return Err(Error::ApiError(format!(
32 "Failed to fetch logs: HTTP {}",
33 response.status()
34 )));
35 }
36
37 Ok(response.json().await?)
38 }
39
40 pub async fn fetch_log_by_id(&self, timestamp: i64) -> Result<LogEntry> {
41 let url = format!("{}/api/logs/{}", self.base_url, timestamp);
42 let response = self.client.get(&url).send().await?;
43
44 if response.status().as_u16() == 404 {
45 return Err(Error::NotFound(format!(
46 "Log at timestamp '{}' not found",
47 timestamp
48 )));
49 }
50
51 if !response.status().is_success() {
52 return Err(Error::ApiError(format!(
53 "Failed to fetch log: HTTP {}",
54 response.status()
55 )));
56 }
57
58 Ok(response.json().await?)
59 }
60
61 pub async fn search_logs(
62 &self,
63 query: &str,
64 params: Vec<(&str, String)>,
65 ) -> Result<LogsResponse> {
66 let url = format!("{}/api/logs", self.base_url);
67 let mut all_params = vec![("search", query.to_string())];
68 all_params.extend(params);
69
70 let response = self.client.get(&url).query(&all_params).send().await?;
71
72 if !response.status().is_success() {
73 return Err(Error::ApiError(format!(
74 "Failed to search logs: HTTP {}",
75 response.status()
76 )));
77 }
78
79 Ok(response.json().await?)
80 }
81
82 pub async fn get_logs(&self, query: &LogsQuery) -> Result<LogsResponse> {
83 let url = format!("{}/api/logs", self.base_url);
84 let response = self.client.get(&url).query(query).send().await?;
85
86 if !response.status().is_success() {
87 return Err(Error::ApiError(format!(
88 "Failed to fetch logs: HTTP {}",
89 response.status()
90 )));
91 }
92
93 Ok(response.json().await?)
94 }
95
96 pub async fn fetch_traces(&self, params: Vec<(&str, String)>) -> Result<TracesResponse> {
97 let url = format!("{}/api/traces", self.base_url);
98 let response = self.client.get(&url).query(¶ms).send().await?;
99
100 if !response.status().is_success() {
101 return Err(Error::ApiError(format!(
102 "Failed to fetch traces: HTTP {}",
103 response.status()
104 )));
105 }
106
107 Ok(response.json().await?)
108 }
109
110 pub async fn fetch_trace_by_id(&self, id: &str) -> Result<Trace> {
111 let url = format!("{}/api/traces/{}", self.base_url, id);
112 let response = self.client.get(&url).send().await?;
113
114 if response.status().as_u16() == 404 {
115 return Err(Error::NotFound(format!("Trace '{}' not found", id)));
116 }
117
118 if !response.status().is_success() {
119 return Err(Error::ApiError(format!(
120 "Failed to fetch trace: HTTP {}",
121 response.status()
122 )));
123 }
124
125 Ok(response.json().await?)
126 }
127
128 pub async fn get_traces(&self, query: &TracesQuery) -> Result<TracesResponse> {
129 let url = format!("{}/api/traces", self.base_url);
130 let response = self.client.get(&url).query(query).send().await?;
131
132 if !response.status().is_success() {
133 return Err(Error::ApiError(format!(
134 "Failed to fetch traces: HTTP {}",
135 response.status()
136 )));
137 }
138
139 Ok(response.json().await?)
140 }
141
142 pub async fn fetch_metrics(&self, params: Vec<(&str, String)>) -> Result<Vec<MetricResponse>> {
143 let url = format!("{}/api/metrics", self.base_url);
144 let response = self.client.get(&url).query(¶ms).send().await?;
145
146 if !response.status().is_success() {
147 return Err(Error::ApiError(format!(
148 "Failed to fetch metrics: HTTP {}",
149 response.status()
150 )));
151 }
152
153 Ok(response.json().await?)
154 }
155
156 pub async fn fetch_metric_by_name(
157 &self,
158 name: &str,
159 params: Vec<(&str, String)>,
160 ) -> Result<Vec<MetricResponse>> {
161 let url = format!("{}/api/metrics", self.base_url);
162 let mut all_params = vec![("name", name.to_string())];
163 all_params.extend(params);
164
165 let response = self.client.get(&url).query(&all_params).send().await?;
166
167 if !response.status().is_success() {
168 return Err(Error::ApiError(format!(
169 "Failed to fetch metric: HTTP {}",
170 response.status()
171 )));
172 }
173
174 let metrics: Vec<MetricResponse> = response.json().await?;
175
176 if metrics.is_empty() {
177 return Err(Error::NotFound(format!("Metric '{}' not found", name)));
178 }
179
180 Ok(metrics)
181 }
182
183 pub async fn export_logs(&self, params: Vec<(&str, String)>) -> Result<String> {
184 let url = format!("{}/api/logs/export", self.base_url);
185 let response = self.client.get(&url).query(¶ms).send().await?;
186
187 if !response.status().is_success() {
188 return Err(Error::ApiError(format!(
189 "Failed to export logs: HTTP {}",
190 response.status()
191 )));
192 }
193
194 Ok(response.text().await?)
195 }
196
197 pub async fn export_traces(&self, params: Vec<(&str, String)>) -> Result<String> {
198 let url = format!("{}/api/traces/export", self.base_url);
199 let response = self.client.get(&url).query(¶ms).send().await?;
200
201 if !response.status().is_success() {
202 return Err(Error::ApiError(format!(
203 "Failed to export traces: HTTP {}",
204 response.status()
205 )));
206 }
207
208 Ok(response.text().await?)
209 }
210
211 pub async fn export_metrics(&self, params: Vec<(&str, String)>) -> Result<String> {
212 let url = format!("{}/api/metrics/export", self.base_url);
213 let response = self.client.get(&url).query(¶ms).send().await?;
214
215 if !response.status().is_success() {
216 return Err(Error::ApiError(format!(
217 "Failed to export metrics: HTTP {}",
218 response.status()
219 )));
220 }
221
222 Ok(response.text().await?)
223 }
224
225 pub async fn health_check(&self) -> Result<bool> {
226 let url = format!("{}/health", self.base_url);
227 match self.client.get(&url).send().await {
228 Ok(response) => Ok(response.status().is_success()),
229 Err(_) => Ok(false),
230 }
231 }
232
233 pub async fn fetch_token_usage(
234 &self,
235 params: Vec<(&str, String)>,
236 ) -> Result<otelite_core::api::TokenUsageResponse> {
237 let url = format!("{}/api/genai/usage", self.base_url);
238 let response = self.client.get(&url).query(¶ms).send().await?;
239 if !response.status().is_success() {
240 return Err(Error::ApiError(format!(
241 "Failed to fetch token usage: HTTP {}",
242 response.status()
243 )));
244 }
245 Ok(response.json().await?)
246 }
247
248 pub async fn fetch_latency_stats(
249 &self,
250 params: Vec<(&str, String)>,
251 ) -> Result<Vec<otelite_core::api::LatencyStats>> {
252 let url = format!("{}/api/genai/latency_stats", self.base_url);
253 let response = self.client.get(&url).query(¶ms).send().await?;
254 if !response.status().is_success() {
255 return Err(Error::ApiError(format!(
256 "Failed to fetch latency stats: HTTP {}",
257 response.status()
258 )));
259 }
260 Ok(response.json().await?)
261 }
262
263 pub async fn fetch_truncation_rate(
264 &self,
265 params: Vec<(&str, String)>,
266 ) -> Result<Vec<otelite_core::api::TruncationRateByModel>> {
267 let url = format!("{}/api/genai/truncation_rate", self.base_url);
268 let response = self.client.get(&url).query(¶ms).send().await?;
269 if !response.status().is_success() {
270 return Err(Error::ApiError(format!(
271 "Failed to fetch truncation rate: HTTP {}",
272 response.status()
273 )));
274 }
275 Ok(response.json().await?)
276 }
277
278 pub async fn fetch_cache_hit_rate(
279 &self,
280 params: Vec<(&str, String)>,
281 ) -> Result<Vec<otelite_core::api::CacheHitRateByModel>> {
282 let url = format!("{}/api/genai/cache_hit_rate", self.base_url);
283 let response = self.client.get(&url).query(¶ms).send().await?;
284 if !response.status().is_success() {
285 return Err(Error::ApiError(format!(
286 "Failed to fetch cache hit rate: HTTP {}",
287 response.status()
288 )));
289 }
290 Ok(response.json().await?)
291 }
292
293 pub async fn fetch_conversation_depth(
294 &self,
295 params: Vec<(&str, String)>,
296 ) -> Result<otelite_core::api::ConversationDepthStats> {
297 let url = format!("{}/api/genai/conversation_depth", self.base_url);
298 let response = self.client.get(&url).query(¶ms).send().await?;
299 if !response.status().is_success() {
300 return Err(Error::ApiError(format!(
301 "Failed to fetch conversation depth: HTTP {}",
302 response.status()
303 )));
304 }
305 Ok(response.json().await?)
306 }
307
308 pub async fn fetch_tool_usage(
309 &self,
310 params: Vec<(&str, String)>,
311 ) -> Result<Vec<otelite_core::api::ToolUsage>> {
312 let url = format!("{}/api/genai/tool_usage", self.base_url);
313 let response = self.client.get(&url).query(¶ms).send().await?;
314 if !response.status().is_success() {
315 return Err(Error::ApiError(format!(
316 "Failed to fetch tool usage: HTTP {}",
317 response.status()
318 )));
319 }
320 Ok(response.json().await?)
321 }
322
323 pub async fn fetch_error_types(
324 &self,
325 params: Vec<(&str, String)>,
326 ) -> Result<Vec<otelite_core::api::ErrorTypeBreakdown>> {
327 let url = format!("{}/api/genai/error_types", self.base_url);
328 let response = self.client.get(&url).query(¶ms).send().await?;
329 if !response.status().is_success() {
330 return Err(Error::ApiError(format!(
331 "Failed to fetch error types: HTTP {}",
332 response.status()
333 )));
334 }
335 Ok(response.json().await?)
336 }
337
338 pub async fn fetch_model_drift(
339 &self,
340 params: Vec<(&str, String)>,
341 ) -> Result<Vec<otelite_core::api::ModelDriftPair>> {
342 let url = format!("{}/api/genai/model_drift", self.base_url);
343 let response = self.client.get(&url).query(¶ms).send().await?;
344 if !response.status().is_success() {
345 return Err(Error::ApiError(format!(
346 "Failed to fetch model drift: HTTP {}",
347 response.status()
348 )));
349 }
350 Ok(response.json().await?)
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357 use crate::error::Error;
358 use mockito::Server;
359
360 #[test]
361 fn test_api_client_creation() {
362 let client = ApiClient::new("http://localhost:8080".to_string(), Duration::from_secs(30));
363 assert!(client.is_ok());
364 }
365
366 #[test]
367 fn test_api_client_invalid_timeout() {
368 let client = ApiClient::new(
369 "http://localhost:8080".to_string(),
370 Duration::from_millis(1),
371 );
372 assert!(client.is_ok());
373 }
374
375 #[tokio::test]
376 async fn test_fetch_logs_success() {
377 let mut server = Server::new_async().await;
378 let mock = server
379 .mock("GET", "/api/logs")
380 .match_query(mockito::Matcher::Any)
381 .with_status(200)
382 .with_header("content-type", "application/json")
383 .with_body(
384 r#"{
385 "logs": [
386 {
387 "timestamp": 1705315800000000000,
388 "severity": "INFO",
389 "severity_text": "INFO",
390 "body": "Test log message",
391 "attributes": {},
392 "resource": null,
393 "trace_id": null,
394 "span_id": null
395 }
396 ],
397 "total": 1,
398 "limit": 10,
399 "offset": 0
400 }"#,
401 )
402 .create_async()
403 .await;
404
405 let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
406 let result = client.fetch_logs(vec![("limit", "10".to_string())]).await;
407
408 mock.assert_async().await;
409 assert!(result.is_ok());
410 let logs = result.unwrap();
411 assert_eq!(logs.logs.len(), 1);
412 assert_eq!(logs.logs[0].timestamp, 1705315800000000000);
413 assert_eq!(logs.logs[0].severity, "INFO");
414 }
415
416 #[tokio::test]
417 async fn test_fetch_logs_empty_response() {
418 let mut server = Server::new_async().await;
419 let mock = server
420 .mock("GET", "/api/logs")
421 .with_status(200)
422 .with_header("content-type", "application/json")
423 .with_body(r#"{"logs": [], "total": 0, "limit": 100, "offset": 0}"#)
424 .create_async()
425 .await;
426
427 let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
428 let result = client.fetch_logs(vec![]).await;
429
430 mock.assert_async().await;
431 assert!(result.is_ok());
432 assert_eq!(result.unwrap().logs.len(), 0);
433 }
434
435 #[tokio::test]
436 async fn test_fetch_logs_server_error() {
437 let mut server = Server::new_async().await;
438 let mock = server
439 .mock("GET", "/api/logs")
440 .with_status(500)
441 .create_async()
442 .await;
443
444 let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
445 let result = client.fetch_logs(vec![]).await;
446
447 mock.assert_async().await;
448 assert!(result.is_err());
449 match result.unwrap_err() {
450 Error::ApiError(msg) => assert!(msg.contains("500")),
451 _ => panic!("Expected ApiError"),
452 }
453 }
454
455 #[tokio::test]
456 async fn test_fetch_log_by_id_success() {
457 let mut server = Server::new_async().await;
458 let mock = server
459 .mock("GET", "/api/logs/1705315800000000000")
460 .with_status(200)
461 .with_header("content-type", "application/json")
462 .with_body(
463 r#"{
464 "timestamp": 1705315800000000000,
465 "severity": "ERROR",
466 "severity_text": "ERROR",
467 "body": "Error occurred",
468 "attributes": {"key": "value"},
469 "resource": null,
470 "trace_id": null,
471 "span_id": null
472 }"#,
473 )
474 .create_async()
475 .await;
476
477 let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
478 let result = client.fetch_log_by_id(1705315800000000000).await;
479
480 mock.assert_async().await;
481 assert!(result.is_ok());
482 let log = result.unwrap();
483 assert_eq!(log.timestamp, 1705315800000000000);
484 assert_eq!(log.severity, "ERROR");
485 assert_eq!(log.body, "Error occurred");
486 }
487
488 #[tokio::test]
489 async fn test_fetch_log_by_id_not_found() {
490 let mut server = Server::new_async().await;
491 let mock = server
492 .mock("GET", "/api/logs/9999999999999999")
493 .with_status(404)
494 .create_async()
495 .await;
496
497 let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
498 let result = client.fetch_log_by_id(9999999999999999).await;
499
500 mock.assert_async().await;
501 assert!(result.is_err());
502 match result.unwrap_err() {
503 Error::NotFound(msg) => assert!(msg.contains("9999999999999999")),
504 _ => panic!("Expected NotFound error"),
505 }
506 }
507
508 #[tokio::test]
509 async fn test_search_logs_success() {
510 let mut server = Server::new_async().await;
511 let mock = server
512 .mock("GET", "/api/logs")
513 .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
514 "search".into(),
515 "error".into(),
516 )]))
517 .with_status(200)
518 .with_header("content-type", "application/json")
519 .with_body(r#"{"logs": [], "total": 0, "limit": 100, "offset": 0}"#)
520 .create_async()
521 .await;
522
523 let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
524 let result = client.search_logs("error", vec![]).await;
525
526 mock.assert_async().await;
527 assert!(result.is_ok());
528 }
529
530 #[tokio::test]
531 async fn test_fetch_traces_success() {
532 let mut server = Server::new_async().await;
533 let mock = server
534 .mock("GET", "/api/traces")
535 .match_query(mockito::Matcher::Any)
536 .with_status(200)
537 .with_header("content-type", "application/json")
538 .with_body(
539 r#"{
540 "traces": [
541 {
542 "trace_id": "trace1",
543 "root_span_name": "http-request",
544 "start_time": 1705315800000000000,
545 "duration": 1500000000,
546 "span_count": 1,
547 "service_names": [],
548 "has_errors": false
549 }
550 ],
551 "total": 1,
552 "limit": 10,
553 "offset": 0
554 }"#,
555 )
556 .create_async()
557 .await;
558
559 let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
560 let result = client.fetch_traces(vec![("limit", "10".to_string())]).await;
561
562 mock.assert_async().await;
563 assert!(result.is_ok());
564 let traces = result.unwrap();
565 assert_eq!(traces.traces.len(), 1);
566 assert_eq!(traces.traces[0].trace_id, "trace1");
567 assert!(!traces.traces[0].has_errors);
568 }
569
570 #[tokio::test]
571 async fn test_fetch_trace_by_id_success() {
572 let mut server = Server::new_async().await;
573 let mock = server
574 .mock("GET", "/api/traces/trace123")
575 .with_status(200)
576 .with_header("content-type", "application/json")
577 .with_body(
578 r#"{
579 "trace_id": "trace123",
580 "spans": [
581 {
582 "span_id": "span1",
583 "trace_id": "trace123",
584 "parent_span_id": null,
585 "name": "database-query",
586 "kind": "Internal",
587 "start_time": 1705315800000000000,
588 "end_time": 1705315800250000000,
589 "duration": 250000000,
590 "attributes": {},
591 "resource": null,
592 "status": {"code": "OK", "message": null},
593 "events": []
594 }
595 ],
596 "start_time": 1705315800000000000,
597 "end_time": 1705315800250000000,
598 "duration": 250000000,
599 "span_count": 1,
600 "service_names": []
601 }"#,
602 )
603 .create_async()
604 .await;
605
606 let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
607 let result = client.fetch_trace_by_id("trace123").await;
608
609 mock.assert_async().await;
610 assert!(result.is_ok());
611 let trace = result.unwrap();
612 assert_eq!(trace.trace_id, "trace123");
613 assert_eq!(trace.spans.len(), 1);
614 }
615
616 #[tokio::test]
617 async fn test_fetch_trace_by_id_not_found() {
618 let mut server = Server::new_async().await;
619 let mock = server
620 .mock("GET", "/api/traces/nonexistent")
621 .with_status(404)
622 .create_async()
623 .await;
624
625 let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
626 let result = client.fetch_trace_by_id("nonexistent").await;
627
628 mock.assert_async().await;
629 assert!(result.is_err());
630 match result.unwrap_err() {
631 Error::NotFound(msg) => assert!(msg.contains("nonexistent")),
632 _ => panic!("Expected NotFound error"),
633 }
634 }
635
636 #[tokio::test]
637 async fn test_fetch_metrics_success() {
638 let mut server = Server::new_async().await;
639 let mock = server
640 .mock("GET", "/api/metrics")
641 .match_query(mockito::Matcher::Any)
642 .with_status(200)
643 .with_header("content-type", "application/json")
644 .with_body(
645 r#"[
646 {
647 "name": "http_requests_total",
648 "description": null,
649 "unit": null,
650 "metric_type": "counter",
651 "value": 1234,
652 "timestamp": 1705315800000000000,
653 "attributes": {},
654 "resource": null
655 }
656 ]"#,
657 )
658 .create_async()
659 .await;
660
661 let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
662 let result = client.fetch_metrics(vec![]).await;
663
664 mock.assert_async().await;
665 assert!(result.is_ok());
666 let metrics = result.unwrap();
667 assert_eq!(metrics.len(), 1);
668 assert_eq!(metrics[0].name, "http_requests_total");
669 }
670
671 #[tokio::test]
672 async fn test_fetch_metric_by_name_not_found() {
673 let mut server = Server::new_async().await;
674 let mock = server
675 .mock("GET", "/api/metrics")
676 .match_query(mockito::Matcher::UrlEncoded(
677 "name".into(),
678 "nonexistent_metric".into(),
679 ))
680 .with_status(200)
681 .with_header("content-type", "application/json")
682 .with_body(r#"[]"#)
683 .create_async()
684 .await;
685
686 let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
687 let result = client
688 .fetch_metric_by_name("nonexistent_metric", vec![])
689 .await;
690
691 mock.assert_async().await;
692 assert!(result.is_err());
693 match result.unwrap_err() {
694 Error::NotFound(msg) => assert!(msg.contains("nonexistent_metric")),
695 _ => panic!("Expected NotFound error"),
696 }
697 }
698
699 #[tokio::test]
700 async fn test_health_check_success() {
701 let mut server = Server::new_async().await;
702 let mock = server
703 .mock("GET", "/health")
704 .with_status(200)
705 .create_async()
706 .await;
707
708 let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
709 let result = client.health_check().await;
710
711 mock.assert_async().await;
712 assert!(result.is_ok());
713 assert!(result.unwrap());
714 }
715
716 #[tokio::test]
717 async fn test_health_check_unreachable() {
718 let client = ApiClient::new(
719 "http://127.0.0.1:19999".to_string(),
720 Duration::from_millis(100),
721 )
722 .unwrap();
723 let result = client.health_check().await;
724 assert!(result.is_ok());
725 assert!(!result.unwrap());
726 }
727}