dyn-logger 1.0.0

Crate to configure tracing logger dynamically via configuration file in toml format.
Documentation
use error::DynLogAPIErr;
use serde::Deserialize;
use std::{
    cell::RefCell,
    fmt::Debug,
    fs::{create_dir_all, read_to_string},
    path::Path,
    str::FromStr,
};
use tracing::metadata::LevelFilter;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::{EnvFilter, Layer, Registry, filter::Targets, fmt, prelude::*};

pub mod error;
mod logger;
mod utils;

use logger::{FileLogger, GlobalLogger, LogFormat, StreamLogger};

#[derive(Debug, Deserialize)]
struct LogConfig {
    global: GlobalLogger,
    stream_logger: StreamLogger,
    file_logger: Option<Vec<FileLogger>>,
}

pub struct DynamicLogger {
    config: LogConfig,
    layers: RefCell<Vec<Box<dyn Layer<Registry> + Send + Sync>>>,
    guards: RefCell<Vec<WorkerGuard>>,
}

impl DynamicLogger {
    pub fn new(path: impl AsRef<Path>) -> Result<Self, DynLogAPIErr> {
        let config = toml::from_str(&read_to_string(path.as_ref()).map_err(|source| {
            DynLogAPIErr::FileReadError {
                filename: path.as_ref().to_string_lossy().to_string(),
                source,
            }
        })?)?;
        Ok(Self {
            config,
            layers: RefCell::new(Vec::new()),
            guards: RefCell::new(Vec::new()),
        })
    }

    pub fn with_file_logger(self) -> Result<Self, DynLogAPIErr> {
        if self.config.global.options.enabled {
            self.init_filelogger()?;
        }

        Ok(self)
    }

    pub fn with_stdout(self) -> Result<Self, DynLogAPIErr> {
        if self.config.global.options.enabled {
            self.init_stdout()?;
        }
        Ok(self)
    }

    pub fn init(&self) -> Result<(), DynLogAPIErr> {
        let global = &self.config.global;
        if global.options.enabled {
            let stream_targets = Targets::from_str(&self.config.stream_logger.modules.join(","))
                .map(|targets| {
                    targets
                        .into_iter()
                        .map(|(filter, _)| (filter, LevelFilter::OFF))
                        .collect::<Targets>()
                })?;

            let layer = fmt::layer()
                .with_file(global.options.file)
                .with_line_number(global.options.line_number)
                .with_thread_names(global.options.thread_name)
                .with_thread_ids(global.options.thread_id);

            let env_layer = match global.options.format {
                LogFormat::Full => layer.boxed(),
                LogFormat::Compact => layer.compact().boxed(),
                LogFormat::Pretty => layer.pretty().boxed(),
                LogFormat::Json => layer.json().boxed(),
            };

            let mut ref_layers = self.layers.borrow_mut();
            if stream_targets.iter().count() > 0 {
                ref_layers.push(
                    env_layer
                        .with_filter(
                            stream_targets.with_default(LevelFilter::from_level(global.log_level)),
                        )
                        .boxed(),
                );
            } else {
                let envfilter =
                    EnvFilter::from_default_env().add_directive(global.log_level.into());
                ref_layers.push(env_layer.with_filter(envfilter).boxed());
            }
        }

        tracing_subscriber::registry()
            .with(self.layers.take())
            .init();

        Ok(())
    }

    fn register_filelogger_target(&self, entry: &FileLogger) -> Result<(), DynLogAPIErr> {
        let log_dir = &entry.path;
        create_dir_all(log_dir).map_err(|source| DynLogAPIErr::CreateLogDirError {
            path: log_dir.to_string_lossy().to_string(),
            source,
        })?;
        let appender = tracing_appender::rolling::never(log_dir, &entry.filename);
        let (file_writer, guard) = tracing_appender::non_blocking(appender);
        self.guards.borrow_mut().push(guard);

        let options = &entry.options;
        let file_targets = Targets::from_str(&entry.modules.join(","))?;
        let file_layer = fmt::Layer::new()
            .with_writer(file_writer)
            .with_ansi(false)
            .with_file(options.file)
            .with_line_number(options.line_number)
            .with_thread_names(options.thread_name)
            .with_thread_ids(options.thread_id);
        let layer = match options.format {
            LogFormat::Full => file_layer.with_filter(file_targets).boxed(),
            LogFormat::Compact => file_layer.compact().with_filter(file_targets).boxed(),
            LogFormat::Pretty => file_layer.pretty().with_filter(file_targets).boxed(),
            LogFormat::Json => file_layer.json().with_filter(file_targets).boxed(),
        };

        self.layers.borrow_mut().push(layer);
        Ok(())
    }

    #[must_use]
    pub fn add_layer(self, layer: Box<dyn Layer<Registry> + Send + Sync>) -> Self {
        self.layers.borrow_mut().push(layer);
        self
    }

    #[must_use]
    pub fn add_layer_with_stream_logger_targets(
        self,
        layer: Box<dyn Layer<Registry> + Send + Sync>,
    ) -> Result<Self, DynLogAPIErr> {
        let target_layer = {
            let file_targets = Targets::from_str(&self.config.stream_logger.modules.join(","))?;
            if file_targets.iter().count() > 0 {
                layer.with_filter(file_targets).boxed()
            } else {
                layer
            }
        };

        self.layers.borrow_mut().push(target_layer);

        Ok(self)
    }

    #[must_use]
    pub fn add_layers<T>(self, layers: T) -> Self
    where
        T: IntoIterator<Item = Box<dyn Layer<Registry> + Send + Sync>>,
    {
        {
            let mut ref_layers = self.layers.borrow_mut();
            for layer in layers {
                ref_layers.push(layer);
            }
        }
        self
    }
}

pub trait DynamicLogging {
    type Error;
    fn init_stdout(&self) -> Result<(), Self::Error>;
    fn init_filelogger(&self) -> Result<(), Self::Error>;
}

impl DynamicLogging for DynamicLogger {
    type Error = DynLogAPIErr;
    fn init_stdout(&self) -> Result<(), Self::Error> {
        let stream_logger = &self.config.stream_logger;
        if !stream_logger.options.enabled {
            return Ok(());
        }

        let targets = Targets::from_str(&stream_logger.modules.join(",")).expect("");
        let stream_layer = fmt::Layer::new()
            .with_writer(std::io::stdout)
            .with_file(stream_logger.options.file)
            .with_line_number(stream_logger.options.line_number)
            .with_thread_names(stream_logger.options.thread_name)
            .with_thread_ids(stream_logger.options.thread_id);
        let layer = match stream_logger.options.format {
            LogFormat::Full => stream_layer
                .with_ansi(stream_logger.color)
                .with_filter(targets)
                .boxed(),
            LogFormat::Compact => stream_layer
                .with_ansi(stream_logger.color)
                .compact()
                .with_filter(targets)
                .boxed(),
            LogFormat::Pretty => stream_layer.pretty().with_filter(targets).boxed(),
            LogFormat::Json => stream_layer.json().with_filter(targets).boxed(),
        };

        self.layers.borrow_mut().push(layer);
        Ok(())
    }

    fn init_filelogger(&self) -> Result<(), Self::Error> {
        let file_logger_table = &self
            .config
            .file_logger
            .as_ref()
            .ok_or(DynLogAPIErr::InitializeFileloggerError)?;
        for entry in file_logger_table.iter().filter(|file| file.options.enabled) {
            self.register_filelogger_target(entry)?;
        }
        Ok(())
    }
}