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"
);
}
}