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