Skip to main content

logtail_rust/http_client/
service.rs

1use super::HttpClient;
2use crate::r#struct::betterstack_log_schema::BetterStackLogSchema;
3use crate::r#struct::env_config::EnvConfig;
4use reqwest::header::{HeaderMap, HeaderValue};
5use serde_json::Value;
6
7/// Pushes a log to the BetterStack logs server asynchronously and returns a value.
8///
9/// # Arguments
10///
11/// * `client` - The HTTP client to use for sending the request.
12/// * `config` - The configuration of the server.
13/// * `log` - The log to be pushed.
14///
15/// # Returns
16///
17/// * If the log is sent successfully, returns `Some` containing the continuation value.
18/// * If there is an error sending the log, prints the error message and returns `None`.
19pub async fn push_log(
20    client: &impl HttpClient,
21    config: &EnvConfig,
22    log: &BetterStackLogSchema,
23) -> Option<Value> {
24    let logs_url = "https://in.logs.betterstack.com";
25    let bearer_header = bearer_headers(config);
26    let body = serde_json::to_value(log).expect("Failed to serialize log to JSON");
27
28    let http_result = client.post_json(logs_url, &body, Some(bearer_header)).await;
29
30    match http_result {
31        Err(err) => {
32            println!("!!! Error sending log : {}", err);
33            // Ignore the error sending logs, so we can continue
34            // logging errors must not crash the app
35            None
36        }
37        Ok(continuation_value) => Some(continuation_value?),
38    }
39}
40
41/// Generate a bearer header for the given server configuration.
42///
43/// # Parameters
44/// - `server_config`: A reference to the server configuration.
45///
46/// # Returns
47/// The generated bearer header as a `HeaderMap`.
48///
49fn bearer_headers(config: &EnvConfig) -> HeaderMap {
50    let logs_source_token = config.logs_source_token.as_str();
51    let bearer_value_str = format!("Bearer {}", logs_source_token);
52    let bearer_value = &bearer_value_str;
53
54    let mut headers = HeaderMap::new();
55
56    headers.insert(
57        "Authorization",
58        HeaderValue::from_str(bearer_value).unwrap(),
59    );
60    headers.insert(
61        "Content-Type",
62        HeaderValue::from_str("application/json").unwrap(),
63    );
64
65    headers
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use crate::http_client::mock::MockHttpClient;
72    use crate::r#struct::env_config::{EnvConfig, EnvEnum};
73    use crate::r#struct::log_level::LogLevel;
74    use std::sync::atomic::Ordering;
75
76    fn test_config() -> EnvConfig {
77        EnvConfig::from_values(
78            "1.0.0".to_string(),
79            EnvEnum::QA,
80            "test-source-token".to_string(),
81            false,
82        )
83    }
84
85    fn test_log() -> BetterStackLogSchema {
86        BetterStackLogSchema {
87            env: EnvEnum::QA,
88            message: "test message".to_string(),
89            context: "test context".to_string(),
90            level: LogLevel::Info,
91            app_version: "1.0.0".to_string(),
92        }
93    }
94
95    #[tokio::test]
96    async fn calls_correct_url() {
97        let mock = MockHttpClient::with_success(None);
98        push_log(&mock, &test_config(), &test_log()).await;
99
100        let url = mock.captured_url.lock().unwrap().clone().unwrap();
101        assert_eq!(url, "https://in.logs.betterstack.com");
102    }
103
104    #[tokio::test]
105    async fn sends_bearer_header() {
106        let mock = MockHttpClient::with_success(None);
107        push_log(&mock, &test_config(), &test_log()).await;
108
109        let headers = mock.captured_headers.lock().unwrap().clone().unwrap();
110        assert_eq!(
111            headers.get("Authorization").unwrap().to_str().unwrap(),
112            "Bearer test-source-token"
113        );
114    }
115
116    #[tokio::test]
117    async fn sends_content_type_json() {
118        let mock = MockHttpClient::with_success(None);
119        push_log(&mock, &test_config(), &test_log()).await;
120
121        let headers = mock.captured_headers.lock().unwrap().clone().unwrap();
122        assert_eq!(
123            headers.get("Content-Type").unwrap().to_str().unwrap(),
124            "application/json"
125        );
126    }
127
128    #[tokio::test]
129    async fn sends_serialized_log_body() {
130        let mock = MockHttpClient::with_success(None);
131        push_log(&mock, &test_config(), &test_log()).await;
132
133        let body = mock.captured_body.lock().unwrap().clone().unwrap();
134        assert_eq!(body["message"], "test message");
135        assert_eq!(body["context"], "test context");
136        assert_eq!(body["level"], "Info");
137        assert_eq!(body["env"], "QA");
138        assert_eq!(body["app_version"], "1.0.0");
139    }
140
141    #[tokio::test]
142    async fn returns_some_on_success() {
143        let response = serde_json::json!({"status": "ok"});
144        let mock = MockHttpClient::with_success(Some(response.clone()));
145
146        let result = push_log(&mock, &test_config(), &test_log()).await;
147        assert_eq!(result.unwrap(), response);
148    }
149
150    #[tokio::test]
151    async fn returns_none_on_error() {
152        let mock = MockHttpClient::with_error("connection refused");
153
154        let result = push_log(&mock, &test_config(), &test_log()).await;
155        assert!(result.is_none());
156    }
157
158    #[tokio::test]
159    async fn returns_none_on_empty_body() {
160        let mock = MockHttpClient::with_success(None);
161
162        let result = push_log(&mock, &test_config(), &test_log()).await;
163        assert!(result.is_none());
164        assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
165    }
166}