From 3ae20d0e3a075938cfcfdf6f21dea433a4865b5b Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sat, 18 Oct 2025 22:14:41 +0200 Subject: [PATCH] correction de la webapp log --- pmoapp/webapp/src/components/LogView.vue | 57 +++++++-------- pmoserver/src/lib.rs | 2 +- pmoserver/src/logs/mod.rs | 90 +++++++++++++++++++++--- pmoserver/src/server.rs | 10 ++- 4 files changed, 116 insertions(+), 43 deletions(-) diff --git a/pmoapp/webapp/src/components/LogView.vue b/pmoapp/webapp/src/components/LogView.vue index a47210fc..9f01fc62 100644 --- a/pmoapp/webapp/src/components/LogView.vue +++ b/pmoapp/webapp/src/components/LogView.vue @@ -8,8 +8,8 @@ - - @@ -17,16 +17,6 @@ - - - @@ -107,7 +97,6 @@ const logs = ref([]) const autoScroll = ref(true) const isConnected = ref(false) const isLoadingHistory = ref(true) -const levelFilter = ref('ALL') const serverLogLevel = ref('TRACE') const searchQuery = ref('') const logContainer = ref(null) @@ -115,25 +104,10 @@ let eventSource = null let historyLoaded = false const seenLogIds = new Set() // Pour détecter les duplicatas -// Ordre de gravité des niveaux (du plus grave au moins grave) -const levelOrder = { - 'ERROR': 0, - 'WARN': 1, - 'INFO': 2, - 'DEBUG': 3, - 'TRACE': 4 -} - -// Pré-calculer filteredLogs de manière optimisée avec recherche +// Filtrer les logs uniquement par recherche const filteredLogs = computed(() => { let filtered = logs.value - // Filtre par niveau - if (levelFilter.value !== 'ALL') { - const filter = levelFilter.value - filtered = filtered.filter(log => log.level === filter) - } - // Filtre par recherche if (searchQuery.value.trim()) { const query = searchQuery.value.toLowerCase() @@ -165,6 +139,20 @@ async function updateServerLogLevel() { if (response.ok) { const data = await response.json() console.log('Log level updated:', data.current_level) + + // Fermer la connexion SSE actuelle + if (eventSource) { + eventSource.close() + } + + // Vider les logs actuels et réinitialiser + logs.value = [] + seenLogIds.clear() + historyLoaded = false + isLoadingHistory.value = true + + // Reconnecter au SSE avec le nouveau niveau + connectSSE() } else { console.error('Failed to update log level') } @@ -466,7 +454,7 @@ onUnmounted(() => { .log-viewer { display: flex; flex-direction: column; - min-height: calc(100vh - 60px); /* Hauteur viewport - nav */ + height: calc(100vh - 60px); /* Hauteur viewport - nav */ width: 100%; margin: 0; padding: 0; @@ -474,6 +462,7 @@ onUnmounted(() => { color: #d4d4d4; font-family: 'Consolas', 'Monaco', monospace; box-sizing: border-box; + overflow: hidden; /* Empêcher le scroll sur le conteneur principal */ } .header { @@ -485,6 +474,7 @@ onUnmounted(() => { border-bottom: 1px solid #3e3e42; flex-wrap: wrap; gap: 0.5rem; + flex-shrink: 0; /* Ne pas réduire le header */ } .header h2 { @@ -558,7 +548,7 @@ button.active { font-size: 0.9rem; } -.filter.server-level { +.filter.log-level { background: #1e3a5f; border-color: #569cd6; font-weight: bold; @@ -1007,6 +997,7 @@ button.active { border-top: 1px solid #3e3e42; font-size: 0.9rem; gap: 1rem; + flex-shrink: 0; /* Ne pas réduire le footer */ } @media (max-width: 768px) { diff --git a/pmoserver/src/lib.rs b/pmoserver/src/lib.rs index 7f9c475f..b68d08f5 100644 --- a/pmoserver/src/lib.rs +++ b/pmoserver/src/lib.rs @@ -72,4 +72,4 @@ pub mod server; pub mod logs; pub use server::{Server, ServerBuilder, ServerInfo, ApiRegistry, ApiRegistryEntry}; -pub use logs::{LogState, SseLayer, log_sse, log_dump, init_logging, LoggingOptions, log_setup_get, log_setup_post}; +pub use logs::{LogState, SseLayer, log_sse, log_dump, init_logging, LoggingOptions, log_setup_get, log_setup_post, create_logs_router, LogsApiDoc}; diff --git a/pmoserver/src/logs/mod.rs b/pmoserver/src/logs/mod.rs index aa0ed96d..b58e2c09 100644 --- a/pmoserver/src/logs/mod.rs +++ b/pmoserver/src/logs/mod.rs @@ -23,12 +23,13 @@ use serde::{Deserialize, Serialize}; use tokio::sync::broadcast; use tracing_subscriber::{ Registry, - layer::SubscriberExt, + layer::{SubscriberExt, Filter}, reload, filter::LevelFilter, util::SubscriberInitExt, }; use tracing::Level; +use utoipa::OpenApi; /// Représente une entrée de log #[derive(Debug, Clone, Serialize)] @@ -66,7 +67,9 @@ impl LogState { // 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); + eprintln!("❌ Failed to reload log level filter: {}", e); + } else { + eprintln!("✅ Log level filter reloaded successfully to: {:?}", level_filter); } } @@ -117,12 +120,18 @@ pub async fn log_sse( ) -> impl IntoResponse { let mut rx = state.subscribe(); - // Récupérer l'historique du buffer + // Récupérer l'historique du buffer et le niveau actuel let history = state.dump(); + let current_level = state.get_max_level(); let stream = async_stream::stream! { - // 1. Envoyer d'abord tous les logs historiques + // 1. Envoyer d'abord tous les logs historiques filtrés par le niveau actuel for entry in history { + // Filtrer par le niveau actuel du serveur + if !is_level_allowed(&entry.level, current_level) { + continue; + } + if !filter_entry(&entry, ¶ms) { continue; } @@ -148,6 +157,28 @@ pub async fn log_dump(State(state): State) -> impl IntoResponse { Json(state.dump()) } +/// Vérifie si un niveau de log est autorisé selon le niveau maximum configuré +fn is_level_allowed(log_level: &str, max_level: Level) -> bool { + let entry_level = match log_level.to_uppercase().as_str() { + "ERROR" => Level::ERROR, + "WARN" => Level::WARN, + "INFO" => Level::INFO, + "DEBUG" => Level::DEBUG, + "TRACE" => Level::TRACE, + _ => return false, + }; + + // Comparer les niveaux : un log est autorisé si son niveau est <= max_level + // ERROR(1) <= WARN(2) <= INFO(3) <= DEBUG(4) <= TRACE(5) + match max_level { + Level::ERROR => matches!(entry_level, Level::ERROR), + Level::WARN => matches!(entry_level, Level::ERROR | Level::WARN), + Level::INFO => matches!(entry_level, Level::ERROR | Level::WARN | Level::INFO), + Level::DEBUG => matches!(entry_level, Level::ERROR | Level::WARN | Level::INFO | Level::DEBUG), + Level::TRACE => true, // Tous les niveaux + } +} + /// Fonction de filtrage fn filter_entry(entry: &LogEntry, q: &LogQuery) -> bool { // Filtrage par niveau @@ -245,7 +276,8 @@ pub fn init_logging() -> LogState { // Créer le LogState avec le handle de rechargement let log_state = LogState::new(buffer_capacity, reload_handle); - // Construire le subscriber avec le filtre rechargeable + // Construire le subscriber avec le filtre rechargeable AVANT le SseLayer + // L'ordre est important : le filtre doit être appliqué en premier let subscriber = Registry::default() .with(filter) .with(SseLayer::new(log_state.clone())); @@ -272,19 +304,27 @@ pub fn init_logging() -> LogState { } /// Request body pour la configuration du logging -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, utoipa::ToSchema)] pub struct LogSetupRequest { pub level: String, } /// Response pour la configuration du logging -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, utoipa::ToSchema)] pub struct LogSetupResponse { pub current_level: String, pub available_levels: Vec, } /// Handler pour GET /api/log_setup - retourne la configuration actuelle +#[utoipa::path( + get, + path = "/api/log_setup", + responses( + (status = 200, description = "Log configuration retrieved successfully", body = LogSetupResponse) + ), + tag = "logs" +)] pub async fn log_setup_get(State(state): State) -> impl IntoResponse { let current = level_to_string(state.get_max_level()); Json(LogSetupResponse { @@ -300,6 +340,16 @@ pub async fn log_setup_get(State(state): State) -> impl IntoResponse { } /// Handler pour POST /api/log_setup - met à jour le niveau de log +#[utoipa::path( + post, + path = "/api/log_setup", + request_body = LogSetupRequest, + responses( + (status = 200, description = "Log level updated successfully", body = LogSetupResponse), + (status = 400, description = "Invalid log level") + ), + tag = "logs" +)] pub async fn log_setup_post( State(state): State, Json(payload): Json, @@ -366,4 +416,28 @@ fn level_to_levelfilter(level: Level) -> LevelFilter { Level::DEBUG => LevelFilter::DEBUG, Level::TRACE => LevelFilter::TRACE, } -} \ No newline at end of file +} + +/// Crée le router pour l'API de gestion des logs +pub fn create_logs_router(log_state: LogState) -> axum::Router { + use axum::routing::{get, post}; + axum::Router::new() + .route("/log_setup", get(log_setup_get).post(log_setup_post)) + .with_state(log_state) +} + +/// API OpenAPI pour la gestion des logs +#[derive(utoipa::OpenApi)] +#[openapi( + paths( + log_setup_get, + log_setup_post, + ), + components( + schemas(LogSetupRequest, LogSetupResponse) + ), + tags( + (name = "logs", description = "Log level configuration endpoints") + ) +)] +pub struct LogsApiDoc; \ No newline at end of file diff --git a/pmoserver/src/server.rs b/pmoserver/src/server.rs index eb27b2ce..bebcd4ff 100644 --- a/pmoserver/src/server.rs +++ b/pmoserver/src/server.rs @@ -29,6 +29,7 @@ use std::sync::Arc; use tokio::{signal, sync::RwLock, task::JoinHandle}; use tracing::info; use utoipa_swagger_ui::SwaggerUi; +use utoipa::OpenApi; /// Info serveur sérialisable #[derive(Clone, Serialize, utoipa::ToSchema)] @@ -572,12 +573,19 @@ impl Server { pub async fn init_logging(&mut self) { let log_state = init_logging(); - // Enregistrer automatiquement les routes de logging + // Enregistrer automatiquement les routes de logging SSE self.add_handler_with_state("/log-sse", log_sse, log_state.clone()) .await; self.add_handler_with_state("/log-dump", log_dump, log_state.clone()) .await; + // Enregistrer l'API REST de configuration des logs via OpenAPI + self.add_openapi( + crate::logs::create_logs_router(log_state.clone()), + crate::logs::LogsApiDoc::openapi(), + "logs" + ).await; + self.log_state = Some(log_state); } }