Skip to main content

libdd_crashtracker/crash_info/
error_data.rs

1// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3use super::stacktrace::StackTrace;
4#[cfg(unix)]
5use alloc::rc::Rc;
6#[cfg(unix)]
7use blazesym::helper::ElfResolver;
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10#[cfg(unix)]
11use std::collections::HashMap;
12#[cfg(unix)]
13use std::path::PathBuf;
14
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
16pub struct ErrorData {
17    pub is_crash: bool,
18    pub kind: ErrorKind,
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub message: Option<String>,
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub thread_name: Option<String>,
23    pub source_type: SourceType,
24    pub stack: StackTrace,
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub threads: Option<Vec<ThreadData>>,
27}
28
29#[cfg(unix)]
30pub struct CachedElfResolvers<'a> {
31    symbolizer: &'a mut blazesym::symbolize::Symbolizer,
32    elf_resolvers: HashMap<PathBuf, Rc<ElfResolver>>,
33}
34
35#[cfg(unix)]
36impl<'a> CachedElfResolvers<'a> {
37    pub fn new(symbolizer: &'a mut blazesym::symbolize::Symbolizer) -> Self {
38        Self {
39            symbolizer,
40            elf_resolvers: HashMap::new(),
41        }
42    }
43
44    pub fn get_or_insert(&mut self, file_path: &PathBuf) -> anyhow::Result<Rc<ElfResolver>> {
45        use anyhow::Context;
46        let entry = self.elf_resolvers.entry(file_path.clone());
47
48        match entry {
49            std::collections::hash_map::Entry::Occupied(o) => Ok(o.get().clone()),
50            std::collections::hash_map::Entry::Vacant(v) => {
51                let resolver = ElfResolver::open(file_path).with_context(|| {
52                    format!(
53                        "ElfResolver::open failed for '{}'",
54                        file_path.to_string_lossy()
55                    )
56                });
57
58                match resolver {
59                    Ok(resolver) => {
60                        let resolver = Rc::new(resolver);
61                        // even if the symbolizer failed at registering the elf resolver, we still
62                        // cache it to avoid trying to open it again
63                        let _ = self
64                            .symbolizer
65                            .register_elf_resolver(file_path.as_path(), Rc::clone(&resolver));
66                        v.insert(Rc::clone(&resolver));
67                        Ok(resolver)
68                    }
69                    Err(e) => Err(e),
70                }
71            }
72        }
73    }
74}
75
76#[cfg(unix)]
77impl ErrorData {
78    pub fn normalize_ips(&mut self, pid: u32) -> anyhow::Result<()> {
79        let mut symbolizer = blazesym::symbolize::Symbolizer::new();
80        let mut elf_resolvers = CachedElfResolvers::new(&mut symbolizer);
81        let normalizer = blazesym::normalize::Normalizer::builder()
82            .enable_vma_caching(true)
83            .enable_build_ids(true)
84            .enable_build_id_caching(true)
85            .build();
86        self.normalize_ips_impl(pid, &normalizer, &mut elf_resolvers)
87    }
88
89    pub(crate) fn normalize_ips_impl(
90        &mut self,
91        pid: u32,
92        normalizer: &blazesym::normalize::Normalizer,
93        elf_resolvers: &mut CachedElfResolvers,
94    ) -> anyhow::Result<()> {
95        let mut errors = 0;
96        let pid = pid.into();
97        self.stack
98            .normalize_ips(normalizer, pid, elf_resolvers)
99            .unwrap_or_else(|_| errors += 1);
100
101        if let Some(threads) = &mut self.threads {
102            for thread in threads {
103                thread
104                    .stack
105                    .normalize_ips(normalizer, pid, elf_resolvers)
106                    .unwrap_or_else(|_| errors += 1);
107            }
108        }
109        anyhow::ensure!(
110            errors == 0,
111            "Failed to normalize ips, see frame comments for details"
112        );
113        Ok(())
114    }
115
116    pub(crate) fn create_symbolizer_source<'a>(
117        pid: u32,
118    ) -> blazesym::symbolize::source::Source<'a> {
119        let mut process = blazesym::symbolize::source::Process::new(pid.into());
120        // https://github.com/libbpf/blazesym/issues/518
121        process.map_files = false;
122        blazesym::symbolize::source::Source::Process(process)
123    }
124
125    pub(crate) fn create_normalizer() -> blazesym::normalize::Normalizer {
126        blazesym::normalize::Normalizer::builder()
127            .enable_vma_caching(true)
128            .enable_build_ids(true)
129            .enable_build_id_caching(true)
130            .build()
131    }
132
133    pub fn resolve_names(&mut self, pid: u32) -> anyhow::Result<()> {
134        let src = Self::create_symbolizer_source(pid);
135        let symbolizer = blazesym::symbolize::Symbolizer::new();
136        self.resolve_names_impl(&symbolizer, &src)
137    }
138
139    pub(crate) fn resolve_names_impl(
140        &mut self,
141        symbolizer: &blazesym::symbolize::Symbolizer,
142        src: &blazesym::symbolize::source::Source,
143    ) -> anyhow::Result<()> {
144        let mut errors = 0;
145        self.stack
146            .resolve_names(src, symbolizer)
147            .unwrap_or_else(|_| errors += 1);
148
149        if let Some(threads) = &mut self.threads {
150            for thread in threads {
151                thread
152                    .stack
153                    .resolve_names(src, symbolizer)
154                    .unwrap_or_else(|_| errors += 1);
155            }
156        }
157        anyhow::ensure!(
158            errors == 0,
159            "Failed to resolve names, see frame comments for details"
160        );
161        Ok(())
162    }
163}
164
165impl ErrorData {
166    pub fn demangle_names(&mut self) -> anyhow::Result<()> {
167        let mut errors = 0;
168        self.stack.demangle_names().unwrap_or_else(|_| errors += 1);
169        if let Some(threads) = &mut self.threads {
170            for thread in threads {
171                thread
172                    .stack
173                    .demangle_names()
174                    .unwrap_or_else(|_| errors += 1);
175            }
176        }
177        anyhow::ensure!(
178            errors == 0,
179            "Failed to demangle names, see frame comments for details"
180        );
181        Ok(())
182    }
183}
184
185#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
186pub enum SourceType {
187    Crashtracking,
188}
189
190#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
191#[repr(C)]
192pub enum ErrorKind {
193    Panic,
194    UnhandledException,
195    UnixSignal,
196}
197
198#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
199pub struct ThreadData {
200    pub crashed: bool,
201    pub name: String,
202    pub stack: StackTrace,
203    #[serde(default, skip_serializing_if = "Option::is_none")]
204    pub state: Option<String>,
205}
206
207#[cfg(test)]
208impl super::test_utils::TestInstance for ErrorData {
209    fn test_instance(seed: u64) -> Self {
210        Self {
211            is_crash: true,
212            kind: ErrorKind::UnixSignal,
213            message: Some(format!("Test crash message for seed {seed}")),
214            thread_name: Some(format!("test-thread-{seed}")),
215            source_type: SourceType::Crashtracking,
216            stack: StackTrace::test_instance(seed),
217            threads: None,
218        }
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use crate::crash_info::test_utils::TestInstance;
226
227    #[test]
228    fn error_data_without_threads_omits_threads_field() {
229        let error = ErrorData {
230            is_crash: true,
231            kind: ErrorKind::UnixSignal,
232            message: None,
233            thread_name: None,
234            source_type: SourceType::Crashtracking,
235            stack: StackTrace::missing(),
236            threads: None,
237        };
238        let json = serde_json::to_value(&error).unwrap();
239        assert!(json.get("threads").is_none());
240    }
241
242    #[test]
243    fn error_data_threads_serializes_as_thread_array() {
244        let thread = ThreadData {
245            crashed: false,
246            name: "worker".to_string(),
247            stack: StackTrace::test_instance(1),
248            state: Some("S".to_string()),
249        };
250        let error = ErrorData {
251            is_crash: true,
252            kind: ErrorKind::UnixSignal,
253            message: None,
254            thread_name: None,
255            source_type: SourceType::Crashtracking,
256            stack: StackTrace::missing(),
257            threads: Some(vec![thread.clone()]),
258        };
259        let json = serde_json::to_value(&error).unwrap();
260        let threads = json["threads"].as_array().unwrap();
261        assert_eq!(threads.len(), 1);
262        assert_eq!(threads[0]["crashed"], false);
263        assert_eq!(threads[0]["name"], "worker");
264        assert_eq!(threads[0]["state"], "S");
265        assert!(threads[0]["stack"].is_object());
266    }
267}