clap_logflag/
fern.rs

1use std::io::IsTerminal as _;
2
3use anyhow::Result;
4use fern::{
5    colors::{Color, ColoredLevelConfig},
6    Dispatch, FormatCallback,
7};
8
9use super::config::{LogDestination, LogDestinationConfig, LoggingConfig};
10
11/// TODO Documentation
12#[macro_export]
13macro_rules! init_logging {
14    ($config:expr, $default_level:expr $(,)?) => {{
15        $crate::_init_logging(
16            $config,
17            $default_level,
18            option_env!("CARGO_BIN_NAME"),
19            env!("CARGO_CRATE_NAME"),
20        )
21        .expect("Failed to initialize logging");
22    }};
23}
24
25/// Don't use this function directly, use the [init_logging!] macro instead.
26pub fn _init_logging(
27    config: LoggingConfig,
28    default_level: log::LevelFilter,
29    cargo_bin_name: Option<&str>,
30    cargo_crate_name: &str,
31) -> Result<()> {
32    match config {
33        LoggingConfig::LoggingDisabled => Ok(()),
34        LoggingConfig::LoggingEnabled { destinations } => {
35            let process_name = process_name(cargo_bin_name, cargo_crate_name);
36
37            let mut main_logger = Dispatch::new();
38            for destination in destinations {
39                if let Ok(logger) = build_logger(destination, default_level, process_name.clone()) {
40                    main_logger = main_logger.chain(logger);
41                }
42            }
43            main_logger.apply()?;
44            Ok(())
45        }
46    }
47}
48
49fn build_logger(
50    config: LogDestinationConfig,
51    default_level: log::LevelFilter,
52    process_name: String,
53) -> Result<Dispatch> {
54    let logger = Dispatch::new().level(config.level.unwrap_or(default_level));
55    let logger = match &config.destination {
56        LogDestination::Stderr => logger.format(log_formatter_stderr).chain(std::io::stderr()),
57        LogDestination::File(path) => logger
58            .format(log_formatter_file)
59            .chain(fern::log_file(path)?),
60        LogDestination::Syslog => {
61            let syslog_formatter = syslog::Formatter3164 {
62                facility: syslog::Facility::LOG_USER,
63                hostname: None,
64                process: process_name,
65                pid: std::process::id(),
66            };
67            logger.chain(syslog::unix(syslog_formatter)?)
68        }
69    };
70    Ok(logger)
71}
72
73fn log_formatter_stderr(out: FormatCallback, message: &std::fmt::Arguments, record: &log::Record) {
74    if std::io::stderr().is_terminal() {
75        log_formatter_tty(out, message, record)
76    } else {
77        log_formatter_file(out, message, record)
78    }
79}
80
81fn log_formatter_tty(out: FormatCallback, message: &std::fmt::Arguments, record: &log::Record) {
82    let colors = ColoredLevelConfig::new()
83        .trace(Color::Magenta)
84        .debug(Color::Cyan)
85        .info(Color::Green)
86        .warn(Color::Yellow)
87        .error(Color::Red);
88    out.finish(format_args!(
89        "[{} {} {}] {}",
90        humantime::format_rfc3339_seconds(std::time::SystemTime::now()),
91        colors.color(record.level()),
92        record.target(),
93        message
94    ))
95}
96
97fn log_formatter_file(out: FormatCallback, message: &std::fmt::Arguments, record: &log::Record) {
98    out.finish(format_args!(
99        "[{} {} {}] {}",
100        humantime::format_rfc3339_seconds(std::time::SystemTime::now()),
101        record.level(),
102        record.target(),
103        message
104    ))
105}
106
107/// Get a process name. Try in the following order:
108/// 1. Try getting it from argv, i.e. the name of the currently running executable
109/// 2. Try getting it from the `CARGO_BIN_NAME` environment variable
110/// 3. Get it from the `CARGO_CRATE_NAME` environment variable
111fn process_name(cargo_bin_name: Option<&str>, cargo_crate_name: &str) -> String {
112    exe_name()
113        .unwrap_or_else(|| {
114            cargo_bin_name
115                .map(str::to_string)
116                .unwrap_or_else(|| cargo_crate_name.to_string())
117        })
118        .to_string()
119}
120
121/// Get the currently running executable name from argv.
122fn exe_name() -> Option<String> {
123    std::env::current_exe()
124        .map(|exe_path| {
125            exe_path
126                .file_name()
127                .and_then(std::ffi::OsStr::to_str)
128                .map(str::to_string)
129        })
130        .unwrap_or(None)
131}