use std::io::IsTerminal;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::OnceLock;
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::util::SubscriberInitExt as _;
use tracing_subscriber::{reload, EnvFilter, Registry};
pub const TARGET: &str = "gunnar::metrics";
const RUNGS: usize = 3;
fn rung_name(rung: usize, startup: &str) -> String {
match rung {
0 => format!("{startup} (the startup filter)"),
1 => "debug".to_owned(),
_ => "trace".to_owned(),
}
}
fn rung_spec(rung: usize, startup: &str) -> String {
match rung {
0 => startup.to_owned(),
1 => "debug".to_owned(),
_ => "trace".to_owned(),
}
}
struct LogControl {
handle: reload::Handle<EnvFilter, Registry>,
startup: String,
rung: AtomicUsize,
}
static CONTROL: OnceLock<LogControl> = OnceLock::new();
pub fn startup_filter(rust_log: Option<&str>, verbose: bool) -> String {
let default = if verbose { "debug" } else { "info" };
let mut filter = rust_log.map(str::trim).filter(|v| !v.is_empty()).unwrap_or(default).to_owned();
if !filter.contains("boringtun") {
filter.push_str(",boringtun::noise::timers=off");
}
if !filter.contains("rustls") {
filter.push_str(",rustls::msgs::handshake=error");
}
filter
}
pub(crate) fn startup_parts(rust_log: Option<&str>, verbose: bool) -> (String, String) {
let default = if verbose { "debug" } else { "info" };
let base = rust_log.map(str::trim).filter(|v| !v.is_empty()).unwrap_or(default);
let operator = EnvFilter::try_new(base).map(|f| f.to_string()).unwrap_or_else(|_| default.to_owned());
let built = startup_filter(Some(&operator), verbose);
(operator, built)
}
pub(crate) fn built(rung: usize, startup: &str) -> String {
match rung {
0 => startup_filter(Some(startup), false),
_ => rung_spec(rung, startup),
}
}
pub fn init(verbose: bool) -> bool {
let (startup, spec) = startup_parts(std::env::var("RUST_LOG").ok().as_deref(), verbose);
let filter = EnvFilter::try_new(&spec).unwrap_or_else(|_| EnvFilter::new(built(0, if verbose { "debug" } else { "info" })));
let (filter_layer, handle) = reload::Layer::new(filter);
let format_layer = tracing_subscriber::fmt::layer()
.with_ansi(at_a_pty());
let installed = if at_a_pty() {
Registry::default()
.with(filter_layer)
.with(crate::logtap::TapLayer::new())
.with(format_layer.with_writer(std::io::stderr))
.try_init()
.is_ok()
} else {
Registry::default()
.with(filter_layer)
.with(crate::logtap::TapLayer::new())
.with(format_layer.with_writer(crate::logqueue::install(crate::logqueue::console_sink())))
.try_init()
.is_ok()
};
if !installed {
return false;
}
let _ = CONTROL.set(LogControl {
handle,
startup,
rung: AtomicUsize::new(0),
});
true
}
fn at_a_pty() -> bool {
if !std::io::stderr().is_terminal() {
return false;
}
let Ok(meta) = std::fs::metadata("/proc/self/fd/2") else {
return true;
};
is_pty_device(std::os::linux::fs::MetadataExt::st_rdev(&meta))
}
fn is_pty_device(rdev: u64) -> bool {
let major = ((rdev >> 8) & 0xfff) | ((rdev >> 32) & !0xfff);
(136..=143).contains(&major)
}
pub fn current() -> Option<String> {
let control = CONTROL.get()?;
Some(rung_spec(
control.rung.load(Ordering::Relaxed),
&control.startup,
))
}
pub fn cycle() -> Option<String> {
let control = CONTROL.get()?;
let next = (control.rung.load(Ordering::Relaxed) + 1) % RUNGS;
apply(control, next)
}
pub fn reset() -> Option<String> {
let control = CONTROL.get()?;
apply(control, 0)
}
fn apply(control: &LogControl, next: usize) -> Option<String> {
let previous = control.rung.load(Ordering::Relaxed);
let spec = rung_spec(next, &control.startup);
let name = rung_name(next, &control.startup);
let filter = EnvFilter::try_new(built(next, &control.startup)).ok()?;
let quieter = next < previous;
if quieter {
announce(&name, previous, &control.startup);
}
control.handle.reload(filter).ok()?;
control.rung.store(next, Ordering::Relaxed);
if !quieter {
announce(&name, next, &control.startup);
}
Some(spec)
}
fn announce(now: &str, under: usize, startup: &str) {
tracing::warn!(
target: TARGET,
event = "log.level",
level = %now,
usr1 = "one rung more verbose",
usr2 = %format!("back to {}", rung_spec(0, startup)),
"gunnar's log level changed"
);
debug_assert!(under < RUNGS);
}
#[cfg(feature = "server")]
pub fn spawn_signal_control() -> tokio::task::JoinHandle<()> {
use tokio::signal::unix::{signal, SignalKind};
tokio::spawn(async move {
let (mut usr1, mut usr2) = match (
signal(SignalKind::user_defined1()),
signal(SignalKind::user_defined2()),
) {
(Ok(one), Ok(two)) => (one, two),
_ => {
tracing::warn!(
target: TARGET,
"SIGUSR1/SIGUSR2 could not be registered, so this server's log level cannot \
be changed without restarting it. Note that the DEFAULT disposition of both \
signals is to terminate the process: do not send them to this one."
);
return;
}
};
if CONTROL.get().is_none() {
tracing::warn!(
target: TARGET,
"the tracing subscriber was installed by something other than gunnar, so \
SIGUSR1/SIGUSR2 will be absorbed and will change nothing."
);
}
loop {
let changed = tokio::select! {
_ = usr1.recv() => cycle(),
_ = usr2.recv() => reset(),
};
if changed.is_none() {
tracing::warn!(
target: TARGET,
"a log-level signal arrived and there is no reloadable filter to move"
);
}
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_ladder_returns_to_what_the_process_started_with() {
let startup = "warn,gunnar::git=info";
assert_eq!(rung_spec(0, startup), startup);
assert_eq!(rung_spec(1, startup), "debug");
assert_eq!(rung_spec(2, startup), "trace");
assert_eq!((2 + 1) % RUNGS, 0);
for rung in 0..RUNGS {
assert!(!rung_name(rung, startup).is_empty());
assert!(!rung_spec(rung, startup).is_empty());
}
}
#[test]
fn the_upper_rungs_are_bare_levels_and_not_gunnar_only() {
for rung in 1..RUNGS {
let spec = rung_spec(rung, "info");
assert!(
!spec.contains('='),
"rung {rung} is a targeted directive ({spec}), so an operator who asked for \
debug would not see russh, gix or h2 — the crates a transport incident is \
usually in"
);
assert!(EnvFilter::try_new(&spec).is_ok(), "{spec} does not parse");
}
}
#[test]
fn a_serial_console_is_a_terminal_and_is_not_a_pty() {
let dev = |major: u64, minor: u64| {
((major & 0xfff) << 8)
| ((major & !0xfffu64) << 32)
| (minor & 0xff)
| ((minor & !0xffu64) << 12)
};
assert!(
!is_pty_device(dev(5, 1)),
"/dev/console must take the queue"
);
assert!(!is_pty_device(dev(4, 64)), "/dev/ttyS0 must take the queue");
assert!(!is_pty_device(dev(4, 65)), "/dev/ttyS1 must take the queue");
assert!(!is_pty_device(dev(4, 1)), "/dev/tty1 must take the queue");
assert!(is_pty_device(dev(136, 0)), "/dev/pts/0 is where a human is");
assert!(is_pty_device(dev(136, 5)), "/dev/pts/5 is where a human is");
assert!(is_pty_device(dev(143, 255)), "the last UNIX98 pts major");
assert!(!is_pty_device(dev(144, 0)), "past the pts range");
let rdev_of = |p: &str| {
std::fs::metadata(p)
.ok()
.map(|m| std::os::linux::fs::MetadataExt::st_rdev(&m))
};
if let Some(rdev) = rdev_of("/dev/null") {
assert_eq!(rdev, dev(1, 3), "makedev() is not encoded as assumed");
assert!(!is_pty_device(rdev), "/dev/null is not a pty");
}
if let Some(rdev) = rdev_of("/dev/console") {
if console_is_a_container_pty_bind() {
assert!(
is_pty_device(rdev),
"/dev/console is a devpts bind (a container's console), rdev {rdev:#x}, \
and is_pty_device did not call it a pty"
);
} else {
assert_eq!(
rdev,
dev(5, 1),
"/dev/console is not a devpts bind, so it is the kernel console (5, 1)"
);
assert!(!is_pty_device(rdev), "the real /dev/console is not a pty");
}
}
}
fn console_is_a_container_pty_bind() -> bool {
std::fs::read_to_string("/proc/self/mountinfo")
.map(|info| console_bind_in(&info))
.unwrap_or(false)
}
fn console_bind_in(mountinfo: &str) -> bool {
mountinfo.lines().any(|line| {
let Some((head, tail)) = line.split_once(" - ") else {
return false;
};
head.split(' ').nth(4) == Some("/dev/console")
&& tail.split(' ').next() == Some("devpts")
})
}
#[test]
fn a_container_console_is_told_from_the_kernel_console() {
let container = "692 657 0:79 /0 /dev/console rw,nosuid,noexec,relatime shared:652 - devpts devpts rw,gid=5,mode=620";
let pts_dir = "690 657 0:79 / /dev/pts rw,nosuid,noexec,relatime shared:650 - devpts devpts rw,gid=5,mode=620";
let booted = "22 26 0:5 / /dev rw,nosuid,relatime shared:2 - devtmpfs udev rw,size=8144k";
assert!(console_bind_in(container));
assert!(!console_bind_in(pts_dir), "/dev/pts itself is not the console");
assert!(!console_bind_in(booted), "a devtmpfs /dev is the kernel console's");
assert!(console_bind_in(&[booted, pts_dir, container].join("\n")));
}
#[test]
fn the_startup_filter_silences_boringtuns_timer_lines_unless_rust_log_names_boringtun() {
assert_eq!(super::startup_filter(None, false), "info,boringtun::noise::timers=off,rustls::msgs::handshake=error");
assert_eq!(super::startup_filter(Some("warn,gunnar=debug"), false), "warn,gunnar=debug,boringtun::noise::timers=off,rustls::msgs::handshake=error");
assert_eq!(super::startup_filter(Some(" "), true), "debug,boringtun::noise::timers=off,rustls::msgs::handshake=error");
assert_eq!(super::startup_filter(Some("info,boringtun=trace"), false), "info,boringtun=trace,rustls::msgs::handshake=error");
}
#[test]
fn rung_zero_is_named_as_the_operator_set_it_and_built_with_the_silence() {
let (operator, built) = super::startup_parts(None, false);
assert_eq!(operator, "info");
assert!(rung_name(0, &operator).starts_with("info "), "{}", rung_name(0, &operator));
assert_eq!(built, "info,boringtun::noise::timers=off,rustls::msgs::handshake=error");
assert_eq!(super::built(0, &operator), built, "rung 0 is built with the silence");
assert_eq!(super::built(1, &operator), "debug", "an operator asking for debug gets all of it");
let (operator, built) = super::startup_parts(Some("warn"), false);
assert_eq!((operator.as_str(), built.as_str()), ("warn", "warn,boringtun::noise::timers=off,rustls::msgs::handshake=error"));
}
fn said_under(spec: &str, emit: impl FnOnce()) -> usize {
use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
use tracing_subscriber::layer::{Context, Layer};
struct Count(Arc<AtomicUsize>);
impl<S: tracing::Subscriber> Layer<S> for Count {
fn on_event(&self, _: &tracing::Event<'_>, _: Context<'_, S>) {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
let n = Arc::new(AtomicUsize::new(0));
let sub = Registry::default().with(EnvFilter::new(spec)).with(Count(Arc::clone(&n)));
tracing::subscriber::with_default(sub, emit);
n.load(Ordering::Relaxed)
}
#[test]
fn a_peers_illegal_sni_is_said_at_the_debug_rung_only() {
let sni = || tracing::warn!(target: "rustls::msgs::handshake", "Illegal SNI extension: ignoring IP address presented as hostname (39352e3131312e3230392e313837)");
let (operator, rung0) = super::startup_parts(None, false);
assert_eq!(said_under(&rung0, sni), 0, "rung 0 ({rung0}) says the peer's SNI");
assert_eq!(said_under(&super::built(1, &operator), sni), 1, "the debug rung hides it");
let named = super::startup_filter(Some("info,rustls=debug"), false);
assert_eq!(said_under(&named, sni), 1, "RUST_LOG named rustls ({named}) and still lost it");
let (_, warn0) = super::startup_parts(Some("warn"), false);
assert_eq!(said_under(&warn0, sni), 0, "rung 0 under RUST_LOG=warn ({warn0}) says it");
assert_eq!(said_under(&rung0, || tracing::warn!(target: "gunnar::twin", "ours")), 1, "our WARN went quiet");
assert_eq!(said_under(&rung0, || tracing::warn!(target: "rustls::common_state", "TLS alert warning received")), 1, "rustls's other WARNs went quiet");
}
}