feat: logging to log file

This commit is contained in:
Michel de Graaf
2025-11-18 15:49:53 +01:00
parent 4c6d7c6850
commit 2ba48663e4
3 changed files with 115 additions and 4 deletions
+103 -1
View File
@@ -1,4 +1,6 @@
use std::env; use std::env;
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::sync::{ use std::sync::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
Mutex, Once, Mutex, Once,
@@ -18,6 +20,7 @@ static mut LOG_LEVEL: LogLevel = LogLevel::Warn;
static BUFFER: Mutex<Vec<String>> = Mutex::new(Vec::new()); static BUFFER: Mutex<Vec<String>> = Mutex::new(Vec::new());
static BUFFERING: AtomicBool = AtomicBool::new(false); static BUFFERING: AtomicBool = AtomicBool::new(false);
static LOG_FILE: Mutex<Option<File>> = Mutex::new(None);
pub fn init() { pub fn init() {
INIT.call_once(|| { INIT.call_once(|| {
@@ -37,6 +40,24 @@ pub fn init() {
_ => LogLevel::Warn, // Default to WARN _ => LogLevel::Warn, // Default to WARN
}; };
} }
// Initialize log file if LOG_FILE env var is set
if let Ok(log_file_path) = env::var("LOG_FILE") {
match OpenOptions::new()
.create(true)
.append(true)
.open(&log_file_path)
{
Ok(file) => {
if let Ok(mut log_file) = LOG_FILE.lock() {
*log_file = Some(file);
}
}
Err(e) => {
eprintln!("Failed to open log file '{}': {}", log_file_path, e);
}
}
}
}); });
} }
@@ -103,6 +124,14 @@ pub fn log(level: LogLevel, message: &str) {
let minutes = (seconds_in_day % 3600) / 60; let minutes = (seconds_in_day % 3600) / 60;
let seconds = seconds_in_day % 60; let seconds = seconds_in_day % 60;
let level_str_plain = match level {
LogLevel::Error => "ERROR",
LogLevel::Warn => "WARN",
LogLevel::Info => "INFO",
LogLevel::Debug => "DEBUG",
LogLevel::Trace => "TRACE",
};
let formatted_message = format!( let formatted_message = format!(
"[{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03}] [{}] {}", "[{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03}] [{}] {}",
year, year,
@@ -116,13 +145,45 @@ pub fn log(level: LogLevel, message: &str) {
message message
); );
let formatted_message_plain = format!(
"[{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03}] [{}] {}",
year,
month,
day,
hours,
minutes,
seconds,
subsec_millis,
level_str_plain,
message
);
if BUFFERING.load(Ordering::Relaxed) { if BUFFERING.load(Ordering::Relaxed) {
// When buffering is enabled, add to buffer for stderr
if let Ok(mut buffer) = BUFFER.lock() { if let Ok(mut buffer) = BUFFER.lock() {
buffer.push(formatted_message); buffer.push(formatted_message.clone());
}
// But ALSO write to file if configured (file logging bypasses buffering)
if let Ok(mut log_file) = LOG_FILE.lock() {
if let Some(file) = log_file.as_mut() {
let _ = writeln!(file, "{}", formatted_message_plain);
let _ = file.flush();
}
} }
} else {
// Write to file if configured, otherwise to stderr
if let Ok(mut log_file) = LOG_FILE.lock() {
if let Some(file) = log_file.as_mut() {
let _ = writeln!(file, "{}", formatted_message_plain);
let _ = file.flush();
} else { } else {
eprintln!("{}", formatted_message); eprintln!("{}", formatted_message);
} }
} else {
// If lock fails, fall back to stderr
eprintln!("{}", formatted_message);
}
}
} }
} }
} }
@@ -139,13 +200,54 @@ pub fn flush_buffered_logs() {
disable_buffering(); disable_buffering();
if let Ok(mut buffer) = BUFFER.lock() { if let Ok(mut buffer) = BUFFER.lock() {
// Write to file if configured, otherwise to stderr
if let Ok(mut log_file) = LOG_FILE.lock() {
if let Some(file) = log_file.as_mut() {
for message in buffer.iter() {
// Strip ANSI codes for file output
let plain_message = strip_ansi_codes(message);
let _ = writeln!(file, "{}", plain_message);
}
let _ = file.flush();
} else {
for message in buffer.iter() { for message in buffer.iter() {
eprintln!("{}", message); eprintln!("{}", message);
} }
}
} else {
// If lock fails, fall back to stderr
for message in buffer.iter() {
eprintln!("{}", message);
}
}
buffer.clear(); buffer.clear();
} }
} }
fn strip_ansi_codes(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\x1b' {
// Skip ANSI escape sequence
if chars.peek() == Some(&'[') {
chars.next(); // consume '['
while let Some(&ch) = chars.peek() {
chars.next();
if ch.is_ascii_alphabetic() {
break;
}
}
}
} else {
result.push(c);
}
}
result
}
#[macro_export] #[macro_export]
macro_rules! error { macro_rules! error {
($($arg:tt)*) => { ($($arg:tt)*) => {
+4
View File
@@ -1,4 +1,5 @@
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command( #[command(
@@ -32,6 +33,9 @@ pub struct Cli {
#[arg(short, long, global = true, action = clap::ArgAction::Count)] #[arg(short, long, global = true, action = clap::ArgAction::Count)]
pub verbose: u8, pub verbose: u8,
#[arg(long, env = "LOG_FILE", help = "Write logs to file instead of stderr")]
pub log_file: Option<PathBuf>,
#[command(subcommand)] #[command(subcommand)]
pub command: Option<Commands>, pub command: Option<Commands>,
+5
View File
@@ -29,6 +29,11 @@ fn main() -> Result<()> {
}; };
env::set_var("LOG_LEVEL", log_level); env::set_var("LOG_LEVEL", log_level);
// Set LOG_FILE env var if provided via CLI
if let Some(log_file) = &cli.log_file {
env::set_var("LOG_FILE", log_file.to_string_lossy().to_string());
}
let username = cli.username let username = cli.username
.ok_or_else(|| color_eyre::eyre::eyre!("Username required: use --username or set SOULSEEK_USERNAME env var"))?; .ok_or_else(|| color_eyre::eyre::eyre!("Username required: use --username or set SOULSEEK_USERNAME env var"))?;