From b6f5b9d08cbd85a1cf00f09e6b810d235d17c67f Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Sat, 9 Nov 2024 16:18:43 -0700 Subject: [PATCH] feat: Added a new command to the main managarr CLI: tail-logs, to enable users to tail the Managarr logs without needing to know where the log file itself is located --- src/cli/mod.rs | 6 +++++ src/main.rs | 2 ++ src/utils.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 05854f0..66c6bb2 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -27,6 +27,12 @@ pub enum Command { #[arg(value_enum)] shell: Shell, }, + + #[command(about = "Tail Managarr logs")] + TailLogs { + #[arg(long, help = "Disable colored log output")] + no_color: bool, + }, } pub trait CliCommandHandler<'a, 'b, T: Into> { diff --git a/src/main.rs b/src/main.rs index 18728cb..388286b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,6 +30,7 @@ use tokio::select; use tokio::sync::mpsc::Receiver; use tokio::sync::{mpsc, Mutex}; use tokio_util::sync::CancellationToken; +use utils::tail_logs; use crate::app::App; use crate::cli::Command; @@ -124,6 +125,7 @@ async fn main() -> Result<()> { let mut cli = Cli::command(); generate(shell, &mut cli, "managarr", &mut io::stdout()) } + Command::TailLogs { no_color } => tail_logs(no_color).await, }, None => { let app_nw = Arc::clone(&app); diff --git a/src/utils.rs b/src/utils.rs index ac37626..816f0fb 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,10 +1,14 @@ -use std::fs; +use std::fs::{self, File}; +use std::io::{BufRead, BufReader, Seek, SeekFrom}; use std::path::PathBuf; +use std::process; +use colored::Colorize; use log::LevelFilter; use log4rs::append::file::FileAppender; use log4rs::config::{Appender, Root}; use log4rs::encode::pattern::PatternEncoder; +use regex::Regex; #[cfg(test)] #[path = "utils_tests.rs"] @@ -21,7 +25,6 @@ pub fn get_log_path() -> PathBuf { log_path.push("managarr"); - // Create the directory if it doesn't exist if let Err(e) = fs::create_dir_all(&log_path) { eprintln!("Failed to create log directory: {:?}", e); } @@ -58,3 +61,64 @@ pub fn convert_runtime(runtime: i64) -> (i64, i64) { (hours, minutes) } + +pub async fn tail_logs(no_color: bool) { + let re = Regex::new(r"^(?P\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s+<(?P[^\s>]+)>\s+\[(?P[A-Z]+)\]\s+(?P[^:]+):(?P\d+)\s+-\s+(?P.*)$").unwrap(); + let file_path = get_log_path(); + let file = File::open(&file_path).expect("Cannot open file"); + let mut reader = BufReader::new(file); + + if let Err(e) = reader.seek(SeekFrom::End(0)) { + eprintln!("Unable to tail log file: {e:?}"); + process::exit(1); + }; + + let mut lines = reader.lines(); + + tokio::spawn(async move { + loop { + if let Some(Ok(line)) = lines.next() { + if no_color { + println!("{}", line); + } else { + let colored_line = colorize_log_line(&line, &re); + println!("{}", colored_line); + } + } + } + }) + .await + .unwrap(); +} + +fn colorize_log_line(line: &str, re: &Regex) -> String { + if let Some(caps) = re.captures(line) { + let level = &caps["level"]; + let message = &caps["message"]; + + let colored_message = match level { + "ERROR" => message.red(), + "WARN" => message.yellow(), + "INFO" => message.green(), + "DEBUG" => message.blue(), + _ => message.normal(), + }; + + let timestamp = &caps["timestamp"]; + let opid = &caps["opid"]; + let logger = &caps["logger"]; + let line_number = &caps["line"]; + + format!( + "{} <{}> [{}] {}:{} - {}", + timestamp.white(), + opid.cyan(), + level.bold(), + logger.magenta(), + line_number.bold(), + colored_message + ) + } else { + line.to_string() + } +}