pub mod auditable;
pub mod config;
pub mod context;
pub mod event;
pub mod layer;
pub mod service;
pub mod writer;
pub use auditable::Auditable;
pub use config::AuditConfig;
pub use context::AuditContext;
pub use event::AuditEvent;
pub use layer::AuditLayer;
pub use chrono;
pub use serde_json;
#[macro_export]
macro_rules! audit {
($ctx:expr, $action:expr, { $($key:tt : $value:expr),* $(,)? }) => {
{
let event = $crate::AuditEvent {
user_id: None,
action: $action.into(),
ip: None,
method: None,
path: None,
status: None,
latency_ms: None,
metadata: Some($crate::serde_json::json!({ $($key: $value),* })),
service_name: String::new(),
ts: $crate::chrono::Utc::now(),
};
$crate::AuditContext::send(&$ctx, event);
}
};
($ctx:expr, $action:expr) => {
{
let event = $crate::AuditEvent {
user_id: None,
action: $action.into(),
ip: None,
method: None,
path: None,
status: None,
latency_ms: None,
metadata: None,
service_name: String::new(),
ts: $crate::chrono::Utc::now(),
};
$crate::AuditContext::send(&$ctx, event);
}
};
}
#[macro_export]
macro_rules! audit_diff {
($ctx:expr, $action:expr, $old:expr, $new:expr) => {
{
let changed: std::collections::HashSet<String> =
$crate::Auditable::changed_fields($old, $new);
let metadata = $crate::serde_json::json!({
"before": $crate::Auditable::to_audit_json($old),
"after": $crate::Auditable::to_audit_json($new),
"changed": changed.iter().collect::<Vec<_>>(),
});
let event = $crate::AuditEvent {
user_id: None,
action: $action.into(),
ip: None,
method: None,
path: None,
status: None,
latency_ms: None,
metadata: Some(metadata),
service_name: String::new(),
ts: $crate::chrono::Utc::now(),
};
$crate::AuditContext::send(&$ctx, event);
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn audit_macro_constructs_event_with_metadata() {
let (ctx, mut rx) = AuditContext::channel();
audit!(ctx, "user.login", {
"method": "passkey",
"success": true,
});
let event = rx.try_recv().expect("event should be sent");
assert_eq!(event.action, "user.login");
assert_eq!(
event.metadata,
Some(serde_json::json!({"method": "passkey", "success": true}))
);
}
#[test]
fn audit_macro_no_metadata() {
let (ctx, mut rx) = AuditContext::channel();
audit!(ctx, "health.check");
let event = rx.try_recv().expect("event should be sent");
assert_eq!(event.action, "health.check");
assert!(event.metadata.is_none());
}
#[test]
fn audit_diff_macro_includes_before_after_changed() {
use crate::auditable::Auditable;
use serde_json::Value;
#[derive(Clone)]
struct User {
name: String,
}
impl Auditable for User {
fn to_audit_json(&self) -> Value {
serde_json::json!({"name": self.name})
}
}
let (ctx, mut rx) = AuditContext::channel();
let old = User { name: "Alice".into() };
let new = User { name: "Bob".into() };
audit_diff!(ctx, "user.renamed", &old, &new);
let event = rx.try_recv().expect("event sent");
assert_eq!(event.action, "user.renamed");
let meta = event.metadata.expect("metadata present");
assert_eq!(meta["before"]["name"], "Alice");
assert_eq!(meta["after"]["name"], "Bob");
assert!(meta["changed"]
.as_array()
.unwrap()
.contains(&serde_json::Value::String("name".into())));
}
}