use chrono::Local;
use std::collections::VecDeque;
use std::sync::{Arc, Mutex, OnceLock};
use tracing::Level;
use tracing_subscriber::fmt::MakeWriter;
const MAX_LOG_ENTRIES: usize = 1000;
#[derive(Debug, Clone)]
pub struct LogEntry {
pub timestamp: String,
pub level: String,
pub target: String,
pub message: String,
}
impl LogEntry {
#[must_use]
pub fn new(level: Level, target: &str, message: String) -> Self {
Self {
timestamp: Local::now().format("%H:%M:%S.%3f").to_string(),
level: level.to_string().to_uppercase(),
target: target.to_string(),
message,
}
}
#[must_use]
pub fn format_for_display(&self) -> String {
format!(
"[{}] {} [{}] {}",
self.timestamp, self.level, self.target, self.message
)
}
}
#[derive(Clone)]
pub struct LogRingBuffer {
entries: Arc<Mutex<VecDeque<LogEntry>>>,
}
impl Default for LogRingBuffer {
fn default() -> Self {
Self::new()
}
}
impl LogRingBuffer {
#[must_use]
pub fn new() -> Self {
Self {
entries: Arc::new(Mutex::new(VecDeque::with_capacity(MAX_LOG_ENTRIES))),
}
}
pub fn push(&self, entry: LogEntry) {
let mut entries = self.entries.lock().unwrap();
if entries.len() >= MAX_LOG_ENTRIES {
entries.pop_front();
}
entries.push_back(entry);
}
#[must_use]
pub fn get_recent(&self, count: usize) -> Vec<LogEntry> {
let entries = self.entries.lock().unwrap();
entries.iter().rev().take(count).rev().cloned().collect()
}
pub fn clear(&self) {
let mut entries = self.entries.lock().unwrap();
entries.clear();
}
#[must_use]
pub fn len(&self) -> usize {
let entries = self.entries.lock().unwrap();
entries.len()
}
}
pub struct RingBufferWriter {
buffer: LogRingBuffer,
}
impl RingBufferWriter {
#[must_use]
pub fn new(buffer: LogRingBuffer) -> Self {
Self { buffer }
}
}
impl std::io::Write for RingBufferWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
if let Ok(message) = std::str::from_utf8(buf) {
let message = message.trim();
if !message.is_empty() {
let (level, rest) = if message.starts_with("TRACE ") {
(Level::TRACE, &message[6..])
} else if message.starts_with("DEBUG ") {
(Level::DEBUG, &message[6..])
} else if message.starts_with("INFO ") {
(Level::INFO, &message[5..])
} else if message.starts_with("WARN ") {
(Level::WARN, &message[5..])
} else if message.starts_with("ERROR ") {
(Level::ERROR, &message[6..])
} else {
self.buffer
.push(LogEntry::new(Level::INFO, "general", message.to_string()));
return Ok(buf.len());
};
let (target, msg) = if let Some(colon_pos) = rest.find(':') {
let potential_target = &rest[..colon_pos];
if potential_target.contains(' ') {
("general", rest)
} else {
(potential_target, rest[colon_pos + 1..].trim())
}
} else {
("general", rest)
};
self.buffer
.push(LogEntry::new(level, target, msg.to_string()));
}
}
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<'a> MakeWriter<'a> for RingBufferWriter {
type Writer = Self;
fn make_writer(&'a self) -> Self::Writer {
self.clone()
}
}
impl Clone for RingBufferWriter {
fn clone(&self) -> Self {
Self {
buffer: self.buffer.clone(),
}
}
}
pub struct DualWriter {
buffer: LogRingBuffer,
dual_logger: &'static crate::dual_logging::DualLogger,
}
impl DualWriter {
#[must_use]
pub fn new(
buffer: LogRingBuffer,
dual_logger: &'static crate::dual_logging::DualLogger,
) -> Self {
Self {
buffer,
dual_logger,
}
}
}
impl std::io::Write for DualWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
if let Ok(message) = std::str::from_utf8(buf) {
let message = message.trim();
if !message.is_empty() {
let (level, rest) = if message.starts_with("TRACE ") {
(Level::TRACE, &message[6..])
} else if message.starts_with("DEBUG ") {
(Level::DEBUG, &message[6..])
} else if message.starts_with("INFO ") {
(Level::INFO, &message[5..])
} else if message.starts_with("WARN ") {
(Level::WARN, &message[5..])
} else if message.starts_with("ERROR ") {
(Level::ERROR, &message[6..])
} else {
if message.starts_with("2025-") || message.starts_with("2024-") {
return Ok(buf.len());
}
let entry = LogEntry::new(Level::INFO, "general", message.to_string());
self.buffer.push(entry.clone());
self.dual_logger.log("INFO", "general", message);
return Ok(buf.len());
};
let (target, msg) = if let Some(colon_pos) = rest.find(':') {
let potential_target = &rest[..colon_pos];
if potential_target.contains(' ') {
("general", rest)
} else {
(potential_target, rest[colon_pos + 1..].trim())
}
} else {
("general", rest)
};
self.buffer
.push(LogEntry::new(level, target, msg.to_string()));
let level_str = match level {
Level::TRACE => "TRACE",
Level::DEBUG => "DEBUG",
Level::INFO => "INFO",
Level::WARN => "WARN",
Level::ERROR => "ERROR",
};
self.dual_logger.log(level_str, target, msg);
}
}
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
self.dual_logger.flush();
Ok(())
}
}
impl<'a> MakeWriter<'a> for DualWriter {
type Writer = Self;
fn make_writer(&'a self) -> Self::Writer {
Self {
buffer: self.buffer.clone(),
dual_logger: self.dual_logger,
}
}
}
impl Clone for DualWriter {
fn clone(&self) -> Self {
Self {
buffer: self.buffer.clone(),
dual_logger: self.dual_logger,
}
}
}
static LOG_BUFFER: OnceLock<LogRingBuffer> = OnceLock::new();
pub fn init_log_buffer() -> LogRingBuffer {
let buffer = LogRingBuffer::new();
LOG_BUFFER.set(buffer.clone()).ok();
buffer
}
pub fn get_log_buffer() -> Option<LogRingBuffer> {
LOG_BUFFER.get().cloned()
}
pub fn init_tracing_with_dual_logging() {
use tracing_subscriber::{
fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer,
};
let dual_logger = crate::dual_logging::init_dual_logger();
let buffer = init_log_buffer();
let dual_writer = DualWriter::new(buffer.clone(), dual_logger);
let fmt_layer = fmt::layer()
.with_writer(dual_writer)
.with_target(true)
.with_level(true)
.with_ansi(false)
.without_time() .compact();
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("trace"));
let use_stderr = std::env::var("RUST_LOG").is_ok();
if use_stderr {
let stderr_layer = fmt::layer()
.with_writer(std::io::stderr)
.with_target(true)
.with_level(true)
.with_ansi(true)
.compact()
.with_filter(filter.clone());
tracing_subscriber::registry()
.with(filter)
.with(fmt_layer)
.with(stderr_layer)
.init();
} else {
tracing_subscriber::registry()
.with(filter)
.with(fmt_layer)
.init();
}
tracing::info!(target: "EnhancedTuiApp", "Logging system initialized with dual output");
}
#[must_use]
pub fn init_tracing() -> LogRingBuffer {
init_tracing_with_dual_logging();
get_log_buffer().unwrap_or_default()
}
#[macro_export]
macro_rules! trace_operation {
($op:expr) => {
tracing::debug!(target: "operation", "{}", $op);
};
}
#[macro_export]
macro_rules! trace_query {
($query:expr) => {
tracing::info!(target: "query", "Executing: {}", $query);
};
}
#[macro_export]
macro_rules! trace_buffer_switch {
($from:expr, $to:expr) => {
tracing::debug!(target: "buffer", "Switching from buffer {} to {}", $from, $to);
};
}
#[macro_export]
macro_rules! trace_key {
($key:expr) => {
tracing::trace!(target: "input", "Key: {:?}", $key);
};
}