1use anyhow::{Result, bail};
7use chrono::Local;
8use fern::colors::{Color, ColoredLevelConfig};
9use fern::{Dispatch, FormatCallback};
10use log::{LevelFilter, Record};
11use std::env;
12use std::fmt::{Arguments, Display};
13use std::fs::OpenOptions;
14use std::io::IsTerminal;
15use std::path::Path;
16use std::sync::OnceLock;
17
18static LOGGER_INIT: OnceLock<()> = OnceLock::new();
20
21const DEFAULT_LOG_LEVEL: &str = "info";
26
27const LOG_INFO_FILE_NAME: &str = "muse2_info.log";
29
30const LOG_ERROR_FILE_NAME: &str = "muse2_error.log";
32
33pub fn is_logger_initialised() -> bool {
35 LOGGER_INIT.get().is_some()
36}
37
38pub fn init(log_level_from_settings: Option<&str>, output_path: &Path) -> Result<()> {
57 let log_level = env::var("MUSE2_LOG_LEVEL").unwrap_or_else(|_| {
59 log_level_from_settings
60 .unwrap_or(DEFAULT_LOG_LEVEL)
61 .to_string()
62 });
63
64 let log_level = match log_level.to_lowercase().as_str() {
66 "off" => LevelFilter::Off,
67 "error" => LevelFilter::Error,
68 "warn" => LevelFilter::Warn,
69 "info" => LevelFilter::Info,
70 "debug" => LevelFilter::Debug,
71 "trace" => LevelFilter::Trace,
72 unknown => bail!("Unknown log level: {}", unknown),
73 };
74
75 let colours = ColoredLevelConfig::new()
77 .error(Color::Red)
78 .warn(Color::Yellow)
79 .info(Color::Green)
80 .debug(Color::Blue)
81 .trace(Color::Magenta);
82
83 let use_colour_stdout = std::io::stdout().is_terminal();
85 let use_colour_stderr = std::io::stderr().is_terminal();
86
87 let new_log_file = |file_name| {
89 OpenOptions::new()
90 .write(true)
91 .create(true)
92 .truncate(true)
93 .open(output_path.join(file_name))
94 };
95 let info_log_file = new_log_file(LOG_INFO_FILE_NAME)?;
96 let err_log_file = new_log_file(LOG_ERROR_FILE_NAME)?;
97
98 let dispatch = Dispatch::new()
100 .chain(
101 Dispatch::new()
103 .filter(|metadata| metadata.level() > LevelFilter::Warn)
104 .format(move |out, message, record| {
105 write_log_colour(out, message, record, use_colour_stdout, &colours);
106 })
107 .level(log_level)
108 .chain(std::io::stdout()),
109 )
110 .chain(
111 Dispatch::new()
113 .format(move |out, message, record| {
114 write_log_colour(out, message, record, use_colour_stderr, &colours);
115 })
116 .level(log_level.min(LevelFilter::Warn))
117 .chain(std::io::stderr()),
118 )
119 .chain(
120 Dispatch::new()
122 .filter(|metadata| metadata.level() > LevelFilter::Warn)
123 .format(write_log_plain)
124 .level(log_level.max(LevelFilter::Info))
125 .chain(info_log_file),
126 )
127 .chain(
128 Dispatch::new()
130 .format(write_log_plain)
131 .level(LevelFilter::Warn)
132 .chain(err_log_file),
133 );
134
135 dispatch.apply().expect("Logger already initialised");
137
138 LOGGER_INIT.set(()).unwrap();
140
141 Ok(())
142}
143
144fn write_log<T: Display>(out: FormatCallback, level: T, target: &str, message: &Arguments) {
146 let timestamp = Local::now().format("%H:%M:%S");
147
148 out.finish(format_args!("[{timestamp} {level} {target}] {message}"));
149}
150
151fn write_log_plain(out: FormatCallback, message: &Arguments, record: &Record) {
153 write_log(out, record.level(), record.target(), message);
154}
155
156fn write_log_colour(
158 out: FormatCallback,
159 message: &Arguments,
160 record: &Record,
161 use_colour: bool,
162 colours: &ColoredLevelConfig,
163) {
164 if use_colour {
166 write_log(out, colours.color(record.level()), record.target(), message);
167 } else {
168 write_log_plain(out, message, record);
169 }
170}