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);
    }
}