use crate::json::Json;
use std::collections::BTreeMap;
use std::sync::{Arc, OnceLock, RwLock};
use std::time::{Duration, SystemTime};
#[derive(Debug, Clone)]
pub struct Event {
pub kind: &'static str,
pub at: SystemTime,
pub duration: Option<Duration>,
pub fields: BTreeMap<String, Json>,
}
impl Event {
pub fn new(kind: &'static str) -> Self {
Event { kind, at: SystemTime::now(), duration: None, fields: BTreeMap::new() }
}
pub fn with(mut self, key: &str, value: impl Into<Json>) -> Self {
self.fields.insert(key.to_string(), value.into());
self
}
pub fn took(mut self, duration: Duration) -> Self {
self.duration = Some(duration);
self
}
pub fn field(&self, key: &str) -> Option<&Json> {
self.fields.get(key)
}
pub fn duration_ms(&self) -> Option<f64> {
self.duration.map(|d| d.as_secs_f64() * 1000.0)
}
pub fn dispatch(self) {
dispatch(self);
}
}
pub trait Subscriber: Send + Sync + 'static {
fn handle(&self, event: &Event);
fn interested_in(&self, _kind: &str) -> bool {
true
}
}
impl<F> Subscriber for F
where
F: Fn(&Event) + Send + Sync + 'static,
{
fn handle(&self, event: &Event) {
self(event)
}
}
type Subscribers = RwLock<Vec<Arc<dyn Subscriber>>>;
fn registry() -> &'static Subscribers {
static REGISTRY: OnceLock<Subscribers> = OnceLock::new();
REGISTRY.get_or_init(|| RwLock::new(Vec::new()))
}
pub fn subscribe(subscriber: impl Subscriber) {
registry().write().expect("event registry poisoned").push(Arc::new(subscriber));
}
pub fn dispatch(event: Event) {
let subscribers = registry().read().expect("event registry poisoned");
for subscriber in subscribers.iter() {
if subscriber.interested_in(event.kind) {
subscriber.handle(&event);
}
}
}
pub fn has_subscribers() -> bool {
!registry().read().expect("event registry poisoned").is_empty()
}
pub fn clear_subscribers() {
registry().write().expect("event registry poisoned").clear();
}
pub fn timed<T>(kind: &'static str, fields: impl FnOnce() -> Vec<(String, Json)>, work: impl FnOnce() -> T) -> T {
if !has_subscribers() {
return work();
}
let started = std::time::Instant::now();
let result = work();
let elapsed = started.elapsed();
let mut event = Event::new(kind).took(elapsed);
for (key, value) in fields() {
event.fields.insert(key, value);
}
dispatch(event);
result
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
static BUS: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn exclusive() -> std::sync::MutexGuard<'static, ()> {
BUS.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[test]
fn subscribers_receive_dispatched_events() {
let _guard = exclusive();
clear_subscribers();
let seen = Arc::new(AtomicUsize::new(0));
let counter = Arc::clone(&seen);
subscribe(move |event: &Event| {
assert_eq!(event.kind, "http.request");
assert_eq!(event.field("path").and_then(Json::as_str), Some("/users"));
counter.fetch_add(1, Ordering::SeqCst);
});
Event::new("http.request").with("path", "/users").dispatch();
assert_eq!(seen.load(Ordering::SeqCst), 1);
clear_subscribers();
}
#[test]
fn timed_records_a_duration() {
let _guard = exclusive();
clear_subscribers();
let millis = Arc::new(RwLock::new(None));
let sink = Arc::clone(&millis);
subscribe(move |event: &Event| {
*sink.write().unwrap() = event.duration_ms();
});
let result = timed("db.query", || vec![("sql".to_string(), Json::from("select 1"))], || 21 * 2);
assert_eq!(result, 42);
assert!(millis.read().unwrap().is_some());
clear_subscribers();
}
}