codewhale_telemetry/
actor.rs1use std::panic::{AssertUnwindSafe, catch_unwind};
15use std::path::PathBuf;
16use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, SyncSender, channel, sync_channel};
17use std::time::Duration;
18
19use crate::buffer;
20use crate::client::{self, SendOutcome};
21use crate::decision::{self, TelemetryDecision};
22use crate::envelope;
23use crate::event::{Batch, Event, SCHEMA_VERSION, Surface};
24
25pub const BATCH_MAX_EVENTS: usize = 200;
27pub const BATCH_MAX_BYTES: usize = 64 * 1024;
29
30pub(crate) enum Message {
31 Event(Box<Event>),
32 Shutdown(SyncSender<FlushOutcome>),
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum FlushOutcome {
38 Empty,
40 DryRun,
42 Sent,
44 Dropped,
46 Suppressed,
48 TimedOut,
50}
51
52#[derive(Debug, Clone)]
54pub(crate) struct Context {
55 pub root: PathBuf,
56 pub endpoint: Option<String>,
57 pub surface: Surface,
58 pub config_path: Option<PathBuf>,
59 pub app_version: String,
60 pub git_sha: Option<String>,
61 pub tty: bool,
62}
63
64#[derive(Debug)]
66pub(crate) struct Handle {
67 tx: Sender<Message>,
68}
69
70impl Handle {
71 pub(crate) fn spawn(context: Context) -> Self {
73 let (tx, rx) = channel::<Message>();
74 let _ = std::thread::Builder::new()
77 .name("codewhale-telemetry".to_string())
78 .spawn(move || run(&context, &rx));
79 Self { tx }
80 }
81
82 pub(crate) fn record(&self, event: Event) {
84 let _ = self.tx.send(Message::Event(Box::new(event)));
85 }
86
87 pub(crate) fn shutdown(&self, deadline: Duration) -> FlushOutcome {
89 self.round_trip(deadline, Message::Shutdown)
90 }
91
92 fn round_trip(
93 &self,
94 deadline: Duration,
95 build: impl FnOnce(SyncSender<FlushOutcome>) -> Message,
96 ) -> FlushOutcome {
97 let (ack_tx, ack_rx) = sync_channel::<FlushOutcome>(1);
98 if self.tx.send(build(ack_tx)).is_err() {
99 return FlushOutcome::TimedOut;
100 }
101 match ack_rx.recv_timeout(deadline) {
102 Ok(outcome) => outcome,
103 Err(RecvTimeoutError::Timeout | RecvTimeoutError::Disconnected) => {
104 FlushOutcome::TimedOut
105 }
106 }
107 }
108}
109
110fn run(context: &Context, rx: &Receiver<Message>) {
111 while let Ok(message) = rx.recv() {
112 let result = catch_unwind(AssertUnwindSafe(|| match message {
116 Message::Event(event) => {
117 append(context, &event);
118 None
119 }
120 Message::Shutdown(ack) => {
121 let _ = ack.send(flush(context));
122 Some(())
123 }
124 }));
125 match result {
126 Ok(Some(())) => return,
127 Ok(None) => {}
128 Err(_) => {
129 tracing::debug!("telemetry writer recovered from a panic");
130 }
131 }
132 }
133}
134
135fn append(context: &Context, event: &Event) {
136 let Ok(line) = serde_json::to_string(event) else {
137 return;
138 };
139 let path = buffer::buffer_path(&context.root);
140 let _ = buffer::append(&context.root, &path, &line);
141}
142
143fn flush(context: &Context) -> FlushOutcome {
150 match decision::re_decide(context.config_path.as_deref(), context.surface) {
151 TelemetryDecision::Enabled(_) => {}
152 TelemetryDecision::OptedOut | TelemetryDecision::ForcedOff => {
153 return FlushOutcome::Suppressed;
154 }
155 }
156 if buffer::tombstone_present(&context.root) {
157 return FlushOutcome::Suppressed;
158 }
159
160 let lines = buffer::drain(&context.root);
161 if lines.is_empty() {
162 return FlushOutcome::Empty;
163 }
164 let events = parse_events(&lines);
165 if events.is_empty() {
166 return FlushOutcome::Empty;
167 }
168
169 let Ok(install) = envelope::read_or_create_install_id(&context.root) else {
170 return FlushOutcome::Dropped;
171 };
172
173 let mut state = envelope::read_state(&context.root);
174 state.schema_version = SCHEMA_VERSION;
175 state.last_flush = Some(envelope::now_rfc3339());
176 let _ = envelope::write_state(&context.root, &state);
179
180 let batch = Batch {
181 schema_version: SCHEMA_VERSION,
182 sent_at: envelope::now_rfc3339(),
183 install_id: install.install_id,
184 app_version: context.app_version.clone(),
185 git_sha: context.git_sha.clone(),
186 surface: context.surface,
187 os: envelope::current_os(),
188 arch: envelope::current_arch(),
189 libc: envelope::current_libc(),
190 tty: context.tty,
191 events,
192 };
193
194 match client::send(&context.root, context.endpoint.as_deref(), &batch) {
195 SendOutcome::DryRun => FlushOutcome::DryRun,
196 SendOutcome::Accepted => FlushOutcome::Sent,
197 SendOutcome::Dropped => FlushOutcome::Dropped,
198 }
199}
200
201pub(crate) fn parse_events(lines: &[String]) -> Vec<Event> {
212 let mut events = Vec::new();
213 let mut bytes = 0usize;
214 for line in lines {
215 if events.len() >= BATCH_MAX_EVENTS || bytes + line.len() > BATCH_MAX_BYTES {
216 break;
217 }
218 if let Ok(event) = serde_json::from_str::<Event>(line) {
219 if !event.is_bounded() {
220 tracing::debug!("telemetry dropped an out-of-bounds buffered event");
221 continue;
222 }
223 bytes += line.len();
224 events.push(event);
225 }
226 }
227 events
228}