|
| 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 | +} |
0 commit comments