Amélioration du visualiseur de log web

This commit is contained in:
2025-10-10 06:58:48 +02:00
parent 4431a3b50b
commit 83fa3287f4
4 changed files with 398 additions and 85 deletions

View File

@@ -72,4 +72,4 @@ pub mod server;
pub mod logs;
pub use server::{Server, ServerBuilder, ServerInfo};
pub use logs::{LogState, SseLayer, log_sse, log_dump, init_logging, LoggingOptions};
pub use logs::{LogState, SseLayer, log_sse, log_dump, init_logging, LoggingOptions, log_setup_get, log_setup_post};

View File

@@ -16,10 +16,18 @@ use axum::{
IntoResponse,
sse::{Event, KeepAlive, Sse},
},
http::StatusCode,
};
use serde::{Deserialize, Serialize};
use tokio::sync::broadcast;
use tracing_subscriber::{Registry, layer::SubscriberExt};
use tracing_subscriber::{
Registry,
layer::SubscriberExt,
reload,
filter::LevelFilter,
util::SubscriberInitExt,
};
use tracing::Level;
/// Représente une entrée de log
#[derive(Debug, Clone, Serialize)]
@@ -35,16 +43,42 @@ pub struct LogEntry {
pub struct LogState {
buffer: Arc<RwLock<VecDeque<LogEntry>>>,
tx: broadcast::Sender<LogEntry>,
max_level: Arc<RwLock<Level>>,
reload_handle: Arc<RwLock<reload::Handle<LevelFilter, Registry>>>,
}
impl LogState {
pub fn new(capacity: usize) -> Self {
pub fn new(capacity: usize, reload_handle: reload::Handle<LevelFilter, Registry>) -> Self {
Self {
buffer: Arc::new(RwLock::new(VecDeque::with_capacity(capacity))),
tx: broadcast::channel(1000).0,
max_level: Arc::new(RwLock::new(Level::TRACE)),
reload_handle: Arc::new(RwLock::new(reload_handle)),
}
}
pub fn set_max_level(&self, level: Level) {
*self.max_level.write().unwrap() = level;
// Convertir Level en LevelFilter
let level_filter = match level {
Level::ERROR => LevelFilter::ERROR,
Level::WARN => LevelFilter::WARN,
Level::INFO => LevelFilter::INFO,
Level::DEBUG => LevelFilter::DEBUG,
Level::TRACE => LevelFilter::TRACE,
};
// Recharger le filtre dynamiquement
if let Err(e) = self.reload_handle.write().unwrap().reload(level_filter) {
tracing::error!("Failed to reload log level filter: {}", e);
}
}
pub fn get_max_level(&self) -> Level {
*self.max_level.read().unwrap()
}
fn push(&self, entry: LogEntry) {
let mut buf = self.buffer.write().unwrap();
if buf.len() == buf.capacity() {
@@ -195,23 +229,116 @@ impl Default for LoggingOptions {
/// });
/// ```
pub fn init_logging(options: LoggingOptions) -> LogState {
let log_state = LogState::new(options.buffer_capacity);
// Créer un filtre rechargeable qui commence à TRACE
let (filter, reload_handle) = reload::Layer::new(LevelFilter::TRACE);
let subscriber = Registry::default().with(SseLayer::new(log_state.clone()));
// Créer le LogState avec le handle de rechargement
let log_state = LogState::new(options.buffer_capacity, reload_handle);
// Construire le subscriber avec le filtre rechargeable
let subscriber = Registry::default()
.with(filter)
.with(SseLayer::new(log_state.clone()));
if options.enable_console {
let subscriber = subscriber.with(
tracing_subscriber::fmt::layer()
.with_target(true)
.with_level(true)
.with_ansi(true),
);
tracing::subscriber::set_global_default(subscriber)
.expect("Failed to set global default subscriber");
subscriber
.with(
tracing_subscriber::fmt::layer()
.with_target(true)
.with_level(true)
.with_ansi(true),
)
.init();
} else {
tracing::subscriber::set_global_default(subscriber)
.expect("Failed to set global default subscriber");
subscriber.init();
}
log_state
}
/// Request body pour la configuration du logging
#[derive(Debug, Deserialize)]
pub struct LogSetupRequest {
pub level: String,
}
/// Response pour la configuration du logging
#[derive(Debug, Serialize)]
pub struct LogSetupResponse {
pub current_level: String,
pub available_levels: Vec<String>,
}
/// Handler pour GET /api/log_setup - retourne la configuration actuelle
pub async fn log_setup_get(State(state): State<LogState>) -> impl IntoResponse {
let current = level_to_string(state.get_max_level());
Json(LogSetupResponse {
current_level: current,
available_levels: vec![
"ERROR".to_string(),
"WARN".to_string(),
"INFO".to_string(),
"DEBUG".to_string(),
"TRACE".to_string(),
],
})
}
/// Handler pour POST /api/log_setup - met à jour le niveau de log
pub async fn log_setup_post(
State(state): State<LogState>,
Json(payload): Json<LogSetupRequest>,
) -> impl IntoResponse {
let level = match string_to_level(&payload.level) {
Some(l) => l,
None => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "Invalid log level. Must be one of: ERROR, WARN, INFO, DEBUG, TRACE"
})),
)
.into_response();
}
};
state.set_max_level(level);
tracing::info!("Log level changed to: {}", payload.level);
(
StatusCode::OK,
Json(LogSetupResponse {
current_level: level_to_string(level),
available_levels: vec![
"ERROR".to_string(),
"WARN".to_string(),
"INFO".to_string(),
"DEBUG".to_string(),
"TRACE".to_string(),
],
}),
)
.into_response()
}
fn string_to_level(s: &str) -> Option<Level> {
match s.to_uppercase().as_str() {
"ERROR" => Some(Level::ERROR),
"WARN" => Some(Level::WARN),
"INFO" => Some(Level::INFO),
"DEBUG" => Some(Level::DEBUG),
"TRACE" => Some(Level::TRACE),
_ => None,
}
}
fn level_to_string(level: Level) -> String {
match level {
Level::ERROR => "ERROR",
Level::WARN => "WARN",
Level::INFO => "INFO",
Level::DEBUG => "DEBUG",
Level::TRACE => "TRACE",
}
.to_string()
}

View File

@@ -48,6 +48,7 @@ where
S: Subscriber,
{
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
// Le filtrage par niveau est maintenant géré par le filtre rechargeable global
let mut visitor = LogVisitor::new();
event.record(&mut visitor);