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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
//Copyright 2021, Akamai Technologies, Inc.
//SPDX-License-Identifier: Apache-2.0

//! Configuration for log4rs
//!
//! log4rs is a very flexible logging framework – much more flexible than we need it to be.
//! This module translates the much simpler configuration language we expose through the
//! Byztime config file into a log4rs configuration and initializes log4rs accordingly.

use log4rs::append::console::Target;
use log4rs::config::*;
use log4rs::encode::pattern::PatternEncoder;
use std::boxed::Box;
use std::fmt;
use std::io;
use std::path;

pub type LogHandle = log4rs::Handle;

///A logging target — either a file or STDOUT or STDERR — coupled with
/// a minimum severity level
pub enum LogConfig {
    ConsoleLog(Target, log::LevelFilter),
    FileLog(path::PathBuf, log::LevelFilter),
}

//log4rs is missing derived traits for Target, so we have to manually reimplement this.
fn clone_target(target: &Target) -> Target {
    match target {
        Target::Stdout => Target::Stdout,
        Target::Stderr => Target::Stderr,
    }
}

impl LogConfig {
    fn name(&self) -> &str {
        match self {
            LogConfig::ConsoleLog(target, _) => match target {
                Target::Stdout => "STDOUT",
                Target::Stderr => "STDERR",
            },
            LogConfig::FileLog(path, _) => path.to_str().expect("malformed UTF-8 in path name"),
        }
    }

    fn level(&self) -> log::LevelFilter {
        match self {
            LogConfig::ConsoleLog(_, level) => *level,
            LogConfig::FileLog(_, level) => *level,
        }
    }

    fn filter(&self) -> Box<dyn log4rs::filter::Filter> {
        Box::new(log4rs::filter::threshold::ThresholdFilter::new(
            self.level(),
        ))
    }

    fn append(&self, pattern: Option<&str>) -> io::Result<Box<dyn log4rs::append::Append>> {
        match self {
            LogConfig::ConsoleLog(target, _) => Ok(Box::new(
                log4rs::append::console::ConsoleAppender::builder()
                    .target(clone_target(target))
                    .encoder(Box::new(PatternEncoder::new(
                        pattern.unwrap_or("{d} {l} {t} - {m}{n}"),
                    )))
                    .build(),
            )),
            LogConfig::FileLog(path, _) => Ok(Box::new(
                log4rs::append::file::FileAppender::builder()
                    .encoder(Box::new(PatternEncoder::new(
                        pattern.unwrap_or("{d} {l} {t} - {m}{n}"),
                    )))
                    .build(path)?,
            )),
        }
    }

    fn appender(&self, pattern: Option<&str>) -> io::Result<Appender> {
        Ok(Appender::builder()
            .filter(self.filter())
            .build(self.name(), self.append(pattern)?))
    }
}

//`Target` is missing a derived `Debug` trait so for `LogConfig` we have to implement
// `Debug` manually rather than deriving it.
impl fmt::Debug for LogConfig {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            LogConfig::ConsoleLog(Target::Stdout, level) => {
                write!(f, "ConsoleLog(Stdout, {:?})", level)
            }
            LogConfig::ConsoleLog(Target::Stderr, level) => {
                write!(f, "ConsoleLog(Stderr, {:?})", level)
            }
            LogConfig::FileLog(path, level) => write!(f, "FileLog({:?}, {:?})", path, level),
        }
    }
}

//Ditto for `Clone`.
impl Clone for LogConfig {
    fn clone(&self) -> LogConfig {
        match self {
            LogConfig::ConsoleLog(target, filter) => {
                LogConfig::ConsoleLog(clone_target(target), *filter)
            }
            LogConfig::FileLog(path, filter) => LogConfig::FileLog(path.clone(), *filter),
        }
    }
}

///Build a log4rs config from a sequence of `LogConfig`s returned by the iterator
fn build_config<'a, I: IntoIterator<Item = &'a LogConfig>>(
    cfgs: I,
    pattern: Option<&str>,
) -> io::Result<Config> {
    let mut config_builder = Config::builder();
    let mut root_builder = Root::builder();
    for cfg in cfgs.into_iter() {
        config_builder = config_builder.appender(cfg.appender(pattern)?);
        root_builder = root_builder.appender(cfg.name());
    }
    let root = root_builder.build(log::LevelFilter::Trace);
    Ok(config_builder
        .build(root)
        .expect("While building log config"))
}

///Initialize logging
pub fn init_logging<'a, I: IntoIterator<Item = &'a LogConfig>>(
    cfgs: I,
    pattern: Option<&str>,
) -> io::Result<LogHandle> {
    Ok(log4rs::init_config(build_config(cfgs, pattern)?).expect("While initializing logging"))
}

///Reinitialize logging (useful for reopening log files after they've been rotated)
pub fn reinit_logging<'a, I: IntoIterator<Item = &'a LogConfig>>(
    cfgs: I,
    pattern: Option<&str>,
    handle: &LogHandle,
) -> io::Result<()> {
    handle.set_config(build_config(cfgs, pattern)?);
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use log::info;
    use std::fs;

    //This test is broken. Its intent is to check that log rotation
    // works, but it panics on init_logging() because cargo's unit
    // test framework has already initialized logging before our unit
    // test ever gets invoked. There's currently no way to fix this,
    // but we can keep the code around in case there ever is. For now
    // there's no #[test] attribute on this function, so it won't be
    // run.
    #[allow(dead_code)]
    fn log_rotation() {
        let tempdir = tempfile::TempDir::new().unwrap();
        let mut logpath = path::PathBuf::from(tempdir.path());
        let mut logpath_rotated = logpath.clone();
        logpath.push("logfile");
        logpath_rotated.push("logfile.old");
        let logconfig = vec![LogConfig::FileLog(logpath.clone(), log::LevelFilter::Info)];
        let handle = init_logging(&logconfig, None).unwrap();
        info!("PRE-ROTATION");
        fs::rename(&logpath, &logpath_rotated).unwrap();
        info!("MID-ROTATION");
        reinit_logging(&logconfig, None, &handle).unwrap();
        info!("POST-ROTATION");

        //Here we would check for the strings "PRE-ROTATION" and
        // "MID-ROTATION" in logfile.old, and "POST-ROTATION" in
        // logfile.
    }
}