use std::io::Write as _;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::mpsc::{sync_channel, Receiver, SyncSender, TrySendError};
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};
use tracing_subscriber::fmt::MakeWriter;
pub const CAPACITY: usize = 4096;
static DROPPED: AtomicU64 = AtomicU64::new(0);
static WRITTEN: AtomicU64 = AtomicU64::new(0);
static QUEUED: AtomicUsize = AtomicUsize::new(0);
static SENDER: OnceLock<SyncSender<Vec<u8>>> = OnceLock::new();
static TAP: OnceLock<fn(&str)> = OnceLock::new();
pub fn set_tap(f: fn(&str)) {
let _ = TAP.set(f);
}
static CONSOLE: OnceLock<std::fs::File> = OnceLock::new();
pub fn set_console(console: std::fs::File) {
let _ = CONSOLE.set(console);
}
pub fn console_sink() -> Box<dyn std::io::Write + Send> {
match CONSOLE.get().and_then(|f| f.try_clone().ok()) {
Some(f) => Box::new(f),
None => Box::new(std::io::stderr()),
}
}
pub fn console_write(text: &str) {
let _ = console_sink().write_all(text.as_bytes());
}
pub fn tap_line(line: &str) {
if let Some(f) = TAP.get() {
f(line);
}
}
pub fn dropped() -> u64 {
DROPPED.load(Ordering::Relaxed)
}
pub fn written() -> u64 {
WRITTEN.load(Ordering::Relaxed)
}
pub fn active() -> bool {
SENDER.get().is_some()
}
pub fn flush(timeout: Duration) {
if SENDER.get().is_none() {
return;
}
let deadline = Instant::now() + timeout;
while QUEUED.load(Ordering::Acquire) > 0 && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(2));
}
let _ = std::io::stderr().flush();
}
pub fn install(sink: Box<dyn std::io::Write + Send>) -> QueueWriter {
let sender = SENDER.get_or_init(|| {
let (tx, rx) = sync_channel::<Vec<u8>>(CAPACITY);
std::thread::Builder::new()
.name("gunnar-console".to_owned())
.spawn(move || drain(rx, sink))
.expect("spawning the console drainer thread");
tx
});
QueueWriter {
sender: sender.clone(),
}
}
fn drain(rx: Receiver<Vec<u8>>, mut sink: Box<dyn std::io::Write + Send>) {
let mut reported: u64 = 0;
while let Ok(line) = rx.recv() {
let _ = sink.write_all(&line);
WRITTEN.fetch_add(1, Ordering::Relaxed);
if QUEUED.load(Ordering::Acquire) == 1 {
let dropped = DROPPED.load(Ordering::Relaxed);
if dropped > reported {
let _ = writeln!(
sink,
"gunnar: the console could not carry {} log lines (INFO and below; {dropped} \
since start) — WARN rows and the dashboard are complete event=\"log.dropped\" \
dropped={} total={dropped}",
dropped - reported,
dropped - reported
);
reported = dropped;
}
let _ = sink.flush();
}
QUEUED.fetch_sub(1, Ordering::AcqRel);
}
let _ = sink.flush();
}
#[derive(Clone)]
pub struct QueueWriter {
sender: SyncSender<Vec<u8>>,
}
impl<'a> MakeWriter<'a> for QueueWriter {
type Writer = LineWriter;
fn make_writer(&'a self) -> Self::Writer {
LineWriter {
sender: self.sender.clone(),
buf: Vec::with_capacity(512),
must_deliver: true,
}
}
fn make_writer_for(&'a self, meta: &tracing::Metadata<'_>) -> Self::Writer {
LineWriter {
sender: self.sender.clone(),
buf: Vec::with_capacity(512),
must_deliver: *meta.level() <= tracing::Level::WARN,
}
}
}
pub struct LineWriter {
sender: SyncSender<Vec<u8>>,
buf: Vec<u8>,
must_deliver: bool,
}
impl std::io::Write for LineWriter {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
self.buf.extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl Drop for LineWriter {
fn drop(&mut self) {
if self.buf.is_empty() {
return;
}
if let Some(f) = TAP.get() {
f(&String::from_utf8_lossy(&self.buf));
}
let line = std::mem::take(&mut self.buf);
QUEUED.fetch_add(1, Ordering::AcqRel);
if self.must_deliver {
if self.sender.send(line).is_err() {
QUEUED.fetch_sub(1, Ordering::AcqRel);
}
return;
}
match self.sender.try_send(line) {
Ok(()) => {}
Err(TrySendError::Full(_)) | Err(TrySendError::Disconnected(_)) => {
QUEUED.fetch_sub(1, Ordering::AcqRel);
DROPPED.fetch_add(1, Ordering::Relaxed);
}
}
}
}
#[doc(hidden)]
pub struct SlowSink {
pub bytes: std::sync::Arc<Mutex<Vec<u8>>>,
pub delay: Duration,
}
impl std::io::Write for SlowSink {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
if !self.delay.is_zero() {
std::thread::sleep(self.delay);
}
self.bytes
.lock()
.unwrap_or_else(|e| e.into_inner())
.extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
#[test]
fn a_slow_console_never_stalls_the_logger_and_loses_no_warn() {
let bytes = Arc::new(Mutex::new(Vec::new()));
let writer = install(Box::new(SlowSink {
bytes: Arc::clone(&bytes),
delay: Duration::from_millis(5),
}));
let info = tracing::Metadata::new(
"t",
"t",
tracing::Level::INFO,
None,
None,
None,
tracing::field::FieldSet::new(&[], tracing::callsite::Identifier(&CALLSITE)),
tracing::metadata::Kind::EVENT,
);
let warn = tracing::Metadata::new(
"t",
"t",
tracing::Level::WARN,
None,
None,
None,
tracing::field::FieldSet::new(&[], tracing::callsite::Identifier(&CALLSITE)),
tracing::metadata::Kind::EVENT,
);
let n = CAPACITY * 3;
let started = Instant::now();
for i in 0..n {
let mut w = writer.make_writer_for(&info);
writeln!(w, "info line {i}").unwrap();
}
let info_elapsed = started.elapsed();
for i in 0..8 {
let mut w = writer.make_writer_for(&warn);
writeln!(w, "WARN line {i}").unwrap();
}
assert!(
info_elapsed < Duration::from_secs(1),
"logging {n} INFO lines to a 5 ms/line console took {info_elapsed:?}: the logger waited on the console"
);
let dropped_now = dropped();
assert!(
dropped_now > 0 && dropped_now < n as u64,
"expected some but not all INFO lines dropped past a {CAPACITY}-line queue, got {dropped_now} of {n}"
);
flush(Duration::from_secs(120));
let out = String::from_utf8_lossy(&bytes.lock().unwrap()).into_owned();
for i in 0..8 {
assert!(
out.contains(&format!("WARN line {i}\n")),
"WARN line {i} was lost"
);
}
assert!(
out.contains("event=\"log.dropped\""),
"the drainer must report on the console what the console missed"
);
assert_eq!(
written() as usize,
n - dropped_now as usize + 8,
"every line not dropped was written"
);
}
struct Callsite;
impl tracing::Callsite for Callsite {
fn set_interest(&self, _: tracing::subscriber::Interest) {}
fn metadata(&self) -> &tracing::Metadata<'_> {
unreachable!("the test never registers this callsite")
}
}
static CALLSITE: Callsite = Callsite;
}