use std::fs::OpenOptions;
use std::io::{Error, Result, Write};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use chrono::Local;
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
impl LogLevel {
pub const ALL: [Self; 5] = [
Self::Error,
Self::Warn,
Self::Info,
Self::Debug,
Self::Trace,
];
pub const fn as_str(self) -> &'static str {
match self {
LogLevel::Trace => "TRACE",
LogLevel::Debug => "DEBUG",
LogLevel::Info => "INFO",
LogLevel::Warn => "WARN",
LogLevel::Error => "ERROR",
}
}
}
impl std::fmt::Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseLogLevelError {
invalid_value: String,
}
impl std::fmt::Display for ParseLogLevelError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"invalid log level '{}'. Expected one of: ERROR, WARN, INFO, DEBUG, TRACE",
self.invalid_value
)
}
}
impl std::error::Error for ParseLogLevelError {}
impl std::str::FromStr for LogLevel {
type Err = ParseLogLevelError;
fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
match value.trim().to_ascii_uppercase().as_str() {
"TRACE" | "5" => Ok(Self::Trace),
"DEBUG" | "4" => Ok(Self::Debug),
"INFO" | "3" => Ok(Self::Info),
"WARN" | "WARNING" | "2" => Ok(Self::Warn),
"ERROR" | "1" => Ok(Self::Error),
_ => Err(ParseLogLevelError {
invalid_value: value.to_owned(),
}),
}
}
}
#[derive(Debug)]
pub struct Logger {
path: PathBuf,
level: LogLevel,
}
impl Logger {
#[must_use]
pub fn new<P: Into<PathBuf>>(path: P) -> Self {
Self {
path: path.into(),
level: LogLevel::Info,
}
}
#[must_use]
pub fn with_level(mut self, level: LogLevel) -> Self {
self.level = level;
self
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn level(&self) -> LogLevel {
self.level
}
pub fn enabled(&self, level: LogLevel) -> bool {
level >= self.level
}
fn try_log_line(&self, msg_level: LogLevel, message: &str) -> Result<()> {
if !self.enabled(msg_level) {
return Ok(());
}
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)
.map_err(|e| {
Error::new(
e.kind(),
format!("Failed to open log file {}: {}", self.path.display(), e),
)
})?;
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S");
let log_entry = format!("[{}][{}] {}\n", timestamp, msg_level.as_str(), message);
file.write_all(log_entry.as_bytes()).map_err(|e| {
Error::new(
e.kind(),
format!("Failed to write to log file {}: {}", self.path.display(), e),
)
})?;
Ok(())
}
fn log_line(&self, msg_level: LogLevel, message: &str) {
if let Err(e) = self.try_log_line(msg_level, message) {
eprintln!("log-easy: {}", e);
}
}
pub fn trace(&self, msg: &str) {
self.log_line(LogLevel::Trace, msg);
}
pub fn debug(&self, msg: &str) {
self.log_line(LogLevel::Debug, msg);
}
pub fn info(&self, msg: &str) {
self.log_line(LogLevel::Info, msg);
}
pub fn warn(&self, msg: &str) {
self.log_line(LogLevel::Warn, msg);
}
pub fn error(&self, msg: &str) {
self.log_line(LogLevel::Error, msg);
}
pub fn try_trace(&self, msg: &str) -> Result<()> {
self.try_log_line(LogLevel::Trace, msg)
}
pub fn try_debug(&self, msg: &str) -> Result<()> {
self.try_log_line(LogLevel::Debug, msg)
}
pub fn try_info(&self, msg: &str) -> Result<()> {
self.try_log_line(LogLevel::Info, msg)
}
pub fn try_warn(&self, msg: &str) -> Result<()> {
self.try_log_line(LogLevel::Warn, msg)
}
pub fn try_error(&self, msg: &str) -> Result<()> {
self.try_log_line(LogLevel::Error, msg)
}
}
static GLOBAL_LOGGER: OnceLock<Logger> = OnceLock::new();
pub fn init<P: Into<PathBuf>>(path: P) -> Result<()> {
GLOBAL_LOGGER.set(Logger::new(path)).map_err(|_| {
Error::new(
std::io::ErrorKind::AlreadyExists,
"logger already initialized",
)
})?;
Ok(())
}
pub fn init_with(logger: Logger) -> Result<()> {
GLOBAL_LOGGER.set(logger).map_err(|_| {
Error::new(
std::io::ErrorKind::AlreadyExists,
"logger already initialized",
)
})?;
Ok(())
}
pub(crate) fn global() -> Option<&'static Logger> {
GLOBAL_LOGGER.get()
}