#[cfg(any(
feature = "console",
feature = "formatted_console",
feature = "file_channels"
))]
use crate::helpers::get_env_log_level;
#[cfg(feature = "console")]
use channels::console::ConsoleLogger;
#[cfg(feature = "daily_file")]
use channels::daily_file::DailyFileLogger;
#[cfg(feature = "formatted_console")]
use channels::formatted_console::FormattedConsoleLogger;
#[cfg(feature = "single_file")]
use channels::single_file::SingleFileLogger;
#[cfg(feature = "timezone")]
pub use chrono_tz::Tz;
use error::FtailError;
use log::{Level, LevelFilter, Log};
#[cfg(feature = "file_channels")]
use std::path::Path;
pub mod ansi_escape;
pub mod channels;
pub mod error;
mod formatters;
mod helpers;
#[cfg(test)]
mod tests;
mod writer;
pub struct Ftail {
channels: Vec<LogChannel>,
initialized_channels: Vec<InitializedLogChannel>,
config: Config,
}
unsafe impl Send for Ftail {}
unsafe impl Sync for Ftail {}
pub(crate) struct LogChannel {
constructor: Box<dyn Fn(Config) -> Box<dyn Log + Send + Sync>>,
level: log::LevelFilter,
}
pub(crate) struct InitializedLogChannel {
channel: Box<dyn Log + Send + Sync>,
}
#[derive(Clone)]
pub struct Config {
pub level_filter: LevelFilter,
pub datetime_format: String,
#[cfg(feature = "timezone")]
pub timezone: chrono_tz::Tz,
pub max_file_size: Option<u64>,
pub retention_days: Option<u64>,
pub levels: Option<Vec<Level>>,
pub targets: Option<Vec<String>>,
}
impl Ftail {
pub fn new() -> Self {
Self {
channels: Vec::new(),
initialized_channels: Vec::new(),
config: Config::new(),
}
}
#[cfg(feature = "timezone")]
pub fn timezone(mut self, timezone: chrono_tz::Tz) -> Self {
self.config.timezone = timezone;
self
}
pub fn datetime_format(mut self, datetime_format: &str) -> Self {
self.config.datetime_format = datetime_format.to_string();
self
}
pub fn max_file_size(mut self, max_file_size_in_mb: u64) -> Self {
self.config.max_file_size = Some(max_file_size_in_mb * 1024 * 1024);
self
}
pub fn retention_days(mut self, retention_days: u64) -> Self {
self.config.retention_days = Some(retention_days);
self
}
pub fn filter_levels(mut self, levels: Vec<Level>) -> Self {
self.config.levels = Some(levels);
self
}
pub fn filter_targets(mut self, targets: Vec<&str>) -> Self {
self.config.targets = Some(targets.iter().map(|s| s.to_string()).collect());
self
}
fn add_channel<F>(mut self, constructor: F, level: log::LevelFilter) -> Self
where
F: Fn(Config) -> Box<dyn Log + Send + Sync> + 'static,
{
self.channels.push(LogChannel::new(constructor, level));
self
}
#[cfg(feature = "console")]
pub fn console(self, level: log::LevelFilter) -> Self {
let constructor =
|config: Config| Box::new(ConsoleLogger::new(config)) as Box<dyn Log + Send + Sync>;
self.add_channel(constructor, level)
}
#[cfg(feature = "console")]
pub fn console_env_level(self) -> Self {
self.console(get_env_log_level())
}
#[cfg(feature = "formatted_console")]
pub fn formatted_console(self, level: log::LevelFilter) -> Self {
let constructor = |config: Config| {
Box::new(FormattedConsoleLogger::new(config)) as Box<dyn Log + Send + Sync>
};
self.add_channel(constructor, level)
}
#[cfg(feature = "formatted_console")]
pub fn formatted_console_env_level(self) -> Self {
self.formatted_console(get_env_log_level())
}
#[cfg(feature = "single_file")]
pub fn single_file(self, path: &Path, append: bool, level: log::LevelFilter) -> Self {
let path = path.to_owned();
let constructor = move |config: Config| {
Box::new(SingleFileLogger::new(&path, append, config).unwrap())
as Box<dyn Log + Send + Sync>
};
self.add_channel(constructor, level)
}
#[cfg(feature = "single_file")]
pub fn single_file_env_level(self, path: &Path, append: bool) -> Self {
self.single_file(path, append, get_env_log_level())
}
#[cfg(feature = "daily_file")]
pub fn daily_file(self, path: &Path, level: log::LevelFilter) -> Self {
let path = path.to_owned();
let constructor = move |config: Config| {
Box::new(DailyFileLogger::new(&path, config).unwrap()) as Box<dyn Log + Send + Sync>
};
self.add_channel(constructor, level)
}
#[cfg(feature = "daily_file")]
pub fn daily_file_env_level(self, path: &Path) -> Self {
self.daily_file(path, get_env_log_level())
}
pub fn custom<F>(self, constructor: F, level: log::LevelFilter) -> Self
where
F: Fn(Config) -> Box<dyn Log + Send + Sync> + 'static,
{
self.add_channel(constructor, level)
}
pub fn init(mut self) -> Result<(), FtailError> {
if self.channels.is_empty() {
return Err(FtailError::NoChannelsError);
}
let channels = std::mem::take(&mut self.channels);
self.initialized_channels = channels
.into_iter()
.map(|channel| {
let mut config = self.config.clone();
config.level_filter = channel.level;
channel.init(config)
})
.collect();
log::set_max_level(log::LevelFilter::Trace);
log::set_boxed_logger(Box::new(self)).map_err(FtailError::SetLoggerError)
}
}
impl LogChannel {
fn new<F>(constructor: F, level: log::LevelFilter) -> Self
where
F: Fn(Config) -> Box<dyn Log + Send + Sync> + 'static,
{
Self {
constructor: Box::new(constructor),
level,
}
}
fn init(self, config: Config) -> InitializedLogChannel {
InitializedLogChannel {
channel: (self.constructor)(config),
}
}
}
impl Log for Ftail {
fn enabled(&self, metadata: &log::Metadata) -> bool {
if self.config.levels.is_some()
&& !self
.config
.levels
.as_ref()
.unwrap()
.contains(&metadata.level())
{
return false;
}
if self.config.targets.is_some()
&& !self
.config
.targets
.as_ref()
.unwrap()
.contains(&metadata.target().to_string())
{
return false;
}
true
}
fn log(&self, record: &log::Record) {
if !self.enabled(record.metadata()) {
return;
}
for channel in &self.initialized_channels {
channel.channel.log(record);
}
}
fn flush(&self) {
for channel in &self.initialized_channels {
channel.channel.flush();
}
}
}
impl Default for Ftail {
fn default() -> Self {
Self::new()
}
}