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
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}