veza/veza-chat-server/src/structured_logging.rs
2025-12-03 20:33:26 +01:00

500 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());
}
}