Skip to main content

otelite_client/
client.rs

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(&params).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(&params).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(&params).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(&params).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(&params).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(&params).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(&params).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(&params).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(&params).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(&params).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(&params).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(&params).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(&params).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(&params).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}