Files
llmx/codex-rs/tui/src/log_layer.rs

95 lines
3.2 KiB
Rust
Raw Normal View History

//! Custom `tracing_subscriber` layer that forwards every formatted log event to the
//! TUI so the status indicator can display the *latest* log line while a task is
//! running.
//!
//! The layer is intentionally extremely small: we implement `on_event()` only and
//! ignore spans/metadata because we only care about the alreadyformatted output
//! that the default `fmt` layer would print. We therefore borrow the same
//! formatter (`tracing_subscriber::fmt::format::FmtSpan`) used by the default
//! fmt layer so the text matches what is written to the log file.
use std::fmt::Write as _;
use tokio::sync::mpsc::UnboundedSender;
use tracing::Event;
use tracing::Subscriber;
use tracing::field::Field;
use tracing::field::Visit;
use tracing_subscriber::Layer;
use tracing_subscriber::layer::Context;
use tracing_subscriber::registry::LookupSpan;
/// Maximum characters forwarded to the TUI. Longer messages are truncated so the
/// singleline status indicator cannot overflow the viewport.
#[allow(dead_code)]
const _DEFAULT_MAX_LEN: usize = 120;
pub struct TuiLogLayer {
tx: UnboundedSender<String>,
max_len: usize,
}
impl TuiLogLayer {
pub fn new(tx: UnboundedSender<String>, max_len: usize) -> Self {
Self {
tx,
max_len: max_len.max(8),
}
}
}
impl<S> Layer<S> for TuiLogLayer
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
// Build a terse line like `[TRACE core::session] message …` by visiting
// fields into a buffer. This avoids pulling in the heavyweight
// formatter machinery.
struct Visitor<'a> {
buf: &'a mut String,
}
impl Visit for Visitor<'_> {
fn record_debug(&mut self, _field: &Field, value: &dyn std::fmt::Debug) {
let _ = write!(self.buf, " {:?}", value);
}
}
let mut buf = String::new();
let _ = write!(
buf,
"[{} {}]",
event.metadata().level(),
event.metadata().target()
);
event.record(&mut Visitor { buf: &mut buf });
// `String::truncate` operates on UTF8 codepoint boundaries and will
// panic if the provided index is not one. Because we limit the log
// line by its **byte** length we can not guarantee that the index we
// want to cut at happens to be on a boundary. Therefore we fall back
// to a simple, boundarysafe loop that pops complete characters until
// the string is within the designated size.
if buf.len() > self.max_len {
// Attempt direct truncate at the byte index. If that is not a
// valid boundary we advance to the next one ( ≤3 bytes away ).
if buf.is_char_boundary(self.max_len) {
buf.truncate(self.max_len);
} else {
let mut idx = self.max_len;
while idx < buf.len() && !buf.is_char_boundary(idx) {
idx += 1;
}
buf.truncate(idx);
}
}
let sanitized = buf.replace(['\n', '\r'], " ");
let _ = self.tx.send(sanitized);
}
}