Skip to main content

libdd_crashtracker/crash_info/
mod.rs

1// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3
4mod builder;
5#[cfg(feature = "cxx")]
6pub mod cxx;
7mod error_data;
8mod errors_intake;
9mod experimental;
10mod metadata;
11mod os_info;
12mod proc_info;
13mod sig_info;
14mod spans;
15mod stacktrace;
16mod telemetry;
17mod test_utils;
18mod ucontext;
19mod unknown_value;
20
21// This is set compile time
22pub(crate) const TARGET_TRIPLE: &str = env!("TARGET");
23
24pub use builder::*;
25pub use error_data::*;
26pub use errors_intake::*;
27pub use experimental::*;
28use libdd_common::Endpoint;
29pub use metadata::Metadata;
30pub use os_info::*;
31pub use proc_info::*;
32pub use sig_info::*;
33pub use spans::*;
34pub use stacktrace::*;
35pub use telemetry::*;
36pub use ucontext::*;
37pub use unknown_value::*;
38
39use anyhow::Context;
40use schemars::JsonSchema;
41use serde::{Deserialize, Serialize};
42use std::{collections::HashMap, fs::File, path::Path};
43
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
45pub struct CrashInfo {
46    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
47    pub counters: HashMap<String, i64>,
48    pub data_schema_version: String,
49    pub error: ErrorData,
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub experimental: Option<Experimental>,
52    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
53    pub files: HashMap<String, Vec<String>>,
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub fingerprint: Option<String>,
56    pub incomplete: bool,
57    #[serde(default, skip_serializing_if = "Vec::is_empty")]
58    pub log_messages: Vec<String>,
59    pub metadata: Metadata,
60    pub os_info: OsInfo,
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub proc_info: Option<ProcInfo>, //TODO, update the schema
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub sig_info: Option<SigInfo>, //TODO, update the schema
65    #[serde(default, skip_serializing_if = "Vec::is_empty")]
66    pub span_ids: Vec<Span>,
67    pub timestamp: String,
68    #[serde(default, skip_serializing_if = "Vec::is_empty")]
69    pub trace_ids: Vec<Span>,
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub ucontext: Option<Ucontext>,
72    pub uuid: String,
73}
74
75impl CrashInfo {
76    pub fn current_schema_version() -> String {
77        "1.8".to_string()
78    }
79
80    pub fn demangle_names(&mut self) -> anyhow::Result<()> {
81        self.error.demangle_names()
82    }
83}
84
85#[cfg(unix)]
86impl CrashInfo {
87    pub fn normalize_ips(&mut self, pid: u32) -> anyhow::Result<()> {
88        self.error.normalize_ips(pid)
89    }
90
91    pub fn resolve_names(&mut self, pid: u32) -> anyhow::Result<()> {
92        self.error.resolve_names(pid)
93    }
94
95    pub fn enrich_callstacks(&mut self, pid: u32) -> anyhow::Result<()> {
96        let src = ErrorData::create_symbolizer_source(pid);
97        let normalizer = ErrorData::create_normalizer();
98        let mut symbolizer = blazesym::symbolize::Symbolizer::new();
99        let mut elf_resolvers = CachedElfResolvers::new(&mut symbolizer);
100
101        // We must call ips normalization first.
102        // This will allow us to feed the symbolizer with the ELF Resolvers
103        let rval1 = self
104            .error
105            .normalize_ips_impl(pid, &normalizer, &mut elf_resolvers);
106        let rval2 = self.error.resolve_names_impl(&symbolizer, &src);
107        anyhow::ensure!(
108            rval1.is_ok() && rval2.is_ok(),
109            "normalize_ips: {rval1:?}\tresolve_names: {rval2:?}"
110        );
111        Ok(())
112    }
113}
114
115impl CrashInfo {
116    /// Emit the CrashInfo as structured json in file `path`.
117    pub fn to_file(&self, path: &Path) -> anyhow::Result<()> {
118        let file = File::options()
119            .create(true)
120            .append(true)
121            .open(path)
122            .with_context(|| format!("Failed to create {}", path.display()))?;
123        serde_json::to_writer_pretty(file, self)
124            .with_context(|| format!("Failed to write json to {}", path.display()))?;
125        Ok(())
126    }
127
128    pub fn upload_to_endpoint(&self, endpoint: &Option<Endpoint>) -> anyhow::Result<()> {
129        let rt = tokio::runtime::Builder::new_current_thread()
130            .enable_all()
131            .build()?;
132
133        rt.block_on(async { self.async_upload_to_endpoint(endpoint).await })
134    }
135
136    pub async fn async_upload_to_endpoint(
137        &self,
138        endpoint: &Option<Endpoint>,
139    ) -> anyhow::Result<()> {
140        // If we're debugging to a file, dump the actual crashinfo into a json
141        if let Some(endpoint) = endpoint {
142            if Some("file") == endpoint.url.scheme_str() {
143                let path = libdd_common::decode_uri_path_in_authority(&endpoint.url)
144                    .context("crash output file path was not correctly formatted")?;
145                self.to_file(&path)?;
146            }
147        }
148
149        let telemetry_future = self.upload_to_telemetry(endpoint);
150        let errors_intake_future = self.upload_to_errors_intake(endpoint);
151        let (_telemetry_result, _errors_intake_result) =
152            tokio::join!(telemetry_future, errors_intake_future);
153        Ok(())
154    }
155
156    async fn upload_to_telemetry(&self, endpoint: &Option<Endpoint>) -> anyhow::Result<()> {
157        let uploader = TelemetryCrashUploader::new(&self.metadata, endpoint)?;
158        uploader.upload_crash_info(self).await?;
159        Ok(())
160    }
161
162    async fn upload_to_errors_intake(&self, endpoint: &Option<Endpoint>) -> anyhow::Result<()> {
163        let uploader = ErrorsIntakeUploader::new(endpoint)?;
164        if uploader.is_enabled() {
165            uploader.upload_crash_info(self).await?;
166        }
167        Ok(())
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use schemars::schema::RootSchema;
174    use std::fs;
175
176    use super::*;
177    #[test]
178    fn test_schema_matches_rfc() {
179        let rfc_schema_filename = concat!(
180            env!("CARGO_MANIFEST_DIR"),
181            "/../docs/RFCs/artifacts/crashtracker-unified-runtime-stack-schema-v1_8.json"
182        );
183        let schema = schemars::schema_for!(CrashInfo);
184        let schema_json = serde_json::to_string_pretty(&schema).expect("Schema to serialize");
185
186        // Try to load the existing RFC schema
187        let path = Path::new(rfc_schema_filename);
188        let existing_schema_json = fs::read_to_string(path);
189
190        match existing_schema_json {
191            Ok(rfc_schema_json) => {
192                let rfc_schema: RootSchema =
193                    serde_json::from_str(&rfc_schema_json).expect("RFC schema to be valid JSON");
194                if rfc_schema != schema {
195                    eprintln!(
196                        "Schema mismatch — updating file at {} with the latest schema.",
197                        rfc_schema_filename
198                    );
199                    fs::write(path, &schema_json).expect("Failed to write updated schema");
200                    panic!("Schema updated. Please commit the new file.");
201                }
202            }
203            Err(_) => {
204                eprintln!(
205                    "RFC schema file not found — creating new schema file at {}",
206                    rfc_schema_filename
207                );
208                fs::create_dir_all(path.parent().unwrap())
209                    .expect("Failed to create parent directories");
210                fs::write(path, &schema_json).expect("Failed to write schema file");
211                panic!("New schema file created. Please commit it.");
212            }
213        }
214    }
215
216    impl test_utils::TestInstance for CrashInfo {
217        fn test_instance(seed: u64) -> Self {
218            let mut counters = HashMap::new();
219            counters.insert("collecting_sample".to_owned(), 1);
220            counters.insert("not_profiling".to_owned(), 0);
221
222            let span_ids = vec![
223                Span {
224                    id: "42".to_string(),
225                    thread_name: Some("thread1".to_string()),
226                },
227                Span {
228                    id: "24".to_string(),
229                    thread_name: Some("thread2".to_string()),
230                },
231            ];
232
233            let trace_ids = vec![
234                Span {
235                    id: "345".to_string(),
236                    thread_name: Some("thread111".to_string()),
237                },
238                Span {
239                    id: "666".to_string(),
240                    thread_name: Some("thread222".to_string()),
241                },
242            ];
243
244            let mut files = HashMap::new();
245            files.insert(
246                "/proc/self/maps".to_string(),
247                vec![
248                    "55a1b2c3d000-55a1b2c4e000 r-xp 00000000 08:01 12345 /usr/bin/test".to_string(),
249                ],
250            );
251
252            Self {
253                counters,
254                data_schema_version: CrashInfo::current_schema_version(),
255                error: ErrorData::test_instance(seed),
256                experimental: Some(Experimental::new().with_additional_tags(vec![
257                    "custom_tag:value1".to_string(),
258                    "another_tag:value2".to_string(),
259                ])),
260                files,
261                fingerprint: None,
262                incomplete: true,
263                log_messages: vec![],
264                metadata: Metadata::test_instance(seed),
265                os_info: ::os_info::get().into(),
266                proc_info: Some(ProcInfo::test_instance(seed)),
267                sig_info: Some(SigInfo::test_instance(seed)),
268                span_ids,
269                timestamp: chrono::DateTime::from_timestamp(1568898000 /* Datadog IPO */, 0)
270                    .unwrap()
271                    .to_string(),
272                trace_ids,
273                ucontext: Some(Ucontext::test_instance(seed)),
274                uuid: uuid::uuid!("1d6b97cb-968c-40c9-af6e-e4b4d71e8781").to_string(),
275            }
276        }
277    }
278}