use std::borrow::Cow;
use std::fs;
use std::io::prelude::*;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use log::Log;
use thiserror::Error;
use powerpack_env as env;
pub use log::LevelFilter;
#[derive(Debug, Error)]
pub enum Error {
#[error("home directory not found")]
NoHomeDir,
#[error("io error")]
Io(#[from] std::io::Error),
#[error("failed to set logger")]
SetLogger(#[from] log::SetLoggerError),
}
#[derive(Debug, Clone)]
pub struct Logger {
file: Arc<Mutex<fs::File>>,
max_level: LevelFilter,
}
#[derive(Debug, Clone)]
pub struct Builder {
directory: Option<PathBuf>,
filename: Option<Cow<'static, str>>,
max_level: Option<LevelFilter>,
}
impl Default for Builder {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl Builder {
#[inline]
pub fn new() -> Self {
Self {
directory: None,
filename: None,
max_level: None,
}
}
#[inline]
pub fn directory(mut self, dir: impl Into<PathBuf>) -> Self {
self.directory = Some(dir.into());
self
}
#[inline]
pub fn filename(mut self, filename: impl Into<Cow<'static, str>>) -> Self {
self.filename = Some(filename.into());
self
}
#[inline]
pub fn max_level(mut self, max_level: LevelFilter) -> Self {
self.max_level = Some(max_level);
self
}
fn build(self) -> Result<Logger, Error> {
let directory = match self.directory {
Some(directory) => directory,
None => env::try_workflow_cache_or_default().ok_or(Error::NoHomeDir)?,
};
let filename = self.filename.as_deref().unwrap_or("powerpack.log");
let path = directory.join(filename);
fs::create_dir_all(directory)?;
let file = Arc::new(Mutex::new(
fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)?,
));
let max_level = self.max_level.unwrap_or_else(|| {
if env::is_debug() {
LevelFilter::Debug
} else {
LevelFilter::Info
}
});
Ok(Logger { file, max_level })
}
#[inline]
pub fn try_init(self) -> Result<(), Error> {
let logger = self.build()?;
let max_level = logger.max_level;
log::set_boxed_logger(Box::new(logger))?;
log::set_max_level(max_level);
Ok(())
}
#[track_caller]
#[inline]
pub fn init(self) {
self.try_init().expect("failed to initialize logger");
}
#[track_caller]
#[inline]
pub fn init_idempotent(self) {
match self.try_init() {
Ok(()) => {}
Err(Error::SetLogger(_)) => {}
r => r.expect("failed to initialize logger"),
}
}
}
impl Logger {
fn try_log(&self, record: &log::Record) -> Result<(), Box<dyn std::error::Error + '_>> {
let time = jiff::Timestamp::now().strftime("%Y-%m-%dT%H:%M:%S");
let mut f = self.file.lock()?;
writeln!(f, "[{}] [{}] {}", time, record.level(), record.args())?;
f.flush()?;
Ok(())
}
fn try_flush(&self) -> Result<(), Box<dyn std::error::Error + '_>> {
let mut f = self.file.lock()?;
f.flush()?;
Ok(())
}
}
impl Log for Logger {
fn enabled(&self, metadata: &log::Metadata) -> bool {
metadata.level() <= self.max_level
}
fn log(&self, record: &log::Record) {
if self.enabled(record.metadata()) {
self.try_log(record).expect("failed to log message");
}
}
fn flush(&self) {
self.try_flush().expect("failed to flush log file");
}
}