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#[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
25pub 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
107fn 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
121fn 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}