Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(server): Extract crashpad annotations into contexts #892

Merged
merged 3 commits into from
Dec 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@

- Support dynamic sampling for error events. ([#883](https://github.com/getsentry/relay/pull/883))


**Bug Fixes**:

- Make all fields but event-id optional to fix regressions in user feedback ingestion. ([#886](https://github.com/getsentry/relay/pull/886))
- Remove `kafka-ssl` feature because it breaks development workflow on macOS. ([#889](https://github.com/getsentry/relay/pull/889))

**Internal**:

- Extract crashpad annotations into contexts. ([#892](https://github.com/getsentry/relay/pull/892))

## 20.12.1

- No documented changes.
Expand Down
39 changes: 9 additions & 30 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions relay-general/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,22 @@ chrono = { version = "0.4.11", features = ["serde"] }
cookie = { version = "0.12.0", features = ["percent-encode"] }
debugid = { version = "0.7.2", features = ["serde"] }
dynfmt = { version = "0.1.4", features = ["python", "curly"] }
enumset = "1.0.1"
failure = "0.1.8"
hmac = "0.7.1"
itertools = "0.8.2"
lazy_static = "1.4.0"
maxminddb = "0.13.0"
memmap = { version = "0.7.0", optional = true }
minidump = { git = "https://github.com/luser/rust-minidump", rev = "f8a18211dbb0bf7dca5dd41e578af4b366fcb9be" }
minidump = { git = "https://github.com/luser/rust-minidump", rev = "7a03c548837ea3006e74c8e8c809afe243bec655" }
num-traits = "0.2.12"
pest = "2.1.3"
pest_derive = "2.1.0"
regex = "1.3.9"
relay-common = { path = "../relay-common" }
relay-general-derive = { path = "derive" }
schemars = { version = "0.8.0", features = ["uuid", "chrono"], optional = true }
scroll = "0.9.0"
scroll = "0.10.2"
serde = { version = "1.0.114", features = ["derive"] }
serde_json = "1.0.55"
serde_urlencoded = "0.5.5"
Expand All @@ -38,7 +39,6 @@ uaparser = { version = "0.3.3", optional = true }
url = "2.1.1"
utf16string = "0.2.0"
uuid = { version = "0.8.1", features = ["v4", "serde"] }
enumset = "1.0.1"

[dev-dependencies]
criterion = "0.3"
Expand Down
2 changes: 1 addition & 1 deletion relay-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ itertools = "0.8.2"
json-forensics = { version = "*", git = "https://github.com/getsentry/rust-json-forensics" }
lazy_static = "1.4.0"
listenfd = "0.3.3"
minidump = { git = "https://github.com/luser/rust-minidump", rev = "f8a18211dbb0bf7dca5dd41e578af4b366fcb9be", optional = true }
minidump = { git = "https://github.com/luser/rust-minidump", rev = "7a03c548837ea3006e74c8e8c809afe243bec655", optional = true }
native-tls = { version = "0.2.4", optional = true }
parking_lot = "0.10.0"
rand_pcg="0.1.2"
Expand Down
122 changes: 119 additions & 3 deletions relay-server/src/utils/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@
//! These functions are invoked by the `EventProcessor`, and are used to prepare native event
//! payloads. See [`process_minidump`] and [`process_apple_crash_report`] for more information.

use std::collections::BTreeMap;

use chrono::{TimeZone, Utc};
use minidump::Minidump;
use minidump::{MinidumpAnnotation, MinidumpCrashpadInfo, MinidumpModuleList, Module};

use relay_general::protocol::{
Context, ContextInner, Contexts, Event, Exception, JsonLenientString, Level, Mechanism, Values,
};
use relay_general::types::{Annotated, Value};

use relay_general::protocol::{Event, Exception, JsonLenientString, Level, Mechanism, Values};
use relay_general::types::Annotated;
type Minidump<'a> = minidump::Minidump<'a, &'a [u8]>;

/// Placeholder payload fragments indicating a native event.
///
Expand Down Expand Up @@ -69,6 +75,109 @@ fn write_native_placeholder(event: &mut Event, placeholder: NativePlaceholder) {
}));
}

/// Generates crashpad contexts for annotations stored in the minidump.
///
/// Returns an error if either the minidump module list or the crashpad information stream cannot be
/// loaded from the minidump. Returns `Ok(())` in all other cases, including when no annotations are
/// present.
///
/// Crashpad has global annotations, and per-module annotations. For each of these, a separate
/// context of type "crashpad" is added, which contains the annotations as key-value mapping. List
/// annotations are added to an "annotations" JSON list.
fn write_crashpad_annotations(
event: &mut Event,
minidump: &Minidump<'_>,
) -> Result<(), minidump::Error> {
let module_list = minidump.get_stream::<MinidumpModuleList>()?;
let crashpad_info = match minidump.get_stream::<MinidumpCrashpadInfo>() {
Err(minidump::Error::StreamNotFound) => return Ok(()),
result => result?,
};

let contexts = event.contexts.get_or_insert_with(Contexts::new);

if !crashpad_info.simple_annotations.is_empty() {
// First, create a generic crashpad context with top-level simple annotations. This context does
// not need a type field, since its type matches the the key.
let crashpad_context = crashpad_info
.simple_annotations
.into_iter()
.map(|(key, value)| (key, Annotated::new(Value::from(value))))
.collect();

contexts.insert(
"crashpad".to_string(),
Annotated::new(ContextInner(Context::Other(crashpad_context))),
);
}

if crashpad_info.module_list.is_empty() {
return Ok(());
}

let modules = module_list.iter().collect::<Vec<_>>();

for module_info in crashpad_info.module_list {
// Resolve the actual module entry in the minidump module list. This entry should always
// exist and crashpad module info with an invalid link can be discarded. Since this is
// non-essential information, we skip gracefully and only emit debug logs.
let module = match modules.get(module_info.module_index) {
Some(module) => module,
None => {
relay_log::debug!(
"Skipping invalid minidump module index {}",
module_info.module_index
);
continue;
}
};

// Use the basename of the code file (library or executable name) as context name. The
// context type must be set explicitly in this case, which will render in Sentry as
// "Module.dll (crashpad)".
let code_file = module.code_file();
let (_, module_name) = symbolic::common::split_path(&code_file);

let mut module_context = BTreeMap::new();
module_context.insert(
"type".to_owned(),
Annotated::new(Value::String("crashpad".to_owned())),
);

for (key, value) in module_info.simple_annotations {
module_context.insert(key, Annotated::new(Value::String(value)));
}

for (key, annotation) in module_info.annotation_objects {
if let MinidumpAnnotation::String(value) = annotation {
module_context.insert(key, Annotated::new(Value::String(value)));
}
}

if !module_info.list_annotations.is_empty() {
// Annotation lists do not maintain a key-value mapping, so instead write them to an
// "annotations" key within the module context. This will render as a JSON list in Sentry.
let annotation_list = module_info
.list_annotations
.into_iter()
.map(|s| Annotated::new(Value::String(s)))
.collect();

module_context.insert(
"annotations".to_owned(),
Annotated::new(Value::Array(annotation_list)),
);
}

contexts.insert(
module_name.to_owned(),
Annotated::new(ContextInner(Context::Other(module_context))),
);
}

Ok(())
}

/// Extracts information from the minidump and writes it into the given event.
///
/// This function operates at best-effort. It always attaches the placeholder and returns
Expand All @@ -93,6 +202,13 @@ pub fn process_minidump(event: &mut Event, data: &[u8]) {
// days in the past, in which case the event may be rejected in store normalization.
let timestamp = Utc.timestamp(minidump.header.time_date_stamp.into(), 0);
event.timestamp.set_value(Some(timestamp.into()));

// Write annotations from the crashpad info stream, but skip gracefully on error. Annotations
// are non-essential to processing.
if let Err(err) = write_crashpad_annotations(event, &minidump) {
// TODO: Consider adding an event error for failed annotation extraction.
relay_log::debug!("Failed to parse minidump module list: {:?}", err);
}
}

/// Writes minimal information into the event to indicate it is associated with an Apple Crash
Expand Down
Binary file not shown.
38 changes: 38 additions & 0 deletions tests/integration/test_minidump.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,3 +469,41 @@ def test_minidump_ratelimit(
# Minidumps never return rate limits
relay.send_minidump(project_id=project_id, files=attachments)
outcomes_consumer.assert_rate_limited("static_disabled_quota")


def test_crashpad_annotations(mini_sentry, relay_with_processing, attachments_consumer):
dmp_path = os.path.join(
os.path.dirname(__file__), "fixtures/native/annotations.dmp"
)
with open(dmp_path, "rb") as f:
content = f.read()

relay = relay_with_processing(
{
# Prevent normalization from overwriting the minidump timestamp
"processing": {"max_secs_in_past": 2 ** 32 - 1}
}
)

project_id = 42
project_config = mini_sentry.add_full_project_config(project_id)

# Disable scurbbing, the basic and full project configs from the mini_sentry fixture
# will modify the minidump since it contains user paths in the module list. This breaks
# get_attachment_chunk() below.
del project_config["config"]["piiConfig"]

attachments_consumer = attachments_consumer()
attachments = [(MINIDUMP_ATTACHMENT_NAME, "minidump.dmp", content)]
relay.send_minidump(project_id=project_id, files=attachments)

# Only one attachment chunk expected
attachments_consumer.get_attachment_chunk()
event, _ = attachments_consumer.get_event()

# Check the placeholder payload
assert event["contexts"]["crashpad"] == {"hello": "world"}
assert event["contexts"]["dyld"] == {
"annotations": ["dyld2 mode"],
"type": "crashpad",
}