use std::fmt;
use std::io::Write;
use tracing::field::{Field, Visit};
use tracing::{Event, Level, Subscriber};
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::layer::{Context, Layer, SubscriberExt};
use tracing_subscriber::registry::Registry;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct Verbosity {
detail: u8,
quiet: bool,
}
impl Verbosity {
#[must_use]
pub(crate) fn new(detail: u8, quiet: bool) -> Self {
Self { detail, quiet }
}
#[must_use]
pub(crate) fn is_quiet(self) -> bool {
self.quiet
}
#[must_use]
pub(crate) fn narrates(self) -> bool {
!self.quiet
}
fn shows(self, verbosity: u8, level: Level) -> bool {
if level == Level::ERROR {
return true;
}
!self.quiet && verbosity <= self.detail
}
}
pub(crate) fn install(verbosity: Verbosity) {
let subscriber = Registry::default().with(ConsoleLayer { verbosity });
let _ = tracing::subscriber::set_global_default(subscriber);
}
struct ConsoleLayer {
verbosity: Verbosity,
}
impl<S: Subscriber> Layer<S> for ConsoleLayer {
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
if !event.metadata().target().starts_with("zond") {
return;
}
let mut fields = EngineEvent::default();
event.record(&mut fields);
if !self
.verbosity
.shows(fields.verbosity, *event.metadata().level())
{
return;
}
let Some(message) = fields.message else {
return;
};
let prefix = match *event.metadata().level() {
Level::ERROR => "error: ",
Level::WARN => "warning: ",
_ => "",
};
let mut stderr = std::io::stderr().lock();
let _ = writeln!(stderr, "{prefix}{message}");
}
fn max_level_hint(&self) -> Option<LevelFilter> {
Some(if self.verbosity.is_quiet() {
LevelFilter::ERROR
} else {
LevelFilter::INFO
})
}
}
#[derive(Default)]
struct EngineEvent {
message: Option<String>,
verbosity: u8,
}
impl Visit for EngineEvent {
fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
if field.name() == "message" {
self.message = Some(format!("{value:?}"));
}
}
fn record_str(&mut self, field: &Field, value: &str) {
if field.name() == "message" {
self.message = Some(value.to_owned());
}
}
fn record_u64(&mut self, field: &Field, value: u64) {
if field.name() == "verbosity" {
self.verbosity = u8::try_from(value).unwrap_or(u8::MAX);
}
}
fn record_i64(&mut self, field: &Field, value: i64) {
if field.name() == "verbosity" {
self.verbosity = u8::try_from(value).unwrap_or(u8::MAX);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_default_run_shows_headlines_only() {
let default = Verbosity::default();
assert!(default.shows(0, Level::INFO));
assert!(!default.shows(1, Level::INFO));
assert!(!default.shows(2, Level::INFO));
}
#[test]
fn each_v_uncovers_one_more_layer_of_detail() {
assert!(Verbosity::new(1, false).shows(1, Level::INFO));
assert!(!Verbosity::new(1, false).shows(2, Level::INFO));
assert!(Verbosity::new(2, false).shows(2, Level::INFO));
}
#[test]
fn quiet_silences_everything_except_errors() {
let quiet = Verbosity::new(0, true);
assert!(!quiet.shows(0, Level::INFO));
assert!(!quiet.shows(0, Level::WARN));
assert!(quiet.shows(0, Level::ERROR));
assert!(quiet.shows(2, Level::ERROR));
}
#[test]
fn quiet_stops_the_renderer_narrating() {
assert!(Verbosity::default().narrates());
assert!(!Verbosity::new(0, true).narrates());
}
}