captains-log 0.15.4

A minimalist customizable logger for rust, based on the `log` crate, but also adapted to `tracing`, for production and testing scenario.
Documentation
//! The recipe module contains some prelude functions that construct a [Builder] for
//! convenience use.
//!
//! All Builder object returned by recipe can be modified additionally by user.
//!
//! Please click to the description and source for reference.

use crate::*;
use log::Level;
use std::path;
use std::path::{Path, PathBuf};
use std::str::FromStr;

pub const DEFAULT_TIME: &'static str = "%Y-%m-%d %H:%M:%S%.6f";

/// [{time}][{level}][{file}:{line}] {msg}
pub const LOG_FORMAT_DEBUG: LogFormat = LogFormat::new(DEFAULT_TIME, debug_format_f);

/// [{time}][{level}][thread_id][{file}:{line}] {msg}
pub const LOG_FORMAT_THREADED_DEBUG: LogFormat =
    LogFormat::new(DEFAULT_TIME, threaded_debug_format_f);

/// [{time}][{level}] {msg}
pub const LOG_FORMAT_PROD: LogFormat = LogFormat::new(DEFAULT_TIME, prod_format_f);

/// formatter function: [{time}][{level}][{file}:{line}] {msg}
pub fn debug_format_f(r: FormatRecord) -> String {
    let time = r.time();
    let level = r.level();
    let file = r.file();
    let line = r.line();
    let msg = r.msg();
    format!("[{time}][{level}][{file}:{line}] {msg}\n").to_string()
}

/// formatter function: [{time}][{level}][thread_id][{file}:{line}] {msg}
pub fn threaded_debug_format_f(r: FormatRecord) -> String {
    let time = r.time();
    let level = r.level();
    let file = r.file();
    let line = r.line();
    let msg = r.msg();
    let thread_id = r.thread_id();
    format!("[{time}][{level}][{:?}][{file}:{line}] {msg}\n", thread_id).to_string()
}

/// formatter function: [{time}][{level}] {msg}
pub fn prod_format_f(r: FormatRecord) -> String {
    let time = r.time();
    let level = r.level();
    let msg = r.msg();
    format!("[{time}][{level}] {msg}\n").to_string()
}

pub fn console_logger(target: ConsoleTarget, max_level: Level) -> Builder {
    let console_config = LogConsole::new(target, max_level, LOG_FORMAT_DEBUG);
    return Builder::default().add_sink(console_config);
}

/// Output to stdout with LOG_FORMAT_DEBUG, with dynamic=true.
///
/// You don't care the speed when output to console.
#[inline]
pub fn stdout_logger(max_level: Level) -> Builder {
    console_logger(ConsoleTarget::Stdout, max_level).test()
}

/// Output to stderr with LOG_FORMAT_DEBUG, with dynamic=true.
///
/// You don't care the speed when output to console.
#[inline]
pub fn stderr_logger(max_level: Level) -> Builder {
    console_logger(ConsoleTarget::Stderr, max_level).test()
}

/// Configure dynamic file/console logger from environment.
///
/// # Arguments:
///
///   - file_env_name:
///
///     If valid as stdout/stderr/1/2, output to console target;
///
///     When a file path is configured, create a raw_file_logger();
///
///     For empty string, default output to Stderr.
///
///   - level_env_name: configure the log level, default to Info.
///
/// # Example:
///
/// ``` rust
/// use captains_log::recipe;
/// let _ = recipe::env_logger("LOG_FILE", "LOG_LEVEL").build();
/// ```
pub fn env_logger(file_env_name: &str, level_env_name: &str) -> Builder {
    let level: Level = crate::env::env_or(level_env_name, Level::Info).into();
    let mut console: Option<ConsoleTarget> = None;
    if let Ok(file_path) = std::env::var(file_env_name) {
        if let Ok(target) = ConsoleTarget::from_str(file_path.as_str()) {
            console = Some(target);
        } else if file_path.len() > 0 {
            return raw_file_logger(file_path, level).test();
        }
    }
    return console_logger(console.unwrap_or(ConsoleTarget::Stderr), level).test();
}

/// Setup one log file, with custom time_fmt & format_func.
///
/// See the source for details.
///
/// # Arguments:
///
/// - `file_path`: can be &str / String / &OsStr / OsString / Path / PathBuf
///
/// - `time_fmt`: The timestamp format inside the logs.
///
/// - `format_func`: function that format the line inside the logs
pub fn raw_file_logger_custom<P: Into<PathBuf>>(
    file_path: P, max_level: Level, time_fmt: &'static str, format_func: FormatFunc,
) -> Builder {
    let format = LogFormat::new(time_fmt, format_func);
    let _file_path = file_path.into();
    let p = path::absolute(&_file_path).expect("path convert to absolute");
    let dir = p.parent().unwrap();
    let file_name = Path::new(p.file_name().unwrap());
    let file = LogRawFile::new(dir, file_name, max_level, format);
    return Builder::default().signal(signal_hook::consts::SIGUSR1).add_sink(file);
}

