use std::collections::VecDeque;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::io::AsyncWriteExt;
use tokio::net::UnixStream;
use tokio::sync::mpsc;
use crate::logd::{EventFrame, Fields, IngestFrame, Level, TelemetryFrame};
pub const DEFAULT_INGEST_SOCK: &str = "/run/ados/logd.sock";
pub const EVENT_CHANNEL_CAPACITY: usize = 256;
pub const INGEST_CHANNEL_CAPACITY: usize = 512;
const BATCH_MAX_FRAMES: usize = 64;
const SHIPPER_BUFFER_MAX_FRAMES: usize = 1024;
const RECONNECT_PACE: crate::retry::RetryPace = crate::retry::LOCAL_SOCKET;
#[derive(Debug, Default)]
pub struct EmitterStats {
enqueued: AtomicU64,
dropped: AtomicU64,
}
impl EmitterStats {
fn record_enqueued(&self) {
self.enqueued.fetch_add(1, Ordering::Relaxed);
}
fn record_dropped(&self) {
self.dropped.fetch_add(1, Ordering::Relaxed);
}
pub fn enqueued(&self) -> u64 {
self.enqueued.load(Ordering::Relaxed)
}
pub fn dropped(&self) -> u64 {
self.dropped.load(Ordering::Relaxed)
}
}
#[derive(Clone)]
pub struct EventEmitter {
source: Arc<str>,
tx: mpsc::Sender<EventFrame>,
stats: Arc<EmitterStats>,
}
impl EventEmitter {
pub fn new(source: impl Into<String>) -> Self {
Self::with_socket(source, DEFAULT_INGEST_SOCK)
}
pub fn with_socket(source: impl Into<String>, socket: impl AsRef<Path>) -> Self {
let (tx, rx) = mpsc::channel::<EventFrame>(EVENT_CHANNEL_CAPACITY);
let stats = Arc::new(EmitterStats::default());
let path = socket.as_ref().to_path_buf();
tokio::spawn(shipper_task(rx, path, Arc::clone(&stats)));
Self {
source: source.into().into(),
tx,
stats,
}
}
pub fn stats(&self) -> Arc<EmitterStats> {
Arc::clone(&self.stats)
}
pub fn emit(&self, kind: impl Into<String>, severity: Level, detail: Fields) {
let mut frame = EventFrame::new(now_us(), kind.into(), self.source.to_string(), severity);
frame.detail = detail;
frame.redact_detail();
match self.tx.try_send(frame) {
Ok(()) => self.stats.record_enqueued(),
Err(mpsc::error::TrySendError::Full(_)) => self.stats.record_dropped(),
Err(mpsc::error::TrySendError::Closed(_)) => self.stats.record_dropped(),
}
}
}
#[derive(Clone)]
pub struct IngestEmitter {
source: Arc<str>,
tx: mpsc::Sender<IngestFrame>,
stats: Arc<EmitterStats>,
}
impl IngestEmitter {
pub fn new(source: impl Into<String>) -> Self {
Self::with_socket(source, DEFAULT_INGEST_SOCK)
}
pub fn with_socket(source: impl Into<String>, socket: impl AsRef<Path>) -> Self {
let (tx, rx) = mpsc::channel::<IngestFrame>(INGEST_CHANNEL_CAPACITY);
let stats = Arc::new(EmitterStats::default());
let path = socket.as_ref().to_path_buf();
tokio::spawn(ingest_shipper_task(rx, path, Arc::clone(&stats)));
Self {
source: source.into().into(),
tx,
stats,
}
}
pub fn stats(&self) -> Arc<EmitterStats> {
Arc::clone(&self.stats)
}
pub fn emit_metric(&self, metric: impl Into<String>, value: f64, tags: Fields) {
let mut frame = TelemetryFrame::new(now_us(), metric, value);
frame.tags = tags;
self.send(IngestFrame::Telemetry(frame));
}
pub fn emit_event(&self, kind: impl Into<String>, severity: Level, detail: Fields) {
let mut frame = EventFrame::new(now_us(), kind.into(), self.source.to_string(), severity);
frame.detail = detail;
frame.redact_detail();
self.send(IngestFrame::Event(frame));
}
fn send(&self, frame: IngestFrame) {
match self.tx.try_send(frame) {
Ok(()) => self.stats.record_enqueued(),
Err(_) => self.stats.record_dropped(),
}
}
}
fn encode_ingest_batch(batch: &[IngestFrame]) -> Vec<u8> {
let mut buf = Vec::new();
for frame in batch {
if let Ok(bytes) = frame.encode() {
buf.extend_from_slice(&bytes);
}
}
buf
}
async fn ingest_shipper_task(
rx: mpsc::Receiver<IngestFrame>,
path: PathBuf,
stats: Arc<EmitterStats>,
) {
run_shipper(rx, path, stats, encode_ingest_batch).await;
}
fn push_bounded<T>(pending: &mut VecDeque<T>, frame: T, stats: &EmitterStats) {
if pending.len() >= SHIPPER_BUFFER_MAX_FRAMES {
pending.pop_front();
stats.record_dropped();
}
pending.push_back(frame);
}
async fn ship_front<T>(
writer: &mut SocketWriter,
pending: &mut VecDeque<T>,
encode: fn(&[T]) -> Vec<u8>,
) -> bool {
let take = pending.len().min(BATCH_MAX_FRAMES);
if take == 0 {
return false;
}
let buf = encode(&pending.make_contiguous()[..take]);
if buf.is_empty() || writer.write_all(&buf).await {
for _ in 0..take {
pending.pop_front();
}
true
} else {
false
}
}
async fn run_shipper<T>(
mut rx: mpsc::Receiver<T>,
path: PathBuf,
stats: Arc<EmitterStats>,
encode: fn(&[T]) -> Vec<u8>,
) {
let mut writer = SocketWriter::new(path);
let mut pending: VecDeque<T> = VecDeque::with_capacity(BATCH_MAX_FRAMES);
let mut closed = false;
loop {
if pending.is_empty() {
if closed {
break;
}
match rx.recv().await {
Some(frame) => push_bounded(&mut pending, frame, &stats),
None => break,
}
}
loop {
match rx.try_recv() {
Ok(frame) => push_bounded(&mut pending, frame, &stats),
Err(mpsc::error::TryRecvError::Empty) => break,
Err(mpsc::error::TryRecvError::Disconnected) => {
closed = true;
break;
}
}
}
if writer.ensure_connected().await {
ship_front(&mut writer, &mut pending, encode).await;
} else if closed {
break;
}
}
while writer.stream.is_some() && ship_front(&mut writer, &mut pending, encode).await {}
}
fn now_us() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_micros() as i64)
.unwrap_or(0)
}
struct SocketWriter {
path: PathBuf,
stream: Option<UnixStream>,
}
impl SocketWriter {
fn new(path: PathBuf) -> Self {
Self { path, stream: None }
}
async fn ensure_connected(&mut self) -> bool {
if self.stream.is_some() {
return true;
}
match UnixStream::connect(&self.path).await {
Ok(s) => {
self.stream = Some(s);
true
}
Err(_) => {
tokio::time::sleep(RECONNECT_PACE.wait()).await;
false
}
}
}
async fn write_all(&mut self, buf: &[u8]) -> bool {
let Some(stream) = self.stream.as_mut() else {
return false;
};
match stream.write_all(buf).await {
Ok(()) => true,
Err(_) => {
self.stream = None;
false
}
}
}
}
fn encode_batch(batch: &[EventFrame]) -> Vec<u8> {
let mut buf = Vec::new();
for frame in batch {
if let Ok(bytes) = IngestFrame::Event(frame.clone()).encode() {
buf.extend_from_slice(&bytes);
}
}
buf
}
async fn shipper_task(rx: mpsc::Receiver<EventFrame>, path: PathBuf, stats: Arc<EmitterStats>) {
run_shipper(rx, path, stats, encode_batch).await;
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use rmpv::Value as MpVal;
use tokio::io::AsyncReadExt;
use tokio::net::{UnixListener, UnixStream};
use crate::frame::{decode_len, HEADER_SIZE};
use crate::logd::LOGD_MAX_FRAME;
fn tmp_sock() -> PathBuf {
static SEQ: AtomicU64 = AtomicU64::new(0);
let mut p = std::env::temp_dir();
let uniq = format!(
"ados-logd-emitter-test-{}-{}-{}.sock",
std::process::id(),
now_us(),
SEQ.fetch_add(1, Ordering::Relaxed)
);
p.push(uniq);
let _ = std::fs::remove_file(&p);
p
}
fn detail(pairs: &[(&str, MpVal)]) -> Fields {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect()
}
async fn read_one(stream: &mut UnixStream) -> IngestFrame {
let mut header = [0u8; HEADER_SIZE];
stream.read_exact(&mut header).await.unwrap();
let len = decode_len(header, LOGD_MAX_FRAME, true).unwrap();
let mut body = vec![0u8; len];
stream.read_exact(&mut body).await.unwrap();
IngestFrame::decode(&body).unwrap()
}
#[tokio::test]
async fn ships_a_redacted_event_to_a_live_socket() {
let path = tmp_sock();
let listener = UnixListener::bind(&path).unwrap();
let accept = {
let listener = listener;
tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
read_one(&mut stream).await
})
};
let emitter = EventEmitter::with_socket("ados-test", &path);
emitter.emit(
"radio.reg_gate",
Level::Warn,
detail(&[
("result", MpVal::from("blocked")),
("channel", MpVal::from(149u64)),
("session_token", MpVal::from("tok_supersecretvalue")),
]),
);
let frame = tokio::time::timeout(Duration::from_secs(5), accept)
.await
.expect("event delivered within the deadline")
.expect("accept task ok");
match frame {
IngestFrame::Event(evt) => {
assert_eq!(evt.source, "ados-test");
assert_eq!(evt.kind, "radio.reg_gate");
assert_eq!(evt.severity, Level::Warn);
assert_eq!(
evt.detail.get("result").and_then(|v| v.as_str()),
Some("blocked")
);
assert_eq!(
evt.detail.get("channel").and_then(|v| v.as_u64()),
Some(149)
);
assert_eq!(
evt.detail.get("session_token").and_then(|v| v.as_str()),
Some("redacted:len=20")
);
}
other => panic!("expected an event frame, got {other:?}"),
}
let _ = std::fs::remove_file(&path);
}
#[tokio::test]
async fn absent_socket_never_blocks_or_panics() {
let path = tmp_sock(); let emitter = EventEmitter::with_socket("ados-test", &path);
for i in 0..20 {
emitter.emit("radio.bind", Level::Info, detail(&[("n", MpVal::from(i))]));
}
assert!(emitter.stats().enqueued() >= 1, "at least one enqueued");
tokio::time::sleep(Duration::from_millis(50)).await;
let _ = std::fs::remove_file(&path);
}
#[tokio::test]
async fn full_channel_drops_without_blocking_and_counts_it() {
let path = tmp_sock(); let emitter = EventEmitter::with_socket("ados-test", &path);
let total = EVENT_CHANNEL_CAPACITY * 2;
for n in 0..total {
emitter.emit(
"radio.rf_unverified",
Level::Warn,
detail(&[("n", MpVal::from(n as u64))]),
);
}
let enq = emitter.stats().enqueued();
let drp = emitter.stats().dropped();
assert_eq!(enq + drp, total as u64, "every event accounted for");
assert!(drp >= 1, "overflow past capacity was dropped and counted");
let _ = std::fs::remove_file(&path);
}
#[tokio::test]
async fn a_clone_shares_the_shipper_and_stats() {
let path = tmp_sock();
let listener = UnixListener::bind(&path).unwrap();
tokio::spawn(async move {
loop {
let Ok((mut stream, _)) = listener.accept().await else {
break;
};
let mut sink = [0u8; 4096];
while stream.read(&mut sink).await.unwrap_or(0) > 0 {}
}
});
let emitter = EventEmitter::with_socket("ados-test", &path);
let clone = emitter.clone();
emitter.emit("radio.bind", Level::Info, Fields::new());
clone.emit("radio.bind", Level::Info, Fields::new());
assert_eq!(emitter.stats().enqueued(), 2);
assert_eq!(clone.stats().dropped(), 0);
let _ = std::fs::remove_file(&path);
}
#[tokio::test]
async fn ingest_ships_a_telemetry_frame_with_tags() {
let path = tmp_sock();
let listener = UnixListener::bind(&path).unwrap();
let accept = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
read_one(&mut stream).await
});
let emitter = IngestEmitter::with_socket("ados-test", &path);
let mut tags = Fields::new();
tags.insert("direction".to_string(), MpVal::from("downlink"));
tags.insert("link".to_string(), MpVal::from("video"));
emitter.emit_metric("link.rssi_dbm", -53.0, tags);
let frame = tokio::time::timeout(Duration::from_secs(5), accept)
.await
.expect("metric delivered within the deadline")
.expect("accept task ok");
match frame {
IngestFrame::Telemetry(t) => {
assert_eq!(t.metric, "link.rssi_dbm");
assert_eq!(t.value, -53.0);
assert_eq!(
t.tags.get("direction").and_then(|v| v.as_str()),
Some("downlink")
);
assert_eq!(t.tags.get("link").and_then(|v| v.as_str()), Some("video"));
}
other => panic!("expected a telemetry frame, got {other:?}"),
}
let _ = std::fs::remove_file(&path);
}
#[tokio::test]
async fn ingest_ships_a_redacted_event() {
let path = tmp_sock();
let listener = UnixListener::bind(&path).unwrap();
let accept = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
read_one(&mut stream).await
});
let emitter = IngestEmitter::with_socket("ados-test", &path);
emitter.emit_event(
"link.unlock",
Level::Warn,
detail(&[
("reason", MpVal::from("loss")),
("session_token", MpVal::from("tok_supersecretvalue")),
]),
);
let frame = tokio::time::timeout(Duration::from_secs(5), accept)
.await
.expect("event delivered within the deadline")
.expect("accept task ok");
match frame {
IngestFrame::Event(evt) => {
assert_eq!(evt.source, "ados-test");
assert_eq!(evt.kind, "link.unlock");
assert_eq!(evt.severity, Level::Warn);
assert_eq!(
evt.detail.get("session_token").and_then(|v| v.as_str()),
Some("redacted:len=20")
);
}
other => panic!("expected an event frame, got {other:?}"),
}
let _ = std::fs::remove_file(&path);
}
#[tokio::test]
async fn ingest_absent_socket_never_blocks_or_panics() {
let path = tmp_sock(); let emitter = IngestEmitter::with_socket("ados-test", &path);
for i in 0..20 {
let mut tags = Fields::new();
tags.insert("n".to_string(), MpVal::from(i as u64));
emitter.emit_metric("video.framerate_hz", 30.0, tags);
}
assert!(emitter.stats().enqueued() >= 1, "at least one enqueued");
tokio::time::sleep(Duration::from_millis(50)).await;
let _ = std::fs::remove_file(&path);
}
#[tokio::test]
async fn ingest_full_channel_drops_without_blocking_and_counts_it() {
let path = tmp_sock(); let emitter = IngestEmitter::with_socket("ados-test", &path);
let total = INGEST_CHANNEL_CAPACITY * 2;
for n in 0..total {
emitter.emit_metric("cpu.utilization_pct", n as f64, Fields::new());
}
let enq = emitter.stats().enqueued();
let drp = emitter.stats().dropped();
assert_eq!(enq + drp, total as u64, "every sample accounted for");
assert!(drp >= 1, "overflow past capacity was dropped and counted");
let _ = std::fs::remove_file(&path);
}
#[test]
fn push_bounded_caps_the_buffer_and_drops_oldest() {
let stats = EmitterStats::default();
let mut pending: VecDeque<u64> = VecDeque::new();
let total = SHIPPER_BUFFER_MAX_FRAMES as u64 + 10;
for i in 0..total {
push_bounded(&mut pending, i, &stats);
}
assert_eq!(pending.len(), SHIPPER_BUFFER_MAX_FRAMES);
assert_eq!(
stats.dropped(),
10,
"the ten oldest were dropped and counted"
);
assert_eq!(*pending.front().unwrap(), 10, "oldest survivor is frame 10");
assert_eq!(*pending.back().unwrap(), total - 1, "newest frame retained");
}
#[tokio::test]
async fn buffered_events_survive_a_late_socket() {
let path = tmp_sock(); let emitter = EventEmitter::with_socket("ados-test", &path);
for i in 0..3u64 {
emitter.emit("radio.bind", Level::Info, detail(&[("n", MpVal::from(i))]));
}
tokio::time::sleep(Duration::from_millis(60)).await;
let listener = UnixListener::bind(&path).unwrap();
let got = tokio::time::timeout(Duration::from_secs(5), async {
let (mut stream, _) = listener.accept().await.unwrap();
let mut ns = Vec::new();
for _ in 0..3 {
match read_one(&mut stream).await {
IngestFrame::Event(e) => {
ns.push(e.detail.get("n").and_then(|v| v.as_u64()).unwrap());
}
other => panic!("expected an event frame, got {other:?}"),
}
}
ns
})
.await
.expect("the held events were delivered after the socket appeared");
assert_eq!(
got,
vec![0, 1, 2],
"all three early events arrived in order"
);
let _ = std::fs::remove_file(&path);
}
#[tokio::test]
async fn ingest_buffered_metrics_survive_a_late_socket() {
let path = tmp_sock(); let emitter = IngestEmitter::with_socket("ados-test", &path);
for i in 0..3u64 {
let mut tags = Fields::new();
tags.insert("n".to_string(), MpVal::from(i));
emitter.emit_metric("cpu.utilization_pct", i as f64, tags);
}
tokio::time::sleep(Duration::from_millis(60)).await;
let listener = UnixListener::bind(&path).unwrap();
let got = tokio::time::timeout(Duration::from_secs(5), async {
let (mut stream, _) = listener.accept().await.unwrap();
let mut ns = Vec::new();
for _ in 0..3 {
match read_one(&mut stream).await {
IngestFrame::Telemetry(t) => {
ns.push(t.tags.get("n").and_then(|v| v.as_u64()).unwrap());
}
other => panic!("expected a telemetry frame, got {other:?}"),
}
}
ns
})
.await
.expect("the held metrics were delivered after the socket appeared");
assert_eq!(
got,
vec![0, 1, 2],
"all three early metrics arrived in order"
);
let _ = std::fs::remove_file(&path);
}
}