use std::{fmt, fs, io, mem, process, slice};
use std::io::Write;
use std::ops::{Deref, DerefMut};
use std::path::PathBuf;
use std::sync::{Arc, OnceLock};
use bytes::Bytes;
use chrono::{DateTime, Utc};
use log::{LevelFilter, Record, error};
use crate::config::{Config, LogTarget};
use crate::error::Failed;
use crate::utils::date::{format_iso_date, format_local_iso_date};
use crate::utils::fmt::WriteOrPanic;
use crate::utils::sync::{Mutex, RwLock};
#[derive(Clone, Debug)]
pub struct LogMessage {
pub when: DateTime<Utc>,
pub level: log::Level,
pub content: String,
}
impl LogMessage {
fn from_record(record: &Record<'_>) -> Self {
Self {
when: Utc::now(),
level: record.level(),
content: record.args().to_string(),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct LogBook {
messages: Vec<LogMessage>,
}
impl LogBook {
pub fn is_empty(&self) -> bool {
self.messages.is_empty()
}
}
impl<'a> IntoIterator for &'a LogBook {
type Item = &'a LogMessage;
type IntoIter = slice::Iter<'a, LogMessage>;
fn into_iter(self) -> Self::IntoIter {
self.messages.iter()
}
}
#[derive(Clone, Debug)]
pub struct LogBookWriter {
book: LogBook,
process_prefix: Option<String>,
}
impl LogBookWriter {
pub fn new(process_prefix: Option<String>) -> Self {
Self {
book: Default::default(),
process_prefix,
}
}
pub fn append(&mut self, mut other: LogBookWriter) {
self.book.messages.append(&mut other.book.messages);
}
pub fn sort(&mut self) {
self.book.messages.sort_unstable_by_key(|message| message.when);
}
pub fn into_book(self) -> LogBook {
self.book
}
pub fn log(&mut self, level: log::Level, args: fmt::Arguments<'_>) {
self.log_record(
&log::Record::builder().level(level).args(args).build()
)
}
pub fn trace(&mut self, args: fmt::Arguments<'_>) {
self.log(log::Level::Trace, args);
}
pub fn debug(&mut self, args: fmt::Arguments<'_>) {
self.log(log::Level::Debug, args);
}
pub fn info(&mut self, args: fmt::Arguments<'_>) {
self.log(log::Level::Info, args);
}
pub fn warn(&mut self, args: fmt::Arguments<'_>) {
self.log(log::Level::Info, args);
}
pub fn error(&mut self, args: fmt::Arguments<'_>) {
self.log(log::Level::Error, args);
}
pub fn log_record(&mut self, record: &Record<'_>) {
let logger = log::logger();
if !logger.enabled(record.metadata()) {
return
}
self.book.messages.push(LogMessage::from_record(record));
if let Some(prefix) = self.process_prefix.as_ref() {
logger.log(
&log::Record::builder()
.args(format_args!("{}{}", prefix, record.args()))
.metadata(record.metadata().clone())
.module_path(record.module_path())
.file(record.file())
.line(record.line())
.build()
);
}
}
}
pub struct Logger {
target: Mutex<LogBackend>,
output: Option<Arc<Mutex<String>>>,
log_level: log::LevelFilter,
}
enum LogBackend {
#[cfg(unix)]
Syslog(SyslogLogger),
File {
file: fs::File,
path: PathBuf,
},
Stderr {
stderr: io::Stderr,
timestamp: bool,
}
}
impl Logger {
pub fn init() -> Result<(), Failed> {
log::set_max_level(LevelFilter::Warn);
if let Err(err) = log::set_logger(&GLOBAL_LOGGER) {
eprintln!("Failed to initialize logger: {err}.\nAborting.");
return Err(Failed)
}
Ok(())
}
#[allow(unused_variables)] pub fn switch_logging(
config: &Config,
daemon: bool,
with_output: bool
) -> Result<Option<LogOutput>, Failed> {
let (output, res) = if with_output {
let output = LogOutput::new();
(Some(output.0), Some(output.1))
}
else {
(None, None)
};
let logger = Logger::new(config, daemon, output)?;
GLOBAL_LOGGER.switch(logger);
log::set_max_level(config.log_level);
Ok(res)
}
pub fn rotate_log() -> Result<(), Failed> {
GLOBAL_LOGGER.rotate()
}
fn new(
config: &Config, daemon: bool, output: Option<Arc<Mutex<String>>>
) -> Result<Self, Failed> {
let target = match config.log_target {
#[cfg(unix)]
LogTarget::Default(facility) => {
if daemon {
Self::new_syslog_target(facility)?
}
else {
Self::new_stderr_target(false)
}
}
#[cfg(unix)]
LogTarget::Syslog(facility) => {
Self::new_syslog_target(facility)?
}
LogTarget::File(ref path) => {
Self::new_file_target(path.clone())?
}
LogTarget::Stderr => {
Self::new_stderr_target(daemon)
}
};
Ok(Self {
target: Mutex::new(target),
output,
log_level: config.log_level,
})
}
#[cfg(unix)]
fn new_syslog_target(
facility: syslog::Facility
) -> Result<LogBackend, Failed> {
SyslogLogger::new(facility).map(LogBackend::Syslog)
}
fn new_file_target(path: PathBuf) -> Result<LogBackend, Failed> {
Ok(LogBackend::File {
file: match Self::open_log_file(&path) {
Ok(file) => file,
Err(err) => {
error!(
"Failed to open log file '{}': {}",
path.display(), err
);
return Err(Failed)
}
},
path
})
}
fn open_log_file(path: &PathBuf) -> Result<fs::File, io::Error> {
fs::OpenOptions::new().create(true).append(true).open(path)
}
fn new_stderr_target(timestamp: bool) -> LogBackend {
LogBackend::Stderr {
stderr: io::stderr(),
timestamp,
}
}
fn log(&self, record: &log::Record) {
if self.should_ignore(record) {
return;
}
if let Some(output) = self.output.as_ref() {
writeln!(output.lock(), "{}", record.args());
}
if let Err(err) = self.try_log(record) {
self.log_failure(err);
}
}
fn try_log(&self, record: &log::Record) -> Result<(), io::Error> {
match self.target.lock().deref_mut() {
#[cfg(unix)]
LogBackend::Syslog(ref mut logger) => logger.log(record),
LogBackend::File { ref mut file, .. } => {
writeln!(
file, "[{}] [{}] {}",
format_local_iso_date(chrono::Local::now()),
record.level(),
record.args()
)
}
LogBackend::Stderr{ ref mut stderr, timestamp } => {
if *timestamp {
let _ = write!(stderr, "[{}] ",
format_local_iso_date(chrono::Local::now()),
);
}
let _ = writeln!(
stderr, "[{}] {}", record.level(), record.args()
);
Ok(())
}
}
}
fn log_failure(&self, err: io::Error) -> ! {
match self.target.lock().deref() {
#[cfg(unix)]
LogBackend::Syslog(_) => {
eprintln!("Logging to syslog failed: {err}. Exiting.");
}
LogBackend::File { ref path, .. } => {
eprintln!(
"Logging to file {} failed: {}. Exiting.",
path.display(),
err
);
}
LogBackend::Stderr { .. } => {
}
}
process::exit(1)
}
fn flush(&self) {
match self.target.lock().deref_mut() {
#[cfg(unix)]
LogBackend::Syslog(ref mut logger) => logger.flush(),
LogBackend::File { ref mut file, .. } => {
let _ = file.flush();
}
LogBackend::Stderr { ref mut stderr, .. } => {
let _ = stderr.lock().flush();
}
}
}
fn should_ignore(&self, record: &log::Record) -> bool {
let module = match record.module_path() {
Some(module) => module,
None => return false,
};
if record.level() > log::Level::Error {
if module.starts_with("rustls") {
return true
}
}
if self.log_level >= log::LevelFilter::Trace {
return false
}
record.level() > log::Level::Info && (
module.starts_with("tokio_reactor")
|| module.starts_with("hyper")
|| module.starts_with("reqwest")
|| module.starts_with("h2")
)
}
fn rotate(&self) -> Result<(), Failed> {
if let LogBackend::File {
ref mut file, ref path
} = self.target.lock().deref_mut() {
*file = match Self::open_log_file(path) {
Ok(file) => file,
Err(err) => {
let _ = writeln!(file,
"Re-opening log file {} failed: {}. Exiting.",
path.display(), err
);
eprintln!(
"Re-opening log file {} failed: {}. Exiting.",
path.display(), err
);
return Err(Failed)
}
}
}
Ok(())
}
}
#[cfg(unix)]
struct SyslogLogger(
syslog::Logger<syslog::LoggerBackend, syslog::Formatter3164>
);
#[cfg(unix)]
impl SyslogLogger {
fn new(facility: syslog::Facility) -> Result<Self, Failed> {
let process = std::env::current_exe().ok().and_then(|path|
path.file_name()
.and_then(std::ffi::OsStr::to_str)
.map(ToString::to_string)
).unwrap_or_else(|| String::from("routinator"));
let formatter = syslog::Formatter3164 {
facility,
hostname: None,
process,
pid: std::process::id(),
};
let logger = syslog::unix(formatter.clone()).or_else(|_| {
syslog::tcp(formatter.clone(), ("127.0.0.1", 601))
}).or_else(|_| {
syslog::udp(formatter, ("127.0.0.1", 0), ("127.0.0.1", 514))
});
match logger {
Ok(logger) => Ok(Self(logger)),
Err(err) => {
error!("Cannot connect to syslog: {err}");
Err(Failed)
}
}
}
fn log(&mut self, record: &log::Record) -> Result<(), io::Error> {
match record.level() {
log::Level::Error => self.0.err(record.args()),
log::Level::Warn => self.0.warning(record.args()),
log::Level::Info => self.0.info(record.args()),
log::Level::Debug => self.0.debug(record.args()),
log::Level::Trace => {
self.0.debug(record.args())
}
}.map_err(|err| {
match err {
syslog::Error::Io(err) => err,
err => io::Error::other(err),
}
})
}
fn flush(&mut self) {
let _ = self.0.backend.flush();
}
}
struct GlobalLogger {
inner: OnceLock<Logger>,
}
static GLOBAL_LOGGER: GlobalLogger = GlobalLogger::new();
impl GlobalLogger {
const fn new() -> Self {
GlobalLogger { inner: OnceLock::new() }
}
fn switch(&self, logger: Logger) {
if self.inner.set(logger).is_err() {
panic!("Tried to switch logger more than once.")
}
}
fn rotate(&self) -> Result<(), Failed> {
match self.inner.get() {
Some(logger) => logger.rotate(),
None => Ok(()),
}
}
}
impl log::Log for GlobalLogger {
fn enabled(&self, _: &log::Metadata<'_>) -> bool {
true
}
fn log(&self, record: &log::Record<'_>) {
match self.inner.get() {
Some(logger) => logger.log(record),
None => {
let _ = writeln!(
io::stderr().lock(), "[{}] {}",
record.level(), record.args()
);
}
}
}
fn flush(&self) {
if let Some(logger) = self.inner.get() {
logger.flush()
}
}
}
#[derive(Debug)]
pub struct LogOutput {
queue: Arc<Mutex<String>>,
current: RwLock<Bytes>,
}
impl LogOutput {
fn new() -> (Arc<Mutex<String>>, Self) {
let queue = Arc::new(Mutex::new(String::new()));
let res = LogOutput {
queue: queue.clone(),
current: RwLock::new(
"Initial validation ongoing. Please wait.".into(),
)
};
(queue, res)
}
pub fn start(&self) {
let new_string = format!(
"Log from validation run started at {}\n\n",
format_iso_date(Utc::now())
);
let _ = mem::replace(self.queue.lock().deref_mut(), new_string);
}
pub fn flush(&self) {
let content = mem::take(self.queue.lock().deref_mut());
*self.current.write() = content.into();
}
pub fn get_output(&self) -> Bytes {
self.current.read().clone()
}
}