audit_layer/
lib.rs

1use std::{fmt::Debug, sync::Arc};
2
3use reqwest::Client;
4use serde_json::{json, Map, Value};
5use tokio::runtime::Handle;
6use tracing::{
7    field::{Field, Visit},
8    Event, Metadata, Subscriber,
9};
10use tracing_subscriber::{layer::Context, registry::LookupSpan, Layer};
11
12pub struct AuditLayer {
13    client: Arc<Client>,
14    username: String,
15    password: String,
16    log_endpoint: String,
17    runtime_handle: Handle,
18}
19
20impl AuditLayer {
21    /// Create an audit layer that works with the tracing system to capture
22    /// and push audit logs to the appropriate logger over HTTP
23    pub fn new(
24        log_endpoint: String,
25        username: String,
26        password: String,
27        runtime_handle: Handle,
28    ) -> Self {
29        let client = Arc::new(reqwest::Client::new());
30
31        Self {
32            client,
33            log_endpoint,
34            username,
35            password,
36            runtime_handle,
37        }
38    }
39}
40
41impl<S> Layer<S> for AuditLayer
42where
43    S: Subscriber + for<'a> LookupSpan<'a>,
44{
45    fn enabled(&self, _: &Metadata<'_>, _: Context<'_, S>) -> bool {
46        true // log everything if it is auditable
47    }
48
49    fn on_event(&self, event: &Event<'_>, _: Context<'_, S>) {
50        let mut visitor = AuditVisitor::default();
51        event.record(&mut visitor);
52
53        if visitor.audit {
54            visitor
55                .json
56                .insert("message".to_owned(), json!(visitor.message));
57
58            let req = self
59                .client
60                .post(&self.log_endpoint)
61                .basic_auth(&self.username, Some(&self.password))
62                .json(&visitor.json);
63
64            self.runtime_handle.spawn(async move {
65                match req.send().await {
66                    Ok(r) => {
67                        if let Err(e) = r.error_for_status() {
68                            println!("{e}")
69                        }
70                    }
71                    Err(e) => eprintln!("Failed to send audit event: {}", e),
72                }
73            });
74        }
75    }
76}
77
78#[derive(Debug, Default)]
79struct AuditVisitor {
80    message: String,
81    json: Map<String, Value>,
82    audit: bool,
83}
84
85impl Visit for AuditVisitor {
86    fn record_bool(&mut self, field: &Field, value: bool) {
87        if field.name() == "audit" {
88            self.audit = value;
89        } else {
90            self.json.insert(field.name().to_owned(), json!(value));
91        }
92    }
93
94    fn record_str(&mut self, field: &Field, value: &str) {
95        if field.name() == "message" {
96            self.message = value.to_owned();
97        } else {
98            self.json.insert(field.name().to_owned(), json!(value));
99        }
100    }
101
102    fn record_f64(&mut self, field: &Field, value: f64) {
103        self.json.insert(field.name().to_owned(), json!(value));
104    }
105
106    fn record_i64(&mut self, field: &Field, value: i64) {
107        self.json.insert(field.name().to_owned(), json!(value));
108    }
109
110    fn record_u64(&mut self, field: &Field, value: u64) {
111        self.json.insert(field.name().to_owned(), json!(value));
112    }
113
114    fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
115        if field.name() == "message" {
116            self.message = format!("{value:?}");
117        } else {
118            self.json
119                .insert(field.name().to_owned(), json!(format!("{value:?}")));
120        }
121    }
122}