feat: logging to log file
This commit is contained in:
@@ -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)*) => {
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|
||||||
|
|||||||
@@ -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"))?;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user