Some effects of this change: - New formatting changes across many files. No functionality changes should occur from that. - Calls to `set_env` are considered unsafe, since this only happens in tests we wrap them in `unsafe` blocks
95 lines
3.2 KiB
Rust
95 lines
3.2 KiB
Rust
//! 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 already‑formatted 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
|
||
/// single‑line 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 UTF‑8 code‑point 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, boundary‑safe 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);
|
||
}
|
||
}
|