Skip to content

Commit dbf4f03

Browse files
committed
feat: impl audit layer draft
1 parent 36da849 commit dbf4f03

File tree

2 files changed

+227
-0
lines changed

2 files changed

+227
-0
lines changed

syndapi/src/serve/layer/audit.rs

+226
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
use std::io::Write;
2+
3+
use tracing::{
4+
field,
5+
span::{self, Attributes},
6+
Event, Subscriber,
7+
};
8+
use tracing_subscriber::{layer::Context, registry::LookupSpan, Layer};
9+
10+
/// let subscriber = tracing_subscriber::registry()
11+
/// .with(layer(std::io::stdout);
12+
pub fn layer<W: MakeWriter>(make_writer: W) -> AuditLayer<W> {
13+
AuditLayer { make_writer }
14+
}
15+
16+
pub trait MakeWriter {
17+
type Writer: Write;
18+
19+
fn make_writer(&self) -> Self::Writer;
20+
}
21+
22+
impl<F, W> MakeWriter for F
23+
where
24+
F: Fn() -> W,
25+
W: std::io::Write,
26+
{
27+
type Writer = W;
28+
29+
fn make_writer(&self) -> Self::Writer {
30+
(self)()
31+
}
32+
}
33+
34+
pub struct AuditLayer<W> {
35+
make_writer: W,
36+
}
37+
38+
struct AuditEventVisitor<'a> {
39+
ctx: &'a mut AuditContext,
40+
}
41+
42+
impl<'a> field::Visit for AuditEventVisitor<'a> {
43+
fn record_debug(&mut self, _field: &field::Field, _value: &dyn std::fmt::Debug) {
44+
// do nothing
45+
}
46+
47+
fn record_str(&mut self, field: &field::Field, value: &str) {
48+
match field.name() {
49+
"organization_id" => self.ctx.organization_id = Some(value.to_owned()),
50+
"operation" => self.ctx.operation = Some(value.to_owned()),
51+
"result" => self.ctx.result = Some(value.to_owned()),
52+
_ => {}
53+
}
54+
}
55+
}
56+
57+
struct AuditContext {
58+
organization_id: Option<String>,
59+
operation: Option<String>,
60+
result: Option<String>,
61+
}
62+
63+
impl AuditContext {
64+
fn new() -> Self {
65+
Self {
66+
organization_id: None,
67+
operation: None,
68+
result: None,
69+
}
70+
}
71+
}
72+
73+
impl<S, W> Layer<S> for AuditLayer<W>
74+
where
75+
S: Subscriber + for<'span> LookupSpan<'span>,
76+
W: MakeWriter + 'static,
77+
{
78+
fn on_new_span(&self, attrs: &Attributes<'_>, id: &span::Id, ctx: Context<'_, S>) {
79+
if attrs.metadata().name() != "audit.root" {
80+
return;
81+
}
82+
let span = ctx.span(id).expect("Span not found, this is a bug");
83+
let mut extensions = span.extensions_mut();
84+
extensions.insert(AuditContext::new());
85+
}
86+
87+
fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
88+
if event.metadata().name() != "audit.event" {
89+
return;
90+
}
91+
92+
// traverse span tree to find audit context
93+
let Some(span) = ctx.lookup_current() else {
94+
return;
95+
};
96+
let Some(audit_span) = span
97+
.scope()
98+
.from_root()
99+
.find(|span| span.metadata().name() == "audit.root")
100+
else {
101+
return;
102+
};
103+
let mut extension = audit_span.extensions_mut();
104+
let Some(audit_ctx) = extension.get_mut::<AuditContext>() else {
105+
return;
106+
};
107+
108+
event.record(&mut AuditEventVisitor { ctx: audit_ctx });
109+
}
110+
111+
fn on_close(&self, id: span::Id, ctx: Context<'_, S>) {
112+
let span = ctx.span(&id).expect("Span not found, this is a bug");
113+
if span.metadata().name() != "audit.root" {
114+
return;
115+
}
116+
let mut extensions = span.extensions_mut();
117+
let Some(AuditContext {
118+
organization_id,
119+
operation,
120+
result,
121+
}) = extensions.remove::<AuditContext>()
122+
else {
123+
return;
124+
};
125+
126+
let mut writer = self.make_writer.make_writer();
127+
128+
writeln!(
129+
writer,
130+
"organization_id: {organization_id:?}, operation: {operation:?}, result:{result:?}"
131+
)
132+
.ok(); // or panic
133+
}
134+
}
135+
136+
#[cfg(test)]
137+
mod tests {
138+
use std::sync::{Arc, Mutex};
139+
140+
use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt;
141+
142+
use super::*;
143+
144+
#[derive(Clone)]
145+
struct TestWriter {
146+
buf: Arc<Mutex<Vec<u8>>>,
147+
}
148+
149+
impl TestWriter {
150+
fn new() -> Self {
151+
Self {
152+
buf: Arc::new(Mutex::new(Vec::new())),
153+
}
154+
}
155+
fn buf(self) -> Vec<u8> {
156+
Arc::into_inner(self.buf).unwrap().into_inner().unwrap()
157+
}
158+
}
159+
160+
impl MakeWriter for TestWriter {
161+
type Writer = Self;
162+
163+
fn make_writer(&self) -> Self::Writer {
164+
self.clone()
165+
}
166+
}
167+
168+
impl std::io::Write for TestWriter {
169+
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
170+
let len = buf.len();
171+
self.buf.lock().unwrap().extend(buf);
172+
Ok(len)
173+
}
174+
175+
fn flush(&mut self) -> std::io::Result<()> {
176+
Ok(())
177+
}
178+
}
179+
180+
use tracing::{info, info_span};
181+
182+
fn root() {
183+
let span = info_span!("audit.root");
184+
let _enter = span.enter();
185+
usecase();
186+
info!(name: "audit.event", result = "Success");
187+
}
188+
189+
fn usecase() {
190+
let span = info_span!("usecase");
191+
let _enter = span.enter();
192+
authorize();
193+
ops();
194+
}
195+
196+
fn authorize() {
197+
let span = info_span!("authorize");
198+
let _enter = span.enter();
199+
info!(name: "audit.event", organization_id = "org-a",);
200+
}
201+
202+
fn ops() {
203+
let span = info_span!("ops");
204+
let _enter = span.enter();
205+
info!(name: "audit.event", operation = "CreateXxx");
206+
}
207+
208+
#[test]
209+
fn handson() {
210+
let buf = TestWriter::new();
211+
let subscriber = tracing_subscriber::registry().with(layer(buf.clone()));
212+
213+
tracing::subscriber::with_default(subscriber, || {
214+
root();
215+
});
216+
217+
let buf = buf.buf();
218+
let buf = String::from_utf8_lossy(&buf);
219+
println!("Result: `{buf}`");
220+
}
221+
222+
#[test]
223+
fn stdout_make_writer() {
224+
layer(std::io::stdout);
225+
}
226+
}

syndapi/src/serve/layer/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod audit;

0 commit comments

Comments
 (0)