Amélioration du visualiseur de log web

This commit is contained in:
2025-10-10 06:58:48 +02:00
parent 5d13255155
commit 3c1b1a449d
4 changed files with 398 additions and 85 deletions

View File

@@ -7,13 +7,24 @@
{{ autoScroll ? '📌 Auto-scroll ON' : '📌 Auto-scroll OFF' }} {{ autoScroll ? '📌 Auto-scroll ON' : '📌 Auto-scroll OFF' }}
</button> </button>
<button @click="clearLogs">🗑 Clear</button> <button @click="clearLogs">🗑 Clear</button>
<!-- Sélection du niveau côté serveur -->
<select v-model="serverLogLevel" @change="updateServerLogLevel" class="filter server-level">
<option value="ERROR">🔴 ERROR only</option>
<option value="WARN">🟡 WARN+</option>
<option value="INFO">🟢 INFO+</option>
<option value="DEBUG">🔵 DEBUG+</option>
<option value="TRACE"> TRACE (all)</option>
</select>
<!-- Filtre côté client -->
<select v-model="levelFilter" class="filter"> <select v-model="levelFilter" class="filter">
<option value="ALL">All Levels</option> <option value="ALL">All Levels</option>
<option value="TRACE">TRACE</option>
<option value="DEBUG">DEBUG</option>
<option value="INFO">INFO</option>
<option value="WARN">WARN</option>
<option value="ERROR">ERROR</option> <option value="ERROR">ERROR</option>
<option value="WARN">WARN</option>
<option value="INFO">INFO</option>
<option value="DEBUG">DEBUG</option>
<option value="TRACE">TRACE</option>
</select> </select>
</div> </div>
</div> </div>
@@ -24,16 +35,28 @@
:key="index" :key="index"
:class="['log-entry', `level-${log.level.toLowerCase()}`, { 'is-history': log.isHistory }]" :class="['log-entry', `level-${log.level.toLowerCase()}`, { 'is-history': log.isHistory }]"
> >
<span class="timestamp">{{ formatTimestamp(log.timestamp) }}</span> <div class="log-header">
<span class="level">{{ log.level }}</span> <span class="timestamp">{{ formatTimestamp(log.timestamp) }}</span>
<span class="target">{{ log.target }}</span> <span class="level">{{ log.level }}</span>
<span class="message markdown-content" v-html="renderMarkdown(log.message)"></span> <span class="target">{{ log.target }}</span>
</div>
<div class="log-content">
<div class="message markdown-content">
<details v-if="isTooLong(log.message)" class="log-details">
<summary class="log-summary">
<span class="truncated-text">{{ truncateMessage(log.message) }}</span>
</summary>
<div class="full-message" v-html="renderMarkdown(log.message)"></div>
</details>
<div v-else v-html="renderMarkdown(log.message)"></div>
</div>
</div>
</div> </div>
<div v-if="isLoadingHistory" class="loading-state"> <div v-if="isLoadingHistory" class="loading-state">
Loading history... Loading history...
</div> </div>
<div v-else-if="filteredLogs.length === 0" class="empty-state"> <div v-else-if="filteredLogs.length === 0" class="empty-state">
{{ isConnected ? 'Waiting for logs...' : 'Connecting to log stream...' }} {{ isConnected ? 'Waiting for logs...' : 'Connecting to log stream...' }}
</div> </div>
@@ -43,6 +66,7 @@
<span :class="['status', { connected: isConnected }]"> <span :class="['status', { connected: isConnected }]">
{{ isConnected ? '🟢 Connected' : '🔴 Disconnected' }} {{ isConnected ? '🟢 Connected' : '🔴 Disconnected' }}
</span> </span>
<span class="server-info">Server level: {{ serverLogLevel }}</span>
<span class="count">{{ filteredLogs.length }} logs</span> <span class="count">{{ filteredLogs.length }} logs</span>
</div> </div>
</div> </div>
@@ -64,11 +88,21 @@ const autoScroll = ref(true)
const isConnected = ref(false) const isConnected = ref(false)
const isLoadingHistory = ref(true) const isLoadingHistory = ref(true)
const levelFilter = ref('ALL') const levelFilter = ref('ALL')
const serverLogLevel = ref('TRACE')
const logContainer = ref(null) const logContainer = ref(null)
let eventSource = null let eventSource = null
let historyLoaded = false let historyLoaded = false
const seenLogIds = new Set() // Pour détecter les duplicatas 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
}
const filteredLogs = computed(() => { const filteredLogs = computed(() => {
if (levelFilter.value === 'ALL') { if (levelFilter.value === 'ALL') {
return logs.value return logs.value
@@ -76,6 +110,43 @@ const filteredLogs = computed(() => {
return logs.value.filter(log => log.level === levelFilter.value) return logs.value.filter(log => log.level === levelFilter.value)
}) })
// Fonction pour mettre à jour le niveau de log côté serveur
async function updateServerLogLevel() {
try {
const response = await fetch('/api/log_setup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
level: serverLogLevel.value
})
})
if (response.ok) {
const data = await response.json()
console.log('Log level updated:', data.current_level)
} else {
console.error('Failed to update log level')
}
} catch (error) {
console.error('Error updating log level:', error)
}
}
// Charger le niveau de log actuel au démarrage
async function loadServerLogLevel() {
try {
const response = await fetch('/api/log_setup')
if (response.ok) {
const data = await response.json()
serverLogLevel.value = data.current_level
}
} catch (error) {
console.error('Error loading log level:', error)
}
}
function formatTimestamp(timestamp) { function formatTimestamp(timestamp) {
const date = new Date(timestamp.secs_since_epoch * 1000) const date = new Date(timestamp.secs_since_epoch * 1000)
return date.toLocaleTimeString('fr-FR', { return date.toLocaleTimeString('fr-FR', {
@@ -86,6 +157,21 @@ function formatTimestamp(timestamp) {
}) })
} }
function isTooLong(message) {
// Un message est trop long s'il a plus d'une ligne OU plus de 200 caractères
const firstLineEnd = message.indexOf('\n')
return firstLineEnd !== -1 || message.length > 200
}
function truncateMessage(message) {
// Prendre la première ligne, ou les 200 premiers caractères si pas de saut de ligne
const firstLineEnd = message.indexOf('\n')
if (firstLineEnd !== -1) {
return message.substring(0, firstLineEnd).trim()
}
return message.substring(0, 200).trim()
}
function renderMarkdown(text) { function renderMarkdown(text) {
// ÉTAPE 1 : Pré-processing pour détecter et protéger le XML // ÉTAPE 1 : Pré-processing pour détecter et protéger le XML
let processedText = text let processedText = text
@@ -115,13 +201,20 @@ function renderMarkdown(text) {
} }
} }
// ÉTAPE 2 : Convertir markdown en HTML // ÉTAPE 2 : Détecter et transformer les liens d'images
// Pattern pour détecter les URLs d'images (png, jpg, jpeg, gif, webp, svg)
const imageUrlPattern = /(https?:\/\/[^\s]+\.(?:png|jpg|jpeg|gif|webp|svg)(?:\?[^\s]*)?)/gi
processedText = processedText.replace(imageUrlPattern, (match) => {
return `\n![Image](${match})\n`
})
// ÉTAPE 3 : Convertir markdown en HTML
const rawHtml = marked.parse(processedText, { async: false }) const rawHtml = marked.parse(processedText, { async: false })
// ÉTAPE 3 : Nettoyer pour la sécurité // ÉTAPE 4 : Nettoyer pour la sécurité
return DOMPurify.sanitize(rawHtml, { return DOMPurify.sanitize(rawHtml, {
ALLOWED_TAGS: ['strong', 'em', 'code', 'pre', 'a', 'ul', 'ol', 'li', 'p', 'br', 'span'], ALLOWED_TAGS: ['strong', 'em', 'code', 'pre', 'a', 'ul', 'ol', 'li', 'p', 'br', 'span', 'img'],
ALLOWED_ATTR: ['href', 'target', 'class'] ALLOWED_ATTR: ['href', 'target', 'class', 'src', 'alt', 'title']
}) })
} }
@@ -157,23 +250,23 @@ function connectSSE() {
eventSource.onmessage = (event) => { eventSource.onmessage = (event) => {
try { try {
const logEntry = JSON.parse(event.data) const logEntry = JSON.parse(event.data)
// Créer un ID unique basé sur timestamp + message + target // Créer un ID unique basé sur timestamp + message + target
const logId = `${logEntry.timestamp.secs_since_epoch}-${logEntry.timestamp.nanos_since_epoch}-${logEntry.message}-${logEntry.target}` const logId = `${logEntry.timestamp.secs_since_epoch}-${logEntry.timestamp.nanos_since_epoch}-${logEntry.message}-${logEntry.target}`
// Ignorer les duplicatas // Ignorer les duplicatas
if (seenLogIds.has(logId)) { if (seenLogIds.has(logId)) {
return return
} }
seenLogIds.add(logId) seenLogIds.add(logId)
// Marquer les logs historiques // Marquer les logs historiques
if (!historyLoaded) { if (!historyLoaded) {
logEntry.isHistory = true logEntry.isHistory = true
} }
logs.value.push(logEntry) logs.value.push(logEntry)
// Limiter à 1000 logs en mémoire // Limiter à 1000 logs en mémoire
if (logs.value.length > 1000) { if (logs.value.length > 1000) {
const removed = logs.value.shift() const removed = logs.value.shift()
@@ -181,7 +274,7 @@ function connectSSE() {
const removedId = `${removed.timestamp.secs_since_epoch}-${removed.timestamp.nanos_since_epoch}-${removed.message}-${removed.target}` const removedId = `${removed.timestamp.secs_since_epoch}-${removed.timestamp.nanos_since_epoch}-${removed.message}-${removed.target}`
seenLogIds.delete(removedId) seenLogIds.delete(removedId)
} }
scrollToBottom() scrollToBottom()
} catch (error) { } catch (error) {
console.error('Failed to parse log entry:', error) console.error('Failed to parse log entry:', error)
@@ -192,7 +285,7 @@ function connectSSE() {
isConnected.value = false isConnected.value = false
isLoadingHistory.value = false isLoadingHistory.value = false
console.error('SSE connection error') console.error('SSE connection error')
// Reconnexion automatique après 3 secondes // Reconnexion automatique après 3 secondes
setTimeout(() => { setTimeout(() => {
if (eventSource.readyState === EventSource.CLOSED) { if (eventSource.readyState === EventSource.CLOSED) {
@@ -209,7 +302,7 @@ function connectSSE() {
eventSource.onmessage = (event) => { eventSource.onmessage = (event) => {
clearTimeout(historyTimeout) clearTimeout(historyTimeout)
originalOnMessage(event) originalOnMessage(event)
if (!historyLoaded) { if (!historyLoaded) {
historyTimeout = setTimeout(() => { historyTimeout = setTimeout(() => {
historyLoaded = true historyLoaded = true
@@ -221,6 +314,7 @@ function connectSSE() {
} }
onMounted(() => { onMounted(() => {
loadServerLogLevel()
connectSSE() connectSSE()
}) })
@@ -233,11 +327,11 @@ onUnmounted(() => {
// Désactiver auto-scroll si l'utilisateur scroll manuellement // Désactiver auto-scroll si l'utilisateur scroll manuellement
watch(logContainer, (container) => { watch(logContainer, (container) => {
if (!container) return if (!container) return
container.addEventListener('scroll', () => { container.addEventListener('scroll', () => {
const isAtBottom = const isAtBottom =
container.scrollHeight - container.scrollTop <= container.clientHeight + 50 container.scrollHeight - container.scrollTop <= container.clientHeight + 50
if (!isAtBottom && autoScroll.value) { if (!isAtBottom && autoScroll.value) {
autoScroll.value = false autoScroll.value = false
} }
@@ -281,7 +375,7 @@ watch(logContainer, (container) => {
.header { .header {
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
} }
.header h2 { .header h2 {
font-size: 1rem; font-size: 1rem;
width: 100%; width: 100%;
@@ -338,6 +432,13 @@ button.active {
border: 1px solid #555; border: 1px solid #555;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
font-size: 0.9rem;
}
.filter.server-level {
background: #1e3a5f;
border-color: #569cd6;
font-weight: bold;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
@@ -357,20 +458,18 @@ button.active {
} }
.log-entry { .log-entry {
display: grid; display: flex;
grid-template-columns: 130px 80px 200px 1fr; flex-direction: column;
gap: 1rem;
padding: 0.5rem; padding: 0.5rem;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
border-left: 3px solid transparent; border-left: 3px solid transparent;
font-size: 0.9rem; font-size: 0.9rem;
line-height: 1.4; line-height: 1.4;
gap: 0.5rem;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.log-entry { .log-entry {
grid-template-columns: 1fr;
gap: 0.3rem;
padding: 0.75rem 0.5rem; padding: 0.75rem 0.5rem;
font-size: 0.85rem; font-size: 0.85rem;
border-left-width: 4px; border-left-width: 4px;
@@ -385,15 +484,32 @@ button.active {
opacity: 0.7; opacity: 0.7;
} }
.log-header {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
@media (max-width: 768px) {
.log-header {
gap: 0.5rem;
}
}
.log-content {
padding-left: 0;
}
.timestamp { .timestamp {
color: #858585; color: #858585;
font-weight: 500; font-weight: 500;
flex-shrink: 0;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.timestamp { .timestamp {
font-size: 0.75rem; font-size: 0.75rem;
order: 1;
} }
} }
@@ -403,12 +519,11 @@ button.active {
padding: 0.1rem 0.5rem; padding: 0.1rem 0.5rem;
border-radius: 3px; border-radius: 3px;
text-align: center; text-align: center;
flex-shrink: 0;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.level { .level {
order: 2;
width: fit-content;
font-size: 0.75rem; font-size: 0.75rem;
padding: 0.2rem 0.6rem; padding: 0.2rem 0.6rem;
} }
@@ -417,11 +532,15 @@ button.active {
.target { .target {
color: #4ec9b0; color: #4ec9b0;
font-style: italic; font-style: italic;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-shrink: 1;
min-width: 0;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.target { .target {
order: 3;
font-size: 0.8rem; font-size: 0.8rem;
color: #6eb8a5; color: #6eb8a5;
} }
@@ -433,11 +552,61 @@ button.active {
text-align: left; text-align: left;
} }
@media (max-width: 768px) { .log-details {
.message { margin: 0;
order: 4; }
margin-top: 0.25rem;
} .log-summary {
cursor: pointer;
color: #569cd6;
list-style: none;
user-select: none;
display: flex;
align-items: baseline;
gap: 0.5rem;
}
.log-summary::-webkit-details-marker {
display: none;
}
.log-summary::marker {
content: '';
}
.log-summary::before {
content: '▶';
display: inline-block;
width: 1em;
transition: transform 0.2s;
color: #569cd6;
font-size: 0.8em;
}
.log-details[open] .log-summary::before {
transform: rotate(90deg);
}
.log-summary:hover {
color: #6fa8dc;
}
.log-summary:hover::before {
color: #6fa8dc;
}
.truncated-text {
color: #d4d4d4;
font-family: 'Consolas', 'Monaco', monospace;
white-space: pre-wrap;
word-break: break-word;
}
.full-message {
margin-top: 0.5rem;
padding-left: 1.5em;
border-left: 2px solid #569cd6;
padding-top: 0.5rem;
} }
.markdown-content { .markdown-content {
@@ -478,6 +647,16 @@ button.active {
color: #ce9178; color: #ce9178;
} }
/* Style pour les images */
.markdown-content :deep(img) {
max-width: 100%;
height: auto;
border-radius: 4px;
margin: 0.5rem 0;
border: 1px solid #3e3e42;
display: block;
}
/* Scrollbar pour les blocs de code longs */ /* Scrollbar pour les blocs de code longs */
.markdown-content :deep(pre)::-webkit-scrollbar { .markdown-content :deep(pre)::-webkit-scrollbar {
width: 8px; width: 8px;
@@ -527,32 +706,14 @@ button.active {
padding-left: 1.5rem; padding-left: 1.5rem;
} }
/* Level colors */ /* Level colors - Classés par ordre de gravité */
.level-trace { .level-error {
border-left-color: #808080; border-left-color: #f48771;
} }
.level-trace .level { .level-error .level {
background: #3a3a3a; background: #5a1e1e;
color: #a0a0a0; color: #f48771;
}
.level-debug {
border-left-color: #569cd6;
}
.level-debug .level {
background: #1e3a5f;
color: #569cd6;
}
.level-info {
border-left-color: #4ec9b0;
}
.level-info .level {
background: #1e4d42;
color: #4ec9b0;
} }
.level-warn { .level-warn {
@@ -564,13 +725,31 @@ button.active {
color: #dcdcaa; color: #dcdcaa;
} }
.level-error { .level-info {
border-left-color: #f48771; border-left-color: #4ec9b0;
} }
.level-error .level { .level-info .level {
background: #5a1e1e; background: #1e4d42;
color: #f48771; color: #4ec9b0;
}
.level-debug {
border-left-color: #569cd6;
}
.level-debug .level {
background: #1e3a5f;
color: #569cd6;
}
.level-trace {
border-left-color: #808080;
}
.level-trace .level {
background: #3a3a3a;
color: #a0a0a0;
} }
.empty-state { .empty-state {
@@ -600,6 +779,7 @@ button.active {
background: #252526; background: #252526;
border-top: 1px solid #3e3e42; border-top: 1px solid #3e3e42;
font-size: 0.9rem; font-size: 0.9rem;
gap: 1rem;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
@@ -617,6 +797,11 @@ button.active {
color: #4ec9b0; color: #4ec9b0;
} }
.server-info {
color: #569cd6;
font-weight: bold;
}
.count { .count {
color: #858585; color: #858585;
} }
@@ -638,4 +823,4 @@ button.active {
.log-container::-webkit-scrollbar-thumb:hover { .log-container::-webkit-scrollbar-thumb:hover {
background: #4e4e4e; background: #4e4e4e;
} }
</style> </style>

View File

@@ -72,4 +72,4 @@ pub mod server;
pub mod logs; pub mod logs;
pub use server::{Server, ServerBuilder, ServerInfo}; 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, IntoResponse,
sse::{Event, KeepAlive, Sse}, sse::{Event, KeepAlive, Sse},
}, },
http::StatusCode,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::broadcast; 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 /// Représente une entrée de log
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
@@ -35,16 +43,42 @@ pub struct LogEntry {
pub struct LogState { pub struct LogState {
buffer: Arc<RwLock<VecDeque<LogEntry>>>, buffer: Arc<RwLock<VecDeque<LogEntry>>>,
tx: broadcast::Sender<LogEntry>, tx: broadcast::Sender<LogEntry>,
max_level: Arc<RwLock<Level>>,
reload_handle: Arc<RwLock<reload::Handle<LevelFilter, Registry>>>,
} }
impl LogState { impl LogState {
pub fn new(capacity: usize) -> Self { pub fn new(capacity: usize, reload_handle: reload::Handle<LevelFilter, Registry>) -> Self {
Self { Self {
buffer: Arc::new(RwLock::new(VecDeque::with_capacity(capacity))), buffer: Arc::new(RwLock::new(VecDeque::with_capacity(capacity))),
tx: broadcast::channel(1000).0, 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) { fn push(&self, entry: LogEntry) {
let mut buf = self.buffer.write().unwrap(); let mut buf = self.buffer.write().unwrap();
if buf.len() == buf.capacity() { if buf.len() == buf.capacity() {
@@ -195,23 +229,116 @@ impl Default for LoggingOptions {
/// }); /// });
/// ``` /// ```
pub fn init_logging(options: LoggingOptions) -> LogState { 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 { if options.enable_console {
let subscriber = subscriber.with( subscriber
tracing_subscriber::fmt::layer() .with(
.with_target(true) tracing_subscriber::fmt::layer()
.with_level(true) .with_target(true)
.with_ansi(true), .with_level(true)
); .with_ansi(true),
tracing::subscriber::set_global_default(subscriber) )
.expect("Failed to set global default subscriber"); .init();
} else { } else {
tracing::subscriber::set_global_default(subscriber) subscriber.init();
.expect("Failed to set global default subscriber");
} }
log_state 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, S: Subscriber,
{ {
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { 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(); let mut visitor = LogVisitor::new();
event.record(&mut visitor); event.record(&mut visitor);