1use crate::http_client::service;
2use crate::http_client::HttpClient;
3use crate::http_client::ReqwestClient;
4use crate::r#struct::env_config::EnvConfig;
5use crate::r#struct::log_level::LogLevel;
6use std::time::Duration;
7pub use crate::http_client::{LogtailError, RetryConfig};
9pub use crate::r#struct::env_config::EnvEnum;
10pub use crate::r#struct::log_schema::LogSchema;
11pub mod http_client;
12mod r#struct;
13
14pub struct Logger<C: HttpClient = ReqwestClient> {
15 env_config: EnvConfig,
16 client: C,
17 retry_config: RetryConfig,
18}
19
20impl Default for Logger<ReqwestClient> {
21 fn default() -> Self {
22 let env_config = EnvConfig::default();
23 Self {
24 env_config,
25 client: ReqwestClient,
26 retry_config: RetryConfig::default(),
27 }
28 }
29}
30
31impl Logger<ReqwestClient> {
32 pub fn new(app_version: String, verbose: bool) -> Self {
33 let env_config = EnvConfig::new(app_version, verbose);
34 Self {
35 env_config,
36 client: ReqwestClient,
37 retry_config: RetryConfig::default(),
38 }
39 }
40
41 pub fn with_retry(retry_config: RetryConfig) -> Self {
42 let env_config = EnvConfig::default();
43 Self {
44 env_config,
45 client: ReqwestClient,
46 retry_config,
47 }
48 }
49
50 pub fn builder() -> LoggerBuilder {
51 LoggerBuilder::default()
52 }
53}
54
55impl<C: HttpClient> Logger<C> {
56 #[cfg(test)]
57 pub(crate) fn env_config(&self) -> &EnvConfig {
58 &self.env_config
59 }
60
61 #[cfg(test)]
62 pub(crate) fn retry_config(&self) -> &RetryConfig {
63 &self.retry_config
64 }
65
66 #[cfg(test)]
67 pub(crate) fn with_client(env_config: EnvConfig, client: C) -> Self {
68 Self {
69 env_config,
70 client,
71 retry_config: RetryConfig::default(),
72 }
73 }
74
75 #[cfg(test)]
76 pub(crate) fn with_client_and_retry(
77 env_config: EnvConfig,
78 client: C,
79 retry_config: RetryConfig,
80 ) -> Self {
81 Self {
82 env_config,
83 client,
84 retry_config,
85 }
86 }
87
88 pub async fn info(&self, log: LogSchema) -> Result<(), LogtailError> {
89 let env_config = &self.env_config;
90 let better_log = log.to_betterstack(env_config, LogLevel::Info);
91 if better_log.env != EnvEnum::Local {
92 service::push_log_with_retry(&self.client, env_config, &better_log, &self.retry_config)
93 .await?;
94 }
95 if env_config.verbose {
96 println!("{}", better_log);
97 }
98 Ok(())
99 }
100
101 pub async fn warn(&self, log: LogSchema) -> Result<(), LogtailError> {
102 let env_config = &self.env_config;
103 let better_log = log.to_betterstack(&self.env_config, LogLevel::Warn);
104 if better_log.env != EnvEnum::Local {
105 service::push_log_with_retry(&self.client, env_config, &better_log, &self.retry_config)
106 .await?;
107 }
108 if self.env_config.verbose {
109 println!("{}", better_log);
110 }
111 Ok(())
112 }
113
114 pub async fn error(&self, log: LogSchema) -> Result<(), LogtailError> {
115 let env_config = &self.env_config;
116 let better_log = log.to_betterstack(&self.env_config, LogLevel::Error);
117 if better_log.env != EnvEnum::Local {
118 service::push_log_with_retry(&self.client, env_config, &better_log, &self.retry_config)
119 .await?;
120 }
121 if self.env_config.verbose {
122 eprintln!("{}", better_log);
123 }
124 Ok(())
125 }
126
127 pub async fn debug(&self, log: LogSchema) -> Result<(), LogtailError> {
128 let better_log = log.to_betterstack(&self.env_config, LogLevel::Debug);
129 println!("{}", better_log);
130 Ok(())
131 }
132}
133
134#[derive(Default)]
135pub struct LoggerBuilder {
136 app_version: Option<String>,
137 verbose: Option<bool>,
138 environment: Option<EnvEnum>,
139 logs_source_token: Option<String>,
140 retry_config: RetryConfig,
141}
142
143impl LoggerBuilder {
144 pub fn app_version(mut self, app_version: impl Into<String>) -> Self {
145 self.app_version = Some(app_version.into());
146 self
147 }
148
149 pub fn verbose(mut self, verbose: bool) -> Self {
150 self.verbose = Some(verbose);
151 self
152 }
153
154 pub fn environment(mut self, environment: EnvEnum) -> Self {
155 self.environment = Some(environment);
156 self
157 }
158
159 pub fn logs_source_token(mut self, token: impl Into<String>) -> Self {
160 self.logs_source_token = Some(token.into());
161 self
162 }
163
164 pub fn max_retries(mut self, max_retries: u32) -> Self {
165 self.retry_config.max_retries = max_retries;
166 self
167 }
168
169 pub fn base_delay(mut self, base_delay: Duration) -> Self {
170 self.retry_config.base_delay = base_delay;
171 self
172 }
173
174 pub fn max_delay(mut self, max_delay: Duration) -> Self {
175 self.retry_config.max_delay = max_delay;
176 self
177 }
178
179 pub fn jitter(mut self, jitter: bool) -> Self {
180 self.retry_config.jitter = jitter;
181 self
182 }
183
184 pub fn build(self) -> Logger<ReqwestClient> {
185 let env_config = match (self.environment, self.logs_source_token) {
186 (Some(environment), Some(token)) => EnvConfig::from_values(
187 self.app_version
188 .unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()),
189 environment,
190 token,
191 self.verbose.unwrap_or(true),
192 ),
193 _ => {
194 let version = self
195 .app_version
196 .unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string());
197 let verbose = self.verbose.unwrap_or(true);
198 EnvConfig::new(version, verbose)
199 }
200 };
201
202 Logger {
203 env_config,
204 client: ReqwestClient,
205 retry_config: self.retry_config,
206 }
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use crate::http_client::mock::MockHttpClient;
214 use crate::r#struct::env_config::EnvConfig;
215 use std::sync::atomic::Ordering;
216
217 fn qa_config() -> EnvConfig {
218 EnvConfig::from_values("1.0.0".to_string(), EnvEnum::QA, "token".to_string(), false)
219 }
220
221 fn local_config() -> EnvConfig {
222 EnvConfig::from_values(
223 "1.0.0".to_string(),
224 EnvEnum::Local,
225 "token".to_string(),
226 false,
227 )
228 }
229
230 fn test_log() -> LogSchema {
231 LogSchema {
232 message: "test".to_string(),
233 context: "ctx".to_string(),
234 }
235 }
236
237 fn no_retry_config() -> RetryConfig {
238 RetryConfig {
239 max_retries: 0,
240 base_delay: Duration::from_millis(1),
241 max_delay: Duration::from_millis(1),
242 jitter: false,
243 }
244 }
245
246 #[tokio::test]
247 async fn info_sends_info_level() {
248 let mock = MockHttpClient::with_success(None);
249 let logger = Logger::with_client(qa_config(), mock);
250
251 let _ = logger.info(test_log()).await;
252
253 let body = logger.client.captured_body.lock().unwrap().clone().unwrap();
254 assert_eq!(body["level"], "Info");
255 }
256
257 #[tokio::test]
258 async fn warn_sends_warn_level() {
259 let mock = MockHttpClient::with_success(None);
260 let logger = Logger::with_client(qa_config(), mock);
261
262 let _ = logger.warn(test_log()).await;
263
264 let body = logger.client.captured_body.lock().unwrap().clone().unwrap();
265 assert_eq!(body["level"], "Warn");
266 }
267
268 #[tokio::test]
269 async fn error_sends_error_level() {
270 let mock = MockHttpClient::with_success(None);
271 let logger = Logger::with_client(qa_config(), mock);
272
273 let _ = logger.error(test_log()).await;
274
275 let body = logger.client.captured_body.lock().unwrap().clone().unwrap();
276 assert_eq!(body["level"], "Error");
277 }
278
279 #[tokio::test]
280 async fn debug_skips_http() {
281 let mock = MockHttpClient::with_success(None);
282 let logger = Logger::with_client(qa_config(), mock);
283
284 let _ = logger.debug(test_log()).await;
285
286 assert_eq!(logger.client.call_count.load(Ordering::SeqCst), 0);
287 }
288
289 #[tokio::test]
290 async fn local_env_skips_http() {
291 let mock = MockHttpClient::with_success(None);
292 let logger = Logger::with_client(local_config(), mock);
293
294 let _ = logger.info(test_log()).await;
295 let _ = logger.warn(test_log()).await;
296 let _ = logger.error(test_log()).await;
297
298 assert_eq!(logger.client.call_count.load(Ordering::SeqCst), 0);
299 }
300
301 #[tokio::test]
302 async fn non_local_env_sends_http() {
303 let mock = MockHttpClient::with_success(None);
304 let logger = Logger::with_client(qa_config(), mock);
305
306 let _ = logger.info(test_log()).await;
307
308 assert_eq!(logger.client.call_count.load(Ordering::SeqCst), 1);
309 }
310
311 #[tokio::test]
312 async fn info_returns_ok_on_success() {
313 let mock = MockHttpClient::with_success(None);
314 let logger = Logger::with_client_and_retry(qa_config(), mock, no_retry_config());
315
316 let result = logger.info(test_log()).await;
317 assert!(result.is_ok());
318 }
319
320 #[tokio::test]
321 async fn info_returns_err_on_failure() {
322 let mock = MockHttpClient::with_error("server error");
323 let logger = Logger::with_client_and_retry(qa_config(), mock, no_retry_config());
324
325 let result = logger.info(test_log()).await;
326 assert!(result.is_err());
327 }
328
329 #[tokio::test]
330 async fn debug_returns_ok() {
331 let mock = MockHttpClient::with_success(None);
332 let logger = Logger::with_client(qa_config(), mock);
333
334 let result = logger.debug(test_log()).await;
335 assert!(result.is_ok());
336 }
337
338 #[tokio::test]
339 async fn local_env_returns_ok() {
340 let mock = MockHttpClient::with_error("server error");
341 let logger = Logger::with_client(local_config(), mock);
342
343 let result = logger.info(test_log()).await;
344 assert!(result.is_ok());
345 }
346
347 #[test]
350 fn builder_with_explicit_env_and_token() {
351 let logger = Logger::builder()
352 .app_version("2.0.0")
353 .verbose(false)
354 .environment(EnvEnum::Prod)
355 .logs_source_token("my-token")
356 .build();
357
358 assert_eq!(logger.env_config().app_version, "2.0.0");
359 assert!(!logger.env_config().verbose);
360 assert_eq!(logger.env_config().environment, EnvEnum::Prod);
361 assert_eq!(logger.env_config().logs_source_token, "my-token");
362 }
363
364 #[test]
365 fn builder_retry_options() {
366 let logger = Logger::builder()
367 .environment(EnvEnum::QA)
368 .logs_source_token("tok")
369 .max_retries(5)
370 .base_delay(Duration::from_millis(200))
371 .max_delay(Duration::from_millis(800))
372 .jitter(false)
373 .build();
374
375 assert_eq!(logger.retry_config().max_retries, 5);
376 assert_eq!(logger.retry_config().base_delay, Duration::from_millis(200));
377 assert_eq!(logger.retry_config().max_delay, Duration::from_millis(800));
378 assert!(!logger.retry_config().jitter);
379 }
380
381 #[test]
382 fn builder_defaults_without_overrides() {
383 let logger = Logger::builder()
384 .environment(EnvEnum::QA)
385 .logs_source_token("tok")
386 .build();
387
388 assert_eq!(logger.env_config().app_version, env!("CARGO_PKG_VERSION"));
389 assert!(logger.env_config().verbose);
390 assert_eq!(logger.retry_config().max_retries, 3);
391 assert_eq!(logger.retry_config().base_delay, Duration::from_secs(1));
392 assert_eq!(logger.retry_config().max_delay, Duration::from_secs(5));
393 assert!(logger.retry_config().jitter);
394 }
395
396 #[tokio::test]
399 async fn with_retry_uses_custom_retry_config() {
400 let custom = RetryConfig {
401 max_retries: 1,
402 base_delay: Duration::from_millis(1),
403 max_delay: Duration::from_millis(1),
404 jitter: false,
405 };
406 let mock = MockHttpClient::with_sequence(vec![
407 Err((500, "fail".to_string())),
408 Ok(Some(serde_json::json!({"ok": true}))),
409 ]);
410 let logger = Logger::with_client_and_retry(qa_config(), mock, custom);
411
412 let result = logger.info(test_log()).await;
413 assert!(result.is_ok());
414 assert_eq!(logger.client.call_count.load(Ordering::SeqCst), 2);
416 }
417}