1use std::collections::VecDeque;
2use std::fmt;
3use std::fs::{File, OpenOptions};
4use std::io::Write as _;
5use std::path::PathBuf;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::{Arc, Mutex};
8use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
9
10use serde::Serialize;
11use tokio::sync::broadcast;
12
13pub type ConnId = u64;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
16#[serde(rename_all = "lowercase")]
17pub enum Protocol {
18 Tcp,
19 Udp,
20}
21
22impl fmt::Display for Protocol {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 Protocol::Tcp => write!(f, "tcp"),
26 Protocol::Udp => write!(f, "udp"),
27 }
28 }
29}
30
31#[derive(Debug, Clone)]
34pub struct ConnectionInfo {
35 pub id: ConnId,
36 pub label: String,
37 pub protocol: Protocol,
38 pub source: String,
39 pub destination: String,
40 pub started_at: Instant,
41 pub bytes_in: Arc<AtomicU64>,
42 pub bytes_out: Arc<AtomicU64>,
43}
44
45impl ConnectionInfo {
46 pub fn duration(&self) -> Duration {
47 self.started_at.elapsed()
48 }
49}
50
51#[derive(Debug, Clone, Serialize)]
52#[serde(tag = "kind", rename_all = "snake_case")]
53pub enum Event {
54 ConnectionOpened {
55 id: ConnId,
56 label: String,
57 protocol: Protocol,
58 source: String,
59 destination: String,
60 },
61 ConnectionClosed {
62 id: ConnId,
63 label: String,
64 protocol: Protocol,
65 source: String,
66 destination: String,
67 bytes_in: u64,
68 bytes_out: u64,
69 duration_ms: u128,
70 },
71 Info {
72 message: String,
73 },
74 Error {
75 message: String,
76 },
77}
78
79impl fmt::Display for Event {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 match self {
82 Event::ConnectionOpened {
83 label,
84 protocol,
85 source,
86 destination,
87 ..
88 } => write!(f, "[open] {label} {protocol} {source} -> {destination}"),
89 Event::ConnectionClosed {
90 label,
91 protocol,
92 source,
93 destination,
94 bytes_in,
95 bytes_out,
96 duration_ms,
97 ..
98 } => write!(
99 f,
100 "[close] {label} {protocol} {source} -> {destination} in={bytes_in}B out={bytes_out}B dur={duration_ms}ms"
101 ),
102 Event::Info { message } => write!(f, "[info] {message}"),
103 Event::Error { message } => write!(f, "[error] {message}"),
104 }
105 }
106}
107
108#[derive(Serialize)]
109struct JsonLogLine<'a> {
110 ts_unix_ms: u128,
111 #[serde(flatten)]
112 event: &'a Event,
113}
114
115const MAX_LOG_LINES: usize = 500;
116
117const EVENT_CHANNEL_CAPACITY: usize = 256;
120
121struct Inner {
122 next_id: AtomicU64,
123 total_bytes_in: AtomicU64,
124 total_bytes_out: AtomicU64,
125 total_connections: AtomicU64,
126 live: Mutex<Vec<ConnectionInfo>>,
127 log: Mutex<VecDeque<String>>,
128 log_file: Mutex<Option<File>>,
129 events: broadcast::Sender<Event>,
130}
131
132#[derive(Clone)]
135pub struct Registry(Arc<Inner>);
136
137impl Registry {
138 pub fn new() -> Self {
139 Registry(Arc::new(Inner {
140 next_id: AtomicU64::new(1),
141 total_bytes_in: AtomicU64::new(0),
142 total_bytes_out: AtomicU64::new(0),
143 total_connections: AtomicU64::new(0),
144 live: Mutex::new(Vec::new()),
145 log: Mutex::new(VecDeque::with_capacity(MAX_LOG_LINES)),
146 log_file: Mutex::new(None),
147 events: broadcast::channel(EVENT_CHANNEL_CAPACITY).0,
148 }))
149 }
150
151 pub fn subscribe(&self) -> broadcast::Receiver<Event> {
156 self.0.events.subscribe()
157 }
158
159 pub fn set_log_file(&self, path: PathBuf) -> anyhow::Result<()> {
160 let file = OpenOptions::new().create(true).append(true).open(path)?;
161 *self.0.log_file.lock().unwrap() = Some(file);
162 Ok(())
163 }
164
165 fn next_id(&self) -> ConnId {
166 self.0.next_id.fetch_add(1, Ordering::Relaxed)
167 }
168
169 pub fn open(
172 &self,
173 label: impl Into<String>,
174 protocol: Protocol,
175 source: impl Into<String>,
176 destination: impl Into<String>,
177 ) -> ConnectionInfo {
178 let info = ConnectionInfo {
179 id: self.next_id(),
180 label: label.into(),
181 protocol,
182 source: source.into(),
183 destination: destination.into(),
184 started_at: Instant::now(),
185 bytes_in: Arc::new(AtomicU64::new(0)),
186 bytes_out: Arc::new(AtomicU64::new(0)),
187 };
188 self.0.total_connections.fetch_add(1, Ordering::Relaxed);
189 self.0.live.lock().unwrap().push(info.clone());
190 self.push_event(Event::ConnectionOpened {
191 id: info.id,
192 label: info.label.clone(),
193 protocol: info.protocol,
194 source: info.source.clone(),
195 destination: info.destination.clone(),
196 });
197 info
198 }
199
200 pub fn close(&self, info: &ConnectionInfo) {
201 let bytes_in = info.bytes_in.load(Ordering::Relaxed);
202 let bytes_out = info.bytes_out.load(Ordering::Relaxed);
203 self.0.total_bytes_in.fetch_add(bytes_in, Ordering::Relaxed);
204 self.0
205 .total_bytes_out
206 .fetch_add(bytes_out, Ordering::Relaxed);
207 self.0.live.lock().unwrap().retain(|c| c.id != info.id);
208 self.push_event(Event::ConnectionClosed {
209 id: info.id,
210 label: info.label.clone(),
211 protocol: info.protocol,
212 source: info.source.clone(),
213 destination: info.destination.clone(),
214 bytes_in,
215 bytes_out,
216 duration_ms: info.duration().as_millis(),
217 });
218 }
219
220 pub fn info(&self, message: impl Into<String>) {
221 self.push_event(Event::Info {
222 message: message.into(),
223 });
224 }
225
226 pub fn error(&self, message: impl Into<String>) {
227 self.push_event(Event::Error {
228 message: message.into(),
229 });
230 }
231
232 fn push_event(&self, event: Event) {
233 let line = event.to_string();
234 {
235 let mut log = self.0.log.lock().unwrap();
236 if log.len() >= MAX_LOG_LINES {
237 log.pop_front();
238 }
239 log.push_back(line);
240 }
241 let _ = self.0.events.send(event.clone());
243 if let Some(file) = self.0.log_file.lock().unwrap().as_mut() {
244 let ts_unix_ms = SystemTime::now()
245 .duration_since(UNIX_EPOCH)
246 .unwrap_or_default()
247 .as_millis();
248 let json = JsonLogLine {
249 ts_unix_ms,
250 event: &event,
251 };
252 if let Ok(mut s) = serde_json::to_string(&json) {
253 s.push('\n');
254 let _ = file.write_all(s.as_bytes());
255 }
256 }
257 }
258
259 pub fn live_connections(&self) -> Vec<ConnectionInfo> {
260 self.0.live.lock().unwrap().clone()
261 }
262
263 pub fn recent_log(&self) -> Vec<String> {
264 self.0.log.lock().unwrap().iter().cloned().collect()
265 }
266
267 pub fn totals(&self) -> Totals {
268 let live = self.0.live.lock().unwrap();
269 let live_bytes_in: u64 = live
270 .iter()
271 .map(|c| c.bytes_in.load(Ordering::Relaxed))
272 .sum();
273 let live_bytes_out: u64 = live
274 .iter()
275 .map(|c| c.bytes_out.load(Ordering::Relaxed))
276 .sum();
277 Totals {
278 live_connections: live.len(),
279 total_connections: self.0.total_connections.load(Ordering::Relaxed),
280 bytes_in: self.0.total_bytes_in.load(Ordering::Relaxed) + live_bytes_in,
281 bytes_out: self.0.total_bytes_out.load(Ordering::Relaxed) + live_bytes_out,
282 }
283 }
284}
285
286impl Default for Registry {
287 fn default() -> Self {
288 Self::new()
289 }
290}
291
292pub struct Totals {
293 pub live_connections: usize,
294 pub total_connections: u64,
295 pub bytes_in: u64,
296 pub bytes_out: u64,
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn subscribers_receive_events_as_they_are_emitted() {
305 let registry = Registry::new();
306 let mut events = registry.subscribe();
307
308 registry.info("hello");
309 let conn = registry.open("web", Protocol::Tcp, "1.2.3.4:5", "B:web");
310 registry.close(&conn);
311
312 assert!(matches!(events.try_recv(), Ok(Event::Info { message }) if message == "hello"));
313 assert!(matches!(
314 events.try_recv(),
315 Ok(Event::ConnectionOpened { .. })
316 ));
317 assert!(matches!(
318 events.try_recv(),
319 Ok(Event::ConnectionClosed { .. })
320 ));
321 }
322
323 #[test]
324 fn events_without_subscribers_are_not_an_error() {
325 let registry = Registry::new();
326 registry.error("nobody is listening");
327 assert_eq!(registry.recent_log().len(), 1);
328 }
329}