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
21pub const 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: &str, log_file_path: Option<&Path>) -> Result<()> {
57 let log_level =
59 env::var("MUSE2_LOG_LEVEL").unwrap_or_else(|_| log_level_from_settings.to_string());
60
61 let log_level = match log_level.to_lowercase().as_str() {
63 "off" => LevelFilter::Off,
64 "error" => LevelFilter::Error,
65 "warn" => LevelFilter::Warn,
66 "info" => LevelFilter::Info,
67 "debug" => LevelFilter::Debug,
68 "trace" => LevelFilter::Trace,
69 unknown => bail!("Unknown log level: {unknown}"),
70 };
71
72 let colours = ColoredLevelConfig::new()
74 .error(Color::Red)
75 .warn(Color::Yellow)
76 .info(Color::Green)
77 .debug(Color::Blue)
78 .trace(Color::Magenta);
79
80 let use_colour_stdout = std::io::stdout().is_terminal();
82 let use_colour_stderr = std::io::stderr().is_terminal();
83
84 let (info_log_file, err_log_file) = if let Some(log_file_path) = log_file_path {
86 let new_log_file = |file_name| {
87 OpenOptions::new()
88 .write(true)
89 .create(true)
90 .truncate(true)
91 .open(log_file_path.join(file_name))
92 };
93 (
94 Some(new_log_file(LOG_INFO_FILE_NAME)?),
95 Some(new_log_file(LOG_ERROR_FILE_NAME)?),
96 )
97 } else {
98 (None, None)
99 };
100
101 let mut dispatch = Dispatch::new()
103 .level_for("highs", LevelFilter::Off) .chain(
105 Dispatch::new()
107 .filter(|metadata| metadata.level() > LevelFilter::Warn)
108 .format(move |out, message, record| {
109 write_log_colour(out, message, record, use_colour_stdout, &colours);
110 })
111 .level(log_level)
112 .chain(std::io::stdout()),
113 )
114 .chain(
115 Dispatch::new()
117 .format(move |out, message, record| {
118 write_log_colour(out, message, record, use_colour_stderr, &colours);
119 })
120 .level(log_level.min(LevelFilter::Warn))
121 .chain(std::io::stderr()),
122 );
123
124 if let Some(info_log_file) = info_log_file {
126 dispatch = dispatch.chain(
127 Dispatch::new()
129 .filter(|metadata| metadata.level() > LevelFilter::Warn)
130 .format(write_log_plain)
131 .level(log_level.max(LevelFilter::Info))
132 .chain(info_log_file),
133 );
134 }
135
136 if let Some(err_log_file) = err_log_file {
137 dispatch = dispatch.chain(
138 Dispatch::new()
140 .format(write_log_plain)
141 .level(LevelFilter::Warn)
142 .chain(err_log_file),
143 );
144 }
145
146 dispatch.apply().expect("Logger already initialised");
148
149 LOGGER_INIT.set(()).unwrap();
151
152 Ok(())
153}
154
155fn write_log<T: Display>(out: FormatCallback, level: T, target: &str, message: &Arguments) {
157 let timestamp = Local::now().format("%H:%M:%S");
158
159 out.finish(format_args!("[{timestamp} {level} {target}] {message}"));
160}
161
162fn write_log_plain(out: FormatCallback, message: &Arguments, record: &Record) {
164 write_log(out, record.level(), record.target(), message);
165}
166
167fn write_log_colour(
169 out: FormatCallback,
170 message: &Arguments,
171 record: &Record,
172 use_colour: bool,
173 colours: &ColoredLevelConfig,
174) {
175 if use_colour {
177 write_log(out, colours.color(record.level()), record.target(), message);
178 } else {
179 write_log_plain(out, message, record);
180 }
181}