clap_logflag/
fern.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
use anyhow::Result;
use fern::{Dispatch, FormatCallback};

use super::config::{LogDestination, LogDestinationConfig, LoggingConfig};

/// TODO Documentation
#[macro_export]
macro_rules! init_logging {
    ($config:expr, $default_level:expr) => {{
        $crate::_init_logging(
            $config.into(),
            $default_level,
            option_env!("CARGO_BIN_NAME"),
            env!("CARGO_CRATE_NAME"),
        );
    }};
}

/// Don't use this function directly, use the [init_logging!] macro instead.
pub fn _init_logging(
    config: LoggingConfig,
    default_level: log::LevelFilter,
    cargo_bin_name: Option<&str>,
    cargo_crate_name: &str,
) {
    match config {
        LoggingConfig::LoggingDisabled => (),
        LoggingConfig::LoggingEnabled { destinations } => {
            let process_name = process_name(cargo_bin_name, cargo_crate_name);

            let mut main_logger = Dispatch::new();
            for destination in destinations {
                if let Ok(logger) = build_logger(destination, default_level, process_name.clone()) {
                    main_logger = main_logger.chain(logger);
                }
            }
            main_logger.apply().unwrap();
        }
    }
}

fn build_logger(
    config: LogDestinationConfig,
    default_level: log::LevelFilter,
    process_name: String,
) -> Result<Dispatch> {
    let logger = Dispatch::new().level(config.level.unwrap_or(default_level));
    let logger = match &config.destination {
        LogDestination::Stderr => logger.format(log_formatter).chain(std::io::stderr()),
        LogDestination::File(path) => logger.format(log_formatter).chain(fern::log_file(path)?),
        LogDestination::Syslog => {
            let syslog_formatter = syslog::Formatter3164 {
                facility: syslog::Facility::LOG_USER,
                hostname: None,
                process: process_name,
                pid: 0,
            };
            logger.chain(syslog::unix(syslog_formatter)?)
        }
    };
    Ok(logger)
}

fn log_formatter(out: FormatCallback, message: &std::fmt::Arguments, record: &log::Record) {
    // TODO Better format, i.e. with time, and colored.
    out.finish(format_args!("[{}] {}", record.level(), message))
}

/// Get a process name. Try in the following order:
/// 1. Try getting it from argv, i.e. the name of the currently running executable
/// 2. Try getting it from the `CARGO_BIN_NAME` environment variable
/// 3. Get it from the `CARGO_CRATE_NAME` environment variable
fn process_name(cargo_bin_name: Option<&str>, cargo_crate_name: &str) -> String {
    exe_name()
        .unwrap_or_else(|| {
            cargo_bin_name
                .map(str::to_string)
                .unwrap_or_else(|| cargo_crate_name.to_string())
        })
        .to_string()
}

/// Get the currently running executable name from argv.
fn exe_name() -> Option<String> {
    std::env::current_exe()
        .map(|exe_path| {
            exe_path
                .file_name()
                .and_then(std::ffi::OsStr::to_str)
                .map(str::to_string)
        })
        .unwrap_or(None)
}