muse2/
log.rs

1//! The `log` module provides initialisation and configuration of the application's logging system.
2//!
3//! This module sets up logging with various levels (error, warn, info, debug, trace) and optional
4//! colourisation based on terminal support. It also allows configuration of the log level through
5//! environment variables.
6use anyhow::{bail, Result};
7use chrono::Local;
8use fern::colors::{Color, ColoredLevelConfig};
9use fern::{log_file, Dispatch, FormatCallback};
10use log::{LevelFilter, Record};
11use std::env;
12use std::fmt::{Arguments, Display};
13use std::io::IsTerminal;
14use std::path::Path;
15
16/// The default log level for the program.
17///
18/// Used as a fallback if the user hasn't specified something else with the MUSE2_LOG_LEVEL
19/// environment variable or the settings.toml file.
20const DEFAULT_LOG_LEVEL: &str = "info";
21
22/// The file name for the log file containing messages about the ordinary operation of MUSE 2.0
23const LOG_INFO_FILE_NAME: &str = "muse2_info.log";
24
25/// The file name for the log file containing warnings and error messages
26const LOG_ERROR_FILE_NAME: &str = "muse2_error.log";
27
28/// Initialise the program logger using the `fern` logging library with colourised output.
29///
30/// The user can specify their preferred logging level via the `settings.toml` file (defaulting to
31/// `info` if not present) or with the `MUSE2_LOG_LEVEL` environment variable. If both are provided,
32/// the environment variable takes precedence.
33///
34/// Possible log level options are:
35///
36/// * `error`
37/// * `warn`
38/// * `info`
39/// * `debug`
40/// * `trace`
41///
42/// # Arguments
43///
44/// * `log_level_from_settings`: The log level specified in `settings.toml`
45/// * `output_path`: The output path for the simulation
46pub fn init(log_level_from_settings: Option<&str>, output_path: &Path) -> Result<()> {
47    // Retrieve the log level from the environment variable or settings, or use the default
48    let log_level = env::var("MUSE2_LOG_LEVEL").unwrap_or_else(|_| {
49        log_level_from_settings
50            .unwrap_or(DEFAULT_LOG_LEVEL)
51            .to_string()
52    });
53
54    // Convert the log level string to a log::LevelFilter
55    let log_level = match log_level.to_lowercase().as_str() {
56        "off" => LevelFilter::Off,
57        "error" => LevelFilter::Error,
58        "warn" => LevelFilter::Warn,
59        "info" => LevelFilter::Info,
60        "debug" => LevelFilter::Debug,
61        "trace" => LevelFilter::Trace,
62        unknown => bail!("Unknown log level: {}", unknown),
63    };
64
65    // Set up colours for log levels
66    let colours = ColoredLevelConfig::new()
67        .error(Color::Red)
68        .warn(Color::Yellow)
69        .info(Color::Green)
70        .debug(Color::Blue)
71        .trace(Color::Magenta);
72
73    // Automatically apply colours only if the output is a terminal
74    let use_colour_stdout = std::io::stdout().is_terminal();
75    let use_colour_stderr = std::io::stderr().is_terminal();
76
77    // Create log files
78    let info_log_file = log_file(output_path.join(LOG_INFO_FILE_NAME))?;
79    let err_log_file = log_file(output_path.join(LOG_ERROR_FILE_NAME))?;
80
81    // Configure the logger
82    let dispatch = Dispatch::new()
83        .chain(
84            // Write non-error messages to stdout
85            Dispatch::new()
86                .filter(|metadata| metadata.level() > LevelFilter::Warn)
87                .format(move |out, message, record| {
88                    write_log_colour(out, message, record, use_colour_stdout, &colours);
89                })
90                .level(log_level)
91                .chain(std::io::stdout()),
92        )
93        .chain(
94            // Write error messages to stderr
95            Dispatch::new()
96                .format(move |out, message, record| {
97                    write_log_colour(out, message, record, use_colour_stderr, &colours);
98                })
99                .level(log_level.min(LevelFilter::Warn))
100                .chain(std::io::stderr()),
101        )
102        .chain(
103            // Write non-error messages to log file
104            Dispatch::new()
105                .filter(|metadata| metadata.level() > LevelFilter::Warn)
106                .format(write_log_plain)
107                .level(log_level.max(LevelFilter::Info))
108                .chain(info_log_file),
109        )
110        .chain(
111            // Write error messages to a different log file
112            Dispatch::new()
113                .format(write_log_plain)
114                .level(LevelFilter::Warn)
115                .chain(err_log_file),
116        );
117
118    // Apply the logger configuration
119    dispatch.apply()?;
120
121    Ok(())
122}
123
124/// Write to the log in the format we want for MUSE 2.0
125fn write_log<T: Display>(out: FormatCallback, level: T, target: &str, message: &Arguments) {
126    let timestamp = Local::now().format("%H:%M:%S");
127
128    out.finish(format_args!(
129        "[{} {} {}] {}",
130        timestamp, level, target, message
131    ));
132}
133
134/// Write to the log with no colours
135fn write_log_plain(out: FormatCallback, message: &Arguments, record: &Record) {
136    write_log(out, record.level(), record.target(), message);
137}
138
139/// Write to the log with optional colours
140fn write_log_colour(
141    out: FormatCallback,
142    message: &Arguments,
143    record: &Record,
144    use_colour: bool,
145    colours: &ColoredLevelConfig,
146) {
147    // Format output with or without colour based on `use_colour`
148    if use_colour {
149        write_log(out, colours.color(record.level()), record.target(), message);
150    } else {
151        write_log_plain(out, message, record);
152    }
153}