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
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use crate::error::Error;
238    use mockito::Server;
239
240    #[test]
241    fn test_api_client_creation() {
242        let client = ApiClient::new("http://localhost:8080".to_string(), Duration::from_secs(30));
243        assert!(client.is_ok());
244    }
245
246    #[test]
247    fn test_api_client_invalid_timeout() {
248        let client = ApiClient::new(
249            "http://localhost:8080".to_string(),
250            Duration::from_millis(1),
251        );
252        assert!(client.is_ok());
253    }
254
255    #[tokio::test]
256    async fn test_fetch_logs_success() {
257        let mut server = Server::new_async().await;
258        let mock = server
259            .mock("GET", "/api/logs")
260            .match_query(mockito::Matcher::Any)
261            .with_status(200)
262            .with_header("content-type", "application/json")
263            .with_body(
264                r#"{
265                "logs": [
266                {
267                    "timestamp": 1705315800000000000,
268                    "severity": "INFO",
269                    "severity_text": "INFO",
270                    "body": "Test log message",
271                    "attributes": {},
272                    "resource": null,
273                    "trace_id": null,
274                    "span_id": null
275                }
276                ],
277                "total": 1,
278                "limit": 10,
279                "offset": 0
280            }"#,
281            )
282            .create_async()
283            .await;
284
285        let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
286        let result = client.fetch_logs(vec![("limit", "10".to_string())]).await;
287
288        mock.assert_async().await;
289        assert!(result.is_ok());
290        let logs = result.unwrap();
291        assert_eq!(logs.logs.len(), 1);
292        assert_eq!(logs.logs[0].timestamp, 1705315800000000000);
293        assert_eq!(logs.logs[0].severity, "INFO");
294    }
295
296    #[tokio::test]
297    async fn test_fetch_logs_empty_response() {
298        let mut server = Server::new_async().await;
299        let mock = server
300            .mock("GET", "/api/logs")
301            .with_status(200)
302            .with_header("content-type", "application/json")
303            .with_body(r#"{"logs": [], "total": 0, "limit": 100, "offset": 0}"#)
304            .create_async()
305            .await;
306
307        let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
308        let result = client.fetch_logs(vec![]).await;
309
310        mock.assert_async().await;
311        assert!(result.is_ok());
312        assert_eq!(result.unwrap().logs.len(), 0);
313    }
314
315    #[tokio::test]
316    async fn test_fetch_logs_server_error() {
317        let mut server = Server::new_async().await;
318        let mock = server
319            .mock("GET", "/api/logs")
320            .with_status(500)
321            .create_async()
322            .await;
323
324        let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
325        let result = client.fetch_logs(vec![]).await;
326
327        mock.assert_async().await;
328        assert!(result.is_err());
329        match result.unwrap_err() {
330            Error::ApiError(msg) => assert!(msg.contains("500")),
331            _ => panic!("Expected ApiError"),
332        }
333    }
334
335    #[tokio::test]
336    async fn test_fetch_log_by_id_success() {
337        let mut server = Server::new_async().await;
338        let mock = server
339            .mock("GET", "/api/logs/1705315800000000000")
340            .with_status(200)
341            .with_header("content-type", "application/json")
342            .with_body(
343                r#"{
344                "timestamp": 1705315800000000000,
345                "severity": "ERROR",
346                "severity_text": "ERROR",
347                "body": "Error occurred",
348                "attributes": {"key": "value"},
349                "resource": null,
350                "trace_id": null,
351                "span_id": null
352            }"#,
353            )
354            .create_async()
355            .await;
356
357        let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
358        let result = client.fetch_log_by_id(1705315800000000000).await;
359
360        mock.assert_async().await;
361        assert!(result.is_ok());
362        let log = result.unwrap();
363        assert_eq!(log.timestamp, 1705315800000000000);
364        assert_eq!(log.severity, "ERROR");
365        assert_eq!(log.body, "Error occurred");
366    }
367
368    #[tokio::test]
369    async fn test_fetch_log_by_id_not_found() {
370        let mut server = Server::new_async().await;
371        let mock = server
372            .mock("GET", "/api/logs/9999999999999999")
373            .with_status(404)
374            .create_async()
375            .await;
376
377        let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
378        let result = client.fetch_log_by_id(9999999999999999).await;
379
380        mock.assert_async().await;
381        assert!(result.is_err());
382        match result.unwrap_err() {
383            Error::NotFound(msg) => assert!(msg.contains("9999999999999999")),
384            _ => panic!("Expected NotFound error"),
385        }
386    }
387
388    #[tokio::test]
389    async fn test_search_logs_success() {
390        let mut server = Server::new_async().await;
391        let mock = server
392            .mock("GET", "/api/logs")
393            .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
394                "search".into(),
395                "error".into(),
396            )]))
397            .with_status(200)
398            .with_header("content-type", "application/json")
399            .with_body(r#"{"logs": [], "total": 0, "limit": 100, "offset": 0}"#)
400            .create_async()
401            .await;
402
403        let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
404        let result = client.search_logs("error", vec![]).await;
405
406        mock.assert_async().await;
407        assert!(result.is_ok());
408    }
409
410    #[tokio::test]
411    async fn test_fetch_traces_success() {
412        let mut server = Server::new_async().await;
413        let mock = server
414            .mock("GET", "/api/traces")
415            .match_query(mockito::Matcher::Any)
416            .with_status(200)
417            .with_header("content-type", "application/json")
418            .with_body(
419                r#"{
420                "traces": [
421                {
422                    "trace_id": "trace1",
423                    "root_span_name": "http-request",
424                    "start_time": 1705315800000000000,
425                    "duration": 1500000000,
426                    "span_count": 1,
427                    "service_names": [],
428                    "has_errors": false
429                }
430                ],
431                "total": 1,
432                "limit": 10,
433                "offset": 0
434            }"#,
435            )
436            .create_async()
437            .await;
438
439        let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
440        let result = client.fetch_traces(vec![("limit", "10".to_string())]).await;
441
442        mock.assert_async().await;
443        assert!(result.is_ok());
444        let traces = result.unwrap();
445        assert_eq!(traces.traces.len(), 1);
446        assert_eq!(traces.traces[0].trace_id, "trace1");
447        assert!(!traces.traces[0].has_errors);
448    }
449
450    #[tokio::test]
451    async fn test_fetch_trace_by_id_success() {
452        let mut server = Server::new_async().await;
453        let mock = server
454            .mock("GET", "/api/traces/trace123")
455            .with_status(200)
456            .with_header("content-type", "application/json")
457            .with_body(
458                r#"{
459                "trace_id": "trace123",
460                "spans": [
461                    {
462                        "span_id": "span1",
463                        "trace_id": "trace123",
464                        "parent_span_id": null,
465                        "name": "database-query",
466                        "kind": "Internal",
467                        "start_time": 1705315800000000000,
468                        "end_time": 1705315800250000000,
469                        "duration": 250000000,
470                        "attributes": {},
471                        "resource": null,
472                        "status": {"code": "OK", "message": null},
473                        "events": []
474                    }
475                ],
476                "start_time": 1705315800000000000,
477                "end_time": 1705315800250000000,
478                "duration": 250000000,
479                "span_count": 1,
480                "service_names": []
481            }"#,
482            )
483            .create_async()
484            .await;
485
486        let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
487        let result = client.fetch_trace_by_id("trace123").await;
488
489        mock.assert_async().await;
490        assert!(result.is_ok());
491        let trace = result.unwrap();
492        assert_eq!(trace.trace_id, "trace123");
493        assert_eq!(trace.spans.len(), 1);
494    }
495
496    #[tokio::test]
497    async fn test_fetch_trace_by_id_not_found() {
498        let mut server = Server::new_async().await;
499        let mock = server
500            .mock("GET", "/api/traces/nonexistent")
501            .with_status(404)
502            .create_async()
503            .await;
504
505        let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
506        let result = client.fetch_trace_by_id("nonexistent").await;
507
508        mock.assert_async().await;
509        assert!(result.is_err());
510        match result.unwrap_err() {
511            Error::NotFound(msg) => assert!(msg.contains("nonexistent")),
512            _ => panic!("Expected NotFound error"),
513        }
514    }
515
516    #[tokio::test]
517    async fn test_fetch_metrics_success() {
518        let mut server = Server::new_async().await;
519        let mock = server
520            .mock("GET", "/api/metrics")
521            .match_query(mockito::Matcher::Any)
522            .with_status(200)
523            .with_header("content-type", "application/json")
524            .with_body(
525                r#"[
526                {
527                    "name": "http_requests_total",
528                    "description": null,
529                    "unit": null,
530                    "metric_type": "counter",
531                    "value": 1234,
532                    "timestamp": 1705315800000000000,
533                    "attributes": {},
534                    "resource": null
535                }
536            ]"#,
537            )
538            .create_async()
539            .await;
540
541        let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
542        let result = client.fetch_metrics(vec![]).await;
543
544        mock.assert_async().await;
545        assert!(result.is_ok());
546        let metrics = result.unwrap();
547        assert_eq!(metrics.len(), 1);
548        assert_eq!(metrics[0].name, "http_requests_total");
549    }
550
551    #[tokio::test]
552    async fn test_fetch_metric_by_name_not_found() {
553        let mut server = Server::new_async().await;
554        let mock = server
555            .mock("GET", "/api/metrics")
556            .match_query(mockito::Matcher::UrlEncoded(
557                "name".into(),
558                "nonexistent_metric".into(),
559            ))
560            .with_status(200)
561            .with_header("content-type", "application/json")
562            .with_body(r#"[]"#)
563            .create_async()
564            .await;
565
566        let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
567        let result = client
568            .fetch_metric_by_name("nonexistent_metric", vec![])
569            .await;
570
571        mock.assert_async().await;
572        assert!(result.is_err());
573        match result.unwrap_err() {
574            Error::NotFound(msg) => assert!(msg.contains("nonexistent_metric")),
575            _ => panic!("Expected NotFound error"),
576        }
577    }
578
579    #[tokio::test]
580    async fn test_health_check_success() {
581        let mut server = Server::new_async().await;
582        let mock = server
583            .mock("GET", "/health")
584            .with_status(200)
585            .create_async()
586            .await;
587
588        let client = ApiClient::new(server.url(), Duration::from_secs(30)).unwrap();
589        let result = client.health_check().await;
590
591        mock.assert_async().await;
592        assert!(result.is_ok());
593        assert!(result.unwrap());
594    }
595
596    #[tokio::test]
597    async fn test_health_check_unreachable() {
598        let client = ApiClient::new(
599            "http://127.0.0.1:19999".to_string(),
600            Duration::from_millis(100),
601        )
602        .unwrap();
603        let result = client.health_check().await;
604        assert!(result.is_ok());
605        assert!(!result.unwrap());
606    }
607}