scouter_client/http/
base.rs

1// we redifine HTTPClient here because the scouterClient needs a blocking httpclient, unlike the producer enum
2
3use crate::error::ClientError;
4use reqwest::blocking::{Client, Response};
5use reqwest::header::AUTHORIZATION;
6use reqwest::header::{HeaderMap, HeaderValue};
7use scouter_settings::http::HttpConfig;
8use scouter_types::http::{JwtToken, RequestType, Routes};
9use serde_json::Value;
10use std::sync::Arc;
11use std::sync::RwLock;
12use tracing::{debug, error, instrument};
13
14const TIMEOUT_SECS: u64 = 60;
15
16/// Create a new HTTP client that can be shared across different clients
17pub fn build_http_client(settings: &HttpConfig) -> Result<Client, ClientError> {
18    let mut headers = HeaderMap::new();
19
20    headers.insert("Username", HeaderValue::from_str(&settings.username)?);
21
22    headers.insert("Password", HeaderValue::from_str(&settings.password)?);
23
24    let client_builder = Client::builder().timeout(std::time::Duration::from_secs(TIMEOUT_SECS));
25    let client = client_builder.default_headers(headers).build()?;
26    Ok(client)
27}
28
29#[derive(Debug, Clone)]
30pub struct HttpClient {
31    client: Client,
32    base_path: String,
33    auth_token: Arc<RwLock<String>>,
34}
35
36impl HttpClient {
37    pub fn new(config: HttpConfig) -> Result<Self, ClientError> {
38        let client = build_http_client(&config)?;
39        debug!("HttpClient created with base path: {}", config.server_uri);
40
41        let api_client = HttpClient {
42            client,
43            auth_token: Arc::new(RwLock::new(String::new())),
44            base_path: format!("{}/{}", config.server_uri, "scouter"),
45        };
46
47        api_client.refresh_token()?;
48
49        debug!(
50            "HTTPClient initialized with base path: {}",
51            api_client.base_path
52        );
53        Ok(api_client)
54    }
55
56    #[instrument(skip_all)]
57    fn refresh_token(&self) -> Result<(), ClientError> {
58        let url = format!("{}/{}", self.base_path, Routes::AuthLogin.as_str());
59        debug!("Getting JWT token from {}", url);
60
61        let response = self.client.get(url).send()?;
62
63        // check if unauthorized
64        if response.status().is_client_error() {
65            error!("Unauthorized login request");
66            return Err(ClientError::Unauthorized);
67        }
68
69        // Try to parse as JSON
70        let token = response.json::<JwtToken>().map_err(|e| {
71            error!("Failed to parse response as JSON: {}", e);
72            ClientError::ParseJwtTokenError(e.to_string())
73        })?;
74
75        if let Ok(mut token_guard) = self.auth_token.write() {
76            *token_guard = token.token;
77        } else {
78            error!("Failed to acquire write lock for token update");
79            return Err(ClientError::UpdateAuthTokenError);
80        }
81
82        Ok(())
83    }
84
85    fn update_token_from_response(&self, response: &Response) {
86        if let Some(new_token) = response
87            .headers()
88            .get(AUTHORIZATION)
89            .and_then(|h| h.to_str().ok())
90            .and_then(|h| h.strip_prefix("Bearer "))
91        {
92            match self.auth_token.write() {
93                Ok(mut token_guard) => {
94                    *token_guard = new_token.to_string();
95                }
96                Err(e) => {
97                    error!("Failed to acquire write lock for jwt token update: {}", e);
98                }
99            }
100        }
101    }
102
103    fn get_current_token(&self) -> String {
104        match self.auth_token.read() {
105            Ok(token_guard) => token_guard.clone(),
106            Err(e) => {
107                error!("Failed to acquire read lock for token: {}", e);
108                "".to_string()
109            }
110        }
111    }
112
113    fn _request(
114        &self,
115        route: Routes,
116        request_type: RequestType,
117        body_params: Option<Value>,
118        query_string: Option<String>,
119        headers: Option<HeaderMap>,
120    ) -> Result<Response, ClientError> {
121        let headers = headers.unwrap_or_default();
122
123        let url = format!("{}/{}", self.base_path, route.as_str());
124        let response = match request_type {
125            RequestType::Get => {
126                let url = if let Some(query_string) = query_string {
127                    format!("{url}?{query_string}")
128                } else {
129                    url
130                };
131
132                self.client
133                    .get(url)
134                    .headers(headers)
135                    .bearer_auth(self.get_current_token())
136                    .send()?
137            }
138            RequestType::Post => self
139                .client
140                .post(url)
141                .headers(headers)
142                .json(&body_params)
143                .bearer_auth(self.get_current_token())
144                .send()?,
145            RequestType::Put => self
146                .client
147                .put(url)
148                .headers(headers)
149                .json(&body_params)
150                .bearer_auth(self.get_current_token())
151                .send()?,
152            RequestType::Delete => {
153                let url = if let Some(query_string) = query_string {
154                    format!("{url}?{query_string}")
155                } else {
156                    url
157                };
158                self.client
159                    .delete(url)
160                    .headers(headers)
161                    .bearer_auth(self.get_current_token())
162                    .send()?
163            }
164        };
165
166        Ok(response)
167    }
168
169    pub fn request(
170        &self,
171        route: Routes,
172        request_type: RequestType,
173        body_params: Option<Value>,
174        query_params: Option<String>,
175        headers: Option<HeaderMap>,
176    ) -> Result<Response, ClientError> {
177        let response = self._request(
178            route.clone(),
179            request_type,
180            body_params,
181            query_params,
182            headers,
183        )?;
184
185        // Check and update token if a new one was provided
186        self.update_token_from_response(&response);
187
188        Ok(response)
189    }
190}