use fern::{ Output, colors::{Color, ColoredLevelConfig}, }; use std::{ fmt, io, path::PathBuf, sync::atomic::{AtomicBool, Ordering}, }; use talpid_core::logging::rotate_log; #[derive(thiserror::Error, Debug)] pub enum Error { /// Unable to open log file for writing #[error("Unable to open log file for writing: {path}")] WriteFile { path: String, #[source] source: io::Error, }, #[error("Unable to rotate daemon log file")] RotateLog(#[from] talpid_core::logging::RotateLogError), #[error("Unable to set logger")] SetLoggerError(#[from] log::SetLoggerError), } pub const WARNING_SILENCED_CRATES: &[&str] = &["netlink_proto", "quinn_udp"]; pub const SILENCED_CRATES: &[&str] = &[ "h2", "tokio_core", "tokio_io", "tokio_proto", "tokio_reactor", "tokio_threadpool", "tokio_util", "tower", "want", "ws", "mio", "hyper", "hyper_util", "rtnetlink", "rustls", "netlink_sys", "tracing", "hickory_proto", "hickory_server", "hickory_resolver", "shadowsocks::relay::udprelay", "quinn_proto", "quinn", ]; const SLIGHTLY_SILENCED_CRATES: &[&str] = &["mnl", "nftnl", "udp_over_tcp"]; const COLORS: ColoredLevelConfig = ColoredLevelConfig { error: Color::Red, warn: Color::Yellow, info: Color::Green, debug: Color::Blue, trace: Color::Black, }; #[cfg(not(windows))] const LINE_SEPARATOR: &str = "\n"; #[cfg(windows)] const LINE_SEPARATOR: &str = "\r\n"; const DATE_TIME_FORMAT_STR: &str = "[%Y-%m-%d %H:%M:%S%.3f]"; /// Whether a [log] logger has been initialized. // the log crate doesn't provide a nice way to tell if a logger has been initialized :( static LOG_ENABLED: AtomicBool = AtomicBool::new(false); /// Check whether logging has been enabled, i.e. if [init_logger] has been called successfully. pub fn is_enabled() -> bool { LOG_ENABLED.load(Ordering::SeqCst) } pub fn init_logger( log_level: log::LevelFilter, log_file: Option<&PathBuf>, output_timestamp: bool, ) -> Result<(), Error> { let mut top_dispatcher = fern::Dispatch::new().level(log_level); for silenced_crate in WARNING_SILENCED_CRATES { top_dispatcher = top_dispatcher.level_for(*silenced_crate, log::LevelFilter::Error); } for silenced_crate in SILENCED_CRATES { top_dispatcher = top_dispatcher.level_for(*silenced_crate, log::LevelFilter::Warn); } for silenced_crate in SLIGHTLY_SILENCED_CRATES { top_dispatcher = top_dispatcher.level_for(*silenced_crate, one_level_quieter(log_level)); } let stdout_formatter = Formatter { output_timestamp, output_color: true, }; let stdout_dispatcher = fern::Dispatch::new() .format(move |out, message, record| stdout_formatter.output_msg(out, message, record)) .chain(io::stdout()); top_dispatcher = top_dispatcher.chain(stdout_dispatcher); if let Some(ref log_file) = log_file { rotate_log(log_file).map_err(Error::RotateLog)?; let file_formatter = Formatter { output_timestamp: true, output_color: false, }; let f = fern::log_file(log_file).map_err(|source| Error::WriteFile { path: log_file.display().to_string(), source, })?; let file_dispatcher = fern::Dispatch::new() .format(move |out, message, record| file_formatter.output_msg(out, message, record)) .chain(Output::file(f, LINE_SEPARATOR)); top_dispatcher = top_dispatcher.chain(file_dispatcher); } #[cfg(all(target_os = "android", debug_assertions))] { use android_logger::{AndroidLogger, Config}; let logger: Box = Box::new(AndroidLogger::new( Config::default().with_tag("mullvad-daemon"), )); top_dispatcher = top_dispatcher.chain(logger); } top_dispatcher.apply().map_err(Error::SetLoggerError)?; LOG_ENABLED.store(true, Ordering::SeqCst); Ok(()) } fn one_level_quieter(level: log::LevelFilter) -> log::LevelFilter { use log::LevelFilter::*; match level { Off => Off, Error => Off, Warn => Error, Info => Warn, Debug => Info, Trace => Debug, } } #[derive(Default, Debug)] struct Formatter { pub output_timestamp: bool, pub output_color: bool, } impl Formatter { fn get_timetsamp_fmt(&self) -> &str { if self.output_timestamp { DATE_TIME_FORMAT_STR } else { "" } } fn get_record_level(&self, level: log::Level) -> Box { if self.output_color && cfg!(not(windows)) { Box::new(COLORS.color(level)) } else { Box::new(level) } } pub fn output_msg( &self, out: fern::FormatCallback<'_>, message: &fmt::Arguments<'_>, record: &log::Record<'_>, ) { let message = escape_newlines(format!("{message}")); out.finish(format_args!( "{}[{}][{}] {}", chrono::Local::now().format(self.get_timetsamp_fmt()), record.target(), self.get_record_level(record.level()), message, )) } } #[cfg(not(windows))] fn escape_newlines(text: String) -> String { text } #[cfg(windows)] fn escape_newlines(text: String) -> String { text.replace('\n', LINE_SEPARATOR) }