1use crate::json::Json;
11use std::collections::BTreeMap;
12use std::sync::{Arc, OnceLock, RwLock};
13use std::time::{Duration, SystemTime};
14
15#[derive(Debug, Clone)]
17pub struct Event {
18 pub kind: &'static str,
20 pub at: SystemTime,
21 pub duration: Option<Duration>,
23 pub fields: BTreeMap<String, Json>,
24}
25
26impl Event {
27 pub fn new(kind: &'static str) -> Self {
28 Event { kind, at: SystemTime::now(), duration: None, fields: BTreeMap::new() }
29 }
30
31 pub fn with(mut self, key: &str, value: impl Into<Json>) -> Self {
32 self.fields.insert(key.to_string(), value.into());
33 self
34 }
35
36 pub fn took(mut self, duration: Duration) -> Self {
37 self.duration = Some(duration);
38 self
39 }
40
41 pub fn field(&self, key: &str) -> Option<&Json> {
42 self.fields.get(key)
43 }
44
45 pub fn duration_ms(&self) -> Option<f64> {
47 self.duration.map(|d| d.as_secs_f64() * 1000.0)
48 }
49
50 pub fn dispatch(self) {
52 dispatch(self);
53 }
54}
55
56pub trait Subscriber: Send + Sync + 'static {
58 fn handle(&self, event: &Event);
59
60 fn interested_in(&self, _kind: &str) -> bool {
63 true
64 }
65}
66
67impl<F> Subscriber for F
68where
69 F: Fn(&Event) + Send + Sync + 'static,
70{
71 fn handle(&self, event: &Event) {
72 self(event)
73 }
74}
75
76type Subscribers = RwLock<Vec<Arc<dyn Subscriber>>>;
77
78fn registry() -> &'static Subscribers {
79 static REGISTRY: OnceLock<Subscribers> = OnceLock::new();
80 REGISTRY.get_or_init(|| RwLock::new(Vec::new()))
81}
82
83pub fn subscribe(subscriber: impl Subscriber) {
85 registry().write().expect("event registry poisoned").push(Arc::new(subscriber));
86}
87
88pub fn dispatch(event: Event) {
94 let subscribers = registry().read().expect("event registry poisoned");
95 for subscriber in subscribers.iter() {
96 if subscriber.interested_in(event.kind) {
97 subscriber.handle(&event);
98 }
99 }
100}
101
102pub fn has_subscribers() -> bool {
105 !registry().read().expect("event registry poisoned").is_empty()
106}
107
108pub fn clear_subscribers() {
110 registry().write().expect("event registry poisoned").clear();
111}
112
113pub fn timed<T>(kind: &'static str, fields: impl FnOnce() -> Vec<(String, Json)>, work: impl FnOnce() -> T) -> T {
115 if !has_subscribers() {
116 return work();
117 }
118 let started = std::time::Instant::now();
119 let result = work();
120 let elapsed = started.elapsed();
121
122 let mut event = Event::new(kind).took(elapsed);
123 for (key, value) in fields() {
124 event.fields.insert(key, value);
125 }
126 dispatch(event);
127 result
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use std::sync::atomic::{AtomicUsize, Ordering};
134
135 #[test]
136 fn subscribers_receive_dispatched_events() {
137 clear_subscribers();
138 let seen = Arc::new(AtomicUsize::new(0));
139 let counter = Arc::clone(&seen);
140
141 subscribe(move |event: &Event| {
142 assert_eq!(event.kind, "http.request");
143 assert_eq!(event.field("path").and_then(Json::as_str), Some("/users"));
144 counter.fetch_add(1, Ordering::SeqCst);
145 });
146
147 Event::new("http.request").with("path", "/users").dispatch();
148 assert_eq!(seen.load(Ordering::SeqCst), 1);
149 clear_subscribers();
150 }
151
152 #[test]
153 fn timed_records_a_duration() {
154 clear_subscribers();
155 let millis = Arc::new(RwLock::new(None));
156 let sink = Arc::clone(&millis);
157
158 subscribe(move |event: &Event| {
159 *sink.write().unwrap() = event.duration_ms();
160 });
161
162 let result = timed("db.query", || vec![("sql".to_string(), Json::from("select 1"))], || 21 * 2);
163
164 assert_eq!(result, 42);
165 assert!(millis.read().unwrap().is_some());
166 clear_subscribers();
167 }
168}