1use crate::error::{BetterstackError, Result};
3use std::sync::Arc;
4use std::time::Duration;
5
6pub const DEFAULT_ENDPOINT: &str = "https://in.logs.betterstack.com/";
8
9pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
11
12pub const DEFAULT_BATCH_SIZE: usize = 10;
14
15pub const DEFAULT_BATCH_DELAY: Duration = Duration::from_secs(2);
17
18pub const DEFAULT_CHANNEL_CAPACITY: usize = 1000;
20
21pub const MAX_LOG_RECORD_SIZE: usize = 1_048_576; pub const MAX_BATCH_SIZE_UNCOMPRESSED: usize = 10_485_760; pub const RECOMMENDED_LOG_SIZE: usize = 102_400; #[derive(Clone)]
32pub struct BetterstackConfig {
33 pub(crate) token: String,
35
36 pub(crate) endpoint: String,
38
39 pub(crate) timeout: Duration,
41
42 pub(crate) batch_size: usize,
44
45 pub(crate) batch_delay: Duration,
47
48 pub(crate) channel_capacity: usize,
50
51 pub(crate) include_span_context: bool,
53
54 pub(crate) logger_name: String,
56
57 pub(crate) logger_version: String,
59
60 pub(crate) on_error: Option<Arc<dyn Fn(BetterstackError) + Send + Sync>>,
62}
63
64impl BetterstackConfig {
65 pub fn token(&self) -> &str {
67 &self.token
68 }
69
70 pub fn endpoint(&self) -> &str {
72 &self.endpoint
73 }
74
75 pub fn timeout(&self) -> Duration {
77 self.timeout
78 }
79
80 pub fn batch_size(&self) -> usize {
82 self.batch_size
83 }
84
85 pub fn batch_delay(&self) -> Duration {
87 self.batch_delay
88 }
89
90 pub fn channel_capacity(&self) -> usize {
92 self.channel_capacity
93 }
94
95 pub fn include_span_context(&self) -> bool {
97 self.include_span_context
98 }
99
100 pub fn logger_name(&self) -> &str {
102 &self.logger_name
103 }
104
105 pub fn logger_version(&self) -> &str {
107 &self.logger_version
108 }
109
110 pub(crate) fn handle_error(&self, error: BetterstackError) {
112 if let Some(ref callback) = self.on_error {
113 callback(error);
114 }
115 }
116}
117
118pub struct BetterstackConfigBuilder {
120 token: String,
121 endpoint: Option<String>,
122 timeout: Option<Duration>,
123 batch_size: Option<usize>,
124 batch_delay: Option<Duration>,
125 channel_capacity: Option<usize>,
126 include_span_context: Option<bool>,
127 logger_name: Option<String>,
128 logger_version: Option<String>,
129 on_error: Option<Arc<dyn Fn(BetterstackError) + Send + Sync>>,
130}
131
132impl BetterstackConfigBuilder {
133 pub fn new(token: impl Into<String>) -> Self {
135 Self {
136 token: token.into(),
137 endpoint: None,
138 timeout: None,
139 batch_size: None,
140 batch_delay: None,
141 channel_capacity: None,
142 include_span_context: None,
143 logger_name: None,
144 logger_version: None,
145 on_error: None,
146 }
147 }
148
149 pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
153 self.endpoint = Some(endpoint.into());
154 self
155 }
156
157 pub fn timeout(mut self, timeout: Duration) -> Self {
161 self.timeout = Some(timeout);
162 self
163 }
164
165 pub fn batch_size(mut self, size: usize) -> Self {
169 self.batch_size = Some(size);
170 self
171 }
172
173 pub fn batch_delay(mut self, delay: Duration) -> Self {
177 self.batch_delay = Some(delay);
178 self
179 }
180
181 pub fn channel_capacity(mut self, capacity: usize) -> Self {
185 self.channel_capacity = Some(capacity);
186 self
187 }
188
189 pub fn include_span_context(mut self, include: bool) -> Self {
193 self.include_span_context = Some(include);
194 self
195 }
196
197 pub fn logger_name(mut self, name: impl Into<String>) -> Self {
201 self.logger_name = Some(name.into());
202 self
203 }
204
205 pub fn logger_version(mut self, version: impl Into<String>) -> Self {
209 self.logger_version = Some(version.into());
210 self
211 }
212
213 pub fn on_error<F>(mut self, callback: F) -> Self
218 where
219 F: Fn(BetterstackError) + Send + Sync + 'static,
220 {
221 self.on_error = Some(Arc::new(callback));
222 self
223 }
224
225 pub fn build(self) -> Result<BetterstackConfig> {
235 if self.token.is_empty() {
237 return Err(BetterstackError::ConfigError(
238 "Token cannot be empty".to_string(),
239 ));
240 }
241
242 let batch_size = self.batch_size.unwrap_or(DEFAULT_BATCH_SIZE);
243 if batch_size == 0 {
244 return Err(BetterstackError::ConfigError(
245 "Batch size must be greater than 0".to_string(),
246 ));
247 }
248
249 let channel_capacity = self.channel_capacity.unwrap_or(DEFAULT_CHANNEL_CAPACITY);
250 if channel_capacity == 0 {
251 return Err(BetterstackError::ConfigError(
252 "Channel capacity must be greater than 0".to_string(),
253 ));
254 }
255
256 let timeout = self.timeout.unwrap_or(DEFAULT_TIMEOUT);
257 if timeout.is_zero() {
258 return Err(BetterstackError::ConfigError(
259 "Timeout must be greater than 0".to_string(),
260 ));
261 }
262
263 Ok(BetterstackConfig {
264 token: self.token,
265 endpoint: self
266 .endpoint
267 .unwrap_or_else(|| DEFAULT_ENDPOINT.to_string()),
268 timeout,
269 batch_size,
270 batch_delay: self.batch_delay.unwrap_or(DEFAULT_BATCH_DELAY),
271 channel_capacity,
272 include_span_context: self.include_span_context.unwrap_or(true),
273 logger_name: self
274 .logger_name
275 .unwrap_or_else(|| env!("CARGO_PKG_NAME").to_string()),
276 logger_version: self
277 .logger_version
278 .unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()),
279 on_error: self.on_error,
280 })
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn test_builder_with_defaults() {
290 let config = BetterstackConfigBuilder::new("test-token")
291 .build()
292 .expect("config should build");
293
294 assert_eq!(config.token(), "test-token");
295 assert_eq!(config.endpoint(), DEFAULT_ENDPOINT);
296 assert_eq!(config.timeout(), DEFAULT_TIMEOUT);
297 assert_eq!(config.batch_size(), DEFAULT_BATCH_SIZE);
298 assert_eq!(config.batch_delay(), DEFAULT_BATCH_DELAY);
299 assert_eq!(config.channel_capacity(), DEFAULT_CHANNEL_CAPACITY);
300 assert!(config.include_span_context());
301 }
302
303 #[test]
304 fn test_builder_with_custom_values() {
305 let config = BetterstackConfigBuilder::new("test-token")
306 .endpoint("https://custom.endpoint.com/")
307 .timeout(Duration::from_secs(5))
308 .batch_size(50)
309 .batch_delay(Duration::from_secs(10))
310 .channel_capacity(500)
311 .include_span_context(false)
312 .logger_name("custom-logger")
313 .logger_version("1.2.3")
314 .build()
315 .expect("config should build");
316
317 assert_eq!(config.endpoint(), "https://custom.endpoint.com/");
318 assert_eq!(config.timeout(), Duration::from_secs(5));
319 assert_eq!(config.batch_size(), 50);
320 assert_eq!(config.batch_delay(), Duration::from_secs(10));
321 assert_eq!(config.channel_capacity(), 500);
322 assert!(!config.include_span_context());
323 assert_eq!(config.logger_name(), "custom-logger");
324 assert_eq!(config.logger_version(), "1.2.3");
325 }
326
327 #[test]
328 fn test_builder_empty_token_fails() {
329 let result = BetterstackConfigBuilder::new("").build();
330 assert!(result.is_err());
331 assert!(matches!(result, Err(BetterstackError::ConfigError(_))));
332 }
333
334 #[test]
335 fn test_builder_zero_batch_size_fails() {
336 let result = BetterstackConfigBuilder::new("token").batch_size(0).build();
337 assert!(result.is_err());
338 }
339
340 #[test]
341 fn test_builder_zero_channel_capacity_fails() {
342 let result = BetterstackConfigBuilder::new("token")
343 .channel_capacity(0)
344 .build();
345 assert!(result.is_err());
346 }
347
348 #[test]
349 fn test_builder_zero_timeout_fails() {
350 let result = BetterstackConfigBuilder::new("token")
351 .timeout(Duration::ZERO)
352 .build();
353 assert!(result.is_err());
354 }
355
356 #[test]
357 fn test_error_callback() {
358 use std::sync::Arc;
359 use std::sync::atomic::{AtomicBool, Ordering};
360
361 let called = Arc::new(AtomicBool::new(false));
362 let called_clone = called.clone();
363
364 let config = BetterstackConfigBuilder::new("token")
365 .on_error(move |_err| {
366 called_clone.store(true, Ordering::SeqCst);
367 })
368 .build()
369 .expect("config should build");
370
371 config.handle_error(BetterstackError::ConfigError("test".to_string()));
372 assert!(called.load(Ordering::SeqCst));
373 }
374
375 #[test]
376 fn test_config_with_extreme_values() {
377 let config = BetterstackConfigBuilder::new("test-token")
379 .batch_size(usize::MAX / 2) .batch_delay(Duration::from_secs(3600 * 24)) .channel_capacity(100_000) .timeout(Duration::from_secs(600)) .build()
384 .expect("config with extreme values should build");
385
386 assert_eq!(config.batch_size(), usize::MAX / 2);
387 assert_eq!(config.batch_delay(), Duration::from_secs(3600 * 24));
388 assert_eq!(config.channel_capacity(), 100_000);
389 assert_eq!(config.timeout(), Duration::from_secs(600));
390 }
391
392 #[test]
393 fn test_config_with_minimal_non_zero_values() {
394 let config = BetterstackConfigBuilder::new("test-token")
396 .batch_size(1) .batch_delay(Duration::from_millis(1)) .channel_capacity(1) .timeout(Duration::from_millis(1)) .build()
401 .expect("config with minimal values should build");
402
403 assert_eq!(config.batch_size(), 1);
404 assert_eq!(config.batch_delay(), Duration::from_millis(1));
405 assert_eq!(config.channel_capacity(), 1);
406 assert_eq!(config.timeout(), Duration::from_millis(1));
407 }
408
409 #[test]
410 fn test_config_clone_preserves_all_fields() {
411 use std::sync::atomic::{AtomicUsize, Ordering};
413
414 let call_count = Arc::new(AtomicUsize::new(0));
415 let call_count_clone = call_count.clone();
416
417 let config1 = BetterstackConfigBuilder::new("test-token")
418 .endpoint("https://custom.endpoint.com/")
419 .timeout(Duration::from_secs(15))
420 .batch_size(42)
421 .batch_delay(Duration::from_secs(3))
422 .channel_capacity(2000)
423 .include_span_context(false)
424 .logger_name("custom-name")
425 .logger_version("2.0.0")
426 .on_error(move |_err| {
427 call_count_clone.fetch_add(1, Ordering::SeqCst);
428 })
429 .build()
430 .expect("config should build");
431
432 let config2 = config1.clone();
434
435 assert_eq!(config1.token(), config2.token());
437 assert_eq!(config1.endpoint(), config2.endpoint());
438 assert_eq!(config1.timeout(), config2.timeout());
439 assert_eq!(config1.batch_size(), config2.batch_size());
440 assert_eq!(config1.batch_delay(), config2.batch_delay());
441 assert_eq!(config1.channel_capacity(), config2.channel_capacity());
442 assert_eq!(
443 config1.include_span_context(),
444 config2.include_span_context()
445 );
446 assert_eq!(config1.logger_name(), config2.logger_name());
447 assert_eq!(config1.logger_version(), config2.logger_version());
448
449 config1.handle_error(BetterstackError::ConfigError("test1".to_string()));
451 assert_eq!(call_count.load(Ordering::SeqCst), 1);
452
453 config2.handle_error(BetterstackError::ConfigError("test2".to_string()));
454 assert_eq!(call_count.load(Ordering::SeqCst), 2);
455 }
456}