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#[doc = include_str!("../examples/simple_cli.rs")]
20#[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
34pub 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 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
116fn 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
130fn 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}