Add TUI module for logging and enhance logging functionality

This commit is contained in:
Andrea Moro 2025-01-22 13:06:46 +01:00
parent 5a8792b9fd
commit 6ebdef9a64
4 changed files with 712 additions and 8 deletions

View file

@ -1,11 +1,14 @@
mod client;
use client::handlers::handle_client;
mod tui; // Add this new module
use colog;
use log::{error, info};
use client::handlers::handle_client;
use log::{error, info, Level, Log, Metadata, Record};
use serde::Deserialize;
use std::sync::mpsc;
use std::thread;
use tokio::net::TcpListener;
use tokio::sync::broadcast;
use tui::{run_app, LogEntry};
#[derive(Deserialize, Debug)]
struct Config {
@ -13,10 +16,47 @@ struct Config {
port: String,
}
struct CustomLogger {
tx: mpsc::Sender<LogEntry>,
}
impl Log for CustomLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= Level::Info
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
let now = chrono::Local::now();
let log_entry = LogEntry {
timestamp: now.format("%H:%M:%S").to_string(),
level: record.level().to_string(),
message: record.args().to_string(),
};
let _ = self.tx.send(log_entry);
}
}
fn flush(&self) {}
}
#[tokio::main]
async fn main() {
// Initialize the logger
colog::init();
// Create a channel for logging
let (tx, rx) = mpsc::channel();
// Create and set the custom logger
let logger = Box::new(CustomLogger { tx });
log::set_boxed_logger(logger).unwrap();
log::set_max_level(log::LevelFilter::Info);
// Start the TUI in a separate thread
let _tui_handle = thread::spawn(move || {
if let Err(e) = run_app(rx) {
eprintln!("Error running TUI: {:?}", e);
}
});
// Load the configuration from config file
let config = match std::fs::read_to_string("config.toml") {
@ -34,6 +74,7 @@ async fn main() {
};
info!("Configuration loaded: {:?}", config);
// Bind a TCP listener to accept incoming connections
let listener = TcpListener::bind(config.address + ":" + config.port.as_str())
.await

138
src/tui/mod.rs Normal file
View file

@ -0,0 +1,138 @@
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, List, ListItem},
Terminal,
};
use std::{
collections::VecDeque,
sync::mpsc,
time::{Duration, Instant},
};
pub struct LogEntry {
pub timestamp: String,
pub level: String,
pub message: String,
}
pub struct App {
logs: VecDeque<LogEntry>,
should_quit: bool,
}
impl App {
pub fn new() -> App {
App {
logs: VecDeque::with_capacity(100),
should_quit: false,
}
}
pub fn add_log(&mut self, entry: LogEntry) {
if self.logs.len() >= 100 {
self.logs.pop_front();
}
self.logs.push_back(entry);
}
}
pub fn run_app(rx: mpsc::Receiver<LogEntry>) -> Result<(), Box<dyn std::error::Error>> {
// Terminal initialization
enable_raw_mode()?;
let mut stdout = std::io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut app = App::new();
let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(250);
loop {
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints(
[
Constraint::Min(0), // Logs
]
.as_ref(),
)
.split(f.area());
// Logs
let logs: Vec<ListItem> = app
.logs
.iter()
.map(|log| {
let color = match log.level.as_str() {
"ERROR" => Color::Red,
"WARN" => Color::Yellow,
"INFO" => Color::Blue,
"DEBUG" => Color::Green,
_ => Color::White,
};
ListItem::new(Line::from(vec![
Span::styled(&log.timestamp, Style::default().fg(Color::DarkGray)),
Span::raw(" "),
Span::styled(
format!("[{}]", log.level),
Style::default().fg(color).add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw(&log.message),
]))
})
.collect();
let logs =
List::new(logs).block(Block::default().borders(Borders::ALL).title("Server Logs"));
f.render_widget(logs, chunks[0]);
})?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
app.should_quit = true;
}
}
}
if last_tick.elapsed() >= tick_rate {
// Check for new log entries
if let Ok(log_entry) = rx.try_recv() {
app.add_log(log_entry);
}
last_tick = Instant::now();
}
if app.should_quit {
break;
}
}
// Restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
Ok(())
}