/// Setup one log file.
///
/// See the source for details.
///
/// # Arguments:
///
/// - `file_path`: can be &str / String / &OsStr / OsString / Path / PathBuf
pub fn raw_file_logger<P: Into<PathBuf>>(file_path: P, max_level: Level) -> Builder {
    raw_file_logger_custom(file_path, max_level, DEFAULT_TIME, debug_format_f)
}

/// Setup two log files.
/// One as "{{name}}.log" for debug purpose, with file line to track problem.
/// One as "{{name}}.log.wf" for error level log.
///
/// See the source for details.
///
/// # Arguments:
///
/// - `dir`: directory of the log files, can be &str / String / &OsStr / OsString / Path / PathBuf.
///
/// - `name`: log name, not including the suffix, can be &str / String.
pub fn split_error_file_logger<P1, P2>(dir: P1, name: P2, max_level: Level) -> Builder
where
    P1: Into<PathBuf>,
    P2: Into<String>,
{
    let _name: String = name.into();
    let debug_file_name = format!("{}.log", _name);
    let _dir: PathBuf = dir.into();
    let debug_file = LogRawFile::new(_dir.clone(), debug_file_name, max_level, LOG_FORMAT_DEBUG);
    let err_file_name = format!("{}.log.wf", _name);
    let error_file = LogRawFile::new(_dir.clone(), err_file_name, Level::Error, LOG_FORMAT_PROD);

    return Builder::default()
        .signal(signal_hook::consts::SIGUSR1)
        .add_sink(debug_file)
        .add_sink(error_file);
}

/// Setup one buffered log file, with custom time_fmt & format_func.
///
/// See the source for details.
///
/// - `file_path`: The type of file_path can be &str / String / &OsStr / OsString / Path / PathBuf
///
/// - `flush_millis`:
///
///     - default to 0, means always flush when no more message to write.
///
///     - when larger than zero, will wait for new message when timeout occur.
///
///     - the max value is 1000 (1 sec).
pub fn buffered_file_logger_custom<P: Into<PathBuf>>(
    file_path: P, max_level: Level, time_fmt: &'static str, format_func: FormatFunc,
    flush_millis: usize, rotate: Option<crate::rotation::Rotation>,
) -> Builder {
    let format = LogFormat::new(time_fmt, format_func);
    let _file_path = file_path.into();
    let p = path::absolute(&_file_path).expect("path convert to absolute");
    let dir = p.parent().unwrap();
    let file_name = Path::new(p.file_name().unwrap());
    let mut file = LogBufFile::new(dir, file_name, max_level, format, flush_millis);
    if let Some(ro) = rotate {
        file = file.rotation(ro);
    }
    return Builder::default().signal(signal_hook::consts::SIGUSR1).add_sink(file);
}

/// Setup one buffered log file, with flush_millis set to 0
///
/// See the source for details.
///
/// # Arguments:
///
/// - `file_path`: The type of file_path can be &str / String / &OsStr / OsString / Path / PathBuf
pub fn buffered_file_logger<P: Into<PathBuf>>(file_path: P, max_level: Level) -> Builder {
    buffered_file_logger_custom(file_path, max_level, DEFAULT_TIME, debug_format_f, 0, None)
}

/// Setup one buffered log file, capable of self rotation, with flush_millis set to 0,
///
/// See the source for details.
///
/// # Arguments:
///
/// - `file_path`: The type of file_path can be &str / String / &OsStr / OsString / Path / PathBuf
///
/// - `rotation`: rotation and archive strategy
pub fn buffered_rotated_file_logger<P: Into<PathBuf>>(
    file_path: P, max_level: Level, rotation: crate::rotation::Rotation,
) -> Builder {
    buffered_file_logger_custom(
        file_path,
        max_level,
        DEFAULT_TIME,
        debug_format_f,
        0,
        Some(rotation),
    )
}

/// Output to local syslog
#[cfg(feature = "syslog")]
#[cfg_attr(docsrs, doc(cfg(feature = "syslog")))]
pub fn syslog_local(facility: syslog::Facility, max_level: Level) -> Builder {
    let syslog = syslog::Syslog::new(facility, max_level);
    return Builder::default().add_sink(syslog);
}

/// Initialize a ring buffer to hold the log. For complete usage, refer to the doc in [ringfile].
///
/// # NOTE:
/// the recipe already register signal and dynamic=true, **do not use test()** here,
/// because [test()](crate::Builder::test()) will clear the signal.
///
/// # Arguments:
///
/// - `file_path`: path on disk to which the log content will be dumped
///
/// - `buf_size`: the size of memory within each thread. limit: 0 < buf_size < i32::MAX
///
/// - `max_level`: filter the log by level.
///
/// - `dump_signal`: Dump the content from memory buffer to disk when the signal received.
#[cfg(feature = "ringfile")]
#[cfg_attr(docsrs, doc(cfg(feature = "ringfile")))]
pub fn ring_file<P: Into<PathBuf>>(
    file_path: P, buf_size: i32, max_level: Level, dump_signal: i32,
) -> Builder {
    let ring =
        ringfile::LogRingFile::new(file_path, buf_size, max_level, LOG_FORMAT_THREADED_DEBUG);
    let mut config = Builder::default().signal(dump_signal).add_sink(ring);
    config.dynamic = true;
    config
}