501 lines
14 KiB
Rust
501 lines
14 KiB
Rust
|
|
//! Logging structuré avec tracing pour le serveur de chat
|
||
|
|
//!
|
||
|
|
//! Ce module fournit un système de logging avancé avec:
|
||
|
|
//! - Logs structurés avec champs contextuels
|
||
|
|
//! - Rotation des logs
|
||
|
|
//! - Filtrage par niveau et module
|
||
|
|
//! - Export vers différents formats (JSON, Pretty, Compact)
|
||
|
|
//! - Intégration avec les métriques
|
||
|
|
|
||
|
|
use crate::config::{LogFormat, LogRotation, LoggingConfig, ServerConfig};
|
||
|
|
use crate::error::{ChatError, Result};
|
||
|
|
use serde::{Deserialize, Serialize};
|
||
|
|
use std::collections::HashMap;
|
||
|
|
use std::path::PathBuf;
|
||
|
|
use std::time::Duration;
|
||
|
|
use tracing::{debug, error, info, trace, warn};
|
||
|
|
use tracing_appender::{
|
||
|
|
non_blocking::{NonBlocking, WorkerGuard},
|
||
|
|
rolling::{RollingFileAppender, Rotation},
|
||
|
|
};
|
||
|
|
use tracing_subscriber::{
|
||
|
|
fmt::{self, format::Writer, time::ChronoUtc},
|
||
|
|
layer::SubscriberExt,
|
||
|
|
util::SubscriberInitExt,
|
||
|
|
EnvFilter, Layer, Registry,
|
||
|
|
};
|
||
|
|
|
||
|
|
/// Configuration du logging structuré
|
||
|
|
#[derive(Debug)]
|
||
|
|
pub struct StructuredLogging {
|
||
|
|
config: LoggingConfig,
|
||
|
|
_guard: Option<WorkerGuard>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl StructuredLogging {
|
||
|
|
/// Initialise le système de logging structuré
|
||
|
|
pub fn new(config: LoggingConfig) -> Result<Self> {
|
||
|
|
Ok(Self {
|
||
|
|
config,
|
||
|
|
_guard: None,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Configure et initialise le subscriber tracing
|
||
|
|
pub fn setup(&self) -> Result<()> {
|
||
|
|
// Filtre d'environnement
|
||
|
|
let env_filter = EnvFilter::try_from_default_env()
|
||
|
|
.or_else(|_| EnvFilter::try_new(&self.config.level))
|
||
|
|
.map_err(|e| {
|
||
|
|
ChatError::configuration_error(&format!("Erreur configuration filtre: {e}"))
|
||
|
|
})?;
|
||
|
|
|
||
|
|
// Configuration du format
|
||
|
|
let format_layer = match self.config.format {
|
||
|
|
LogFormat::Json => fmt::layer()
|
||
|
|
.json()
|
||
|
|
.with_timer(ChronoUtc::rfc_3339())
|
||
|
|
.with_target(true)
|
||
|
|
.with_file(true)
|
||
|
|
.with_line_number(true)
|
||
|
|
.boxed(),
|
||
|
|
LogFormat::Pretty => fmt::layer()
|
||
|
|
.pretty()
|
||
|
|
.with_timer(ChronoUtc::rfc_3339())
|
||
|
|
.with_target(true)
|
||
|
|
.with_file(true)
|
||
|
|
.with_line_number(true)
|
||
|
|
.boxed(),
|
||
|
|
LogFormat::Compact => fmt::layer()
|
||
|
|
.compact()
|
||
|
|
.with_timer(ChronoUtc::rfc_3339())
|
||
|
|
.with_target(true)
|
||
|
|
.boxed(),
|
||
|
|
};
|
||
|
|
|
||
|
|
// Configuration de la sortie
|
||
|
|
let registry = Registry::default().with(env_filter).with(format_layer);
|
||
|
|
registry.init();
|
||
|
|
|
||
|
|
info!(
|
||
|
|
level = %self.config.level,
|
||
|
|
format = ?self.config.format,
|
||
|
|
file = ?self.config.file,
|
||
|
|
"🔧 Système de logging structuré initialisé"
|
||
|
|
);
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Macros de logging contextuel pour le chat
|
||
|
|
pub mod chat_logs {
|
||
|
|
use super::*;
|
||
|
|
use std::collections::HashMap;
|
||
|
|
use tracing::{debug, error, info, trace, warn};
|
||
|
|
use uuid::Uuid;
|
||
|
|
|
||
|
|
/// Log d'authentification
|
||
|
|
pub fn auth_success(user_id: i32, username: &str, ip: &str) {
|
||
|
|
info!(
|
||
|
|
event = "auth_success",
|
||
|
|
user_id = %user_id,
|
||
|
|
username = %username,
|
||
|
|
ip_address = %ip,
|
||
|
|
"🔐 Utilisateur authentifié"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Log d'échec d'authentification
|
||
|
|
pub fn auth_failure(username: &str, ip: &str, reason: &str) {
|
||
|
|
warn!(
|
||
|
|
event = "auth_failure",
|
||
|
|
username = %username,
|
||
|
|
ip_address = %ip,
|
||
|
|
reason = %reason,
|
||
|
|
"❌ Échec d'authentification"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Log de connexion WebSocket
|
||
|
|
pub fn websocket_connected(connection_id: &str, user_id: i32, username: &str) {
|
||
|
|
info!(
|
||
|
|
event = "websocket_connected",
|
||
|
|
connection_id = %connection_id,
|
||
|
|
user_id = %user_id,
|
||
|
|
username = %username,
|
||
|
|
"🔌 Connexion WebSocket établie"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Log de déconnexion WebSocket
|
||
|
|
pub fn websocket_disconnected(connection_id: &str, user_id: i32, username: &str, reason: &str) {
|
||
|
|
info!(
|
||
|
|
event = "websocket_disconnected",
|
||
|
|
connection_id = %connection_id,
|
||
|
|
user_id = %user_id,
|
||
|
|
username = %username,
|
||
|
|
reason = %reason,
|
||
|
|
"🔌 Connexion WebSocket fermée"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Log d'envoi de message
|
||
|
|
pub fn message_sent(
|
||
|
|
message_id: String,
|
||
|
|
user_id: i32,
|
||
|
|
username: &str,
|
||
|
|
room_id: &str,
|
||
|
|
message_type: &str,
|
||
|
|
content_length: usize,
|
||
|
|
) {
|
||
|
|
info!(
|
||
|
|
event = "message_sent",
|
||
|
|
message_id = %message_id,
|
||
|
|
user_id = %user_id,
|
||
|
|
username = %username,
|
||
|
|
room_id = %room_id,
|
||
|
|
message_type = %message_type,
|
||
|
|
content_length = %content_length,
|
||
|
|
"💬 Message envoyé"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Log de réception de message
|
||
|
|
pub fn message_received(
|
||
|
|
message_id: i32,
|
||
|
|
user_id: i32,
|
||
|
|
username: &str,
|
||
|
|
room_id: &str,
|
||
|
|
message_type: &str,
|
||
|
|
) {
|
||
|
|
debug!(
|
||
|
|
event = "message_received",
|
||
|
|
message_id = %message_id,
|
||
|
|
user_id = %user_id,
|
||
|
|
username = %username,
|
||
|
|
room_id = %room_id,
|
||
|
|
message_type = %message_type,
|
||
|
|
"📨 Message reçu"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Log de création de salon
|
||
|
|
pub fn room_created(room_id: &str, room_name: &str, creator_id: i32, creator_username: &str) {
|
||
|
|
info!(
|
||
|
|
event = "room_created",
|
||
|
|
room_id = %room_id,
|
||
|
|
room_name = %room_name,
|
||
|
|
creator_id = %creator_id,
|
||
|
|
creator_username = %creator_username,
|
||
|
|
"🏠 Salon créé"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Log de suppression de salon
|
||
|
|
pub fn room_deleted(room_id: &str, room_name: &str, deleter_id: i32, deleter_username: &str) {
|
||
|
|
warn!(
|
||
|
|
event = "room_deleted",
|
||
|
|
room_id = %room_id,
|
||
|
|
room_name = %room_name,
|
||
|
|
deleter_id = %deleter_id,
|
||
|
|
deleter_username = %deleter_username,
|
||
|
|
"🗑️ Salon supprimé"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Log d'erreur système
|
||
|
|
pub fn system_error(error_type: &str, context: &str, error: &str) {
|
||
|
|
error!(
|
||
|
|
event = "system_error",
|
||
|
|
error_type = %error_type,
|
||
|
|
context = %context,
|
||
|
|
error = %error,
|
||
|
|
"💥 Erreur système"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Log d'erreur de validation
|
||
|
|
pub fn validation_error(field: &str, value: &str, reason: &str) {
|
||
|
|
warn!(
|
||
|
|
event = "validation_error",
|
||
|
|
field = %field,
|
||
|
|
value = %value,
|
||
|
|
reason = %reason,
|
||
|
|
"⚠️ Erreur de validation"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Log de rate limiting
|
||
|
|
pub fn rate_limit_triggered(
|
||
|
|
user_id: i32,
|
||
|
|
username: &str,
|
||
|
|
limit_type: &str,
|
||
|
|
current_count: u32,
|
||
|
|
) {
|
||
|
|
warn!(
|
||
|
|
event = "rate_limit_triggered",
|
||
|
|
user_id = %user_id,
|
||
|
|
username = %username,
|
||
|
|
limit_type = %limit_type,
|
||
|
|
current_count = %current_count,
|
||
|
|
"🚫 Rate limit déclenché"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Log de métriques
|
||
|
|
pub fn metrics_updated(metric_name: &str, value: f64, labels: &HashMap<String, String>) {
|
||
|
|
trace!(
|
||
|
|
event = "metrics_updated",
|
||
|
|
metric_name = %metric_name,
|
||
|
|
value = %value,
|
||
|
|
labels = ?labels,
|
||
|
|
"📊 Métrique mise à jour"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Log de performance
|
||
|
|
pub fn performance_measurement(
|
||
|
|
operation: &str,
|
||
|
|
duration_ms: f64,
|
||
|
|
success: bool,
|
||
|
|
additional_data: Option<&HashMap<String, String>>,
|
||
|
|
) {
|
||
|
|
let level = if success { "info" } else { "warn" };
|
||
|
|
let message = if success {
|
||
|
|
"⚡ Opération terminée"
|
||
|
|
} else {
|
||
|
|
"🐌 Opération lente"
|
||
|
|
};
|
||
|
|
|
||
|
|
match level {
|
||
|
|
"info" => {
|
||
|
|
info!(
|
||
|
|
event = "performance_measurement",
|
||
|
|
operation = %operation,
|
||
|
|
duration_ms = %duration_ms,
|
||
|
|
success = %success,
|
||
|
|
additional_data = ?additional_data,
|
||
|
|
"{}", message
|
||
|
|
);
|
||
|
|
}
|
||
|
|
_ => {
|
||
|
|
warn!(
|
||
|
|
event = "performance_measurement",
|
||
|
|
operation = %operation,
|
||
|
|
duration_ms = %duration_ms,
|
||
|
|
success = %success,
|
||
|
|
additional_data = ?additional_data,
|
||
|
|
"{}", message
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Log de démarrage du serveur
|
||
|
|
pub fn server_started(bind_addr: &str, environment: &str, version: &str) {
|
||
|
|
info!(
|
||
|
|
event = "server_started",
|
||
|
|
bind_addr = %bind_addr,
|
||
|
|
environment = %environment,
|
||
|
|
version = %version,
|
||
|
|
"🚀 Serveur de chat démarré"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Log d'arrêt du serveur
|
||
|
|
pub fn server_stopped(reason: &str, uptime_seconds: u64) {
|
||
|
|
info!(
|
||
|
|
event = "server_stopped",
|
||
|
|
reason = %reason,
|
||
|
|
uptime_seconds = %uptime_seconds,
|
||
|
|
"🛑 Serveur de chat arrêté"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Log de configuration
|
||
|
|
pub fn config_loaded(config_source: &str, config_path: Option<&str>) {
|
||
|
|
info!(
|
||
|
|
event = "config_loaded",
|
||
|
|
config_source = %config_source,
|
||
|
|
config_path = ?config_path,
|
||
|
|
"⚙️ Configuration chargée"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Log de base de données
|
||
|
|
pub fn database_operation(operation: &str, table: &str, duration_ms: f64, success: bool) {
|
||
|
|
let level = if success { "debug" } else { "error" };
|
||
|
|
let message = if success {
|
||
|
|
"🗄️ Opération DB réussie"
|
||
|
|
} else {
|
||
|
|
"💥 Erreur DB"
|
||
|
|
};
|
||
|
|
|
||
|
|
match level {
|
||
|
|
"debug" => {
|
||
|
|
debug!(
|
||
|
|
event = "database_operation",
|
||
|
|
operation = %operation,
|
||
|
|
table = %table,
|
||
|
|
duration_ms = %duration_ms,
|
||
|
|
success = %success,
|
||
|
|
"{}", message
|
||
|
|
);
|
||
|
|
}
|
||
|
|
_ => {
|
||
|
|
error!(
|
||
|
|
event = "database_operation",
|
||
|
|
operation = %operation,
|
||
|
|
table = %table,
|
||
|
|
duration_ms = %duration_ms,
|
||
|
|
success = %success,
|
||
|
|
"{}", message
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Log de cache
|
||
|
|
pub fn cache_operation(operation: &str, key: &str, hit: bool, duration_ms: f64) {
|
||
|
|
debug!(
|
||
|
|
event = "cache_operation",
|
||
|
|
operation = %operation,
|
||
|
|
key = %key,
|
||
|
|
hit = %hit,
|
||
|
|
duration_ms = %duration_ms,
|
||
|
|
"💾 Opération de cache"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Log de sécurité
|
||
|
|
pub fn security_event(event_type: &str, user_id: Option<i32>, ip: &str, details: &str) {
|
||
|
|
warn!(
|
||
|
|
event = "security_event",
|
||
|
|
event_type = %event_type,
|
||
|
|
user_id = ?user_id,
|
||
|
|
ip_address = %ip,
|
||
|
|
details = %details,
|
||
|
|
"🔒 Événement de sécurité"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Wrapper pour les logs avec contexte utilisateur
|
||
|
|
pub struct UserContextLogger {
|
||
|
|
user_id: i32,
|
||
|
|
username: String,
|
||
|
|
ip_address: String,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UserContextLogger {
|
||
|
|
pub fn new(user_id: i32, username: String, ip_address: String) -> Self {
|
||
|
|
Self {
|
||
|
|
user_id,
|
||
|
|
username,
|
||
|
|
ip_address,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn info(&self, message: &str, fields: Option<&HashMap<String, String>>) {
|
||
|
|
info!(
|
||
|
|
user_id = %self.user_id,
|
||
|
|
username = %self.username,
|
||
|
|
ip_address = %self.ip_address,
|
||
|
|
additional_fields = ?fields,
|
||
|
|
"{}", message
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn warn(&self, message: &str, fields: Option<&HashMap<String, String>>) {
|
||
|
|
warn!(
|
||
|
|
user_id = %self.user_id,
|
||
|
|
username = %self.username,
|
||
|
|
ip_address = %self.ip_address,
|
||
|
|
additional_fields = ?fields,
|
||
|
|
"{}", message
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn error(&self, message: &str, fields: Option<&HashMap<String, String>>) {
|
||
|
|
error!(
|
||
|
|
user_id = %self.user_id,
|
||
|
|
username = %self.username,
|
||
|
|
ip_address = %self.ip_address,
|
||
|
|
additional_fields = ?fields,
|
||
|
|
"{}", message
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Initialise le système de logging depuis la configuration du serveur
|
||
|
|
pub fn init_logging_from_config(config: &ServerConfig) -> Result<StructuredLogging> {
|
||
|
|
let logging_config = config.logging.clone();
|
||
|
|
let structured_logging = StructuredLogging::new(logging_config)?;
|
||
|
|
structured_logging.setup()?;
|
||
|
|
|
||
|
|
chat_logs::config_loaded("server_config", None);
|
||
|
|
chat_logs::server_started(
|
||
|
|
&config.server.bind_addr.to_string(),
|
||
|
|
&config.server.environment.to_string(),
|
||
|
|
env!("CARGO_PKG_VERSION"),
|
||
|
|
);
|
||
|
|
|
||
|
|
Ok(structured_logging)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Configuration par défaut pour les tests
|
||
|
|
pub fn init_test_logging() -> Result<()> {
|
||
|
|
let config = LoggingConfig {
|
||
|
|
level: "debug".to_string(),
|
||
|
|
format: LogFormat::Compact,
|
||
|
|
file: None,
|
||
|
|
rotation: None,
|
||
|
|
filters: vec!["chat_server=debug".to_string()],
|
||
|
|
};
|
||
|
|
|
||
|
|
let structured_logging = StructuredLogging::new(config)?;
|
||
|
|
structured_logging.setup()?;
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
use std::collections::HashMap;
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_user_context_logger() {
|
||
|
|
let logger = UserContextLogger::new(1, "testuser".to_string(), "127.0.0.1".to_string());
|
||
|
|
|
||
|
|
// Test que le logger peut être créé sans erreur
|
||
|
|
assert_eq!(logger.user_id, 1);
|
||
|
|
assert_eq!(logger.username, "testuser");
|
||
|
|
assert_eq!(logger.ip_address, "127.0.0.1");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_chat_logs_macros() {
|
||
|
|
// Test que les macros de logging peuvent être appelées
|
||
|
|
// (elles ne feront rien en mode test sans subscriber)
|
||
|
|
chat_logs::auth_success(1, "testuser", "127.0.0.1");
|
||
|
|
chat_logs::message_sent(1, 1, "testuser", "room1", "text", 10);
|
||
|
|
chat_logs::system_error("test", "context", "error");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_structured_logging_creation() {
|
||
|
|
let config = LoggingConfig {
|
||
|
|
level: "info".to_string(),
|
||
|
|
format: LogFormat::Pretty,
|
||
|
|
file: None,
|
||
|
|
rotation: None,
|
||
|
|
filters: vec![],
|
||
|
|
};
|
||
|
|
|
||
|
|
let result = StructuredLogging::new(config);
|
||
|
|
assert!(result.is_ok());
|
||
|
|
}
|
||
|
|
}
|