1pub mod auditable;
19pub mod config;
20pub mod context;
21pub mod event;
22pub mod layer;
23pub mod service;
24pub mod writer;
25
26pub use auditable::Auditable;
27pub use config::AuditConfig;
28pub use context::AuditContext;
29pub use event::AuditEvent;
30pub use layer::AuditLayer;
31
32pub use chrono;
34pub use serde_json;
35
36#[macro_export]
56macro_rules! audit {
57 ($ctx:expr, $action:expr, { $($key:tt : $value:expr),* $(,)? }) => {
59 {
60 let event = $crate::AuditEvent {
61 user_id: None,
62 action: $action.into(),
63 ip: None,
64 method: None,
65 path: None,
66 status: None,
67 latency_ms: None,
68 metadata: Some($crate::serde_json::json!({ $($key: $value),* })),
69 service_name: String::new(),
70 ts: $crate::chrono::Utc::now(),
71 };
72 $crate::AuditContext::send(&$ctx, event);
73 }
74 };
75 ($ctx:expr, $action:expr) => {
77 {
78 let event = $crate::AuditEvent {
79 user_id: None,
80 action: $action.into(),
81 ip: None,
82 method: None,
83 path: None,
84 status: None,
85 latency_ms: None,
86 metadata: None,
87 service_name: String::new(),
88 ts: $crate::chrono::Utc::now(),
89 };
90 $crate::AuditContext::send(&$ctx, event);
91 }
92 };
93}
94
95#[macro_export]
109macro_rules! audit_diff {
110 ($ctx:expr, $action:expr, $old:expr, $new:expr) => {
111 {
112 let changed: std::collections::HashSet<String> =
113 $crate::Auditable::changed_fields($old, $new);
114
115 let metadata = $crate::serde_json::json!({
116 "before": $crate::Auditable::to_audit_json($old),
117 "after": $crate::Auditable::to_audit_json($new),
118 "changed": changed.iter().collect::<Vec<_>>(),
119 });
120
121 let event = $crate::AuditEvent {
122 user_id: None,
123 action: $action.into(),
124 ip: None,
125 method: None,
126 path: None,
127 status: None,
128 latency_ms: None,
129 metadata: Some(metadata),
130 service_name: String::new(),
131 ts: $crate::chrono::Utc::now(),
132 };
133 $crate::AuditContext::send(&$ctx, event);
134 }
135 };
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn audit_macro_constructs_event_with_metadata() {
144 let (ctx, mut rx) = AuditContext::channel();
145
146 audit!(ctx, "user.login", {
147 "method": "passkey",
148 "success": true,
149 });
150
151 let event = rx.try_recv().expect("event should be sent");
152 assert_eq!(event.action, "user.login");
153 assert_eq!(
154 event.metadata,
155 Some(serde_json::json!({"method": "passkey", "success": true}))
156 );
157 }
158
159 #[test]
160 fn audit_macro_no_metadata() {
161 let (ctx, mut rx) = AuditContext::channel();
162
163 audit!(ctx, "health.check");
164
165 let event = rx.try_recv().expect("event should be sent");
166 assert_eq!(event.action, "health.check");
167 assert!(event.metadata.is_none());
168 }
169
170 #[test]
171 fn audit_diff_macro_includes_before_after_changed() {
172 use crate::auditable::Auditable;
173 use serde_json::Value;
174
175 #[derive(Clone)]
176 struct User {
177 name: String,
178 }
179 impl Auditable for User {
180 fn to_audit_json(&self) -> Value {
181 serde_json::json!({"name": self.name})
182 }
183 }
184
185 let (ctx, mut rx) = AuditContext::channel();
186 let old = User { name: "Alice".into() };
187 let new = User { name: "Bob".into() };
188
189 audit_diff!(ctx, "user.renamed", &old, &new);
190
191 let event = rx.try_recv().expect("event sent");
192 assert_eq!(event.action, "user.renamed");
193
194 let meta = event.metadata.expect("metadata present");
195 assert_eq!(meta["before"]["name"], "Alice");
196 assert_eq!(meta["after"]["name"], "Bob");
197 assert!(meta["changed"]
198 .as_array()
199 .unwrap()
200 .contains(&serde_json::Value::String("name".into())));
201 }
202}