use anyhow::{Context as _, Result};
use rolling_file::{BasicRollingFileAppender, RollingConditionBasic};
use std::path::Path;
use tracing::debug;
pub use tracing_appender::non_blocking::WorkerGuard;
use tracing_log::LogTracer;
use tracing_subscriber::fmt;
use tracing_subscriber::fmt::time::FormatTime;
use tracing_subscriber::prelude::*;
pub fn trace_filter(level: &str) -> String {
format!(
"{},hyper=error,tokio_postgres=info,pingora_core=error,pingora_core::services::listening=off,pingora_core::apps=off,pingora_proxy=off,pingora_pool=error,tungstenite=info",
level
)
}
pub fn log_max_level(level: &str) -> log::LevelFilter {
match level.trim().to_ascii_lowercase().as_str() {
"off" => log::LevelFilter::Off,
"error" => log::LevelFilter::Error,
"warn" => log::LevelFilter::Warn,
"debug" => log::LevelFilter::Debug,
"trace" => log::LevelFilter::Trace,
_ => log::LevelFilter::Info,
}
}
pub enum LogZone {
Utc,
Local,
}
fn log_timer(zone: LogZone) -> Result<impl FormatTime + Clone> {
let format = time::format_description::parse_borrowed::<2>(
"[year]-[month padding:zero]-[day padding:zero] [hour]:[minute]:[second]",
)
.context("Failed to parse time format")?;
let offset = match zone {
LogZone::Utc => time::UtcOffset::UTC,
LogZone::Local => time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC),
};
Ok(fmt::time::OffsetTime::new(offset, format))
}
pub fn init_log(
level: &str,
log_file: &Path,
stderr: bool,
zone: LogZone,
max_size: usize,
max_files: usize,
) -> Result<WorkerGuard> {
let file_appender = BasicRollingFileAppender::new(
log_file,
RollingConditionBasic::new().max_size(max_size as u64), max_files,
)
.context("Failed to create rolling file appender")?;
let (file_writer, guard) = tracing_appender::non_blocking(file_appender);
let trace_cfg = format!(
"{},{}",
trace_filter(level),
std::env::var("RUST_LOG").unwrap_or_default()
);
let timer = log_timer(zone).context("Failed to build log timer")?;
let file_layer = fmt::Layer::default()
.with_timer(timer.clone())
.with_ansi(false)
.with_writer(file_writer);
let registry = tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new(trace_cfg))
.with(file_layer);
if stderr {
let stderr_layer = fmt::Layer::default()
.with_timer(timer)
.with_ansi(cfg!(unix))
.with_writer(std::io::stderr);
tracing::subscriber::set_global_default(registry.with(stderr_layer))
.context("Failed to set global default subscriber")?;
} else {
tracing::subscriber::set_global_default(registry)
.context("Failed to set global default subscriber")?;
}
LogTracer::builder()
.with_max_level(log_max_level(level))
.init()
.context("Failed to initialize log tracer")?;
log_panics::init();
debug!("Tracing initialized ({})", level);
Ok(guard)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io;
use std::sync::{Arc, Mutex};
use tracing::subscriber::with_default;
use tracing_subscriber::fmt::MakeWriter;
use tracing_subscriber::layer::{Context, Layer};
use tracing_subscriber::registry::LookupSpan;
#[derive(Clone, Default)]
struct Capture(Arc<Mutex<Vec<String>>>);
impl<S: tracing::Subscriber + for<'a> LookupSpan<'a>> Layer<S> for Capture {
fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
self.0
.lock()
.unwrap()
.push(event.metadata().target().to_string());
}
}
#[test]
fn trace_filter_suppresses_scanner_noise() {
let capture = Capture::default();
let filter = tracing_subscriber::EnvFilter::new(trace_filter("error"));
let subscriber = tracing_subscriber::registry()
.with(filter)
.with(capture.clone());
with_default(subscriber, || {
tracing::error!(
target: "pingora_core::services::listening",
"Downstream handshake error: TLS accept() failed"
);
tracing::error!(
target: "pingora_core::apps::http_app",
"HTTP server fails to read from downstream: InvalidHTTPHeader"
);
tracing::warn!(
target: "pingora_core::connectors",
"Unexpected data read in idle connection"
);
tracing::warn!(
target: "pingora_core::protocols::http::v1::server",
"Respond header is already sent, cannot send again"
);
tracing::error!(target: "server::control", "real error");
});
let targets = capture.0.lock().unwrap();
assert!(
!targets.iter().any(|t| t.starts_with("pingora_core")),
"pingora scanner noise must be filtered out, got: {:?}",
targets
);
assert!(
targets.iter().any(|t| t == "server::control"),
"the server's own errors must not be filtered out, got: {:?}",
targets
);
}
#[derive(Clone, Default)]
struct LineSink(Arc<Mutex<Vec<u8>>>);
impl<'a> MakeWriter<'a> for LineSink {
type Writer = LineWriter;
fn make_writer(&'a self) -> LineWriter {
LineWriter(self.0.clone())
}
}
struct LineWriter(Arc<Mutex<Vec<u8>>>);
impl io::Write for LineWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
fn stamp_as_utc(line: &str) -> time::OffsetDateTime {
let stamp = &line[..19]; let fields: Vec<&str> = stamp.split(['-', ' ', ':']).collect();
assert_eq!(fields.len(), 6, "malformed stamp {stamp:?} in {line:?}");
let number = |i: usize| {
fields[i]
.parse::<u32>()
.unwrap_or_else(|e| panic!("field {i} of {stamp:?}: {e}"))
};
let month = u8::try_from(number(1)).unwrap();
time::Date::from_calendar_date(
number(0) as i32,
time::Month::try_from(month).unwrap(),
u8::try_from(number(2)).unwrap(),
)
.unwrap()
.with_hms(
u8::try_from(number(3)).unwrap(),
u8::try_from(number(4)).unwrap(),
u8::try_from(number(5)).unwrap(),
)
.unwrap()
.assume_utc()
}
#[test]
fn log_timestamp_is_rendered_in_utc() {
let sink = LineSink::default();
let layer = fmt::Layer::default()
.with_timer(log_timer(LogZone::Utc).expect("failed to build log timer"))
.with_ansi(false)
.with_writer(sink.clone());
let subscriber = tracing_subscriber::registry().with(layer);
with_default(subscriber, || {
tracing::info!("timestamp zone probe");
});
let line = String::from_utf8(std::mem::take(&mut *sink.0.lock().unwrap()))
.expect("log line must be valid UTF-8");
let logged = stamp_as_utc(&line);
let skew =
(logged.unix_timestamp() - time::OffsetDateTime::now_utc().unix_timestamp()).abs();
assert!(
skew <= 120,
"log stamp {logged} is {skew}s away from UTC now - \
stamps must render UTC, got line: {line:?}"
);
}
#[test]
fn log_max_level_follows_configured_level() {
use log::LevelFilter;
assert_eq!(log_max_level("error"), LevelFilter::Error);
assert_eq!(log_max_level("warn"), LevelFilter::Warn);
assert_eq!(log_max_level("info"), LevelFilter::Info);
assert_eq!(log_max_level("debug"), LevelFilter::Debug);
assert_eq!(log_max_level("trace"), LevelFilter::Trace);
assert_eq!(log_max_level("off"), LevelFilter::Off);
assert_eq!(log_max_level("INFO"), LevelFilter::Info);
assert_eq!(log_max_level("Trace"), LevelFilter::Trace);
assert_eq!(log_max_level(" debug "), LevelFilter::Debug);
assert_eq!(log_max_level("bogus"), LevelFilter::Info);
assert_eq!(log_max_level(""), LevelFilter::Info);
}
}