use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use log::{Level, LevelFilter, Log, Metadata, Record};
#[derive(Debug, thiserror::Error)]
pub enum LogError {
#[error("cannot open log file {path}")]
OpenFile {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("cannot install logger")]
InstallLogger {
#[source]
source: log::SetLoggerError,
},
}
struct FileLogger {
file: Option<Mutex<File>>,
}
impl Log for FileLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= Level::Debug
}
fn log(&self, record: &Record) {
let Some(file) = &self.file else {
return;
};
if let Ok(mut handle) = file.lock() {
let _ = writeln!(
handle,
"[{:<5}] {}: {}",
record.level(),
record.target(),
record.args()
);
}
}
fn flush(&self) {
if let Some(file) = &self.file
&& let Ok(mut handle) = file.lock()
{
let _ = handle.flush();
}
}
}
pub fn init(level: LevelFilter, file: Option<&Path>) -> Result<(), LogError> {
let sink = match file {
Some(path) => Some(Mutex::new(open_append(path)?)),
None => None,
};
let logger = FileLogger { file: sink };
log::set_boxed_logger(Box::new(logger))
.map_err(|source| LogError::InstallLogger { source })?;
log::set_max_level(level);
Ok(())
}
fn open_append(path: &Path) -> Result<File, LogError> {
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
OpenOptions::new()
.create(true)
.append(true)
.open(path)
.map_err(|source| LogError::OpenFile {
path: path.to_path_buf(),
source,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_unopenable_log_file_is_reported_with_its_path() {
let blocker = std::env::temp_dir()
.join(format!("calcli-log-test-{}", std::process::id()));
std::fs::write(&blocker, b"not a directory")
.expect("the temp directory is writable");
let path = blocker.join("nested").join("calcli.log");
let error = init(LevelFilter::Info, Some(&path))
.expect_err("a file cannot act as a parent directory");
assert!(matches!(error, LogError::OpenFile { .. }));
assert!(error.to_string().contains("calcli.log"));
let _ = std::fs::remove_file(&blocker);
}
#[test]
fn a_logger_without_a_file_swallows_records() {
let logger = FileLogger { file: None };
logger.log(
&Record::builder()
.args(format_args!("dropped"))
.level(Level::Info)
.build(),
);
logger.flush();
}
}