use serde::{Deserialize, Serialize};
use crate::breadcrumbs::Breadcrumb;
pub const SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EventKind {
Panic,
NativeCrash,
Corruption,
InvariantViolation,
Error,
}
impl EventKind {
#[must_use]
pub fn slug(self) -> &'static str {
match self {
EventKind::Panic => "panic",
EventKind::NativeCrash => "native_crash",
EventKind::Corruption => "corruption",
EventKind::InvariantViolation => "invariant_violation",
EventKind::Error => "error",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Meta {
pub project: String,
pub version: String,
pub git_sha: Option<String>,
pub build_id: Option<String>,
pub captured_at_ms: u128,
pub pid: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ppid: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thread: Option<String>,
}
impl Meta {
#[must_use]
pub fn current_ppid() -> Option<u32> {
#[cfg(any(target_os = "linux", target_os = "android"))]
{
let status = std::fs::read_to_string("/proc/self/status").ok()?;
status
.lines()
.find_map(|l| l.strip_prefix("PPid:"))
.and_then(|v| v.trim().parse().ok())
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
{
None
}
}
#[must_use]
pub fn current_thread_name() -> Option<String> {
std::thread::current().name().map(str::to_owned)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Frame {
pub address: Option<u64>,
pub symbol: Option<String>,
pub location: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Artifact {
pub kind: String,
pub rel_path: String,
pub note: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub digest: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bytes: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Env {
pub os: String,
pub arch: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub features: Vec<String>,
}
impl Env {
#[must_use]
pub fn current() -> Self {
Env {
os: std::env::consts::OS.to_owned(),
arch: std::env::consts::ARCH.to_owned(),
features: Vec::new(),
}
}
#[must_use]
pub fn with_features(features: Vec<String>) -> Self {
Env {
features,
..Env::current()
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Report {
pub schema_version: u32,
pub kind: EventKind,
pub message: String,
pub meta: Meta,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub error_chain: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub backtrace: Vec<Frame>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub breadcrumbs: Vec<Breadcrumb>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub domain_kind: Option<String>,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub domain: serde_json::Value,
pub env: Env,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub artifacts: Vec<Artifact>,
pub fingerprint: String,
pub occurrences: u64,
pub first_seen_ms: u128,
pub last_seen_ms: u128,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn report_roundtrips_through_json() {
let report = Report {
schema_version: SCHEMA_VERSION,
kind: EventKind::Corruption,
message: "AEAD/MAC verification failed on read".to_owned(),
meta: Meta {
project: "myapp".to_owned(),
version: "0.1.0".to_owned(),
git_sha: Some("abc123".to_owned()),
build_id: Some("deadbeef".to_owned()),
captured_at_ms: 1,
pid: 42,
ppid: Some(7),
thread: Some("compactor".to_owned()),
},
error_chain: vec!["ChecksumFailure".to_owned()],
backtrace: vec![Frame {
address: Some(0x1000),
symbol: None,
location: None,
}],
breadcrumbs: Vec::new(),
domain_kind: Some("store.dangling_child".to_owned()),
domain: serde_json::json!({ "page_id": 828, "kind": "0x09" }),
env: Env::with_features(vec!["encryption".to_owned()]),
artifacts: vec![Artifact {
kind: "store-snapshot".to_owned(),
rel_path: "store.corrupt".to_owned(),
note: Some("fsck --deep".to_owned()),
digest: Some("00ff00ff00ff00ff".to_owned()),
bytes: Some(4096),
}],
fingerprint: "ff00ff00".to_owned(),
occurrences: 3,
first_seen_ms: 1,
last_seen_ms: 900,
};
let json = serde_json::to_string(&report).unwrap();
let back: Report = serde_json::from_str(&json).unwrap();
assert_eq!(back.kind, EventKind::Corruption);
assert_eq!(back.domain["page_id"], 828);
assert_eq!(back.artifacts.len(), 1);
assert_eq!(
back.artifacts[0].digest.as_deref(),
Some("00ff00ff00ff00ff")
);
assert_eq!(back.fingerprint, "ff00ff00");
assert_eq!(back.meta.thread.as_deref(), Some("compactor"));
assert_eq!(back.meta.ppid, Some(7));
assert_eq!(back.env.features, vec!["encryption".to_owned()]);
assert_eq!(
(back.occurrences, back.first_seen_ms, back.last_seen_ms),
(3, 1, 900)
);
}
#[test]
fn empty_collections_are_omitted_from_json() {
let report = Report {
schema_version: SCHEMA_VERSION,
kind: EventKind::Panic,
message: "boom".to_owned(),
meta: Meta {
project: "myapp".to_owned(),
version: "0.1.0".to_owned(),
git_sha: None,
build_id: None,
captured_at_ms: 0,
pid: 1,
ppid: None,
thread: None,
},
error_chain: Vec::new(),
backtrace: Vec::new(),
breadcrumbs: Vec::new(),
domain_kind: None,
domain: serde_json::Value::Null,
env: Env::current(),
artifacts: Vec::new(),
fingerprint: "0".to_owned(),
occurrences: 1,
first_seen_ms: 0,
last_seen_ms: 0,
};
let json = serde_json::to_string(&report).unwrap();
assert!(!json.contains("error_chain"));
assert!(!json.contains("breadcrumbs"));
assert!(!json.contains("\"domain\""));
assert!(!json.contains("artifacts"));
assert!(!json.contains("ppid"));
assert!(!json.contains("thread"));
}
}