Merge pull request #558 from kinode-dao/hf/terminal-log-rotation

terminal: add log rotation by default
This commit is contained in:
nick.kino 2024-09-25 11:48:29 -07:00 committed by GitHub
commit 976b1e0bc1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 144 additions and 26 deletions

View File

@ -120,7 +120,7 @@ The `sys` publisher is not a real node ID, but it's also not a special case valu
- CTRL+J to toggle debug mode
- CTRL+S to step through events in debug mode
- CTRL+L to toggle logging mode, which writes all terminal output to the `.terminal_log` file. Off by default, this will write all events and verbose prints with timestamps.
- CTRL+L to toggle logging mode, which writes all terminal output to the `.terminal_log` file. On by default, this will write all events and verbose prints with timestamps.
- CTRL+A to jump to beginning of input
- CTRL+E to jump to end of input

View File

@ -78,7 +78,9 @@ async fn main() {
let password = matches.get_one::<String>("password");
// logging mode is toggled at runtime by CTRL+L
let is_logging = *matches.get_one::<bool>("logging").unwrap();
let is_logging = !*matches.get_one::<bool>("logging-off").unwrap();
let max_log_size = matches.get_one::<u64>("max-log-size");
let number_log_files = matches.get_one::<u64>("number-log-files");
// detached determines whether terminal is interactive
let detached = *matches.get_one::<bool>("detached").unwrap();
@ -427,6 +429,8 @@ async fn main() {
detached,
verbose_mode,
is_logging,
max_log_size.copied(),
number_log_files.copied(),
) => {
match quit {
Ok(()) => {
@ -653,7 +657,7 @@ fn build_command() -> Command {
.value_parser(value_parser!(u8)),
)
.arg(
arg!(-l --logging <IS_LOGGING> "Run in logging mode (toggled at runtime by CTRL+L): write all terminal output to .terminal_log file")
arg!(-l --"logging-off" <IS_NOT_LOGGING> "Run in non-logging mode (toggled at runtime by CTRL+L): do not write all terminal output to file in .terminal_logs directory")
.action(clap::ArgAction::SetTrue),
)
.arg(
@ -666,7 +670,15 @@ fn build_command() -> Command {
.action(clap::ArgAction::SetTrue),
)
.arg(arg!(--rpc <RPC> "Add a WebSockets RPC URL at boot"))
.arg(arg!(--password <PASSWORD> "Node password (in double quotes)"));
.arg(arg!(--password <PASSWORD> "Node password (in double quotes)"))
.arg(
arg!(--"max-log-size" <MAX_LOG_SIZE_BYTES> "Max size of all logs in bytes; setting to 0 -> no size limit (default 16MB)")
.value_parser(value_parser!(u64)),
)
.arg(
arg!(--"number-log-files" <NUMBER_LOG_FILES> "Number of logs to rotate (default 4)")
.value_parser(value_parser!(u64)),
);
#[cfg(feature = "simulation-mode")]
let app = app

View File

@ -13,7 +13,7 @@ use lib::types::core::{
};
use std::{
fs::{read_to_string, OpenOptions},
io::{BufWriter, Write},
io::BufWriter,
};
use tokio::signal::unix::{signal, SignalKind};
use unicode_segmentation::UnicodeSegmentation;
@ -22,8 +22,8 @@ pub mod utils;
struct State {
pub stdout: std::io::Stdout,
/// handle for writing to on-disk log (disabled by default, triggered by CTRL+L)
pub log_writer: BufWriter<std::fs::File>,
/// handle and settings for on-disk log (disabled by default, triggered by CTRL+L)
pub logger: utils::Logger,
/// in-memory searchable command history that persists itself on disk (default size: 1000)
pub command_history: utils::CommandHistory,
/// terminal window width, 0 is leftmost column
@ -182,6 +182,8 @@ pub async fn terminal(
is_detached: bool,
verbose_mode: u8,
is_logging: bool,
max_log_size: Option<u64>,
number_log_files: Option<u64>,
) -> anyhow::Result<()> {
let (stdout, _maybe_raw_mode) = utils::splash(&our, version, is_detached)?;
@ -214,20 +216,15 @@ pub async fn terminal(
// if CTRL+L is used to turn on logging, all prints to terminal
// will also be written with their full timestamp to the .terminal_log file.
// logging mode is always off by default. TODO add a boot flag to change this.
let log_path = std::fs::canonicalize(&home_directory_path)
.expect("terminal: could not get path for .terminal_log file")
.join(".terminal_log");
let log_handle = OpenOptions::new()
.append(true)
.create(true)
.open(&log_path)
.expect("terminal: could not open/create .terminal_log");
let log_writer = BufWriter::new(log_handle);
// logging mode is always on by default
let log_dir_path = std::fs::canonicalize(&home_directory_path)
.expect("terminal: could not get path for .terminal_logs dir")
.join(".terminal_logs");
let logger = utils::Logger::new(log_dir_path, max_log_size, number_log_files);
let mut state = State {
stdout,
log_writer,
logger,
command_history,
win_cols,
win_rows,
@ -320,21 +317,16 @@ fn handle_printout(printout: Printout, state: &mut State) -> anyhow::Result<()>
// lock here so that runtime can still use println! without freezing..
// can lock before loop later if we want to reduce overhead
let mut stdout = state.stdout.lock();
let now = Local::now();
// always write print to log if in logging mode
if state.logging_mode {
writeln!(
state.log_writer,
"[{}] {}",
now.to_rfc2822(),
printout.content
)?;
state.logger.write(&printout.content)?;
}
// skip writing print to terminal if it's of a greater
// verbosity level than our current mode
if printout.verbosity > state.verbose_mode {
return Ok(());
}
let now = Local::now();
execute!(
stdout,
// print goes immediately above the dedicated input line at bottom

View File

@ -2,12 +2,16 @@ use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use lib::types::core::Identity;
use std::{
collections::VecDeque,
fs::File,
fs::{File, OpenOptions},
io::{BufWriter, Stdout, Write},
path::{Path, PathBuf},
};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
const DEFAULT_MAX_LOGS_BYTES: u64 = 16_000_000;
const DEFAULT_NUMBER_LOG_FILES: u64 = 4;
pub struct RawMode;
impl RawMode {
fn new() -> std::io::Result<Self> {
@ -331,3 +335,113 @@ pub fn truncate_in_place(
.collect::<String>()
}
}
pub struct Logger {
pub log_dir_path: PathBuf,
pub strategy: LoggerStrategy,
log_writer: BufWriter<std::fs::File>,
}
pub enum LoggerStrategy {
Rotating {
max_log_dir_bytes: u64,
number_log_files: u64,
},
Infinite,
}
impl LoggerStrategy {
fn new(max_log_size: Option<u64>, number_log_files: Option<u64>) -> Self {
let max_log_size = max_log_size.unwrap_or_else(|| DEFAULT_MAX_LOGS_BYTES);
let number_log_files = number_log_files.unwrap_or_else(|| DEFAULT_NUMBER_LOG_FILES);
if max_log_size == 0 {
LoggerStrategy::Infinite
} else {
LoggerStrategy::Rotating {
max_log_dir_bytes: max_log_size,
number_log_files,
}
}
}
}
impl Logger {
pub fn new(
log_dir_path: PathBuf,
max_log_size: Option<u64>,
number_log_files: Option<u64>,
) -> Self {
let log_writer = make_log_writer(&log_dir_path).unwrap();
Self {
log_dir_path,
log_writer,
strategy: LoggerStrategy::new(max_log_size, number_log_files),
}
}
pub fn write(&mut self, line: &str) -> anyhow::Result<()> {
let now = chrono::Local::now();
let line = &format!("[{}] {}", now.to_rfc2822(), line);
match self.strategy {
LoggerStrategy::Infinite => {}
LoggerStrategy::Rotating {
max_log_dir_bytes,
number_log_files,
} => {
// check whether to rotate
let line_bytes = line.len();
let file_bytes = self.log_writer.get_ref().metadata()?.len() as usize;
if line_bytes + file_bytes >= (max_log_dir_bytes / number_log_files) as usize {
// rotate
self.log_writer = make_log_writer(&self.log_dir_path)?;
// clean up oldest if necessary
remove_oldest_if_exceeds(&self.log_dir_path, number_log_files as usize)?;
}
}
}
writeln!(self.log_writer, "{}", line)?;
Ok(())
}
}
fn make_log_writer(log_dir_path: &Path) -> anyhow::Result<BufWriter<std::fs::File>> {
if !log_dir_path.exists() {
std::fs::create_dir(log_dir_path)?;
}
let now = chrono::Local::now();
let log_name = format!("{}.log", now.format("%Y-%m-%d-%H:%M:%S"));
let log_path = log_dir_path.join(log_name);
let log_handle = OpenOptions::new()
.append(true)
.create(true)
.open(&log_path)?;
Ok(BufWriter::new(log_handle))
}
fn remove_oldest_if_exceeds<P: AsRef<Path>>(path: P, max_items: usize) -> anyhow::Result<()> {
let mut entries = Vec::new();
// Collect all entries and their modification times
for entry in std::fs::read_dir(path)? {
let entry = entry?;
if let Ok(metadata) = entry.metadata() {
if let Ok(modified) = metadata.modified() {
entries.push((modified, entry.path()));
}
}
}
// If the number of entries exceeds the max_items, remove the oldest
while entries.len() > max_items {
// Sort entries by modification time (oldest first)
entries.sort_by_key(|e| e.0);
let (_, path) = entries.remove(0);
std::fs::remove_file(&path)?;
}
Ok(())
}