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