trace4rs 0.5.0

log4rs like usability with the `tracing` crate as the base
Documentation
#![allow(clippy::single_char_lifetime_names)]
use std::{
    borrow::Cow,
    collections::HashMap,
    convert::TryFrom,
    fs::{self,},
    io::{
        self,
        LineWriter,
        Write,
    },
    ops::Deref,
    path::{
        Path,
        PathBuf,
    },
    sync::Arc,
};

use parking_lot::Mutex;
use path_absolutize::Absolutize;
use tracing_subscriber::fmt::MakeWriter;

use crate::{
    config::{
        self,
        AppenderId,
        Policy,
    },
    env::expand_env_vars,
    error::{
        Error,
        Result,
    },
};

mod rolling;
use rolling::RollingFile;

#[cfg(test)]
mod test;

/// Shorthand for the Map of `AppenderId` to Appender.
type AppenderMap = HashMap<AppenderId, Appender>;

/// Appenders holds the global map of appenders which can be referenced by
/// Layers, it may be cheaply cloned.
#[derive(Clone)]
pub struct Appenders {
    appenders: Arc<AppenderMap>,
}

impl<'a> IntoIterator for &'a Appenders {
    type IntoIter = std::collections::hash_map::Values<'a, AppenderId, Appender>;
    type Item = &'a Appender;

    fn into_iter(self) -> Self::IntoIter {
        self.appenders.as_ref().values()
    }
}

impl Appenders {
    pub fn new(m: AppenderMap) -> Self {
        Self {
            appenders: Arc::new(m),
        }
    }

    pub fn correct_paths(&self) -> Result<()> {
        for a in self {
            a.correct_path()?;
        }
        Ok(())
    }

    pub fn flush(&self) -> Result<()> {
        for a in self {
            Appender::flush_io(a)?;
        }
        Ok(())
    }
}
impl Deref for Appenders {
    type Target = AppenderMap;

    fn deref(&self) -> &Self::Target {
        &*self.appenders
    }
}
impl TryFrom<&HashMap<AppenderId, config::Appender>> for Appenders {
    type Error = Error;

    fn try_from(m: &HashMap<AppenderId, config::Appender>) -> Result<Self> {
        let mut out = HashMap::new();
        for (k, v) in m {
            out.insert(k.clone(), v.try_into()?);
        }
        Ok(Self::new(out))
    }
}
impl TryFrom<&config::Appender> for Appender {
    type Error = Error;

    fn try_from(value: &config::Appender) -> Result<Self> {
        match value {
            config::Appender::Null => Ok(crate::Appender::Null),
            config::Appender::Console { .. } => Ok(crate::Appender::new_console()),
            config::Appender::File { path, .. } => crate::Appender::new_file(path),
            config::Appender::RollingFile {
                path,
                policy:
                    Policy {
                        max_size_roll_backups,
                        maximum_file_size,
                        pattern,
                    },
                ..
            } => Appender::new_rolling(
                path,
                pattern.as_deref(),
                *max_size_roll_backups as usize,
                maximum_file_size,
            ),
        }
    }
}

/// An Appender represents a sink where logs can be written.
#[derive(Clone)]
pub enum Appender {
    /// Logs are written to stdout.
    Console(Console),
    /// A file appender.
    File(Arc<Mutex<File>>),
    /// A file appender which rolls files.
    RollingFile(Arc<Mutex<RollingFile>>),
    /// Logs are ignored
    Null,
}
impl Appender {
    /// Construct a new null appender.
    #[must_use]
    pub fn new_null() -> Self {
        Self::Null
    }

    /// Construct a new console appender.
    #[must_use]
    pub fn new_console() -> Self {
        Self::Console(Console::new())
    }

    /// Construct a new file appender.
    ///
    /// # Errors
    /// - We may fail to open the file for write.
    pub fn new_file(p: impl AsRef<str>) -> Result<Self> {
        Ok(Self::File(Arc::new(Mutex::new(File::new(p)?))))
    }

    /// Construct a new rolling file appender.
    ///
    /// # Errors
    /// - We may fail to calculate the size limit for the roll trigger.
    /// - We may fail to open the file for write.
    pub fn new_rolling(
        path_str: impl AsRef<str>,
        pattern_opt: Option<&str>,
        count: usize,
        size: &str,
    ) -> Result<Self> {
        use rolling::{
            Roller,
            Trigger,
        };
        let p = Path::new(path_str.as_ref());
        let abs_path = p.absolutize().unwrap_or_else(|_| p.into());
        let pattern = RollingFile::make_qualified_pattern(&abs_path, pattern_opt);
        let abs_path_str = abs_path.to_string_lossy();

        let trigger = Trigger::Size {
            limit: config::Policy::calculate_maximum_file_size(size)?,
        };
        let roller = if count == 0 {
            Roller::Delete
        } else {
            Roller::new_fixed(pattern, count)
        };
        Ok(Self::RollingFile(Arc::new(Mutex::new(RollingFile::new(
            abs_path_str,
            trigger,
            roller,
        )?))))
    }

