use std::panic::{AssertUnwindSafe, catch_unwind};
use std::path::PathBuf;
use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, SyncSender, channel, sync_channel};
use std::time::Duration;
use crate::buffer;
use crate::client::{self, SendOutcome};
use crate::decision::{self, TelemetryDecision};
use crate::envelope;
use crate::event::{Batch, Event, SCHEMA_VERSION, Surface};
pub const BATCH_MAX_EVENTS: usize = 200;
pub const BATCH_MAX_BYTES: usize = 64 * 1024;
pub(crate) enum Message {
Event(Box<Event>),
Shutdown(SyncSender<FlushOutcome>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlushOutcome {
Empty,
DryRun,
Sent,
Dropped,
Suppressed,
TimedOut,
}
#[derive(Debug, Clone)]
pub(crate) struct Context {
pub root: PathBuf,
pub endpoint: Option<String>,
pub surface: Surface,
pub config_path: Option<PathBuf>,
pub app_version: String,
pub git_sha: Option<String>,
pub tty: bool,
}
#[derive(Debug)]
pub(crate) struct Handle {
tx: Sender<Message>,
}
impl Handle {
pub(crate) fn spawn(context: Context) -> Self {
let (tx, rx) = channel::<Message>();
let _ = std::thread::Builder::new()
.name("codewhale-telemetry".to_string())
.spawn(move || run(&context, &rx));
Self { tx }
}
pub(crate) fn record(&self, event: Event) {
let _ = self.tx.send(Message::Event(Box::new(event)));
}
pub(crate) fn shutdown(&self, deadline: Duration) -> FlushOutcome {
self.round_trip(deadline, Message::Shutdown)
}
fn round_trip(
&self,
deadline: Duration,
build: impl FnOnce(SyncSender<FlushOutcome>) -> Message,
) -> FlushOutcome {
let (ack_tx, ack_rx) = sync_channel::<FlushOutcome>(1);
if self.tx.send(build(ack_tx)).is_err() {
return FlushOutcome::TimedOut;
}
match ack_rx.recv_timeout(deadline) {
Ok(outcome) => outcome,
Err(RecvTimeoutError::Timeout | RecvTimeoutError::Disconnected) => {
FlushOutcome::TimedOut
}
}
}
}
fn run(context: &Context, rx: &Receiver<Message>) {
while let Ok(message) = rx.recv() {
let result = catch_unwind(AssertUnwindSafe(|| match message {
Message::Event(event) => {
append(context, &event);
None
}
Message::Shutdown(ack) => {
let _ = ack.send(flush(context));
Some(())
}
}));
match result {
Ok(Some(())) => return,
Ok(None) => {}
Err(_) => {
tracing::debug!("telemetry writer recovered from a panic");
}
}
}
}
fn append(context: &Context, event: &Event) {
let Ok(line) = serde_json::to_string(event) else {
return;
};
let path = buffer::buffer_path(&context.root);
let _ = buffer::append(&context.root, &path, &line);
}
fn flush(context: &Context) -> FlushOutcome {
match decision::re_decide(context.config_path.as_deref(), context.surface) {
TelemetryDecision::Enabled(_) => {}
TelemetryDecision::OptedOut | TelemetryDecision::ForcedOff => {
return FlushOutcome::Suppressed;
}
}
if buffer::tombstone_present(&context.root) {
return FlushOutcome::Suppressed;
}
let lines = buffer::drain(&context.root);
if lines.is_empty() {
return FlushOutcome::Empty;
}
let events = parse_events(&lines);
if events.is_empty() {
return FlushOutcome::Empty;
}
let Ok(install) = envelope::read_or_create_install_id(&context.root) else {
return FlushOutcome::Dropped;
};
let mut state = envelope::read_state(&context.root);
state.schema_version = SCHEMA_VERSION;
state.last_flush = Some(envelope::now_rfc3339());
let _ = envelope::write_state(&context.root, &state);
let batch = Batch {
schema_version: SCHEMA_VERSION,
sent_at: envelope::now_rfc3339(),
install_id: install.install_id,
app_version: context.app_version.clone(),
git_sha: context.git_sha.clone(),
surface: context.surface,
os: envelope::current_os(),
arch: envelope::current_arch(),
libc: envelope::current_libc(),
tty: context.tty,
events,
};
match client::send(&context.root, context.endpoint.as_deref(), &batch) {
SendOutcome::DryRun => FlushOutcome::DryRun,
SendOutcome::Accepted => FlushOutcome::Sent,
SendOutcome::Dropped => FlushOutcome::Dropped,
}
}
pub(crate) fn parse_events(lines: &[String]) -> Vec<Event> {
let mut events = Vec::new();
let mut bytes = 0usize;
for line in lines {
if events.len() >= BATCH_MAX_EVENTS || bytes + line.len() > BATCH_MAX_BYTES {
break;
}
if let Ok(event) = serde_json::from_str::<Event>(line) {
if !event.is_bounded() {
tracing::debug!("telemetry dropped an out-of-bounds buffered event");
continue;
}
bytes += line.len();
events.push(event);
}
}
events
}