mod context;
mod lifecycle;
mod redact;
pub use context::{client_ip, origin_from_headers, outcome_from_http_status, prepare};
pub use lifecycle::{current, end, new_from_context, prepare_event, scope, EventHandle, OutcomeCapture, Recorder};
pub use redact::apply_redaction;
use std::collections::BTreeMap;
use chrono::{DateTime, Utc};
use serde::{Serialize, Serializer};
use serde_json::Value;
use uuid::Uuid;
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct Actor {
pub r#type: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub id: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub display_name: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub email: String,
}
impl Actor {
pub fn new(r#type: impl Into<String>) -> Self {
Actor {
r#type: r#type.into(),
..Default::default()
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct Target {
#[serde(skip_serializing_if = "String::is_empty")]
pub r#type: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub id: String,
}
impl Target {
pub fn new(r#type: impl Into<String>, id: impl Into<String>) -> Self {
Target {
r#type: r#type.into(),
id: id.into(),
}
}
pub fn is_empty(&self) -> bool {
self.r#type.is_empty() && self.id.is_empty()
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct Origin {
#[serde(skip_serializing_if = "String::is_empty")]
pub ip: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub user_agent: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub request_id: String,
}
impl Origin {
pub fn is_empty(&self) -> bool {
self.ip.is_empty() && self.user_agent.is_empty() && self.request_id.is_empty()
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct Outcome {
#[serde(skip_serializing_if = "String::is_empty")]
pub status: String,
#[serde(skip_serializing_if = "is_zero")]
pub code: i32,
#[serde(skip_serializing_if = "message_is_empty")]
pub message: Option<Value>,
}
impl Outcome {
pub fn is_empty(&self) -> bool {
self.status.is_empty() && self.code == 0 && message_is_empty(&self.message)
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct Change {
#[serde(skip_serializing_if = "Option::is_none")]
pub before: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub patch: Option<Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Event {
pub id: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub tenant_id: String,
#[serde(serialize_with = "serialize_rfc3339")]
pub occurred_at: DateTime<Utc>,
pub actor: Actor,
pub action: String,
#[serde(skip_serializing_if = "Target::is_empty")]
pub target: Target,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub metadata: BTreeMap<String, Value>,
#[serde(skip_serializing_if = "Origin::is_empty")]
pub origin: Origin,
#[serde(rename = "result", skip_serializing_if = "Outcome::is_empty")]
pub outcome: Outcome,
#[serde(skip_serializing_if = "Option::is_none")]
pub change: Option<Change>,
#[serde(skip_serializing_if = "String::is_empty")]
pub idempotency_key: String,
}
impl Default for Event {
fn default() -> Self {
Event::new("")
}
}
impl Event {
pub fn new(action: impl Into<String>) -> Self {
Event {
id: Uuid::new_v4().to_string(),
tenant_id: String::new(),
occurred_at: Utc::now(),
actor: Actor::default(),
action: action.into(),
target: Target::default(),
metadata: BTreeMap::new(),
origin: Origin::default(),
outcome: Outcome::default(),
change: None,
idempotency_key: String::new(),
}
}
pub fn with_field(&mut self, key: impl Into<String>, value: impl Serialize) -> &mut Self {
if let Ok(v) = serde_json::to_value(value) {
self.metadata.insert(key.into(), v);
}
self
}
pub fn with_fields(&mut self, fields: impl IntoIterator<Item = (String, Value)>) -> &mut Self {
self.metadata.extend(fields);
self
}
pub fn diff(&mut self, before: &impl Serialize, after: &impl Serialize) -> &mut Self {
self.diff_redacted(before, after, &[])
}
pub fn diff_redacted(
&mut self,
before: &impl Serialize,
after: &impl Serialize,
redact_paths: &[&str],
) -> &mut Self {
let (Ok(b), Ok(a)) = (serde_json::to_value(before), serde_json::to_value(after)) else {
return self;
};
self.change = Some(Change {
before: Some(apply_redaction(b, redact_paths)),
after: Some(apply_redaction(a, redact_paths)),
patch: None,
});
self
}
pub fn raw_diff(
&mut self,
before: Option<Value>,
after: Option<Value>,
patch: Option<Value>,
) -> &mut Self {
if before.is_none() && after.is_none() && patch.is_none() {
return self;
}
self.change = Some(Change {
before,
after,
patch,
});
self
}
}
fn is_zero(n: &i32) -> bool {
*n == 0
}
fn message_is_empty(m: &Option<Value>) -> bool {
match m {
None => true,
Some(Value::String(s)) => s.is_empty(),
_ => false,
}
}
fn serialize_rfc3339<S: Serializer>(dt: &DateTime<Utc>, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&dt.to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true))
}