Skip to main content

vtcode_core/utils/
trace_writer.rs

1//! Shared buffered trace log writer with flush-on-exit support.
2//!
3//! Provides a `BufWriter`-backed file writer wrapped in `Arc<Mutex<..>>` so the
4//! tracing `fmt::layer` can write efficiently (batched syscalls) while still
5//! allowing an explicit `flush_trace_log()` call on process exit or signal.
6
7use std::fs::{File, OpenOptions};
8use std::io::{BufWriter, Write};
9use std::path::Path;
10use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
11
12use anyhow::{Context, Result};
13
14/// Capacity of the internal `BufWriter` (64 KiB — large enough to batch many
15/// log lines before issuing a single `write` syscall).
16const BUF_CAPACITY: usize = 64 * 1024;
17
18/// Global handle to the active trace log writer so `flush_trace_log` can be
19/// called from signal handlers / shutdown hooks without threading the writer
20/// through the entire call stack.
21static GLOBAL_WRITER: OnceLock<FlushableWriter> = OnceLock::new();
22
23/// A clonable, thread-safe buffered writer that implements `std::io::Write`
24/// so it can be passed directly to `tracing_subscriber::fmt::layer().with_writer(..)`.
25#[derive(Clone)]
26pub struct FlushableWriter {
27    inner: Arc<Mutex<BufWriter<File>>>,
28}
29
30impl FlushableWriter {
31    /// Open (or create) a log file and wrap it in a buffered writer.
32    pub fn open(path: &Path) -> Result<Self> {
33        let file = OpenOptions::new()
34            .create(true)
35            .append(true)
36            .open(path)
37            .with_context(|| format!("Failed to open trace log file: {}", path.display()))?;
38        let writer = BufWriter::with_capacity(BUF_CAPACITY, file);
39        let flushable = Self {
40            inner: Arc::new(Mutex::new(writer)),
41        };
42        // Store globally so `flush_trace_log` works from anywhere.
43        let _ = GLOBAL_WRITER.set(flushable.clone());
44        // Register the flush hook in vtcode-commons so crates that don't
45        // depend on vtcode-core (e.g. vtcode-tui) can still trigger a flush.
46        vtcode_commons::trace_flush::register_trace_flush_hook(flush_trace_log);
47        Ok(flushable)
48    }
49
50    /// Flush the internal buffer to disk.
51    pub fn flush(&self) {
52        let _ = self.flush_locked();
53    }
54
55    fn lock_writer(&self) -> std::io::Result<MutexGuard<'_, BufWriter<File>>> {
56        self.inner
57            .lock()
58            .map_err(|_| std::io::Error::other("trace writer lock poisoned"))
59    }
60
61    fn flush_locked(&self) -> std::io::Result<()> {
62        let mut guard = self.lock_writer()?;
63        guard.flush()
64    }
65}
66
67impl Write for FlushableWriter {
68    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
69        let mut guard = self.lock_writer()?;
70        guard.write(buf)
71    }
72
73    fn flush(&mut self) -> std::io::Result<()> {
74        self.flush_locked()
75    }
76}
77
78/// Flush the global trace log writer to disk.
79///
80/// Safe to call from signal handlers, shutdown hooks, or `Drop` implementations.
81/// No-op if no trace writer has been initialized.
82pub fn flush_trace_log() {
83    if let Some(writer) = GLOBAL_WRITER.get() {
84        writer.flush();
85    }
86}