diff --git a/Cargo.lock b/Cargo.lock index 1f9c3739a..2e5585eb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1334,6 +1334,8 @@ dependencies = [ "nix 0.27.1", "os_info", "page_size", + "portable-atomic", + "rand", "serde", "serde_json", "tempfile", @@ -3547,6 +3549,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +dependencies = [ + "serde", +] + [[package]] name = "powerfmt" version = "0.2.0" diff --git a/LICENSE-3rdparty.yml b/LICENSE-3rdparty.yml index fad82f75c..02092bb0e 100644 --- a/LICENSE-3rdparty.yml +++ b/LICENSE-3rdparty.yml @@ -18399,6 +18399,215 @@ third_party_libraries: shall be included in all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF + ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT + SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR + IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +- package_name: portable-atomic + package_version: 1.6.0 + repository: https://github.com/taiki-e/portable-atomic + license: Apache-2.0 OR MIT + licenses: + - license: Apache-2.0 + text: |2 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + - license: MIT + text: | + Permission is hereby granted, free of charge, to any + person obtaining a copy of this software and associated + documentation files (the "Software"), to deal in the + Software without restriction, including without + limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software + is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice + shall be included in all copies or substantial portions + of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A diff --git a/crashtracker/Cargo.toml b/crashtracker/Cargo.toml index 7653a2734..5769e67e3 100644 --- a/crashtracker/Cargo.toml +++ b/crashtracker/Cargo.toml @@ -31,6 +31,8 @@ uuid = { version = "1.4.1", features = ["v4", "serde"] } ddtelemetry = {path = "../ddtelemetry"} tokio = { version = "1.23", features = ["rt", "macros"] } http = "0.2" +portable-atomic = { version = "1.6.0", features = ["serde"] } +rand = "0.8.5" [dev-dependencies] tempfile = { version = "3.3" } diff --git a/crashtracker/src/api.rs b/crashtracker/src/api.rs index e2345e9b6..bda726f41 100644 --- a/crashtracker/src/api.rs +++ b/crashtracker/src/api.rs @@ -3,6 +3,7 @@ #![cfg(unix)] use crate::{ + clear_spans, clear_traces, configuration::CrashtrackerReceiverConfig, counters::reset_counters, crash_handler::{ @@ -57,6 +58,8 @@ pub fn on_fork( receiver_config: CrashtrackerReceiverConfig, metadata: CrashtrackerMetadata, ) -> anyhow::Result<()> { + clear_spans()?; + clear_traces()?; reset_counters()?; // Leave the old signal handler in place: they are unaffected by fork. // https://man7.org/linux/man-pages/man2/sigaction.2.html @@ -171,6 +174,10 @@ fn test_crash() -> anyhow::Result<()> { ); init_with_receiver(config, receiver_config, metadata)?; begin_profiling_op(crate::ProfilingOpTypes::CollectingSample)?; + super::insert_span(42)?; + super::insert_trace(u128::MAX)?; + super::insert_span(12)?; + super::insert_trace(99399939399939393993)?; let tag = tag!("apple", "banana"); let metadata2 = CrashtrackerMetadata::new( diff --git a/crashtracker/src/constants.rs b/crashtracker/src/constants.rs index 52b901f7d..7905de48a 100644 --- a/crashtracker/src/constants.rs +++ b/crashtracker/src/constants.rs @@ -7,7 +7,9 @@ pub const DD_CRASHTRACK_BEGIN_FILE: &str = "DD_CRASHTRACK_BEGIN_FILE"; pub const DD_CRASHTRACK_BEGIN_METADATA: &str = "DD_CRASHTRACK_BEGIN_METADATA"; pub const DD_CRASHTRACK_BEGIN_PROCINFO: &str = "DD_CRASHTRACK_BEGIN_PROCESSINFO"; pub const DD_CRASHTRACK_BEGIN_SIGINFO: &str = "DD_CRASHTRACK_BEGIN_SIGINFO"; +pub const DD_CRASHTRACK_BEGIN_SPAN_IDS: &str = "DD_CRASHTRACK_BEGIN_SPAN_IDS"; pub const DD_CRASHTRACK_BEGIN_STACKTRACE: &str = "DD_CRASHTRACK_BEGIN_STACKTRACE"; +pub const DD_CRASHTRACK_BEGIN_TRACE_IDS: &str = "DD_CRASHTRACK_BEGIN_TRACE_IDS"; pub const DD_CRASHTRACK_DONE: &str = "DD_CRASHTRACK_DONE"; pub const DD_CRASHTRACK_END_CONFIG: &str = "DD_CRASHTRACK_END_CONFIG"; pub const DD_CRASHTRACK_END_COUNTERS: &str = "DD_CRASHTRACK_END_COUNTERS"; @@ -15,4 +17,6 @@ pub const DD_CRASHTRACK_END_FILE: &str = "DD_CRASHTRACK_END_FILE"; pub const DD_CRASHTRACK_END_METADATA: &str = "DD_CRASHTRACK_END_METADATA"; pub const DD_CRASHTRACK_END_PROCINFO: &str = "DD_CRASHTRACK_END_PROCESSINFO"; pub const DD_CRASHTRACK_END_SIGINFO: &str = "DD_CRASHTRACK_END_SIGINFO"; +pub const DD_CRASHTRACK_END_SPAN_IDS: &str = "DD_CRASHTRACK_END_SPAN_IDS"; pub const DD_CRASHTRACK_END_STACKTRACE: &str = "DD_CRASHTRACK_END_STACKTRACE"; +pub const DD_CRASHTRACK_END_TRACE_IDS: &str = "DD_CRASHTRACK_END_TRACE_IDS"; diff --git a/crashtracker/src/crash_handler.rs b/crashtracker/src/crash_handler.rs index 37d23580e..f0aceee23 100644 --- a/crashtracker/src/crash_handler.rs +++ b/crashtracker/src/crash_handler.rs @@ -4,6 +4,7 @@ #![cfg(unix)] use crate::configuration::CrashtrackerReceiverConfig; +use crate::spans::{emit_spans, emit_traces}; use super::collectors::emit_backtrace_by_frames; #[cfg(target_os = "linux")] @@ -360,6 +361,10 @@ fn emit_crashreport( pipe.flush()?; emit_counters(pipe)?; pipe.flush()?; + emit_spans(pipe)?; + pipe.flush()?; + emit_traces(pipe)?; + pipe.flush()?; #[cfg(target_os = "linux")] emit_proc_self_maps(pipe)?; diff --git a/crashtracker/src/crash_info.rs b/crashtracker/src/crash_info.rs index 2d3d626b5..6161f8cb1 100644 --- a/crashtracker/src/crash_info.rs +++ b/crashtracker/src/crash_info.rs @@ -62,6 +62,7 @@ pub struct CrashInfo { #[serde(skip_serializing_if = "HashMap::is_empty")] #[serde(default)] pub files: HashMap>, + pub incomplete: bool, #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] pub metadata: Option, @@ -74,8 +75,13 @@ pub struct CrashInfo { pub siginfo: Option, #[serde(skip_serializing_if = "Vec::is_empty")] #[serde(default)] + pub span_ids: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default)] pub stacktrace: Vec, - pub incomplete: bool, + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default)] + pub trace_ids: Vec, /// Any additional data goes here #[serde(skip_serializing_if = "HashMap::is_empty")] #[serde(default)] @@ -146,9 +152,11 @@ impl CrashInfo { os_info, proc_info: None, siginfo: None, + span_ids: vec![], stacktrace: vec![], tags: HashMap::new(), timestamp: None, + trace_ids: vec![], uuid, } } @@ -210,6 +218,11 @@ impl CrashInfo { self.siginfo = Some(siginfo); Ok(()) } + pub fn set_span_ids(&mut self, ids: Vec) -> anyhow::Result<()> { + anyhow::ensure!(self.span_ids.is_empty()); + self.span_ids = ids; + Ok(()) + } pub fn set_stacktrace( &mut self, @@ -236,6 +249,12 @@ impl CrashInfo { pub fn set_timestamp_to_now(&mut self) -> anyhow::Result<()> { self.set_timestamp(Utc::now()) } + + pub fn set_trace_ids(&mut self, ids: Vec) -> anyhow::Result<()> { + anyhow::ensure!(self.trace_ids.is_empty()); + self.trace_ids = ids; + Ok(()) + } } impl CrashInfo { diff --git a/crashtracker/src/lib.rs b/crashtracker/src/lib.rs index 1574273ed..869e1e839 100644 --- a/crashtracker/src/lib.rs +++ b/crashtracker/src/lib.rs @@ -54,6 +54,7 @@ mod counters; mod crash_handler; mod crash_info; mod receiver; +mod spans; mod stacktrace; mod telemetry; @@ -69,4 +70,5 @@ pub use crash_handler::{update_config, update_metadata}; pub use crash_info::*; #[cfg(unix)] pub use receiver::{receiver_entry_point_stdin, reciever_entry_point_unix_socket}; +pub use spans::{clear_spans, clear_traces, insert_span, insert_trace, remove_span, remove_trace}; pub use stacktrace::{NormalizedAddress, NormalizedAddressMeta, StackFrame, StackFrameNames}; diff --git a/crashtracker/src/receiver.rs b/crashtracker/src/receiver.rs index 1686b1a12..9c1a046ed 100644 --- a/crashtracker/src/receiver.rs +++ b/crashtracker/src/receiver.rs @@ -83,7 +83,9 @@ enum StdinState { Metadata, ProcInfo, SigInfo, + SpanIds, StackTrace(Vec), + TraceIds, Waiting, } @@ -160,6 +162,13 @@ fn process_line( StdinState::SigInfo } + StdinState::SpanIds if line.starts_with(DD_CRASHTRACK_END_SPAN_IDS) => StdinState::Waiting, + StdinState::SpanIds => { + let v: Vec = serde_json::from_str(&line)?; + crashinfo.set_span_ids(v)?; + StdinState::SpanIds + } + StdinState::StackTrace(stacktrace) if line.starts_with(DD_CRASHTRACK_END_STACKTRACE) => { crashinfo.set_stacktrace(None, stacktrace)?; StdinState::Waiting @@ -170,6 +179,15 @@ fn process_line( StdinState::StackTrace(stacktrace) } + StdinState::TraceIds if line.starts_with(DD_CRASHTRACK_END_TRACE_IDS) => { + StdinState::Waiting + } + StdinState::TraceIds => { + let v: Vec = serde_json::from_str(&line)?; + crashinfo.set_trace_ids(v)?; + StdinState::TraceIds + } + StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_CONFIG) => StdinState::Config, StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_COUNTERS) => { StdinState::Counters @@ -185,9 +203,15 @@ fn process_line( StdinState::ProcInfo } StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_SIGINFO) => StdinState::SigInfo, + StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_SPAN_IDS) => { + StdinState::SpanIds + } StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_STACKTRACE) => { StdinState::StackTrace(vec![]) } + StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_TRACE_IDS) => { + StdinState::TraceIds + } StdinState::Waiting if line.starts_with(DD_CRASHTRACK_DONE) => StdinState::Done, StdinState::Waiting => { //TODO: Do something here? diff --git a/crashtracker/src/spans.rs b/crashtracker/src/spans.rs new file mode 100644 index 000000000..f375cc8e6 --- /dev/null +++ b/crashtracker/src/spans.rs @@ -0,0 +1,265 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use portable_atomic::{AtomicU128, AtomicUsize}; +use rand::Rng; +use std::io::Write; +use std::sync::atomic::Ordering::SeqCst; + +static ACTIVE_SPANS: AtomicU128Set<2048> = AtomicU128Set::new(); +static ACTIVE_TRACES: AtomicU128Set<2048> = AtomicU128Set::new(); + +pub fn clear_spans() -> anyhow::Result<()> { + ACTIVE_SPANS.clear() +} + +#[allow(dead_code)] +pub fn emit_spans(w: &mut impl Write) -> anyhow::Result<()> { + use super::constants::*; + writeln!(w, "{DD_CRASHTRACK_BEGIN_SPAN_IDS}")?; + ACTIVE_SPANS.emit(w)?; + writeln!(w, "{DD_CRASHTRACK_END_SPAN_IDS}")?; + Ok(()) +} + +pub fn insert_span(value: u128) -> anyhow::Result { + ACTIVE_SPANS.insert(value) +} + +pub fn remove_span(value: u128, idx: usize) -> anyhow::Result<()> { + ACTIVE_SPANS.remove(value, idx) +} + +pub fn clear_traces() -> anyhow::Result<()> { + ACTIVE_TRACES.clear() +} + +#[allow(dead_code)] +pub fn emit_traces(w: &mut impl Write) -> anyhow::Result<()> { + use super::constants::*; + writeln!(w, "{DD_CRASHTRACK_BEGIN_TRACE_IDS}")?; + ACTIVE_TRACES.emit(w)?; + writeln!(w, "{DD_CRASHTRACK_END_TRACE_IDS}")?; + Ok(()) +} + +pub fn insert_trace(value: u128) -> anyhow::Result { + ACTIVE_TRACES.insert(value) +} + +pub fn remove_trace(value: u128, idx: usize) -> anyhow::Result<()> { + ACTIVE_TRACES.remove(value, idx) +} + +struct AtomicU128Set { + used: AtomicUsize, + set: [AtomicU128; LEN], +} + +#[allow(dead_code)] +impl AtomicU128Set { + /// Atomicity: This is NOT ATOMIC. If other code modifies the set while this is happening, + /// badness will occur. + pub fn clear(&self) -> anyhow::Result<()> { + if self.is_empty() { + for v in self.set.iter() { + let old = v.swap(0, SeqCst); + if old != 0 { + self.used.sub(1, SeqCst) + } + } + } + Ok(()) + } + + pub fn emit(&self, w: &mut impl Write) -> anyhow::Result<()> { + write!(w, "[")?; + + if self.used.load(SeqCst) > 0 { + let mut first = true; + for it in self.set.iter() { + let v = it.load(SeqCst); + if v != 0 { + if !first { + write!(w, ", ")?; + } + first = false; + write!(w, "{v}")?; + } + } + } + writeln!(w, "]")?; + + Ok(()) + } + + pub const fn new() -> Self { + // In this case, we actually WANT multiple copies of the interior mutable struct + #[allow(clippy::declare_interior_mutable_const)] + const ATOMIC_ZERO: AtomicU128 = AtomicU128::new(0); + Self { + used: AtomicUsize::new(0), + set: [ATOMIC_ZERO; LEN], + } + } + + /// Add + pub fn insert(&self, value: u128) -> anyhow::Result { + let used = self.used.fetch_add(1, SeqCst); + if used >= self.set.len() / 2 { + // We only fill to half full to get good amortized behaviour + self.used.fetch_sub(1, SeqCst); + anyhow::bail!("Crashtracker: No space to store span {value}"); + } + + // Start at a random position. + // Since the array is only at most half full, and since we start scanning at random + // indicies, every slot should independently have <.5 probability of being occupied. + // Long scans become exponentially unlikely, giving amortized constant time insertion. + let shift: usize = rand::thread_rng().gen_range(0..self.set.len()); + for i in 0..self.set.len() { + let idx = (i + shift) % self.set.len(); + if self.set[idx] + .compare_exchange(0, value, SeqCst, SeqCst) + .is_ok() + { + return Ok(idx); + } + } + anyhow::bail!("This should be unreachable: we ensure that there was at least one empty slot before entering the loop") + } + + pub fn is_empty(&self) -> bool { + self.len() != 0 + } + + pub fn len(&self) -> usize { + self.used.load(SeqCst) + } + + pub fn remove(&self, value: u128, idx: usize) -> anyhow::Result<()> { + anyhow::ensure!(idx < self.set.len(), "Idx {idx} out of range"); + match self.set[idx].compare_exchange(value, 0, SeqCst, SeqCst) { + Ok(_) => { + self.used.fetch_sub(1, SeqCst); + Ok(()) + } + Err(old) => { + anyhow::bail!("Invalid index/span_id pair: Expected {value} at {idx}, got {old}") + } + } + } + + pub fn values(&self) -> anyhow::Result> { + let mut rval = Vec::with_capacity(self.used.load(SeqCst)); + if self.used.load(SeqCst) > 0 { + for it in self.set.iter() { + let v = it.load(SeqCst); + if v != 0 { + rval.push(v); + } + } + } + rval.sort(); + Ok(rval) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new() -> anyhow::Result<()> { + let s: AtomicU128Set<16> = AtomicU128Set::new(); + assert_eq!(s.len(), 0); + assert_eq!(&s.values()?, &[]); + Ok(()) + } + + #[test] + fn test_ops() -> anyhow::Result<()> { + let mut expected = std::collections::BTreeMap::::new(); + let s: AtomicU128Set<8> = AtomicU128Set::new(); + compare(&s, &expected); + insert_and_compare(&s, &mut expected, 42); + insert_and_compare(&s, &mut expected, 21); + insert_and_compare(&s, &mut expected, 19); + insert_and_compare(&s, &mut expected, 3); + insert(&s, &mut expected, 8).expect_err("Should stop when half full"); + + s.remove(42, 200) + .expect_err("Shouldn't let us go outside the range"); + // Try removing a value at the wrong idx, nothing happens. + let idx = expected.get(&42).unwrap(); + s.remove(43, (*idx + 1) % s.len()).unwrap_err(); + compare(&s, &expected); + + remove_and_compare(&s, &mut expected, 42); + insert_and_compare(&s, &mut expected, 12); + remove_and_compare(&s, &mut expected, 19); + + s.clear()?; + expected.clear(); + compare(&s, &expected); + insert_and_compare(&s, &mut expected, 12); + + Ok(()) + } + + #[test] + fn test_emit() { + let s: AtomicU128Set<8> = AtomicU128Set::new(); + s.insert(42).unwrap(); + s.insert(21).unwrap(); + let mut buf = Vec::new(); + s.emit(&mut buf).unwrap(); + let actual = String::from_utf8(buf).unwrap(); + assert!(actual == "[42, 21]\n" || actual == "[21, 42]\n"); + } + + fn remove_and_compare( + s: &AtomicU128Set<8>, + expected: &mut std::collections::BTreeMap, + v: u128, + ) { + remove(s, expected, v).unwrap(); + compare(s, expected); + } + + fn remove( + s: &AtomicU128Set<8>, + expected: &mut std::collections::BTreeMap, + v: u128, + ) -> anyhow::Result<()> { + let idx = expected.get(&v).unwrap(); + s.remove(v, *idx).unwrap(); + expected.remove(&v); + Ok(()) + } + + fn compare(s: &AtomicU128Set<8>, expected: &std::collections::BTreeMap) { + let actual = s.values().unwrap(); + let golden: Vec = expected.keys().cloned().collect(); + assert_eq!(actual, golden); + assert_eq!(expected.len(), s.len()); + } + + fn insert( + s: &AtomicU128Set<8>, + expected: &mut std::collections::BTreeMap, + v: u128, + ) -> anyhow::Result<()> { + expected.insert(v, s.insert(v)?); + Ok(()) + } + + fn insert_and_compare( + s: &AtomicU128Set<8>, + expected: &mut std::collections::BTreeMap, + v: u128, + ) { + insert(s, expected, v).unwrap(); + compare(s, expected); + } +} diff --git a/crashtracker/src/telemetry.rs b/crashtracker/src/telemetry.rs index ad2469f35..f6a8ff744 100644 --- a/crashtracker/src/telemetry.rs +++ b/crashtracker/src/telemetry.rs @@ -47,7 +47,9 @@ struct TelemetryCrashInfoMessage<'a> { pub files: &'a HashMap>, pub metadata: Option<&'a CrashtrackerMetadata>, pub os_info: &'a os_info::Info, + pub span_ids: &'a Vec, pub tags: &'a HashMap, + pub trace_ids: &'a Vec, } pub struct TelemetryCrashUploader { @@ -128,7 +130,9 @@ impl TelemetryCrashUploader { files: &crash_info.files, metadata: crash_info.metadata.as_ref(), os_info: &crash_info.os_info, + span_ids: &crash_info.span_ids, tags: &crash_info.tags, + trace_ids: &crash_info.trace_ids, })?; let stack_trace = serde_json::to_string(&crash_info.stacktrace)?; @@ -287,6 +291,8 @@ mod tests { }), proc_info: None, stacktrace: vec![], + span_ids: vec![42, 24], + trace_ids: vec![345, 666], additional_stacktraces: HashMap::new(), tags: HashMap::new(), timestamp: DateTime::from_timestamp(1702465105, 0), diff --git a/profiling-ffi/src/crashtracker/datatypes.rs b/profiling-ffi/src/crashtracker/datatypes.rs index 77112c79c..23d7e5744 100644 --- a/profiling-ffi/src/crashtracker/datatypes.rs +++ b/profiling-ffi/src/crashtracker/datatypes.rs @@ -221,6 +221,22 @@ impl From for StringWrapperResult { } } +#[repr(C)] +pub enum CrashtrackerUsizeResult { + Ok(usize), + #[allow(dead_code)] + Err(Error), +} + +impl From> for CrashtrackerUsizeResult { + fn from(value: anyhow::Result) -> Self { + match value { + Ok(x) => Self::Ok(x), + Err(err) => Self::Err(err.into()), + } + } +} + #[repr(C)] pub enum CrashtrackerGetCountersResult { Ok([i64; ProfilingOpTypes::SIZE as usize]), diff --git a/profiling-ffi/src/crashtracker/mod.rs b/profiling-ffi/src/crashtracker/mod.rs index 98fe3ebc5..cba3828b2 100644 --- a/profiling-ffi/src/crashtracker/mod.rs +++ b/profiling-ffi/src/crashtracker/mod.rs @@ -7,6 +7,7 @@ mod counters; mod crash_info; mod datatypes; mod demangler; +mod spans; #[cfg(unix)] pub use collector::*; @@ -14,3 +15,4 @@ pub use counters::*; pub use crash_info::*; pub use datatypes::*; pub use demangler::*; +pub use spans::*; diff --git a/profiling-ffi/src/crashtracker/spans.rs b/profiling-ffi/src/crashtracker/spans.rs new file mode 100644 index 000000000..036089ac5 --- /dev/null +++ b/profiling-ffi/src/crashtracker/spans.rs @@ -0,0 +1,166 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use crate::crashtracker::datatypes::*; +use anyhow::Context; + +/// Resets all stored spans to 0. +/// Expected to be used after a fork, to reset the spans on the child +/// ATOMICITY: +/// This is NOT ATOMIC. +/// Should only be used when no conflicting updates can occur, +/// e.g. after a fork but before profiling ops start on the child. +/// # Safety +/// No safety concerns. +#[no_mangle] +#[must_use] +pub unsafe extern "C" fn ddog_prof_Crashtracker_clear_span_ids() -> CrashtrackerResult { + datadog_crashtracker::clear_spans() + .context("ddog_prof_Crashtracker_clear_span_ids failed") + .into() +} + +/// Resets all stored traces to 0. +/// Expected to be used after a fork, to reset the traces on the child +/// ATOMICITY: +/// This is NOT ATOMIC. +/// Should only be used when no conflicting updates can occur, +/// e.g. after a fork but before profiling ops start on the child. +/// # Safety +/// No safety concerns. +#[no_mangle] +#[must_use] +pub unsafe extern "C" fn ddog_prof_Crashtracker_clear_trace_ids() -> CrashtrackerResult { + datadog_crashtracker::clear_traces() + .context("ddog_prof_Crashtracker_clear_trace_ids failed") + .into() +} + +#[no_mangle] +#[must_use] +/// Atomically registers an active traceId. +/// Useful for tracking what operations were occurring when a crash occurred. +/// 0 is reserved for "NoId" +/// The set does not check for duplicates. Adding the same id twice is an error. +/// +/// Inputs: +/// id: the 128 bit id, broken into 2 64 bit chunks (see note) +/// +/// Returns: +/// Ok(handle) on success. The handle is needed to later remove the id; +/// Err() on failure. The most likely cause of failure is that the underlying set is full. +/// +/// Note: 128 bit ints in FFI were not stabilized until Rust 1.77 +/// https://blog.rust-lang.org/2024/03/30/i128-layout-update.html +/// We're currently locked into 1.71, have to do an ugly workaround involving 2 64 bit ints +/// until we can upgrade. +/// +/// # Safety +/// No safety concerns. +pub unsafe extern "C" fn ddog_prof_Crashtracker_insert_trace_id( + id_high: u64, + id_low: u64, +) -> CrashtrackerUsizeResult { + let id: u128 = (id_high as u128) << 64 | (id_low as u128); + datadog_crashtracker::insert_trace(id) + .context("ddog_prof_Crashtracker_insert_trace_id failed") + .into() +} + +#[no_mangle] +#[must_use] +/// Atomically registers an active SpanId. +/// Useful for tracking what operations were occurring when a crash occurred. +/// 0 is reserved for "NoId". +/// The set does not check for duplicates. Adding the same id twice is an error. +/// +/// Inputs: +/// id: the 128 bit id, broken into 2 64 bit chunks (see note) +/// +/// Returns: +/// Ok(handle) on success. The handle is needed to later remove the id; +/// Err() on failure. The most likely cause of failure is that the underlying set is full. +/// +/// Note: 128 bit ints in FFI were not stabilized until Rust 1.77 +/// https://blog.rust-lang.org/2024/03/30/i128-layout-update.html +/// We're currently locked into 1.71, have to do an ugly workaround involving 2 64 bit ints +/// until we can upgrade. + +/// +/// # Safety +/// No safety concerns. +pub unsafe extern "C" fn ddog_prof_Crashtracker_insert_span_id( + id_high: u64, + id_low: u64, +) -> CrashtrackerUsizeResult { + let id: u128 = (id_high as u128) << 64 | (id_low as u128); + datadog_crashtracker::insert_span(id) + .context("ddog_prof_Crashtracker_insert_span_id failed") + .into() +} + +#[no_mangle] +#[must_use] +/// Atomically removes a completed SpanId. +/// Useful for tracking what operations were occurring when a crash occurred. +/// 0 is reserved for "NoId" +/// +/// Inputs: +/// id: the 128 bit id, broken into 2 64 bit chunks (see note) +/// idx: The handle for the id, from a previous successful call to `insert_span_id`. +/// Attempting to remove the same element twice is an error. +/// Returns: +/// `Ok` on success. +/// `Err` on failure. If `id` is not found at `idx`, `Err` will be returned and the set will not +/// be modified. +/// +/// Note: 128 bit ints in FFI were not stabilized until Rust 1.77 +/// https://blog.rust-lang.org/2024/03/30/i128-layout-update.html +/// We're currently locked into 1.71, have to do an ugly workaround involving 2 64 bit ints +/// until we can upgrade. +/// +/// # Safety +/// No safety concerns. +pub unsafe extern "C" fn ddog_prof_Crashtracker_remove_span_id( + id_high: u64, + id_low: u64, + idx: usize, +) -> CrashtrackerResult { + let id: u128 = (id_high as u128) << 64 | (id_low as u128); + datadog_crashtracker::remove_span(id, idx) + .context("ddog_prof_Crashtracker_remove_span_id failed") + .into() +} + +#[no_mangle] +#[must_use] +/// Atomically removes a completed TraceId. +/// Useful for tracking what operations were occurring when a crash occurred. +/// 0 is reserved for "NoId" +/// +/// Inputs: +/// id: the 128 bit id, broken into 2 64 bit chunks (see note) +/// idx: The handle for the id, from a previous successful call to `insert_span_id`. +/// Attempting to remove the same element twice is an error. +/// Returns: +/// `Ok` on success. +/// `Err` on failure. If `id` is not found at `idx`, `Err` will be returned and the set will not +/// be modified. +/// +/// Note: 128 bit ints in FFI were not stabilized until Rust 1.77 +/// https://blog.rust-lang.org/2024/03/30/i128-layout-update.html +/// We're currently locked into 1.71, have to do an ugly workaround involving 2 64 bit ints +/// until we can upgrade. +/// +/// # Safety +/// No safety concerns. +pub unsafe extern "C" fn ddog_prof_Crashtracker_remove_trace_id( + id_high: u64, + id_low: u64, + idx: usize, +) -> CrashtrackerResult { + let id: u128 = (id_high as u128) << 64 | (id_low as u128); + datadog_crashtracker::remove_trace(id, idx) + .context("ddog_prof_Crashtracker_remove_trace_id failed") + .into() +}