use tracing_subscriber::{Registry, filter::LevelFilter, prelude::*, util::SubscriberInitExt};
use crate::{
LogLevel,
error::{BuildError, InitError},
layer::BackendLayer,
sink::FormatterConfig,
sinks::console::ConsoleSink,
};
#[cfg(feature = "file")]
use crate::sinks::file::{FileSink, validate_file_config};
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
pub struct Logger {
max_level: LogLevel,
ansi: bool,
target: bool,
timestamp: bool,
#[cfg(feature = "file")]
file: Option<FileConfig>,
#[cfg(feature = "log")]
log_bridge: bool,
}
impl Default for Logger {
fn default() -> Self {
Self {
max_level: LogLevel::Info,
ansi: true,
target: true,
timestamp: true,
#[cfg(feature = "file")]
file: None,
#[cfg(feature = "log")]
log_bridge: false,
}
}
}
impl Logger {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_max_level(mut self, level: LogLevel) -> Self {
self.max_level = level;
self
}
#[must_use]
pub fn with_ansi(mut self, enabled: bool) -> Self {
self.ansi = enabled;
self
}
#[must_use]
pub fn with_target(mut self, enabled: bool) -> Self {
self.target = enabled;
self
}
#[must_use]
pub fn with_timestamp(mut self, enabled: bool) -> Self {
self.timestamp = enabled;
self
}
#[cfg(feature = "file")]
#[cfg_attr(docsrs, doc(cfg(feature = "file")))]
#[must_use]
pub fn with_file(mut self, config: FileConfig) -> Self {
self.file = Some(config);
self
}
#[cfg(feature = "log")]
#[cfg_attr(docsrs, doc(cfg(feature = "log")))]
#[must_use]
pub fn with_log_bridge(mut self, enabled: bool) -> Self {
self.log_bridge = enabled;
self
}
pub fn build(self) -> Result<BackendLayer, BuildError> {
let formatter = FormatterConfig {
ansi: self.ansi,
target: self.target,
timestamp: self.timestamp,
};
#[allow(unused_mut)]
let mut sinks = vec![Box::new(ConsoleSink) as _];
#[cfg(feature = "file")]
if let Some(file) = self.file {
validate_file_config(&file)?;
sinks.push(Box::new(FileSink::new(&file)?) as _);
}
Ok(BackendLayer::new(formatter, sinks))
}
pub fn init(self) -> Result<(), InitError> {
let max_level = self.max_level;
#[cfg(feature = "log")]
let log_bridge = self.log_bridge;
let layer = self.build()?;
#[cfg(feature = "log")]
if log_bridge {
tracing_log::LogTracer::init().map_err(|_| InitError::LogBridgeAlreadyInitialized)?;
}
Registry::default()
.with(LevelFilter::from(max_level))
.with(layer)
.try_init()
.map_err(|_| InitError::AlreadyInitialized)
}
}
#[cfg(feature = "file")]
#[cfg_attr(docsrs, doc(cfg(feature = "file")))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileConfig {
pub(crate) directory: std::path::PathBuf,
pub(crate) latest_file_name: String,
pub(crate) session_file_prefix: Option<String>,
}
#[cfg(feature = "file")]
impl FileConfig {
#[must_use]
pub fn new(directory: impl Into<std::path::PathBuf>) -> Self {
Self {
directory: directory.into(),
latest_file_name: String::from("latest.log"),
session_file_prefix: None,
}
}
#[must_use]
pub fn with_latest_file_name(mut self, file_name: impl Into<String>) -> Self {
self.latest_file_name = file_name.into();
self
}
#[must_use]
pub fn with_session_file_prefix(mut self, prefix: impl Into<String>) -> Self {
self.session_file_prefix = Some(prefix.into());
self
}
#[must_use]
pub fn directory(&self) -> &std::path::Path {
&self.directory
}
#[must_use]
pub fn latest_file_name(&self) -> &str {
&self.latest_file_name
}
#[must_use]
pub fn session_file_prefix(&self) -> Option<&str> {
self.session_file_prefix.as_deref()
}
}
#[cfg(test)]
mod tests {
use tracing_subscriber::filter::LevelFilter;
use super::Logger;
#[test]
fn defaults_match_public_contract() {
let logger = Logger::new();
assert_eq!(logger.max_level, LevelFilter::INFO);
assert!(logger.ansi);
assert!(logger.target);
assert!(logger.timestamp);
#[cfg(feature = "log")]
assert!(!logger.log_bridge);
#[cfg(feature = "file")]
assert!(logger.file.is_none());
}
#[test]
fn max_level_is_stored() {
let logger = Logger::new().with_max_level(LevelFilter::DEBUG);
assert_eq!(logger.max_level, LevelFilter::DEBUG);
}
}