use serde::Serialize;
use serde_json::{Map, Value};
use crate::level::Level;
#[derive(Debug, Clone, Serialize)]
pub struct Event {
pub ingestion_id: String,
pub level: Level,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub backtrace: Option<String>,
pub environment: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub release: Option<String>,
pub occurred_at: String,
#[serde(skip_serializing_if = "Map::is_empty")]
pub metadata: Map<String, Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<User>,
#[serde(skip_serializing_if = "std::collections::BTreeMap::is_empty")]
pub tags: std::collections::BTreeMap<String, String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub breadcrumbs: Vec<Breadcrumb>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fingerprint: Option<Vec<String>>,
}
impl Event {
pub fn new(level: Level, message: impl Into<String>) -> Self {
Event {
ingestion_id: crate::util::new_uuid(),
level,
message: message.into(),
backtrace: None,
environment: "production".to_string(),
release: None,
occurred_at: crate::util::now_iso8601(),
metadata: Map::new(),
user: None,
tags: std::collections::BTreeMap::new(),
breadcrumbs: Vec::new(),
fingerprint: None,
}
}
pub fn set_metadata(&mut self, key: impl Into<String>, value: impl Into<Value>) -> &mut Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn set_fingerprint<I, S>(&mut self, parts: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let v: Vec<String> = parts.into_iter().map(Into::into).collect();
self.fingerprint = if v.is_empty() { None } else { Some(v) };
self
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct User {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ip_address: Option<String>,
}
impl User {
pub fn new() -> Self {
Self::default()
}
pub fn id(mut self, v: impl Into<String>) -> Self {
self.id = Some(v.into());
self
}
pub fn email(mut self, v: impl Into<String>) -> Self {
self.email = Some(v.into());
self
}
pub fn username(mut self, v: impl Into<String>) -> Self {
self.username = Some(v.into());
self
}
pub fn ip_address(mut self, v: impl Into<String>) -> Self {
self.ip_address = Some(v.into());
self
}
pub fn is_empty(&self) -> bool {
self.id.is_none()
&& self.email.is_none()
&& self.username.is_none()
&& self.ip_address.is_none()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Breadcrumb {
pub timestamp: String,
pub category: String,
pub level: Level,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Map<String, Value>>,
}
impl Breadcrumb {
pub fn new(category: impl Into<String>, message: impl Into<String>) -> Self {
Breadcrumb {
timestamp: crate::util::now_iso8601(),
category: category.into(),
level: Level::Info,
message: message.into(),
data: None,
}
}
pub fn level(mut self, level: Level) -> Self {
self.level = level;
self
}
pub fn data(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
self.data
.get_or_insert_with(Map::new)
.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Frame {
pub filename: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub abs_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lineno: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub colno: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub function: Option<String>,
pub in_app: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub pre_context: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context_line: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub post_context: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Cause {
#[serde(skip_serializing_if = "Option::is_none")]
pub class: Option<String>,
pub message: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn event_serializes_expected_fields() {
let mut e = Event::new(Level::Error, "boom");
e.environment = "test".into();
e.set_metadata("exception_class", "std::io::Error");
let v: Value = serde_json::to_value(&e).unwrap();
assert_eq!(v["level"], "error");
assert_eq!(v["message"], "boom");
assert_eq!(v["environment"], "test");
assert_eq!(v["metadata"]["exception_class"], "std::io::Error");
assert!(v.get("tags").is_none());
assert!(v.get("breadcrumbs").is_none());
assert!(v.get("backtrace").is_none());
assert!(v.get("fingerprint").is_none());
assert!(v["ingestion_id"].as_str().unwrap().len() >= 32);
}
#[test]
fn user_is_empty_detection() {
assert!(User::new().is_empty());
assert!(!User::new().email("a@b.c").is_empty());
}
#[test]
fn fingerprint_empty_is_none() {
let mut e = Event::new(Level::Info, "x");
e.set_fingerprint(Vec::<String>::new());
assert!(e.fingerprint.is_none());
e.set_fingerprint(["a", "b"]);
assert_eq!(e.fingerprint.as_ref().unwrap().len(), 2);
}
}