use std::fs::{File, OpenOptions};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Mutex;
use crate::{errors::LoggerError, time::LocalTimer};
use tracing::Level;
use tracing_subscriber::{
EnvFilter, Layer, Registry,
filter::LevelFilter,
fmt::{MakeWriter, format::FmtSpan},
layer::SubscriberExt,
util::SubscriberInitExt,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LogFormat {
#[default]
Text,
Json,
}
impl LogFormat {
fn as_str(self) -> &'static str {
match self {
Self::Text => "text",
Self::Json => "json",
}
}
}
impl std::fmt::Display for LogFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for LogFormat {
type Err = LoggerError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"text" => Ok(Self::Text),
"json" => Ok(Self::Json),
other => Err(LoggerError::InvalidFormat(other.to_string())),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum Output {
#[default]
Stdout,
File(PathBuf),
Both(PathBuf),
}
pub struct Logger {
level: Level,
format: LogFormat,
env_filter: Option<String>,
with_file: bool,
with_target: bool,
output: Output,
}
impl Logger {
#[must_use]
pub fn new() -> Self {
Self {
level: Level::INFO,
format: LogFormat::Text,
env_filter: None,
with_file: false,
with_target: true,
output: Output::Stdout,
}
}
#[must_use]
pub fn with_level(mut self, level: Level) -> Self {
self.level = level;
self
}
#[must_use]
pub fn with_format(mut self, format: LogFormat) -> Self {
self.format = format;
self
}
#[must_use]
pub fn with_env_filter(mut self, filter: impl Into<String>) -> Self {
self.env_filter = Some(filter.into());
self
}
#[must_use]
pub fn with_file(mut self, enabled: bool) -> Self {
self.with_file = enabled;
self
}
#[must_use]
pub fn with_target(mut self, enabled: bool) -> Self {
self.with_target = enabled;
self
}
#[must_use]
pub fn with_output(mut self, output: Output) -> Self {
self.output = output;
self
}
#[must_use]
pub fn level(&self) -> Level {
self.level
}
#[must_use]
pub fn format(&self) -> LogFormat {
self.format
}
pub fn init(self) -> Result<(), LoggerError> {
let filter = EnvFilter::builder()
.with_default_directive(LevelFilter::from_level(self.level).into())
.parse(self.env_filter.as_deref().unwrap_or(""))
.map_err(|e| LoggerError::InvalidEnvFilter(e.to_string()))?;
let (to_stdout, file_path) = match &self.output {
Output::Stdout => (true, None),
Output::File(path) => (false, Some(path.clone())),
Output::Both(path) => (true, Some(path.clone())),
};
let mut layers: Vec<Box<dyn Layer<Registry> + Send + Sync>> = Vec::new();
if to_stdout {
layers.push(self.fmt_layer(std::io::stdout, true));
}
if let Some(path) = file_path {
let file = open_log_file(&path)?;
layers.push(self.fmt_layer(Mutex::new(file), false));
}
tracing_subscriber::registry()
.with(layers)
.with(filter)
.try_init()
.map_err(|e| LoggerError::TryInitError(e.to_string()))
}
fn fmt_layer<W>(&self, writer: W, ansi: bool) -> Box<dyn Layer<Registry> + Send + Sync>
where
W: for<'writer> MakeWriter<'writer> + Send + Sync + 'static,
{
let layer = tracing_subscriber::fmt::layer()
.with_writer(writer)
.with_ansi(ansi)
.with_thread_names(false)
.with_span_events(FmtSpan::NONE)
.with_file(self.with_file)
.with_target(self.with_target)
.with_timer(LocalTimer);
match self.format {
LogFormat::Json => layer.json().boxed(),
LogFormat::Text => layer.boxed(),
}
}
}
fn open_log_file(path: &Path) -> Result<File, LoggerError> {
OpenOptions::new()
.create(true)
.append(true)
.open(path)
.map_err(|e| LoggerError::OpenLogFile {
path: path.display().to_string(),
message: e.to_string(),
})
}
impl Default for Logger {
fn default() -> Self {
Self::new()
}
}