Skip to main content

libdd_crashtracker/crash_info/
builder.rs

1// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::runtime_callback::RuntimeStack;
5
6use chrono::{DateTime, Utc};
7use error_data::ThreadData;
8use stacktrace::StackTrace;
9use std::io::{BufRead, BufReader};
10use unknown_value::UnknownValue;
11use uuid::Uuid;
12
13use super::*;
14
15#[derive(Debug, Default, PartialEq)]
16pub struct ErrorDataBuilder {
17    pub kind: Option<ErrorKind>,
18    pub message: Option<String>,
19    pub thread_name: Option<String>,
20    pub stack: Option<StackTrace>,
21    pub threads: Option<Vec<ThreadData>>,
22}
23
24impl ErrorDataBuilder {
25    pub fn build(self) -> anyhow::Result<(ErrorData, bool /* incomplete */)> {
26        let incomplete = self.stack.is_none();
27        let is_crash = true;
28        let kind = self.kind.context("required field 'kind' missing")?;
29        let message = self.message;
30        let thread_name = self.thread_name;
31        let source_type = SourceType::Crashtracking;
32        let stack = self.stack.unwrap_or_else(StackTrace::missing);
33        let threads = self.threads;
34        Ok((
35            ErrorData {
36                is_crash,
37                kind,
38                message,
39                thread_name,
40                source_type,
41                stack,
42                threads,
43            },
44            incomplete,
45        ))
46    }
47
48    pub fn new() -> Self {
49        Self::default()
50    }
51
52    pub fn with_kind(&mut self, kind: ErrorKind) -> anyhow::Result<()> {
53        self.kind = Some(kind);
54        Ok(())
55    }
56
57    pub fn with_message(&mut self, message: String) -> anyhow::Result<()> {
58        self.message = Some(message);
59        Ok(())
60    }
61
62    pub fn with_thread_name(&mut self, thread_name: String) -> anyhow::Result<()> {
63        if thread_name.trim().is_empty() {
64            return Ok(());
65        }
66        self.thread_name = Some(thread_name);
67        Ok(())
68    }
69
70    pub fn with_stack(&mut self, stack: StackTrace) -> anyhow::Result<()> {
71        self.stack = Some(stack);
72        Ok(())
73    }
74
75    pub fn with_stack_frame(&mut self, frame: StackFrame, incomplete: bool) -> anyhow::Result<()> {
76        if let Some(stack) = &mut self.stack {
77            stack.push_frame(frame, incomplete)?;
78        } else {
79            self.stack = Some(StackTrace::from_frames(vec![frame], incomplete));
80        }
81        Ok(())
82    }
83
84    pub fn with_stack_set_complete(&mut self) -> anyhow::Result<()> {
85        if let Some(stack) = &mut self.stack {
86            stack.set_complete()?;
87        } else {
88            // No frames were received, but we got the end marker. This can happen on
89            // certain platforms/contexts where stack unwinding fails to capture any
90            // frames (e.g., musl-based Linux). Initialize an empty but incomplete stack
91            // to indicate that stack collection did not succeed.
92            self.stack = Some(StackTrace::new_incomplete());
93        }
94        Ok(())
95    }
96
97    pub fn with_threads(&mut self, threads: Vec<ThreadData>) -> anyhow::Result<()> {
98        if threads.is_empty() {
99            return Ok(());
100        }
101        self.threads = Some(threads);
102        Ok(())
103    }
104
105    pub fn with_thread(&mut self, thread: ThreadData) -> anyhow::Result<()> {
106        self.threads.get_or_insert_with(Vec::new).push(thread);
107        Ok(())
108    }
109}
110
111#[derive(Debug, PartialEq)]
112pub struct CrashInfoBuilder {
113    pub counters: Option<HashMap<String, i64>>,
114    pub error: ErrorDataBuilder,
115    pub experimental: Option<Experimental>,
116    pub files: Option<HashMap<String, Vec<String>>>,
117    pub fingerprint: Option<String>,
118    pub incomplete: Option<bool>,
119    pub log_messages: Option<Vec<String>>,
120    pub metadata: Option<Metadata>,
121    pub os_info: Option<OsInfo>,
122    pub proc_info: Option<ProcInfo>,
123    pub sig_info: Option<SigInfo>,
124    pub span_ids: Option<Vec<Span>>,
125    pub timestamp: Option<DateTime<Utc>>,
126    pub trace_ids: Option<Vec<Span>>,
127    pub ucontext: Option<Ucontext>,
128    pub uuid: Uuid,
129}
130
131impl Default for CrashInfoBuilder {
132    fn default() -> Self {
133        Self {
134            counters: None,
135            error: ErrorDataBuilder::default(),
136            experimental: None,
137            files: None,
138            fingerprint: None,
139            incomplete: None,
140            log_messages: None,
141            metadata: None,
142            os_info: None,
143            proc_info: None,
144            sig_info: None,
145            span_ids: None,
146            timestamp: None,
147            trace_ids: None,
148            ucontext: None,
149            uuid: Uuid::new_v4(),
150        }
151    }
152}
153
154impl CrashInfoBuilder {
155    pub fn build(self) -> anyhow::Result<CrashInfo> {
156        let counters = self.counters.unwrap_or_default();
157        let data_schema_version = CrashInfo::current_schema_version().to_string();
158        let (error, incomplete_error) = self.error.build()?;
159        let experimental = self.experimental;
160        let files = self.files.unwrap_or_default();
161        let fingerprint = self.fingerprint;
162        let incomplete = incomplete_error || self.incomplete.unwrap_or(false);
163        let log_messages = self.log_messages.unwrap_or_default();
164        let metadata = self.metadata.unwrap_or_else(Metadata::unknown_value);
165        let os_info = self.os_info.unwrap_or_else(OsInfo::unknown_value);
166        let proc_info = self.proc_info;
167        let sig_info = self.sig_info;
168        let span_ids = self.span_ids.unwrap_or_default();
169        let timestamp = self.timestamp.unwrap_or_else(Utc::now).to_string();
170        let trace_ids = self.trace_ids.unwrap_or_default();
171        let ucontext = self.ucontext;
172        let uuid = self.uuid;
173        Ok(CrashInfo {
174            counters,
175            data_schema_version,
176            error,
177            experimental,
178            files,
179            fingerprint,
180            incomplete,
181            log_messages,
182            metadata,
183            os_info,
184            proc_info,
185            sig_info,
186            span_ids,
187            timestamp,
188            trace_ids,
189            ucontext,
190            uuid: uuid.to_string(),
191        })
192    }
193
194    /// True if the builder received anything at all. The `uuid` is generated
195    /// when the builder is created, so it is excluded: comparing the whole struct
196    /// against `Self::default()` would always differ on it and always report data
197    pub fn has_data(&self) -> bool {
198        let blank = Self {
199            uuid: self.uuid,
200            ..Self::default()
201        };
202
203        self != &blank
204    }
205
206    pub fn new() -> Self {
207        Self::default()
208    }
209
210    /// Inserts the given counter to the current set of counters in the builder.
211    pub fn with_counter(&mut self, name: String, value: i64) -> anyhow::Result<()> {
212        anyhow::ensure!(!name.is_empty(), "Empty counter name not allowed");
213        if let Some(ref mut counters) = &mut self.counters {
214            counters.insert(name, value);
215        } else {
216            self.counters = Some(HashMap::from([(name, value)]));
217        }
218        Ok(())
219    }
220
221    pub fn with_counters(&mut self, counters: HashMap<String, i64>) -> anyhow::Result<()> {
222        self.counters = Some(counters);
223        Ok(())
224    }
225
226    pub fn with_experimental_additional_tags(
227        &mut self,
228        additional_tags: Vec<String>,
229    ) -> anyhow::Result<()> {
230        if let Some(experimental) = &mut self.experimental {
231            experimental.additional_tags = additional_tags;
232        } else {
233            self.experimental = Some(Experimental::new().with_additional_tags(additional_tags));
234        }
235        Ok(())
236    }
237
238    pub fn with_experimental_runtime_stack(
239        &mut self,
240        runtime_stack: RuntimeStack,
241    ) -> anyhow::Result<()> {
242        if let Some(experimental) = &mut self.experimental {
243            experimental.runtime_stack = Some(runtime_stack);
244        } else {
245            self.experimental = Some(Experimental::new().with_runtime_stack(runtime_stack));
246        }
247        Ok(())
248    }
249
250    pub fn with_kind(&mut self, kind: ErrorKind) -> anyhow::Result<()> {
251        self.error.with_kind(kind)
252    }
253
254    pub fn with_file(&mut self, filename: String) -> anyhow::Result<()> {
255        let file = File::open(&filename).with_context(|| format!("filename: {filename}"))?;
256        let lines: std::io::Result<Vec<_>> = BufReader::new(file).lines().collect();
257        self.with_file_and_contents(filename, lines?)?;
258        Ok(())
259    }
260
261    /// Appends the given file to the current set of files in the builder.
262    pub fn with_file_and_contents(
263        &mut self,
264        filename: String,
265        contents: Vec<String>,
266    ) -> anyhow::Result<()> {
267        if let Some(ref mut files) = &mut self.files {
268            files.insert(filename, contents);
269        } else {
270            self.files = Some(HashMap::from([(filename, contents)]));
271        }
272        Ok(())
273    }
274
275    /// Sets the current set of files in the builder.
276    pub fn with_files(&mut self, files: HashMap<String, Vec<String>>) -> anyhow::Result<()> {
277        self.files = Some(files);
278        Ok(())
279    }
280
281    pub fn with_fingerprint(&mut self, fingerprint: String) -> anyhow::Result<()> {
282        anyhow::ensure!(!fingerprint.is_empty(), "Expect non-empty fingerprint");
283        self.fingerprint = Some(fingerprint);
284        Ok(())
285    }
286
287    pub fn with_incomplete(&mut self, incomplete: bool) -> anyhow::Result<()> {
288        self.incomplete = Some(incomplete);
289        Ok(())
290    }
291
292    /// Appends the given message to the current set of messages in the builder.
293    pub fn with_log_message(&mut self, message: String, also_print: bool) -> anyhow::Result<()> {
294        if also_print {
295            eprintln!("{message}");
296        }
297
298        if let Some(ref mut messages) = &mut self.log_messages {
299            messages.push(message);
300        } else {
301            self.log_messages = Some(vec![message]);
302        }
303        Ok(())
304    }
305
306    pub fn with_log_messages(&mut self, log_messages: Vec<String>) -> anyhow::Result<()> {
307        self.log_messages = Some(log_messages);
308        Ok(())
309    }
310
311    pub fn with_message(&mut self, message: String) -> anyhow::Result<()> {
312        self.error.with_message(message)
313    }
314
315    pub fn with_thread_name(&mut self, thread_name: String) -> anyhow::Result<()> {
316        self.error.with_thread_name(thread_name)
317    }
318
319    pub fn with_metadata(&mut self, metadata: Metadata) -> anyhow::Result<()> {
320        self.metadata = Some(metadata);
321        Ok(())
322    }
323
324    pub fn with_os_info(&mut self, os_info: OsInfo) -> anyhow::Result<()> {
325        self.os_info = Some(os_info);
326        Ok(())
327    }
328
329    pub fn with_os_info_this_machine(&mut self) -> anyhow::Result<()> {
330        self.with_os_info(::os_info::get().into())?;
331        Ok(())
332    }
333
334    pub fn with_proc_info(&mut self, proc_info: ProcInfo) -> anyhow::Result<()> {
335        self.proc_info = Some(proc_info);
336        Ok(())
337    }
338
339    pub fn with_sig_info(&mut self, sig_info: SigInfo) -> anyhow::Result<()> {
340        self.sig_info = Some(sig_info);
341        Ok(())
342    }
343
344    pub fn with_span_id(&mut self, span_id: Span) -> anyhow::Result<()> {
345        if let Some(ref mut span_ids) = &mut self.span_ids {
346            span_ids.push(span_id);
347        } else {
348            self.span_ids = Some(vec![span_id]);
349        }
350        Ok(())
351    }
352
353    pub fn with_span_ids(&mut self, span_ids: Vec<Span>) -> anyhow::Result<()> {
354        self.span_ids = Some(span_ids);
355        Ok(())
356    }
357
358    pub fn with_stack(&mut self, stack: StackTrace) -> anyhow::Result<()> {
359        self.error.with_stack(stack)
360    }
361
362    pub fn with_stack_frame(&mut self, frame: StackFrame, incomplete: bool) -> anyhow::Result<()> {
363        self.error.with_stack_frame(frame, incomplete)
364    }
365
366    pub fn with_stack_set_complete(&mut self) -> anyhow::Result<()> {
367        let had_no_frames = self.error.stack.is_none();
368        self.error.with_stack_set_complete()?;
369        if had_no_frames {
370            self.with_log_message(
371                "No native stack frames received; stack unwinding may have failed".to_string(),
372                true,
373            )?;
374        }
375        Ok(())
376    }
377
378    pub fn with_thread(&mut self, thread: ThreadData) -> anyhow::Result<()> {
379        self.error.with_thread(thread)
380    }
381
382    pub fn with_threads(&mut self, threads: Vec<ThreadData>) -> anyhow::Result<()> {
383        self.error.with_threads(threads)
384    }
385
386    pub fn with_timestamp(&mut self, timestamp: DateTime<Utc>) -> anyhow::Result<()> {
387        self.timestamp = Some(timestamp);
388        Ok(())
389    }
390
391    pub fn with_timestamp_now(&mut self) -> anyhow::Result<()> {
392        self.with_timestamp(Utc::now())?;
393        Ok(())
394    }
395
396    pub fn with_trace_id(&mut self, trace_id: Span) -> anyhow::Result<()> {
397        if let Some(ref mut trace_ids) = &mut self.trace_ids {
398            trace_ids.push(trace_id);
399        } else {
400            self.trace_ids = Some(vec![trace_id]);
401        }
402        Ok(())
403    }
404
405    pub fn with_trace_ids(&mut self, trace_ids: Vec<Span>) -> anyhow::Result<()> {
406        self.trace_ids = Some(trace_ids);
407        Ok(())
408    }
409
410    pub fn with_ucontext(&mut self, ucontext: Ucontext) -> anyhow::Result<()> {
411        self.ucontext = Some(ucontext);
412        Ok(())
413    }
414
415    /// This method requires that the builder has Metadata and Kind set
416    pub fn build_crash_ping(&self) -> anyhow::Result<CrashPing> {
417        let metadata = self.metadata.clone().context("metadata is required")?;
418        let kind = self.error.kind.clone().context("kind is required")?;
419        let message = self.error.message.clone();
420        let sig_info = self.sig_info.clone();
421
422        let mut builder = CrashPingBuilder::new(self.uuid)
423            .with_metadata(metadata)
424            .with_kind(kind);
425        if let Some(sig_info) = sig_info {
426            builder = builder.with_sig_info(sig_info);
427        }
428        if let Some(message) = message {
429            builder = builder.with_custom_message(message);
430        }
431        builder.build()
432    }
433
434    pub fn is_ping_ready(&self) -> bool {
435        self.metadata.is_some() && self.error.kind.is_some()
436    }
437
438    pub fn has_message(&self) -> bool {
439        self.error.message.is_some()
440    }
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446    use crate::crash_info::test_utils::TestInstance;
447
448    #[test]
449    fn test_crash_info_builder_to_crash_ping() {
450        let sig_info = SigInfo::test_instance(42);
451        let metadata = Metadata::test_instance(1);
452
453        let mut crash_info_builder = CrashInfoBuilder::new();
454        crash_info_builder.with_sig_info(sig_info.clone()).unwrap();
455        crash_info_builder.with_metadata(metadata.clone()).unwrap();
456        crash_info_builder.with_kind(ErrorKind::Panic).unwrap();
457
458        let crash_ping = crash_info_builder.build_crash_ping().unwrap();
459
460        assert!(!crash_ping.crash_uuid().is_empty());
461        assert!(Uuid::parse_str(crash_ping.crash_uuid()).is_ok());
462        assert_eq!(crash_ping.siginfo(), Some(&sig_info));
463        assert_eq!(crash_ping.metadata(), &metadata);
464        assert!(crash_ping.message().contains("crash processing started"));
465    }
466
467    #[test]
468    fn test_with_message() {
469        let mut builder = CrashInfoBuilder::new();
470
471        builder.with_kind(ErrorKind::UnixSignal).unwrap();
472        let test_message = "Test error message".to_string();
473
474        let result = builder.with_message(test_message.clone());
475        assert!(result.is_ok());
476        assert!(builder.has_message());
477
478        // Build and verify message is present
479        let sig_info = SigInfo::test_instance(42);
480        builder.with_sig_info(sig_info).unwrap();
481        builder.with_metadata(Metadata::test_instance(1)).unwrap();
482        builder.with_kind(ErrorKind::UnixSignal).unwrap();
483
484        let crash_ping = builder.build_crash_ping().unwrap();
485        assert!(crash_ping.message().contains(&test_message));
486    }
487
488    #[test]
489    fn test_has_data_empty() {
490        // A fresh builder has a freshly generated uuid, which must not count as
491        // received data.
492        assert!(!CrashInfoBuilder::new().has_data());
493    }
494
495    #[test]
496    fn test_has_data_after_setting() {
497        let mut builder = CrashInfoBuilder::new();
498        builder.with_kind(ErrorKind::Panic).unwrap();
499        assert!(builder.has_data());
500
501        let mut builder = CrashInfoBuilder::new();
502        builder.with_metadata(Metadata::test_instance(1)).unwrap();
503        assert!(builder.has_data());
504
505        let mut builder = CrashInfoBuilder::new();
506        builder.with_incomplete(false).unwrap();
507        assert!(builder.has_data());
508    }
509
510    #[test]
511    fn test_has_message_empty() {
512        let builder = CrashInfoBuilder::new();
513        assert!(!builder.has_message());
514    }
515
516    #[test]
517    fn test_has_message_after_setting() {
518        let mut builder = CrashInfoBuilder::new();
519        builder.with_message("test".to_string()).unwrap();
520        assert!(builder.has_message());
521    }
522
523    #[test]
524    fn test_message_overwrite() {
525        let mut builder = CrashInfoBuilder::new();
526
527        builder.with_message("first message".to_string()).unwrap();
528        assert!(builder.has_message());
529
530        // Overwrite with second message
531        builder.with_message("second message".to_string()).unwrap();
532        assert!(builder.has_message());
533
534        // Build and verify only second message is present
535        let sig_info = SigInfo::test_instance(42);
536        builder.with_sig_info(sig_info).unwrap();
537        builder.with_metadata(Metadata::test_instance(1)).unwrap();
538        builder.with_kind(ErrorKind::UnixSignal).unwrap();
539
540        let crash_ping = builder.build_crash_ping().unwrap();
541        assert!(crash_ping.message().contains("second message"));
542        assert!(!crash_ping.message().contains("first message"));
543        builder.with_kind(ErrorKind::Panic).unwrap();
544
545        let report = builder.build().unwrap();
546        assert_eq!(report.error.message.as_deref(), Some("second message"));
547    }
548
549    #[test]
550    fn test_message_with_special_characters() {
551        let mut builder = CrashInfoBuilder::new();
552        let special_message = "Error: 'panic' with \"quotes\" and\nnewlines\t\ttabs";
553
554        builder.with_message(special_message.to_string()).unwrap();
555        builder.with_sig_info(SigInfo::test_instance(42)).unwrap();
556        builder.with_metadata(Metadata::test_instance(1)).unwrap();
557        builder.with_kind(ErrorKind::UnixSignal).unwrap();
558
559        let crash_ping = builder.build_crash_ping().unwrap();
560        assert!(crash_ping.message().contains(special_message));
561        builder.with_kind(ErrorKind::UnixSignal).unwrap();
562
563        let report = builder.build().unwrap();
564        assert_eq!(report.error.message.as_deref(), Some(special_message));
565    }
566
567    #[test]
568    fn test_very_long_message() {
569        let mut builder = CrashInfoBuilder::new();
570        let long_message = "x".repeat(10000); // 10KB message
571
572        builder.with_message(long_message.clone()).unwrap();
573        assert!(builder.has_message());
574
575        builder.with_sig_info(SigInfo::test_instance(42)).unwrap();
576        builder.with_metadata(Metadata::test_instance(1)).unwrap();
577        builder.with_kind(ErrorKind::UnixSignal).unwrap();
578
579        let crash_ping = builder.build_crash_ping().unwrap();
580        assert!(crash_ping.message().len() >= 10000);
581
582        builder.with_kind(ErrorKind::UnixSignal).unwrap();
583        let report = builder.build().unwrap();
584        assert!(report.error.message.as_ref().unwrap().len() >= 10000);
585    }
586
587    #[test]
588    fn test_no_frames_is_incomplete() {
589        // When we receive an end stacktrace marker but no frames were collected,
590        // we should create an empty incomplete stack rather than erroring.
591        let mut builder = ErrorDataBuilder::new();
592        assert!(builder.stack.is_none());
593
594        // This should succeed and create an empty incomplete stack
595        let result = builder.with_stack_set_complete();
596        assert!(result.is_ok());
597
598        // Verify we now have a stack that is incomplete (no frames were captured)
599        assert!(builder.stack.is_some());
600        let stack = builder.stack.as_ref().unwrap();
601        assert!(stack.frames.is_empty());
602        assert!(stack.incomplete);
603    }
604
605    #[test]
606    fn test_with_stack_set_complete_with_frames() {
607        // When we have frames and call set_complete, it should mark them complete
608        let mut builder = ErrorDataBuilder::new();
609
610        // Add a frame (which creates an incomplete stack)
611        let frame = StackFrame::test_instance(1);
612        builder.with_stack_frame(frame, true).unwrap();
613        assert!(builder.stack.as_ref().unwrap().incomplete);
614
615        // Mark complete
616        builder.with_stack_set_complete().unwrap();
617
618        // Verify stack is now complete
619        let stack = builder.stack.as_ref().unwrap();
620        assert_eq!(stack.frames.len(), 1);
621        assert!(!stack.incomplete);
622    }
623
624    #[test]
625    fn test_crash_info_builder_empty_stack_is_incomplete() {
626        // When no frames were captured, the stack and CrashInfo should be marked
627        // incomplete to indicate that stack collection did not succeed.
628        let mut builder = CrashInfoBuilder::new();
629        builder.with_kind(ErrorKind::UnixSignal).unwrap();
630
631        // Simulate receiving BEGIN_STACKTRACE then END_STACKTRACE with no frames
632        builder.with_stack_set_complete().unwrap();
633
634        let crash_info = builder.build().unwrap();
635
636        // The stack should be empty and incomplete (no frames were captured)
637        assert!(crash_info.error.stack.frames.is_empty());
638        assert!(crash_info.error.stack.incomplete);
639
640        // The overall crash info should be marked complete
641        assert!(!crash_info.incomplete);
642
643        // A log message should be recorded noting that no frames were received
644        assert!(crash_info
645            .log_messages
646            .iter()
647            .any(|msg| msg.contains("No native stack frames received")));
648    }
649
650    #[test]
651    #[cfg_attr(miri, ignore)] // os_info::get() spawns subprocess, unsupported by Miri
652    fn test_with_os_info_this_machine() {
653        let mut builder = CrashInfoBuilder::new();
654        builder.with_kind(ErrorKind::UnixSignal).unwrap();
655
656        builder.with_os_info_this_machine().unwrap();
657
658        let crash_info = builder.build().unwrap();
659
660        // Verify os_info was populated with non-empty values from the current machine
661        assert!(!crash_info.os_info.architecture.is_empty());
662        assert!(!crash_info.os_info.bitness.is_empty());
663        assert!(!crash_info.os_info.os_type.is_empty());
664        assert!(!crash_info.os_info.version.is_empty());
665
666        // Verify that the os_info is not the "unknown" default values
667        assert_ne!(
668            crash_info.os_info.architecture, "unknown",
669            "architecture should not be 'unknown'"
670        );
671        assert_ne!(
672            crash_info.os_info.bitness, "unknown bitness",
673            "bitness should not be 'unknown bitness'"
674        );
675        assert_ne!(
676            crash_info.os_info.os_type, "Unknown",
677            "os_type should not be 'Unknown'"
678        );
679        assert_ne!(
680            crash_info.os_info.version, "Unknown",
681            "version should not be 'Unknown'"
682        );
683    }
684}