use std::collections::{BTreeMap, HashMap, VecDeque};
use std::net::SocketAddr;
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
use std::time::{SystemTime, UNIX_EPOCH};
use serde::Serialize;
use tokio::sync::{broadcast, Notify};
const MAX_EVENTS: usize = 500;
pub const HISTORY_SAMPLES: usize = 900;
const NO_BLOCK: u64 = u64::MAX;
pub fn now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
}
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Level {
Info,
Warn,
Error,
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EventKind {
Started,
UpstreamUp,
UpstreamDown,
UpstreamStale,
UpstreamFresh,
UpstreamQuarantined,
SessionFailover,
SessionFailed,
Control,
}
#[derive(Debug, Clone, Serialize)]
pub struct Event {
pub id: u64,
pub t_ms: u64,
pub level: Level,
pub kind: EventKind,
pub listener: Option<String>,
pub upstream: Option<String>,
pub session: Option<u64>,
pub message: String,
}
impl Event {
pub fn new(level: Level, kind: EventKind, message: impl Into<String>) -> Self {
Event {
id: 0,
t_ms: 0,
level,
kind,
listener: None,
upstream: None,
session: None,
message: message.into(),
}
}
pub fn listener(mut self, name: &str) -> Self {
self.listener = Some(name.to_string());
self
}
pub fn upstream(mut self, endpoint: &str) -> Self {
self.upstream = Some(endpoint.to_string());
self
}
pub fn session(mut self, id: u64) -> Self {
self.session = Some(id);
self
}
}
#[derive(Debug, Clone, Serialize)]
pub struct UpstreamSample {
pub sessions: usize,
pub head_block: Option<u32>,
pub blocks_total: u64,
pub bytes_total: u64,
pub status: &'static str,
}
#[derive(Debug, Clone, Serialize)]
pub struct Sample {
pub t_ms: u64,
pub upstreams: BTreeMap<String, UpstreamSample>,
}
#[derive(Debug, Default)]
pub struct ListenerStats {
pub accepted_total: AtomicU64,
pub rejected_total: AtomicU64,
}
#[derive(Debug)]
pub struct SessionInfo {
pub id: u64,
pub listener: Arc<str>,
pub client: SocketAddr,
pub started_ms: u64,
upstream: Mutex<String>,
start_block: AtomicU64,
last_block: AtomicU64,
finality: AtomicBool,
blocks: AtomicU64,
bytes: AtomicU64,
failovers: AtomicU32,
move_requested: AtomicBool,
close_requested: AtomicBool,
pub control: Notify,
}
impl SessionInfo {
fn block(value: &AtomicU64) -> Option<u32> {
match value.load(Ordering::Relaxed) {
NO_BLOCK => None,
block => u32::try_from(block).ok(),
}
}
pub fn upstream(&self) -> String {
lock(&self.upstream).clone()
}
pub fn set_upstream(&self, endpoint: &str) {
*lock(&self.upstream) = endpoint.to_string();
}
pub fn start_block(&self) -> Option<u32> {
Self::block(&self.start_block)
}
pub fn last_block(&self) -> Option<u32> {
Self::block(&self.last_block)
}
pub fn next_block(&self) -> Option<u32> {
self.last_block()
.map(|block| block.saturating_add(1))
.or_else(|| self.start_block())
}
pub fn finality(&self) -> bool {
self.finality.load(Ordering::Relaxed)
}
pub fn blocks(&self) -> u64 {
self.blocks.load(Ordering::Relaxed)
}
pub fn bytes(&self) -> u64 {
self.bytes.load(Ordering::Relaxed)
}
pub fn failovers(&self) -> u32 {
self.failovers.load(Ordering::Relaxed)
}
pub fn note_request(&self, start_block: u32, finality: bool) {
self.start_block
.store(u64::from(start_block), Ordering::Relaxed);
self.finality.store(finality, Ordering::Relaxed);
}
pub fn note_frame(&self, bytes: usize, block: Option<u32>) {
self.bytes
.fetch_add(u64::try_from(bytes).unwrap_or(u64::MAX), Ordering::Relaxed);
if let Some(block) = block {
self.blocks.fetch_add(1, Ordering::Relaxed);
self.last_block.store(u64::from(block), Ordering::Relaxed);
}
}
pub fn note_failover(&self) {
self.failovers.fetch_add(1, Ordering::Relaxed);
}
pub fn request_move(&self) {
self.move_requested.store(true, Ordering::Release);
self.control.notify_one();
}
pub fn request_close(&self) {
self.close_requested.store(true, Ordering::Release);
self.control.notify_one();
}
pub fn take_move_request(&self) -> bool {
self.move_requested.swap(false, Ordering::AcqRel)
}
pub fn close_requested(&self) -> bool {
self.close_requested.load(Ordering::Acquire)
}
}
pub struct SessionRegistration {
pub info: Arc<SessionInfo>,
}
impl Drop for SessionRegistration {
fn drop(&mut self) {
lock(&global().sessions).remove(&self.info.id);
}
}
pub struct Telemetry {
pub started_ms: u64,
next_event_id: AtomicU64,
events: Mutex<VecDeque<Event>>,
events_tx: broadcast::Sender<Event>,
history: Mutex<VecDeque<Sample>>,
next_session_id: AtomicU64,
sessions: Mutex<HashMap<u64, Arc<SessionInfo>>>,
}
static TELEMETRY: OnceLock<Telemetry> = OnceLock::new();
pub fn global() -> &'static Telemetry {
TELEMETRY.get_or_init(|| Telemetry {
started_ms: now_ms(),
next_event_id: AtomicU64::new(1),
events: Mutex::new(VecDeque::with_capacity(MAX_EVENTS)),
events_tx: broadcast::channel(256).0,
history: Mutex::new(VecDeque::with_capacity(HISTORY_SAMPLES)),
next_session_id: AtomicU64::new(1),
sessions: Mutex::new(HashMap::new()),
})
}
impl Telemetry {
pub fn emit(&self, mut event: Event) {
event.t_ms = now_ms();
let mut events = lock(&self.events);
event.id = self.next_event_id.fetch_add(1, Ordering::Relaxed);
if events.len() == MAX_EVENTS {
events.pop_front();
}
events.push_back(event.clone());
drop(events);
let _ = self.events_tx.send(event);
}
pub fn events_after(&self, after: u64) -> Vec<Event> {
lock(&self.events)
.iter()
.filter(|event| event.id > after)
.cloned()
.collect()
}
pub fn subscribe(&self) -> broadcast::Receiver<Event> {
self.events_tx.subscribe()
}
pub fn record_sample(&self, sample: Sample) {
let mut history = lock(&self.history);
if history.len() == HISTORY_SAMPLES {
history.pop_front();
}
history.push_back(sample);
}
pub fn history(&self) -> Vec<Sample> {
lock(&self.history).iter().cloned().collect()
}
pub fn register_session(
&self,
listener: Arc<str>,
client: SocketAddr,
upstream: &str,
) -> SessionRegistration {
let info = Arc::new(SessionInfo {
id: self.next_session_id.fetch_add(1, Ordering::Relaxed),
listener,
client,
started_ms: now_ms(),
upstream: Mutex::new(upstream.to_string()),
start_block: AtomicU64::new(NO_BLOCK),
last_block: AtomicU64::new(NO_BLOCK),
finality: AtomicBool::new(false),
blocks: AtomicU64::new(0),
bytes: AtomicU64::new(0),
failovers: AtomicU32::new(0),
move_requested: AtomicBool::new(false),
close_requested: AtomicBool::new(false),
control: Notify::new(),
});
lock(&self.sessions).insert(info.id, info.clone());
SessionRegistration { info }
}
pub fn session(&self, id: u64) -> Option<Arc<SessionInfo>> {
lock(&self.sessions).get(&id).cloned()
}
pub fn sessions(&self) -> Vec<Arc<SessionInfo>> {
let mut sessions: Vec<_> = lock(&self.sessions).values().cloned().collect();
sessions.sort_by_key(|session| session.id);
sessions
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn events_get_increasing_ids_and_after_filters() {
let telemetry = global();
let before = telemetry.events_after(0).last().map_or(0, |event| event.id);
telemetry.emit(Event::new(Level::Info, EventKind::Control, "first"));
telemetry.emit(Event::new(Level::Warn, EventKind::Control, "second").listener("eos"));
let newer: Vec<_> = telemetry
.events_after(before)
.into_iter()
.filter(|event| event.message == "first" || event.message == "second")
.collect();
assert_eq!(newer.len(), 2);
assert!(newer[0].id < newer[1].id);
assert_eq!(newer[1].listener.as_deref(), Some("eos"));
}
#[test]
fn registration_is_removed_on_drop_and_tracks_frames() {
let telemetry = global();
let client: SocketAddr = "127.0.0.1:5000".parse().unwrap();
let registration = telemetry.register_session(Arc::from("t"), client, "up:1");
let id = registration.info.id;
let info = telemetry.session(id).unwrap();
assert_eq!(info.start_block(), None);
assert_eq!(info.next_block(), None);
info.note_request(100, true);
assert_eq!(info.next_block(), Some(100));
info.note_frame(10, Some(100));
info.note_frame(5, None);
assert_eq!(info.start_block(), Some(100));
assert_eq!(info.last_block(), Some(100));
assert_eq!(info.next_block(), Some(101));
assert_eq!((info.blocks(), info.bytes()), (1, 15));
assert!(info.finality());
info.request_move();
assert!(info.take_move_request());
assert!(!info.take_move_request());
drop(registration);
assert!(telemetry.session(id).is_none());
}
}