    /// Correct the appender file path to what was originally opened by
    /// abandoning the current file handle and opening a new one.
    ///
    /// # Errors
    /// - We may fail to open the file for write at the given path.
    pub fn correct_path(&self) -> Result<()> {
        match self {
            Self::Null | Self::Console(_) => Ok(()),
            Self::File(x) => {
                let mut inner = x.lock();
                inner
                    .correct_path()
                    .map_err(|e| Error::PathCorrectionFail(inner.path_str(), e))
            },
            Self::RollingFile(x) => {
                let mut inner = x.lock();
                inner
                    .correct_path()
                    .map_err(|e| Error::PathCorrectionFail(inner.path_str(), e))
            },
        }
    }

    /// Flush the pending output
    ///
    /// # Errors
    /// - An io error may occur.
    pub fn flush_io(&self) -> Result<()> {
        match self {
            Self::Null | Self::Console(_) => Ok(()),
            Self::File(x) => {
                let mut inner = x.lock();
                inner
                    .flush()
                    .map_err(|e| Error::FlushFail(inner.path_str(), e))
            },
            Self::RollingFile(x) => {
                let mut inner = x.lock();
                inner
                    .flush()
                    .map_err(|e| Error::FlushFail(inner.path_str(), e))
            },
        }
    }
}
impl Default for Appender {
    fn default() -> Self {
        Self::Null
    }
}
impl<'a> MakeWriter<'a> for Appender {
    type Writer = Appender;

    fn make_writer(&self) -> Self::Writer {
        self.clone()
    }
}
impl io::Write for Appender {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        match self {
            Self::Console(x) => x.write(buf),
            Self::File(x) => x.deref().lock().write(buf),
            Self::RollingFile(x) => x.deref().lock().write(buf),
            Self::Null => Ok(buf.len()),
        }
    }

    fn flush(&mut self) -> io::Result<()> {
        match self {
            Self::Console(x) => x.flush(),
            Self::File(x) => x.lock().flush(),
            Self::RollingFile(x) => x.lock().flush(),
            Self::Null => Ok(()),
        }
    }
}

/// An appender which writes to stdout.
#[derive(Clone, Default)]
pub struct Console;
impl Console {
    pub fn new() -> Self {
        Self::default()
    }
}
impl io::Write for Console {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        io::stdout().write(buf)
    }

    fn flush(&mut self) -> io::Result<()> {
        Ok(())
    }
}

/// An appender which writes to a file.
pub struct File {
    path:   PathBuf,
    writer: LineWriter<fs::File>,
}
impl File {
    /// Create a new File
    ///
    ///
    /// `File` accepts a path that will be the target of logs. Env
    /// variables are expanded with the following syntax:
    /// `$ENV{var_name}`.
    ///
    /// Note: If the variable fails to resolve, `$ENV{var_name}` will NOT
    /// be replaced in the path.
    pub fn new(p: impl AsRef<str>) -> Result<Self> {
        let path = PathBuf::from(expand_env_vars(p.as_ref()).as_ref());
        let parent: Cow<Path> = path
            .parent()
            .map_or_else(|| Cow::Owned(PathBuf::from("/")), Into::into);

        fs::create_dir_all(&*parent).map_err(|source| Error::CreateFailed {
            path: parent.deref().to_owned(),
            source,
        })?;
        let writer = Self::new_writer(&path).map_err(|source| Error::CreateFailed {
            path: path.clone(),
            source,
        })?;
        Ok(Self { path, writer })
    }

    /// Verify that the currently open file is still at the original path.
    pub fn correct_path(&mut self) -> io::Result<()> {
        let correct = fs::metadata(&self.path);
        let existing = self.writer.get_ref().metadata();

        if rolling::needs_remount(Some(existing), correct) {
            self.remount()?;
        }
        Ok(())
    }

    /// Get the target path as a string.
    pub fn path_str(&self) -> String {
        self.path.to_string_lossy().to_string()
    }

    /// Remount the file at the specified path.
    /// This is useful when the file has been moved since the fd was originally
    /// mounted.
    fn remount(&mut self) -> io::Result<()> {
        self.writer.flush()?;
        self.writer = Self::new_writer(&self.path)?;
        Ok(())
    }

    fn new_writer(path: &Path) -> io::Result<LineWriter<fs::File>> {
        let f = fs::File::options().append(true).create(true).open(path)?;

        Ok(LineWriter::new(f))
    }
}

impl io::Write for File {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        self.writer.write(buf)
    }

    fn flush(&mut self) -> io::Result<()> {
        self.writer.flush()
    }
}