poa-governance-notifications/src/logger.rs

258 lines
8.0 KiB
Rust

use std::fs::{self, create_dir, read_dir, remove_file, File};
use std::io::stderr;
use std::path::Path;
use chrono::{DateTime, TimeZone as _TimeZone, Utc};
use slog::{info, o, warn, Drain as _Drain};
use slog_term::{FullFormat, PlainSyncDecorator};
use web3::types::BlockNumber;
use crate::config::Config;
use crate::error::Error;
use crate::notify::Notification;
// The date format used to name log files; e.g. "Oct-08-2018-14:09:00".
const FILE_NAME_DATE_FORMAT: &str = "%b-%d-%Y-%H:%M:%S";
// The directory (relative to Cargo.toml) to store logs.
const LOGS_DIR: &str = "logs";
const MAX_NUMBER_OF_LOG_FILES: usize = 3;
const MAX_LOG_FILE_SIZE_MB: usize = 4;
const MAX_LOG_FILE_SIZE_BYTES: usize = MAX_LOG_FILE_SIZE_MB * 1024 * 1024;
// We dont want to check the log file's size after every log that is written, this constant states
// "after this many logs have been written, check the log file's size". This value assumes an
// average log is around 100 ASCII characters (bytes) long.
const INITIAL_CHECK_FILE_SIZE_AT: usize = MAX_LOG_FILE_SIZE_BYTES / 100;
fn create_logs_dir() {
let logs_dir = Path::new(LOGS_DIR);
if !logs_dir.exists() {
create_dir(logs_dir)
.unwrap_or_else(|e| panic!("could not create ./logs directory: {:?}", e));
}
}
fn read_logs_dir() -> Vec<LogFile> {
let mut log_files: Vec<LogFile> = read_dir(LOGS_DIR)
.unwrap_or_else(|e| panic!("could not read ./logs directory: {:?}", e))
.filter_map(|res| {
let path = res.ok()?.path();
let file_name = path.file_name().unwrap().to_str().unwrap();
LogFile::from_file_name(file_name).ok()
})
.collect();
log_files.sort_unstable();
log_files
}
fn rotate_log_files(log_files: &mut Vec<LogFile>) -> File {
while log_files.len() >= MAX_NUMBER_OF_LOG_FILES {
let log_file_to_remove = log_files.remove(0);
log_file_to_remove.remove_file();
}
let log_file = LogFile::now();
let file = log_file.create_file();
log_files.push(log_file);
file
}
fn get_file_size_in_bytes(path: &str) -> usize {
fs::metadata(&path)
.unwrap_or_else(|_| panic!("log file does not exist: {}", path))
.len() as usize
}
enum LogLocation {
Stderr,
File(File),
}
fn create_slog_logger(log_location: LogLocation) -> slog::Logger {
if let LogLocation::File(file) = log_location {
let decorator = PlainSyncDecorator::new(file);
let drain = FullFormat::new(decorator).build().fuse();
slog::Logger::root(drain, o!())
} else {
let decorator = PlainSyncDecorator::new(stderr());
let drain = FullFormat::new(decorator).build().fuse();
slog::Logger::root(drain, o!())
}
}
#[derive(Eq, Ord, PartialEq, PartialOrd)]
struct LogFile(DateTime<Utc>);
impl LogFile {
fn now() -> Self {
LogFile(Utc::now())
}
fn from_file_name(file_name: &str) -> Result<Self, ()> {
if let Ok(dt) = Utc.datetime_from_str(file_name, FILE_NAME_DATE_FORMAT) {
Ok(LogFile(dt))
} else {
Err(())
}
}
fn file_name(&self) -> String {
self.0.format(FILE_NAME_DATE_FORMAT).to_string()
}
fn path(&self) -> String {
format!("{}/{}", LOGS_DIR, self.file_name())
}
fn create_file(&self) -> File {
let path = self.path();
File::create(&path).unwrap_or_else(|_| panic!("failed to create log file: {}", path))
}
fn remove_file(&self) {
let path = self.path();
remove_file(&path).unwrap_or_else(|_| panic!("failed to delete log file: {}", path))
}
}
pub struct Logger {
logger: slog::Logger,
log_files: Vec<LogFile>,
log_count: usize,
check_file_size_at: usize,
}
impl Logger {
pub fn new(config: &Config) -> Self {
let (logger, log_files) = if config.log_to_file {
create_logs_dir();
let mut log_files = read_logs_dir();
let current_log_file = rotate_log_files(&mut log_files);
let logger = create_slog_logger(LogLocation::File(current_log_file));
(logger, log_files)
} else {
let logger = create_slog_logger(LogLocation::Stderr);
(logger, vec![])
};
Logger {
logger,
log_files,
log_count: 0,
check_file_size_at: INITIAL_CHECK_FILE_SIZE_AT,
}
}
fn logging_to_file(&self) -> bool {
!self.log_files.is_empty()
}
fn should_rotate_log_file(&mut self) -> bool {
if self.logging_to_file() {
if self.log_count >= self.check_file_size_at {
let path = self.log_files.last().unwrap().path();
let file_size = get_file_size_in_bytes(&path);
if file_size >= MAX_LOG_FILE_SIZE_BYTES {
return true;
}
let avg_bytes_per_log = file_size / self.log_count;
let remaining_bytes = MAX_LOG_FILE_SIZE_BYTES - file_size;
let remaining_logs = remaining_bytes / avg_bytes_per_log;
self.check_file_size_at += remaining_logs;
}
}
false
}
fn rotate_log_file(&mut self) {
let new_log_file = rotate_log_files(&mut self.log_files);
self.logger = create_slog_logger(LogLocation::File(new_log_file));
self.log_count = 0;
self.check_file_size_at = INITIAL_CHECK_FILE_SIZE_AT;
}
fn increment_log_count(&mut self) {
self.log_count += 1;
if self.should_rotate_log_file() {
self.rotate_log_file();
}
}
pub fn log_starting_poagov(&mut self) {
info!(&self.logger, "starting poagov...");
self.increment_log_count();
}
pub fn log_ctrlc_pressed(&mut self) {
warn!(
&self.logger,
"received ctrl-c signal, gracefully shutting down..."
);
self.increment_log_count();
}
pub fn log_no_email_recipients_configured(&mut self) {
warn!(
&self.logger,
"email notifications are enabled, but there are no email recipients"
);
self.increment_log_count();
}
pub fn log_notification_email_body(&mut self, notif: &Notification) {
info!(
&self.logger,
"governance notification\n{}",
notif.email_text()
);
self.increment_log_count();
}
pub fn log_notification(&mut self, notif: &Notification) {
let ballot_created_log = notif.log();
info!(
&self.logger,
"governance notification";
"ballot" => format!("{:?}", ballot_created_log.ballot_type),
"ballot_id" => format!("{}", ballot_created_log.ballot_id),
"block_number" => format!("{}", ballot_created_log.block_number)
);
self.increment_log_count();
}
pub fn log_failed_to_build_email(&mut self, e: Error) {
warn!(&self.logger, "failed to build email"; "error" => format!("{:?}", e));
self.increment_log_count();
}
pub fn log_failed_to_send_email(&mut self, recipient: &str, e: Error) {
warn!(
&self.logger,
"failed to send email";
"recipient" => recipient,
"error" => format!("{:?}", e)
);
self.increment_log_count();
}
pub fn log_email_sent(&mut self, recipient: &str) {
info!(&self.logger, "email sent"; "to" => recipient);
self.increment_log_count();
}
pub fn log_reached_notification_limit(&mut self, notification_limit: usize) {
warn!(
&self.logger,
"reached notification limit, gracefully shutting down...";
"limit" => notification_limit
);
self.increment_log_count();
}
pub fn log_finished_block_window(&mut self, start: BlockNumber, stop: BlockNumber) {
let block_range = format!("{:?}...{:?}", start, stop);
info!(&self.logger, "finished checking blocks"; "block_range" => block_range);
self.increment_log_count();
}
}