Skip to main content

libdd_crashtracker/shared/configuration/
builder.rs

1// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3use crate::{default_signals, shared::constants, signal_from_signum};
4use alloc::borrow::Cow;
5use core::time::Duration;
6use libdd_common::Endpoint;
7
8use super::{default_max_threads, CrashtrackerConfiguration, StacktraceCollection};
9
10#[derive(Debug, Default)]
11pub struct CrashtrackerConfigurationBuilder {
12    additional_files: Vec<String>,
13    collect_all_threads: bool,
14    create_alt_stack: bool,
15    demangle_names: bool,
16    endpoint_url: Option<String>,
17    endpoint_api_key: Option<String>,
18    endpoint_timeout_ms: Option<u64>,
19    endpoint_test_token: Option<String>,
20    endpoint_use_system_resolver: bool,
21    max_threads: Option<usize>,
22    resolve_frames: StacktraceCollection,
23    signals: Vec<i32>,
24    timeout: Option<Duration>,
25    unix_socket_path: Option<String>,
26    #[cfg(unix)]
27    unix_socket_connector: Option<fn(&str) -> std::os::fd::RawFd>,
28    use_alt_stack: bool,
29}
30
31impl CrashtrackerConfigurationBuilder {
32    pub fn additional_files(mut self, files: Vec<String>) -> Self {
33        self.additional_files = files;
34        self
35    }
36
37    pub fn collect_all_threads(mut self, collect: bool) -> Self {
38        self.collect_all_threads = collect;
39        self
40    }
41
42    pub fn create_alt_stack(mut self, create: bool) -> Self {
43        self.create_alt_stack = create;
44        self
45    }
46
47    pub fn use_alt_stack(mut self, use_it: bool) -> Self {
48        self.use_alt_stack = use_it;
49        self
50    }
51
52    pub fn demangle_names(mut self, demangle: bool) -> Self {
53        self.demangle_names = demangle;
54        self
55    }
56
57    pub fn endpoint_url(mut self, url: &str) -> Self {
58        if !url.is_empty() {
59            self.endpoint_url = Some(url.to_string());
60        }
61        self
62    }
63
64    pub fn endpoint_api_key(mut self, api_key: &str) -> Self {
65        self.endpoint_api_key = Some(api_key.to_string());
66        self
67    }
68
69    pub fn endpoint_timeout_ms(mut self, timeout_ms: u64) -> Self {
70        self.endpoint_timeout_ms = Some(timeout_ms);
71        self
72    }
73
74    pub fn endpoint_test_token(mut self, test_token: &str) -> Self {
75        self.endpoint_test_token = Some(test_token.to_string());
76        self
77    }
78
79    pub fn endpoint_use_system_resolver(mut self, use_system_resolver: bool) -> Self {
80        self.endpoint_use_system_resolver = use_system_resolver;
81        self
82    }
83
84    pub fn max_threads(mut self, max: usize) -> Self {
85        self.max_threads = Some(max);
86        self
87    }
88
89    pub fn resolve_frames(mut self, resolve: StacktraceCollection) -> Self {
90        self.resolve_frames = resolve;
91        self
92    }
93
94    pub fn signals(mut self, signals: Vec<i32>) -> Self {
95        self.signals = signals;
96        self
97    }
98
99    pub fn timeout(mut self, timeout: Duration) -> Self {
100        self.timeout = Some(timeout);
101        self
102    }
103
104    pub fn unix_socket_path(mut self, path: String) -> Self {
105        self.unix_socket_path = Some(path);
106        self
107    }
108
109    pub fn unix_socket_connector(mut self, connector: fn(&str) -> std::os::fd::RawFd) -> Self {
110        self.unix_socket_connector = Some(connector);
111        self
112    }
113
114    pub fn build(self) -> anyhow::Result<CrashtrackerConfiguration> {
115        // Requesting to create, but not use, the altstack is considered paradoxical.
116        anyhow::ensure!(
117            !self.create_alt_stack || self.use_alt_stack,
118            "Cannot create an altstack without using it"
119        );
120        let timeout = self
121            .timeout
122            .unwrap_or(constants::DD_CRASHTRACK_DEFAULT_TIMEOUT);
123        let endpoint = self
124            .endpoint_url
125            .map(|url| {
126                Ok::<Endpoint, anyhow::Error>(Endpoint {
127                    url: libdd_common::parse_uri(&url)?,
128                    api_key: self.endpoint_api_key.map(Cow::Owned),
129                    timeout_ms: self
130                        .endpoint_timeout_ms
131                        .unwrap_or(Endpoint::DEFAULT_TIMEOUT),
132                    test_token: self.endpoint_test_token.map(Cow::Owned),
133                    use_system_resolver: self.endpoint_use_system_resolver,
134                })
135            })
136            .transpose()?;
137
138        let mut signals = self.signals;
139        if signals.is_empty() {
140            signals = default_signals();
141        } else {
142            // Ensure we don't have double elements in the signals list.
143            let before_len = signals.len();
144            signals.sort();
145            signals.dedup();
146            anyhow::ensure!(
147                before_len == signals.len(),
148                "Signals contained duplicate elements"
149            );
150            // Ensure that all signal values translate to a valid signum
151            signals
152                .iter()
153                .try_for_each(|x| signal_from_signum(*x).map(|_| ()))?;
154        }
155
156        // Note: don't check the receiver socket upfront, since a configuration can be interned
157        // before the receiver is started when using an async-receiver.
158        Ok(CrashtrackerConfiguration {
159            additional_files: self.additional_files,
160            collect_all_threads: self.collect_all_threads,
161            create_alt_stack: self.create_alt_stack,
162            use_alt_stack: self.use_alt_stack,
163            endpoint,
164            max_threads: self.max_threads.unwrap_or(default_max_threads()),
165            resolve_frames: self.resolve_frames,
166            signals,
167            timeout,
168            unix_socket_path: self.unix_socket_path,
169            unix_socket_connector: self
170                .unix_socket_connector
171                .unwrap_or(super::default_unix_socket_connector),
172            demangle_names: self.demangle_names,
173        })
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use crate::{default_signals, shared::constants};
181    use core::time::Duration;
182
183    #[test]
184    fn test_build_defaults() -> anyhow::Result<()> {
185        let config = CrashtrackerConfiguration::builder().build()?;
186        assert!(config.additional_files().is_empty());
187        assert!(!config.create_alt_stack());
188        assert!(!config.use_alt_stack());
189        assert!(!config.demangle_names());
190        assert!(config.endpoint().is_none());
191        assert_eq!(config.resolve_frames(), StacktraceCollection::Disabled);
192        assert_eq!(config.signals(), &default_signals());
193        assert_eq!(config.timeout(), constants::DD_CRASHTRACK_DEFAULT_TIMEOUT);
194        assert!(config.unix_socket_path().is_none());
195        Ok(())
196    }
197
198    #[test]
199    fn test_create_alt_stack_without_use_fails() {
200        let result = CrashtrackerConfiguration::builder()
201            .create_alt_stack(true)
202            .use_alt_stack(false)
203            .build();
204        assert!(result.is_err());
205    }
206
207    #[test]
208    fn test_create_and_use_alt_stack_succeeds() -> anyhow::Result<()> {
209        let config = CrashtrackerConfiguration::builder()
210            .create_alt_stack(true)
211            .use_alt_stack(true)
212            .build()?;
213        assert!(config.create_alt_stack());
214        assert!(config.use_alt_stack());
215        Ok(())
216    }
217
218    #[test]
219    fn test_endpoint_empty_url() -> anyhow::Result<()> {
220        let config = CrashtrackerConfiguration::builder()
221            .endpoint_url("")
222            .build()?;
223        assert!(config.endpoint().is_none());
224        Ok(())
225    }
226
227    #[test]
228    fn test_endpoint_file_url() -> anyhow::Result<()> {
229        let config = CrashtrackerConfiguration::builder()
230            .endpoint_url("file:///tmp/crashreport.json")
231            .build()?;
232        let endpoint = config.endpoint().as_ref().expect("endpoint should be set");
233        assert_eq!(endpoint.url.scheme_str(), Some("file"));
234        Ok(())
235    }
236
237    #[test]
238    fn test_endpoint_http_url() -> anyhow::Result<()> {
239        let config = CrashtrackerConfiguration::builder()
240            .endpoint_url("http://localhost:8126/api/v2/profile")
241            .build()?;
242        let endpoint = config.endpoint().as_ref().expect("endpoint should be set");
243        assert_eq!(endpoint.url.scheme_str(), Some("http"));
244        assert_eq!(endpoint.url.port().unwrap().as_u16(), 8126);
245        assert_eq!(endpoint.url.host(), Some("localhost"));
246        Ok(())
247    }
248
249    #[test]
250    fn test_endpoint_default_timeout_ms() -> anyhow::Result<()> {
251        let config = CrashtrackerConfiguration::builder()
252            .endpoint_url("http://localhost:8126")
253            .build()?;
254        let endpoint = config.endpoint().as_ref().expect("endpoint should be set");
255        assert_eq!(endpoint.url.port().unwrap().as_u16(), 8126);
256        assert_eq!(endpoint.timeout_ms, Endpoint::DEFAULT_TIMEOUT);
257        Ok(())
258    }
259
260    #[test]
261    fn test_endpoint_custom_timeout_ms() -> anyhow::Result<()> {
262        let config = CrashtrackerConfiguration::builder()
263            .endpoint_url("http://localhost:8126")
264            .endpoint_timeout_ms(1234)
265            .build()?;
266        let endpoint = config.endpoint().as_ref().expect("endpoint should be set");
267        assert_eq!(endpoint.timeout_ms, 1234);
268        Ok(())
269    }
270
271    #[test]
272    fn test_endpoint_with_api_key() -> anyhow::Result<()> {
273        let config = CrashtrackerConfiguration::builder()
274            .endpoint_url("http://localhost:8126")
275            .endpoint_api_key("my-api-key")
276            .build()?;
277        let endpoint = config.endpoint().as_ref().expect("endpoint should be set");
278        assert_eq!(endpoint.api_key.as_deref(), Some("my-api-key"));
279        Ok(())
280    }
281
282    #[test]
283    fn test_endpoint_with_test_token() -> anyhow::Result<()> {
284        let config = CrashtrackerConfiguration::builder()
285            .endpoint_url("http://localhost:8126")
286            .endpoint_test_token("test-token")
287            .build()?;
288        let endpoint = config.endpoint().as_ref().expect("endpoint should be set");
289        assert_eq!(endpoint.test_token.as_deref(), Some("test-token"));
290        Ok(())
291    }
292
293    #[test]
294    fn test_endpoint_with_system_resolver() -> anyhow::Result<()> {
295        let config = CrashtrackerConfiguration::builder()
296            .endpoint_url("http://localhost:8126")
297            .endpoint_use_system_resolver(true)
298            .build()?;
299        let endpoint = config.endpoint().as_ref().expect("endpoint should be set");
300        assert!(endpoint.use_system_resolver);
301        Ok(())
302    }
303
304    #[test]
305    fn test_signals_default() -> anyhow::Result<()> {
306        let config = CrashtrackerConfiguration::builder().build()?;
307        assert_eq!(config.signals(), &default_signals());
308        Ok(())
309    }
310
311    #[test]
312    fn test_signals_custom() -> anyhow::Result<()> {
313        let signals = vec![libc::SIGSEGV, libc::SIGBUS];
314        let config = CrashtrackerConfiguration::builder()
315            .signals(signals.clone())
316            .build()?;
317        let mut expected = signals;
318        expected.sort();
319        assert_eq!(config.signals(), &expected);
320        Ok(())
321    }
322
323    #[test]
324    fn test_signals_duplicates_fail() {
325        let result = CrashtrackerConfiguration::builder()
326            .signals(vec![libc::SIGSEGV, libc::SIGSEGV])
327            .build();
328        assert!(result.is_err());
329    }
330
331    #[test]
332    fn test_signals_invalid_fail() {
333        let result = CrashtrackerConfiguration::builder()
334            .signals(vec![9999])
335            .build();
336        assert!(result.is_err());
337    }
338
339    #[test]
340    fn test_timeout_default() -> anyhow::Result<()> {
341        let config = CrashtrackerConfiguration::builder().build()?;
342        assert_eq!(config.timeout(), constants::DD_CRASHTRACK_DEFAULT_TIMEOUT);
343        Ok(())
344    }
345
346    #[test]
347    fn test_timeout_custom() -> anyhow::Result<()> {
348        let config = CrashtrackerConfiguration::builder()
349            .timeout(Duration::from_secs(10))
350            .build()?;
351        assert_eq!(config.timeout(), Duration::from_secs(10));
352        Ok(())
353    }
354
355    #[test]
356    fn test_additional_files() -> anyhow::Result<()> {
357        let files = vec!["/tmp/file1.txt".to_string(), "/tmp/file2.txt".to_string()];
358        let config = CrashtrackerConfiguration::builder()
359            .additional_files(files.clone())
360            .build()?;
361        assert_eq!(config.additional_files(), &files);
362        Ok(())
363    }
364
365    #[test]
366    fn test_demangle_names() -> anyhow::Result<()> {
367        let config = CrashtrackerConfiguration::builder()
368            .demangle_names(true)
369            .build()?;
370        assert!(config.demangle_names());
371        Ok(())
372    }
373
374    #[test]
375    fn test_resolve_frames() -> anyhow::Result<()> {
376        for variant in [
377            StacktraceCollection::Disabled,
378            StacktraceCollection::WithoutSymbols,
379            StacktraceCollection::EnabledWithInprocessSymbols,
380            StacktraceCollection::EnabledWithSymbolsInReceiver,
381        ] {
382            let config = CrashtrackerConfiguration::builder()
383                .resolve_frames(variant)
384                .build()?;
385            assert_eq!(config.resolve_frames(), variant);
386        }
387        Ok(())
388    }
389
390    #[test]
391    fn test_unix_socket_path() -> anyhow::Result<()> {
392        let config = CrashtrackerConfiguration::builder()
393            .unix_socket_path("/tmp/crashtracker.sock".to_string())
394            .build()?;
395        assert_eq!(
396            config.unix_socket_path(),
397            &Some("/tmp/crashtracker.sock".to_string())
398        );
399        Ok(())
400    }
401}