1mod bounded;
25mod context;
26mod event;
27mod writer;
28
29pub use context::{OperationId, in_operation, operation_id};
30pub use event::{
31 ContentPolicy, EventKind, Limits, MAX_CAPTURE_BYTES, MAX_CAPTURE_EVENTS, MAX_RECORD_BYTES,
32 SCHEMA_VERSION, TERMINAL_RESERVE, TraceOptions,
33};
34
35use arc_swap::ArcSwapOption;
36use parking_lot::{Condvar, Mutex, MutexGuard};
37use serde::Serialize;
38use std::fs::OpenOptions;
39use std::path::{Path, PathBuf};
40use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
41use std::sync::mpsc::{SyncSender, sync_channel};
42use std::sync::{Arc, LazyLock};
43use std::thread::JoinHandle;
44use std::time::{Duration, Instant};
45
46pub(crate) const QUEUE_CAPACITY: usize = 64;
49const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
52const PANIC_MESSAGE_CHARS: usize = 2048;
55const PANIC_BACKTRACE_CHARS: usize = 8192;
56
57static ACTIVE: LazyLock<ArcSwapOption<Recorder>> = LazyLock::new(ArcSwapOption::empty);
58static LIFECYCLE: Mutex<()> = Mutex::new(());
61static SESSION_PRESENT: AtomicBool = AtomicBool::new(false);
63
64mod close {
67 pub(crate) const FINISH: u8 = 1;
68 pub(crate) const ABANDON: u8 = 2;
69}
70
71#[derive(Debug, thiserror::Error)]
72pub enum TraceError {
73 #[error("a trace session is already active")]
74 AlreadyActive,
75 #[error("cannot create trace {path}: {source}")]
76 Open {
77 path: PathBuf,
78 source: std::io::Error,
79 },
80 #[error("cannot start trace writer: {0}")]
81 Spawn(std::io::Error),
82 #[error("incomplete trace: {0}")]
83 Incomplete(String),
84}
85
86#[derive(Default)]
88pub(crate) struct Failure {
89 message: Mutex<Option<String>>,
90 reported: AtomicBool,
91 failed: AtomicBool,
92}
93
94impl Failure {
95 pub(crate) fn set(&self, message: impl FnOnce() -> String) {
96 let mut failure = self.message.lock();
97 if failure.is_none() {
98 *failure = Some(message());
99 }
100 self.failed.store(true, Ordering::Release);
101 }
102 pub(crate) fn present(&self) -> bool {
103 self.failed.load(Ordering::Acquire)
104 }
105}
106
107#[derive(Default)]
108pub(crate) struct SignalState {
109 pub(crate) panic_flushed: bool,
110 pub(crate) done: bool,
111}
112
113#[derive(Default)]
116pub(crate) struct Signal {
117 state: Mutex<SignalState>,
118 ready: Condvar,
119}
120
121impl Signal {
122 pub(crate) fn mark_panic_flushed(&self) {
123 self.state.lock().panic_flushed = true;
124 self.ready.notify_all();
125 }
126 pub(crate) fn mark_done(&self) {
127 self.state.lock().done = true;
128 self.ready.notify_all();
129 }
130 pub(crate) fn wait_for(
132 &self,
133 satisfied: impl Fn(&SignalState) -> bool,
134 limit: Duration,
135 ) -> bool {
136 let mut state = self.state.lock();
137 let deadline = Instant::now() + limit;
138 while !satisfied(&state) {
139 let Some(remaining) = deadline.checked_duration_since(Instant::now()) else {
140 return false;
141 };
142 if remaining.is_zero() {
143 return false;
144 }
145 if self.ready.wait_for(&mut state, remaining).timed_out() && !satisfied(&state) {
146 return false;
147 }
148 }
149 true
150 }
151}
152
153#[derive(Default)]
155pub(crate) struct Shared {
156 pub(crate) failure: Failure,
157 pub(crate) close_kind: AtomicU8,
158 pub(crate) signal: Signal,
159 pub(crate) panicked: AtomicBool,
160}
161
162impl Shared {
163 pub(crate) fn close_kind(&self) -> u8 {
164 self.close_kind.load(Ordering::Relaxed)
165 }
166 fn capture_dead(&self) -> bool {
169 self.failure.present() || self.panicked.load(Ordering::Acquire) || self.close_kind() != 0
170 }
171}
172
173pub(crate) struct Record {
174 pub(crate) kind: EventKind,
175 pub(crate) elapsed_us: u128,
176 pub(crate) thread: Arc<str>,
177 pub(crate) operation: Option<OperationId>,
178 pub(crate) fields: Vec<u8>,
179}
180
181pub(crate) struct Recorder {
182 pub(crate) sender: Mutex<Option<SyncSender<Record>>>,
183 pub(crate) shared: Arc<Shared>,
184 started: Instant,
185 max_record: usize,
186 content: ContentPolicy,
187}
188
189pub struct TraceSession {
193 recorder: Arc<Recorder>,
194 worker: Option<JoinHandle<()>>,
195}
196
197pub fn start(path: &Path, options: TraceOptions) -> Result<TraceSession, TraceError> {
205 if !options.limits.valid() {
206 return Err(TraceError::Incomplete("invalid capture limits".into()));
207 }
208 let _lifecycle = LIFECYCLE.lock();
209 if ACTIVE.load().is_some() {
210 return Err(TraceError::AlreadyActive);
211 }
212 let mut open = OpenOptions::new();
213 open.write(true).create_new(true);
214 #[cfg(unix)]
215 {
216 use std::os::unix::fs::OpenOptionsExt;
217 open.mode(0o600);
218 }
219 let file = open.open(path).map_err(|source| TraceError::Open {
220 path: path.to_path_buf(),
221 source,
222 })?;
223 let (sender, receiver) = sync_channel(QUEUE_CAPACITY);
224 let shared = Arc::new(Shared::default());
225 let writer_shared = Arc::clone(&shared);
226 let limits = options.limits;
227 let worker = std::thread::Builder::new()
228 .name("rootle-trace".into())
229 .spawn(move || writer::run(file, receiver, writer_shared, limits))
230 .map_err(TraceError::Spawn)?;
231 let recorder = Arc::new(Recorder {
232 sender: Mutex::new(Some(sender)),
233 shared,
234 started: Instant::now(),
235 max_record: limits.max_record_bytes,
236 content: options.content,
237 });
238 ACTIVE.store(Some(Arc::clone(&recorder)));
239 SESSION_PRESENT.store(true, Ordering::Release);
240 Ok(TraceSession {
241 recorder,
242 worker: Some(worker),
243 })
244}
245
246#[inline]
247pub fn enabled() -> bool {
248 SESSION_PRESENT.load(Ordering::Relaxed)
249 && ACTIVE
250 .load()
251 .as_ref()
252 .is_some_and(|recorder| !recorder.shared.capture_dead())
253}
254
255#[inline]
258pub fn capture_content() -> bool {
259 SESSION_PRESENT.load(Ordering::Relaxed)
260 && ACTIVE.load().as_ref().is_some_and(|recorder| {
261 !recorder.shared.capture_dead() && recorder.content == ContentPolicy::Full
262 })
263}
264
265pub fn record_with<T: Serialize>(kind: EventKind, fields: impl FnOnce() -> T) {
267 let Some(recorder) = recording_snapshot() else {
268 return;
269 };
270 let fields = fields();
273 record_to(&recorder, kind, &fields);
274}
275
276pub fn record<T: Serialize>(kind: EventKind, fields: &T) {
277 let Some(recorder) = recording_snapshot() else {
278 return;
279 };
280 record_to(&recorder, kind, fields);
281}
282
283fn recording_snapshot() -> Option<Arc<Recorder>> {
284 if !SESSION_PRESENT.load(Ordering::Relaxed) {
285 return None;
286 }
287 let recorder = ACTIVE.load_full()?;
288 (!recorder.shared.capture_dead()).then_some(recorder)
289}
290
291fn record_to<T: Serialize>(recorder: &Recorder, kind: EventKind, fields: &T) {
292 let mut sender = recorder.sender.lock();
293 admit(
294 recorder,
295 &mut sender,
296 kind,
297 |out| serde_json::to_writer(out, fields),
298 false,
299 );
300}
301
302fn admit(
307 recorder: &Recorder,
308 sender: &mut MutexGuard<'_, Option<SyncSender<Record>>>,
309 kind: EventKind,
310 encode: impl FnOnce(&mut bounded::Bytes) -> serde_json::Result<()>,
311 urgent: bool,
312) -> bool {
313 if sender.is_none() {
314 return false;
315 }
316 if !urgent && recorder.shared.capture_dead() {
317 sender.take();
318 return false;
319 }
320 let mut bytes = bounded::Bytes::new(recorder.max_record);
321 if encode(&mut bytes).is_err() {
322 recorder
323 .shared
324 .failure
325 .set(|| "record exceeds cap or cannot serialize".into());
326 sender.take();
327 return false;
328 }
329 let record = Record {
330 kind,
331 elapsed_us: recorder.started.elapsed().as_micros(),
332 thread: context::thread_label(),
333 operation: context::current_operation(),
334 fields: bytes.into_vec(),
335 };
336 if sender
337 .as_ref()
338 .expect("admission checked the sender")
339 .try_send(record)
340 .is_err()
341 {
342 recorder
343 .shared
344 .failure
345 .set(|| "capture queue full or writer unavailable".into());
346 sender.take();
347 return false;
348 }
349 true
350}
351
352pub fn panic_record(
362 location: Option<(&str, u32, u32)>,
363 message: Option<&str>,
364 backtrace: Option<&str>,
365) {
366 if !SESSION_PRESENT.load(Ordering::Relaxed) {
367 return;
368 }
369 let Some(recorder) = ACTIVE.load_full() else {
370 return;
371 };
372 recorder.shared.panicked.store(true, Ordering::Release);
375 let full = recorder.content == ContentPolicy::Full;
376 let message = full
377 .then_some(message)
378 .flatten()
379 .map(|text| cap_text(text, PANIC_MESSAGE_CHARS));
380 let backtrace = full
381 .then_some(backtrace)
382 .flatten()
383 .map(|text| cap_text(text, PANIC_BACKTRACE_CHARS));
384 let fields = serde_json::json!({
385 "location": location.map(|(file, line, column)| serde_json::json!({
386 "file": file,
387 "line": line,
388 "column": column,
389 })),
390 "message": message,
391 "backtrace": backtrace,
392 });
393 let admitted = match recorder.sender.try_lock() {
394 Some(mut sender) => admit(
395 &recorder,
396 &mut sender,
397 EventKind::Panic,
398 |out| serde_json::to_writer(out, &fields),
399 true,
400 ),
401 None => false,
403 };
404 if admitted {
405 recorder
406 .shared
407 .signal
408 .wait_for(|state| state.panic_flushed || state.done, SHUTDOWN_TIMEOUT);
409 }
410}
411
412pub fn take_failure() -> Option<String> {
415 if !SESSION_PRESENT.load(Ordering::Relaxed) {
416 return None;
417 }
418 let recorder = ACTIVE.load_full()?;
419 if recorder.shared.failure.reported.load(Ordering::Relaxed) {
420 return None;
421 }
422 let message = if recorder.shared.panicked.load(Ordering::Acquire) {
423 "capture interrupted by panic".to_owned()
424 } else {
425 recorder.shared.failure.message.lock().clone()?
426 };
427 (!recorder
428 .shared
429 .failure
430 .reported
431 .swap(true, Ordering::Relaxed))
432 .then_some(message)
433}
434
435pub fn mark_incomplete(reason: &'static str) {
438 let Some(recorder) = recording_snapshot() else {
439 return;
440 };
441 record_to(
442 &recorder,
443 EventKind::Error,
444 &serde_json::json!({"scope":"capture","reason":reason}),
445 );
446 recorder.shared.failure.set(|| reason.to_owned());
447 recorder.sender.lock().take();
448}
449
450fn cap_text(text: &str, max_chars: usize) -> String {
451 let mut chars = text.chars();
452 let mut capped: String = chars.by_ref().take(max_chars).collect();
453 if chars.next().is_some() {
454 capped.push('…');
455 }
456 capped
457}
458
459impl TraceSession {
460 pub fn finish(mut self) -> Result<(), TraceError> {
464 self.close(true)
465 }
466
467 fn close(&mut self, explicit: bool) -> Result<(), TraceError> {
468 let Some(worker) = self.worker.take() else {
469 return Ok(());
470 };
471 let shared = &self.recorder.shared;
472 {
473 let _lifecycle = LIFECYCLE.lock();
474 shared.close_kind.store(
475 if explicit {
476 close::FINISH
477 } else {
478 close::ABANDON
479 },
480 Ordering::Release,
481 );
482 self.recorder.sender.lock().take();
484 if ACTIVE
485 .load()
486 .as_ref()
487 .is_some_and(|active| Arc::ptr_eq(active, &self.recorder))
488 {
489 SESSION_PRESENT.store(false, Ordering::Release);
490 ACTIVE.store(None);
491 }
492 }
493 if !shared.signal.wait_for(|state| state.done, SHUTDOWN_TIMEOUT) {
498 shared.failure.set(|| {
499 format!(
500 "trace writer did not finish within {}s",
501 SHUTDOWN_TIMEOUT.as_secs()
502 )
503 });
504 } else if worker.join().is_err() {
505 shared.failure.set(|| "trace writer panicked".into());
506 }
507 if shared.panicked.load(Ordering::Acquire) {
510 shared.failure.set(|| "capture interrupted by panic".into());
511 }
512 match shared.failure.message.lock().clone() {
513 Some(error) => Err(TraceError::Incomplete(error)),
514 None => Ok(()),
515 }
516 }
517}
518
519impl Drop for TraceSession {
520 fn drop(&mut self) {
521 let _ = self.close(false);
525 }
526}
527
528#[cfg(test)]
529mod tests;