use std::fs::{self, File, OpenOptions};
use std::io::{self, Write};
use std::path::{PathBuf};
use std::sync::mpsc::{channel, Sender, Receiver};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use crate::config::Config;
use crate::formatter::{Formatter, OutputFormat};
use crate::color::ColorFormatter;
#[derive(Debug, Clone)]
pub struct LogEntry {
level: String,
message: String,
timestamp: String,
}
impl LogEntry {
pub fn new(level: &str, message: &str, timestamp: &str) -> Self {
LogEntry {
level: level.to_string(),
message: message.to_string(),
timestamp: timestamp.to_string(),
}
}
pub fn format_text(&self) -> String {
format!("[{}] [{}] {}", self.timestamp, self.level, self.message)
}
pub fn format_json(&self) -> String {
format!(
r#"{{"time":"{}","level":"{}","message":"{}"}}"#,
self.timestamp, self.level, self.message
)
}
}
#[derive(Debug, Clone)]
pub struct AsyncWriterConfig {
pub channel_capacity: usize,
pub batch_size: usize,
pub max_wait_ms: u64,
}
impl Default for AsyncWriterConfig {
fn default() -> Self {
AsyncWriterConfig {
channel_capacity: 1000,
batch_size: 10,
max_wait_ms: 100,
}
}
}
fn get_timestamp() -> String {
crate::formatter::Formatter::get_timestamp()
}
pub struct Writer {
sender: Sender<LogEntry>,
is_running: Arc<Mutex<bool>>,
formatter: Formatter,
color_formatter: ColorFormatter,
config: Config,
console_enabled: Mutex<bool>,
}
impl Writer {
pub fn new(config: Config) -> Self {
let (sender, receiver) = channel::<LogEntry>();
let is_running = Arc::new(Mutex::new(true));
let running_clone = Arc::clone(&is_running);
let file_path = config.get_log_file_path();
let batch_size = 10;
let max_wait_ms = 100;
let output_format_clone = config.output_format.clone();
let console_enabled = config.console_enabled;
thread::spawn(move || {
Self::writer_thread(receiver, &file_path, batch_size, max_wait_ms, &running_clone, output_format_clone);
});
let formatter = match &config.output_format {
OutputFormat::Custom(pattern) => Formatter::with_pattern(pattern.clone()),
format => Formatter::with_format(format.clone()),
};
let color_formatter = ColorFormatter::with_scheme(config.color_scheme.clone());
color_formatter.set_enabled(config.console_colors);
Writer {
sender,
is_running,
formatter,
color_formatter,
config,
console_enabled: Mutex::new(console_enabled),
}
}
fn writer_thread(
receiver: Receiver<LogEntry>,
file_path: &str,
batch_size: usize,
max_wait_ms: u64,
is_running: &Arc<Mutex<bool>>,
output_format: OutputFormat,
) {
let path = PathBuf::from(file_path);
if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent);
}
let mut file = match OpenOptions::new()
.create(true)
.append(true)
.open(&path)
{
Ok(f) => f,
Err(e) => {
eprintln!("Failed to open log file: {}", e);
return;
}
};
let mut buffer: Vec<LogEntry> = Vec::with_capacity(batch_size);
while *is_running.lock().unwrap() {
match receiver.recv_timeout(Duration::from_millis(max_wait_ms)) {
Ok(entry) => {
buffer.push(entry);
if buffer.len() >= batch_size {
Self::flush_buffer(&mut buffer, &mut file, &output_format);
}
}
Err(_) => {
if !buffer.is_empty() {
Self::flush_buffer(&mut buffer, &mut file, &output_format);
}
}
}
}
if !buffer.is_empty() {
Self::flush_buffer(&mut buffer, &mut file, &output_format);
}
}
fn flush_buffer(buffer: &mut Vec<LogEntry>, file: &mut File, output_format: &OutputFormat) {
let mut lines = String::new();
for entry in buffer.drain(..) {
match output_format {
OutputFormat::Json => lines.push_str(&entry.format_json()),
_ => lines.push_str(&entry.format_text()),
}
lines.push('\n');
}
let _ = file.write_all(lines.as_bytes());
let _ = file.flush();
}
pub fn write(&self, level: crate::level::LogLevel, message: &str) {
let timestamp = get_timestamp();
let file_log_line = self.formatter.format(level, message);
let console_log_line = if *self.console_enabled.lock().unwrap() {
self.color_formatter.format(×tamp, level, message, true)
} else {
file_log_line.clone()
};
if *self.console_enabled.lock().unwrap() {
let mut stdout = io::stdout();
let _ = writeln!(stdout, "{}", console_log_line);
}
let entry = LogEntry::new(&level.to_string(), message, ×tamp);
let _ = self.sender.send(entry);
}
pub fn set_console_enabled(&self, enabled: bool) {
*self.console_enabled.lock().unwrap() = enabled;
}
pub fn set_console_colors(&self, enabled: bool) {
self.color_formatter.set_enabled(enabled);
}
pub fn config(&self) -> &Config {
&self.config
}
pub fn stop(&self) {
*self.is_running.lock().unwrap() = false;
}
}
impl Drop for Writer {
fn drop(&mut self) {
self.stop();
}
}