veza/veza-common/src/logging.rs
senke 0e7b6fede1 [T0-002] fix(rust): Corriger erreurs compilation Rust
- Conflit SQLx résolu (alignement sur version 0.7)
- build.rs configurés pour protoc dans chat/stream servers
- API Prometheus migrée vers HistogramOpts
- Traits Display/Debug corrigés (String au lieu de &dyn Display)
- API TOTP corrigée (totp-rs 5.4 avec Secret::Encoded)
- Layers tracing-subscriber corrigés (types conditionnels)
- VezaError/VezaResult exportés dans lib.rs
- TransactionProvider simplifié (retour void au lieu de Box<dyn>)
- VezaConfig contraint Serialize pour to_json()

Files: veza-common/Cargo.toml, veza-common/src/*.rs, veza-chat-server/Cargo.toml, veza-chat-server/build.rs, veza-stream-server/Cargo.toml, veza-stream-server/build.rs, VEZA_ROADMAP.json
Hours: 8 estimated, 3 actual
2026-01-04 01:44:20 +01:00

585 lines
18 KiB
Rust

//! Logging utilities for Veza Rust services
//!
//! This module provides centralized logging configuration and utilities.
//! FIX #14: Support rotation des logs avec tracing-appender
use tracing::Level;
use tracing_subscriber::{
fmt::{self, format::FmtSpan},
layer::SubscriberExt,
util::SubscriberInitExt,
filter::LevelFilter,
EnvFilter, Registry, Layer,
};
use tracing_appender::{
non_blocking::WorkerGuard,
rolling::{RollingFileAppender, Rotation},
};
use crate::error::server::{VezaError, VezaResult};
use std::path::PathBuf;
/// FIX #24: Normaliser le niveau de log depuis LOG_LEVEL ou RUST_LOG
/// Convertit les niveaux Go (INFO, DEBUG, WARN, ERROR) vers les niveaux Rust (info, debug, warn, error)
fn normalize_log_level() -> String {
// FIX #24: Lire LOG_LEVEL en priorité (standardisé), fallback sur RUST_LOG
let log_level = std::env::var("LOG_LEVEL")
.or_else(|_| std::env::var("RUST_LOG"))
.unwrap_or_else(|_| "INFO".to_string());
// Normaliser les niveaux Go vers Rust (case-insensitive)
let log_level_upper = log_level.to_uppercase();
let normalized = match log_level_upper.as_str() {
"DEBUG" => "debug".to_string(),
"INFO" => "info".to_string(),
"WARN" => "warn".to_string(),
"ERROR" => "error".to_string(),
"TRACE" => "trace".to_string(),
// Si déjà en format Rust, utiliser tel quel
_ => log_level.to_lowercase(),
};
normalized
}
/// Initialize logging for Veza services
pub fn init() -> VezaResult<()> {
// FIX #24: Utiliser LOG_LEVEL standardisé
let log_level = normalize_log_level();
std::env::set_var("RUST_LOG", &log_level);
// Create environment filter
let env_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(&log_level));
// Create formatting layer
let fmt_layer = fmt::layer()
.with_target(true)
.with_thread_ids(true)
.with_thread_names(true)
.with_span_events(FmtSpan::CLOSE)
.with_file(true)
.with_line_number(true)
.with_ansi(true);
// Initialize the subscriber
Registry::default()
.with(env_filter)
.with(fmt_layer)
.init();
tracing::info!("Logging initialized");
Ok(())
}
/// Initialize logging with custom configuration
/// FIX #14: Support rotation des logs si config.file est défini
/// Note: Le WorkerGuard doit être gardé en vie par l'appelant pour maintenir la rotation active
/// Pour l'instant, on utilise une variable statique leakée (acceptable car le guard doit vivre toute la durée de vie de l'app)
pub fn init_with_config(config: LoggingConfig) -> VezaResult<()> {
// FIX #24: Normaliser le niveau de log depuis la config ou LOG_LEVEL
let log_level = if !config.level.is_empty() && config.level != "info" {
// Si un niveau est fourni dans la config, normaliser les niveaux Go vers Rust
match config.level.to_uppercase().as_str() {
"DEBUG" => "debug".to_string(),
"INFO" => "info".to_string(),
"WARN" => "warn".to_string(),
"ERROR" => "error".to_string(),
"TRACE" => "trace".to_string(),
_ => config.level.to_lowercase(),
}
} else {
// Si config.level est vide ou "info" (valeur par défaut), utiliser LOG_LEVEL ou RUST_LOG
normalize_log_level()
};
std::env::set_var("RUST_LOG", &log_level);
// Create environment filter
let env_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(&log_level));
// FIX #14: Configurer la rotation des logs si un fichier est spécifié
// Créer deux fichiers : module.log (tous les logs) et module-error.log (erreurs uniquement)
let (file_writer, error_file_writer, guard) = if let Some(file_path) = &config.file {
let path = PathBuf::from(file_path);
let log_dir = path.parent()
.ok_or_else(|| VezaError::Config("Invalid log file path".to_string()))?;
let file_prefix = path.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| VezaError::Config("Invalid log file name".to_string()))?;
// Déterminer la rotation selon max_size
let rotation = if config.max_size > 100 * 1024 * 1024 {
// > 100MB -> rotation quotidienne
Rotation::DAILY
} else {
// <= 100MB -> rotation horaire
Rotation::HOURLY
};
// Fichier pour tous les logs
let file_appender = RollingFileAppender::new(rotation.clone(), log_dir, file_prefix);
let (non_blocking, worker_guard) = tracing_appender::non_blocking(file_appender);
// Fichier pour les erreurs uniquement
let error_file_prefix = format!("{}-error", file_prefix);
let error_file_appender = RollingFileAppender::new(rotation, log_dir, &error_file_prefix);
let (error_non_blocking, error_worker_guard) = tracing_appender::non_blocking(error_file_appender);
// FIX #14: Le WorkerGuard doit être gardé en vie pour maintenir la rotation active
// On utilise Box::leak pour le garder en vie pendant toute la durée de vie de l'application
Box::leak(Box::new(worker_guard));
Box::leak(Box::new(error_worker_guard));
(Some(non_blocking), Some(error_non_blocking), true)
} else {
(None, None, false)
};
// FIX #25: Standardiser sur JSON en production/staging, texte en développement
// Create formatting layer based on format
let base_fmt_layer = match config.format.as_str() {
"json" => {
// Use JSON formatter with json feature enabled
fmt::layer()
.with_target(true)
.with_thread_ids(true)
.with_thread_names(true)
.with_span_events(FmtSpan::CLOSE)
.with_file(true)
.with_line_number(true)
},
"text" => {
fmt::layer()
.with_target(true)
.with_thread_ids(true)
.with_thread_names(true)
.with_span_events(FmtSpan::CLOSE)
.with_file(true)
.with_line_number(true)
.with_ansi(true)
},
_ => return Err(VezaError::Config(format!("Invalid log format: {}", config.format))),
};
// FIX #14: Ajouter des layers pour les fichiers si rotation configurée
// Construire le registry de manière conditionnelle pour éviter les problèmes de types
// Initialiser directement dans chaque branche car chaque .with() crée un nouveau type
// Note: file_writer et error_file_writer sont déplacés dans chaque branche
match (file_writer, error_file_writer) {
(Some(writer), Some(error_writer)) => {
// Les deux fichiers sont configurés
let file_layer = fmt::layer()
.with_writer(writer)
.with_target(true)
.with_thread_ids(true)
.with_thread_names(true)
.with_span_events(FmtSpan::CLOSE)
.with_file(true)
.with_line_number(true);
let error_file_layer = fmt::layer()
.with_writer(error_writer)
.with_target(true)
.with_thread_ids(true)
.with_thread_names(true)
.with_span_events(FmtSpan::CLOSE)
.with_file(true)
.with_line_number(true)
.with_filter(LevelFilter::ERROR);
Registry::default()
.with(env_filter)
.with(base_fmt_layer)
.with(file_layer)
.with(error_file_layer)
.init();
},
(Some(writer), None) => {
// Seulement le fichier principal est configuré
let file_layer = fmt::layer()
.with_writer(writer)
.with_target(true)
.with_thread_ids(true)
.with_thread_names(true)
.with_span_events(FmtSpan::CLOSE)
.with_file(true)
.with_line_number(true);
Registry::default()
.with(env_filter)
.with(base_fmt_layer)
.with(file_layer)
.init();
},
(None, Some(error_writer)) => {
// Seulement le fichier d'erreur est configuré
let error_file_layer = fmt::layer()
.with_writer(error_writer)
.with_target(true)
.with_thread_ids(true)
.with_thread_names(true)
.with_span_events(FmtSpan::CLOSE)
.with_file(true)
.with_line_number(true)
.with_filter(LevelFilter::ERROR);
Registry::default()
.with(env_filter)
.with(base_fmt_layer)
.with(error_file_layer)
.init();
},
(None, None) => {
// Aucun fichier configuré
Registry::default()
.with(env_filter)
.with(base_fmt_layer)
.init();
},
}
if guard {
tracing::info!(
file = ?config.file,
max_size = config.max_size,
max_files = config.max_files,
"Logging initialized with file rotation"
);
} else {
tracing::info!("Logging initialized with custom config");
}
Ok(())
}
/// Logging configuration
#[derive(Debug, Clone)]
pub struct LoggingConfig {
pub level: String,
pub format: String,
pub file: Option<String>,
pub max_size: u64,
pub max_files: u32,
pub compress: bool,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: "info".to_string(),
format: "json".to_string(),
file: None,
max_size: 100 * 1024 * 1024, // 100MB
max_files: 5,
compress: true,
}
}
}
/// Structured logging macros
#[macro_export]
macro_rules! log_info {
($($arg:tt)*) => {
tracing::info!($($arg)*)
};
}
#[macro_export]
macro_rules! log_warn {
($($arg:tt)*) => {
tracing::warn!($($arg)*)
};
}
#[macro_export]
macro_rules! log_error {
($($arg:tt)*) => {
tracing::error!($($arg)*)
};
}
#[macro_export]
macro_rules! log_debug {
($($arg:tt)*) => {
tracing::debug!($($arg)*)
};
}
#[macro_export]
macro_rules! log_trace {
($($arg:tt)*) => {
tracing::trace!($($arg)*)
};
}
/// Log performance metrics
pub fn log_performance(operation: &str, duration: std::time::Duration, metadata: &[(&str, String)]) {
let mut fields = std::collections::HashMap::new();
for (key, value) in metadata {
fields.insert(key.to_string(), value.clone());
}
tracing::info!(
operation = operation,
duration_ms = duration.as_millis(),
metadata = ?fields,
"Performance metric"
);
}
/// Log security events
pub fn log_security_event(event: &str, user_id: Option<uuid::Uuid>, ip_address: Option<&str>, metadata: &[(&str, String)]) {
let mut fields = std::collections::HashMap::new();
for (key, value) in metadata {
fields.insert(key.to_string(), value.clone());
}
tracing::warn!(
event = event,
user_id = ?user_id,
ip_address = ip_address,
metadata = ?fields,
"Security event"
);
}
/// Log business events
pub fn log_business_event(event: &str, user_id: Option<uuid::Uuid>, resource_id: Option<uuid::Uuid>, metadata: &[(&str, String)]) {
let mut fields = std::collections::HashMap::new();
for (key, value) in metadata {
fields.insert(key.to_string(), value.clone());
}
tracing::info!(
event = event,
user_id = ?user_id,
resource_id = ?resource_id,
metadata = ?fields,
"Business event"
);
}
/// Log system events
pub fn log_system_event(event: &str, component: &str, metadata: &[(&str, String)]) {
let mut fields = std::collections::HashMap::new();
for (key, value) in metadata {
fields.insert(key.to_string(), value.clone());
}
tracing::info!(
event = event,
component = component,
metadata = ?fields,
"System event"
);
}
/// Log error with context
pub fn log_error_with_context(error: &VezaError, context: &[(&str, String)]) {
let mut fields = std::collections::HashMap::new();
for (key, value) in context {
fields.insert(key.to_string(), value.clone());
}
if error.should_log() {
tracing::error!(
error = %error,
context = ?fields,
"Error occurred"
);
} else {
tracing::debug!(
error = %error,
context = ?fields,
"Error occurred"
);
}
}
/// Log request/response
pub fn log_request_response(
method: &str,
path: &str,
status_code: u16,
duration: std::time::Duration,
user_id: Option<uuid::Uuid>,
request_id: Option<&str>,
) {
let level = if status_code >= 500 {
Level::ERROR
} else if status_code >= 400 {
Level::WARN
} else {
Level::INFO
};
match level {
Level::ERROR => tracing::error!(
method = method,
path = path,
status_code = status_code,
duration_ms = duration.as_millis(),
user_id = ?user_id,
request_id = request_id,
"HTTP request"
),
Level::WARN => tracing::warn!(
method = method,
path = path,
status_code = status_code,
duration_ms = duration.as_millis(),
user_id = ?user_id,
request_id = request_id,
"HTTP request"
),
Level::INFO => tracing::info!(
method = method,
path = path,
status_code = status_code,
duration_ms = duration.as_millis(),
user_id = ?user_id,
request_id = request_id,
"HTTP request"
),
Level::DEBUG => tracing::debug!(
method = method,
path = path,
status_code = status_code,
duration_ms = duration.as_millis(),
user_id = ?user_id,
request_id = request_id,
"HTTP request"
),
Level::TRACE => tracing::trace!(
method = method,
path = path,
status_code = status_code,
duration_ms = duration.as_millis(),
user_id = ?user_id,
request_id = request_id,
"HTTP request"
),
}
}
/// Log database query
pub fn log_db_query(
query: &str,
duration: std::time::Duration,
rows_affected: Option<u64>,
error: Option<&str>,
) {
if let Some(error) = error {
tracing::error!(
query = query,
duration_ms = duration.as_millis(),
rows_affected = ?rows_affected,
error = error,
"Database query failed"
);
} else {
tracing::debug!(
query = query,
duration_ms = duration.as_millis(),
rows_affected = ?rows_affected,
"Database query"
);
}
}
/// Log cache operations
pub fn log_cache_operation(
operation: &str,
key: &str,
hit: bool,
duration: std::time::Duration,
error: Option<&str>,
) {
if let Some(error) = error {
tracing::warn!(
operation = operation,
key = key,
hit = hit,
duration_ms = duration.as_millis(),
error = error,
"Cache operation failed"
);
} else {
tracing::debug!(
operation = operation,
key = key,
hit = hit,
duration_ms = duration.as_millis(),
"Cache operation"
);
}
}
/// Log WebSocket events
pub fn log_websocket_event(
event: &str,
user_id: Option<uuid::Uuid>,
connection_id: Option<uuid::Uuid>,
metadata: &[(&str, String)],
) {
let mut fields = std::collections::HashMap::new();
for (key, value) in metadata {
fields.insert(key.to_string(), value.clone());
}
tracing::info!(
event = event,
user_id = ?user_id,
connection_id = ?connection_id,
metadata = ?fields,
"WebSocket event"
);
}
/// Log streaming events
pub fn log_streaming_event(
event: &str,
track_id: Option<uuid::Uuid>,
user_id: Option<uuid::Uuid>,
metadata: &[(&str, String)],
) {
let mut fields = std::collections::HashMap::new();
for (key, value) in metadata {
fields.insert(key.to_string(), value.clone());
}
tracing::info!(
event = event,
track_id = ?track_id,
user_id = ?user_id,
metadata = ?fields,
"Streaming event"
);
}
/// Create a span for tracing
pub fn create_span(_name: &str, metadata: &[(&str, String)]) -> tracing::Span {
let mut fields = std::collections::HashMap::new();
for (key, value) in metadata {
fields.insert(key.to_string(), value.clone());
}
tracing::info_span!("span", metadata = ?fields)
}
/// Log startup information
pub fn log_startup(service_name: &str, version: &str, config: &[(&str, String)]) {
let mut fields = std::collections::HashMap::new();
for (key, value) in config {
fields.insert(key.to_string(), value.clone());
}
tracing::info!(
service = service_name,
version = version,
config = ?fields,
"Service starting"
);
}
/// Log shutdown information
pub fn log_shutdown(service_name: &str, uptime: std::time::Duration) {
tracing::info!(
service = service_name,
uptime_seconds = uptime.as_secs(),
"Service shutting down"
);
}