use chrono::Utc;
use somatize_core::event::Event;
use somatize_core::tracking::{EventEnvelope, EventSink};
use std::fs::{File, OpenOptions};
use std::io::{BufWriter, Write};
use std::path::Path;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
pub struct JsonlEventSink {
events: Mutex<BufWriter<File>>,
metrics: Option<Mutex<BufWriter<File>>>,
seq: AtomicU64,
flush_every: u64,
}
impl JsonlEventSink {
pub fn create(
events_path: &Path,
metrics_path: Option<&Path>,
flush_every: usize,
) -> std::io::Result<Self> {
Self::open(events_path, metrics_path, flush_every, 0, false)
}
pub fn append(
events_path: &Path,
metrics_path: Option<&Path>,
flush_every: usize,
start_seq: u64,
) -> std::io::Result<Self> {
Self::open(events_path, metrics_path, flush_every, start_seq, true)
}
fn open(
events_path: &Path,
metrics_path: Option<&Path>,
flush_every: usize,
start_seq: u64,
append: bool,
) -> std::io::Result<Self> {
let open = |path: &Path| -> std::io::Result<BufWriter<File>> {
let mut opts = OpenOptions::new();
opts.create(true).write(true);
if append {
opts.append(true);
} else {
opts.truncate(true);
}
Ok(BufWriter::new(opts.open(path)?))
};
Ok(Self {
events: Mutex::new(open(events_path)?),
metrics: metrics_path.map(|p| open(p).map(Mutex::new)).transpose()?,
seq: AtomicU64::new(start_seq),
flush_every: flush_every.max(1) as u64,
})
}
pub fn next_seq(&self) -> u64 {
self.seq.load(Ordering::SeqCst)
}
fn metric_line(event: &Event) -> Option<serde_json::Value> {
match event {
Event::TrialMetric {
study_id: _,
trial_id,
metric,
} => Some(serde_json::json!({
"ts": metric.timestamp,
"name": metric.name,
"value": metric.value,
"step": metric.step,
"trial_id": trial_id,
"node_id": null,
})),
Event::MetricReported {
run_id: _,
metric,
node_id,
trial_id,
} => Some(serde_json::json!({
"ts": metric.timestamp,
"name": metric.name,
"value": metric.value,
"step": metric.step,
"trial_id": trial_id,
"node_id": node_id,
})),
_ => None,
}
}
fn write_line(writer: &Mutex<BufWriter<File>>, line: &str, do_flush: bool) {
let mut guard = match writer.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
};
if let Err(e) = writeln!(guard, "{line}") {
tracing::warn!("tracking: failed to write event line: {e}");
return;
}
if do_flush && let Err(e) = guard.flush() {
tracing::warn!("tracking: failed to flush event log: {e}");
}
}
}
impl EventSink for JsonlEventSink {
fn record(&self, event: &Event) {
let seq = self.seq.fetch_add(1, Ordering::SeqCst);
let envelope = EventEnvelope {
seq,
ts: Utc::now(),
event: event.clone(),
};
let do_flush = seq % self.flush_every == self.flush_every - 1;
match serde_json::to_string(&envelope) {
Ok(line) => Self::write_line(&self.events, &line, do_flush),
Err(e) => tracing::warn!("tracking: failed to serialize event: {e}"),
}
if let Some(metrics) = &self.metrics
&& let Some(line) = Self::metric_line(event)
{
Self::write_line(metrics, &line.to_string(), do_flush);
}
}
fn flush(&self) {
for writer in std::iter::once(&self.events).chain(self.metrics.iter()) {
let mut guard = match writer.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
};
if let Err(e) = guard.flush() {
tracing::warn!("tracking: failed to flush event log: {e}");
}
}
}
}
impl Drop for JsonlEventSink {
fn drop(&mut self) {
self.flush();
}
}