# đŸ’» EXEMPLES D'IMPLÉMENTATION - SYSTÈME DE LOGS VEZA **Date** : 2025-01-27 **Version** : 1.0 Ce document contient des exemples de code concrets pour implĂ©menter les corrections identifiĂ©es dans l'audit. --- ## 1. BACKEND GO - CORRECTIONS ### 1.1 Corriger la Configuration du Logger #### ❌ Code Actuel (ProblĂ©matique) ```go // internal/config/config.go:205 logger, err := zap.NewProduction() // Ignore LOG_LEVEL if err != nil { return nil, err } // ... plus tard ligne 221 logLevel := getEnv("LOG_LEVEL", "INFO") // Lu aprĂšs l'initialisation ``` #### ✅ Code CorrigĂ© ```go // internal/config/config.go func NewConfig() (*Config, error) { env := DetectEnvironment() // Lire LOG_LEVEL EN PREMIER logLevel := getEnv("LOG_LEVEL", "INFO") // CrĂ©er la configuration du logger selon l'environnement var zapConfig zap.Config if env == "production" { zapConfig = zap.NewProductionConfig() zapConfig.Encoding = "json" } else { zapConfig = zap.NewDevelopmentConfig() zapConfig.Encoding = "console" } // Configurer le niveau de log level, err := zapcore.ParseLevel(logLevel) if err != nil { level = zapcore.InfoLevel } zapConfig.Level = zap.NewAtomicLevelAt(level) // CrĂ©er le logger logger, err := zapConfig.Build( zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel), ) if err != nil { return nil, fmt.Errorf("failed to create logger: %w", err) } // Si agrĂ©gation activĂ©e, remplacer par logger avec agrĂ©gation if config.LogAggregationEnabled && config.LogAggregationEndpoint != "" { aggConfig := &logging.AggregationConfig{ EndpointURL: config.LogAggregationEndpoint, Enabled: true, BatchSize: config.LogAggregationBatchSize, FlushInterval: config.LogAggregationFlushInterval, Timeout: config.LogAggregationTimeout, Labels: config.LogAggregationLabels, } aggLogger, err := logging.NewLoggerWithAggregation(env, logLevel, aggConfig) if err != nil { logger.Warn("Failed to initialize logger with aggregation, using standard logger", zap.Error(err), ) } else { logger = aggLogger.GetZapLogger() } } config.Logger = logger // ... } ``` ### 1.2 ImplĂ©menter le Filtre de Secrets #### Nouveau Fichier : `internal/logging/secret_filter.go` ```go package logging import ( "strings" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // SecretFilterCore filtre les champs sensibles dans les logs type SecretFilterCore struct { core zapcore.Core } // NewSecretFilterCore crĂ©e un core qui filtre les secrets func NewSecretFilterCore(core zapcore.Core) zapcore.Core { return &SecretFilterCore{core: core} } // Enabled retourne si le niveau est activĂ© func (f *SecretFilterCore) Enabled(level zapcore.Level) bool { return f.core.Enabled(level) } // With ajoute des champs func (f *SecretFilterCore) With(fields []zapcore.Field) zapcore.Core { return &SecretFilterCore{core: f.core.With(f.filterFields(fields))} } // Check vĂ©rifie si l'entrĂ©e doit ĂȘtre loggĂ©e func (f *SecretFilterCore) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry { return f.core.Check(entry, checked) } // Write Ă©crit l'entrĂ©e en filtrant les secrets func (f *SecretFilterCore) Write(entry zapcore.Entry, fields []zapcore.Field) error { return f.core.Write(entry, f.filterFields(fields)) } // Sync synchronise le core func (f *SecretFilterCore) Sync() error { return f.core.Sync() } // filterFields filtre les champs sensibles func (f *SecretFilterCore) filterFields(fields []zapcore.Field) []zapcore.Field { filtered := make([]zapcore.Field, 0, len(fields)) for _, field := range fields { if f.isSecretField(field.Key) { // Remplacer la valeur par [REDACTED] filtered = append(filtered, zap.String(field.Key, "[REDACTED]")) } else { filtered = append(filtered, field) } } return filtered } // isSecretField vĂ©rifie si un champ contient des informations sensibles func (f *SecretFilterCore) isSecretField(key string) bool { keyLower := strings.ToLower(key) secretPatterns := []string{ "password", "secret", "token", "key", "credential", "api_key", "access_key", "private_key", "jwt_secret", "auth_token", "refresh_token", } for _, pattern := range secretPatterns { if strings.Contains(keyLower, pattern) { return true } } return false } // WrapLoggerWithSecretFilter enveloppe un logger avec le filtre de secrets func WrapLoggerWithSecretFilter(logger *zap.Logger) *zap.Logger { core := logger.Core() filteredCore := NewSecretFilterCore(core) return zap.New(filteredCore, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel)) } ``` #### Utilisation dans config.go ```go // AprĂšs crĂ©ation du logger logger = WrapLoggerWithSecretFilter(logger) ``` ### 1.3 Remplacer fmt.Println #### ❌ Code Actuel (ProblĂ©matique) ```go // internal/core/auth/service.go:233 fmt.Printf(">>> ERROR STRING: %s\n", result.Error.Error()) ``` #### ✅ Code CorrigĂ© ```go // internal/core/auth/service.go logger.Error("Registration failed", zap.Error(err), zap.String("request_id", requestID), zap.String("email", email), zap.String("username", username), ) ``` #### ❌ Code Actuel (Logs de Debug Excessifs) ```go // internal/handlers/auth.go:150-167 logger.Info("=== REGISTER HANDLER CALLED ===", ...) logger.Info("=== BEFORE BindAndValidateJSON ===") logger.Info("=== AFTER BindAndValidateJSON SUCCESS ===") ``` #### ✅ Code CorrigĂ© ```go // internal/handlers/auth.go logger.Debug("Register handler called", zap.String("path", c.Request.URL.Path), zap.String("method", c.Request.Method), ) // ... dans le code logger.Debug("Before BindAndValidateJSON") if err := c.BindAndValidateJSON(&req); err != nil { // ... } logger.Debug("After BindAndValidateJSON", zap.String("email", req.Email), ) ``` ### 1.4 Propager Request ID vers Services Rust #### Dans les Services qui Appellent Rust ```go // internal/services/chat_service.go func (s *ChatService) SendMessage(ctx context.Context, requestID string, message string) error { req, err := http.NewRequestWithContext(ctx, "POST", s.chatServerURL+"/messages", body) if err != nil { return err } // Propager le Request ID req.Header.Set("X-Request-ID", requestID) // Propager le Trace ID si disponible if traceID := ctx.Value("trace_id"); traceID != nil { req.Header.Set("X-Trace-ID", traceID.(string)) } resp, err := s.httpClient.Do(req) // ... } ``` --- ## 2. SERVICES RUST - CORRECTIONS ### 2.1 Utiliser veza-common::logging #### ❌ Code Actuel (DupliquĂ©) ```rust // veza-chat-server/src/main.rs:84-101 let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")); let is_prod = std::env::var("APP_ENV").unwrap_or_default() == "production"; if is_prod { tracing_subscriber::fmt() .with_env_filter(env_filter) .json() .init(); } else { tracing_subscriber::fmt() .with_env_filter(env_filter) .init(); } ``` #### ✅ Code CorrigĂ© ```rust // veza-chat-server/src/main.rs use veza_common::logging; #[tokio::main] async fn main() -> Result<(), ChatError> { // Lire LOG_LEVEL (standardisĂ©) let log_level = std::env::var("LOG_LEVEL") .unwrap_or_else(|_| "info".to_string()); let is_prod = std::env::var("APP_ENV").unwrap_or_default() == "production"; let config = logging::LoggingConfig { level: log_level, format: if is_prod { "json" } else { "text" }.to_string(), file: Some("/var/log/veza/chat.log".to_string()), max_size: 100 * 1024 * 1024, // 100MB max_files: 5, compress: true, }; logging::init_with_config(config)?; // ... } ``` ### 2.2 Extraire Request ID des Headers #### Middleware pour Extraire Request ID ```rust // veza-chat-server/src/middleware/request_id.rs use axum::{ extract::Request, middleware::Next, response::Response, }; use tracing::{info_span, Instrument}; pub async fn request_id_middleware( mut request: Request, next: Next, ) -> Response { // Extraire X-Request-ID du header let request_id = request.headers() .get("x-request-id") .and_then(|h| h.to_str().ok()) .map(|s| s.to_string()) .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); // Extraire X-Trace-ID si prĂ©sent let trace_id = request.headers() .get("x-trace-id") .and_then(|h| h.to_str().ok()) .map(|s| s.to_string()); // Ajouter au contexte de la requĂȘte request.extensions_mut().insert(request_id.clone()); if let Some(tid) = trace_id { request.extensions_mut().insert(tid); } // CrĂ©er un span avec le request_id let span = info_span!( "request", request_id = %request_id, trace_id = ?trace_id, ); // ExĂ©cuter la requĂȘte dans le span next.run(request).instrument(span).await } ``` #### Utilisation dans main.rs ```rust // veza-chat-server/src/main.rs use axum::middleware; let app = Router::new() .route("/messages", post(send_message)) .layer(middleware::from_fn(request_id_middleware)); ``` ### 2.3 Utiliser Request ID dans les Logs ```rust // Dans les handlers use axum::extract::Extension; async fn send_message( Extension(request_id): Extension, // ... autres paramĂštres ) -> Result>, StatusCode> { // Le request_id est automatiquement disponible via Extension tracing::info!( request_id = %request_id, "Sending message", conversation_id = %conversation_id, ); // ... } ``` --- ## 3. FRONTEND REACT - CORRECTIONS ### 3.1 Logger StructurĂ© #### Nouveau Fichier : `apps/web/src/utils/structuredLogger.ts` ```typescript /** * Logger structurĂ© pour Veza Frontend * Remplace console.log par un systĂšme de logging structurĂ© avec corrĂ©lation */ export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; export interface LogEntry { level: LogLevel; message: string; timestamp: string; service: string; env: string; request_id?: string; user_id?: string; context?: Record; } class StructuredLogger { private requestId?: string; private userId?: string; private service = 'veza-web'; private env: string; constructor() { this.env = import.meta.env.MODE || 'development'; } /** * DĂ©finit le Request ID (extrait des rĂ©ponses API) */ setRequestId(requestId: string) { this.requestId = requestId; } /** * DĂ©finit l'User ID (depuis le store d'authentification) */ setUserId(userId: string) { this.userId = userId; } /** * Log une entrĂ©e */ private log( level: LogLevel, message: string, context?: Record ) { const entry: LogEntry = { level, message, timestamp: new Date().toISOString(), service: this.service, env: this.env, request_id: this.requestId, user_id: this.userId, context, }; // En production, envoyer vers endpoint backend (optionnel) if (import.meta.env.PROD) { this.sendToBackend(entry).catch(() => { // En cas d'Ă©chec, fallback vers console this.logToConsole(entry); }); } else { // En dĂ©veloppement, afficher dans la console this.logToConsole(entry); } } /** * Affiche dans la console (dĂ©veloppement) */ private logToConsole(entry: LogEntry) { const prefix = `[${entry.level.toUpperCase()}]`; const json = JSON.stringify(entry, null, 2); switch (entry.level) { case 'debug': console.debug(prefix, json); break; case 'info': console.info(prefix, json); break; case 'warn': console.warn(prefix, json); break; case 'error': console.error(prefix, json); break; } } /** * Envoie vers endpoint backend (production) */ private async sendToBackend(entry: LogEntry): Promise { // Optionnel : envoyer vers /api/v1/logs // Pour l'instant, on ne fait rien (peut ĂȘtre activĂ© plus tard) } /** * MĂ©thodes publiques */ debug(message: string, context?: Record) { if (import.meta.env.DEV) { this.log('debug', message, context); } } info(message: string, context?: Record) { this.log('info', message, context); } warn(message: string, context?: Record) { this.log('warn', message, context); } error(message: string, context?: Record) { this.log('error', message, context); } } // Instance singleton export const logger = new StructuredLogger(); // Export par dĂ©faut export default logger; ``` ### 3.2 Extraire Request ID des RĂ©ponses API #### Modifier le Client API ```typescript // apps/web/src/services/api/client.ts import { logger } from '@/utils/structuredLogger'; // Dans la fonction qui fait les requĂȘtes const response = await fetch(url, options); // Extraire X-Request-ID de la rĂ©ponse const requestId = response.headers.get('X-Request-ID'); if (requestId) { logger.setRequestId(requestId); } return response; ``` ### 3.3 Remplacer console.log #### ❌ Code Actuel ```typescript // apps/web/src/services/api/auth.ts console.log('Login attempt', { email }); ``` #### ✅ Code CorrigĂ© ```typescript // apps/web/src/services/api/auth.ts import { logger } from '@/utils/structuredLogger'; logger.info('Login attempt', { email }); ``` --- ## 4. CONFIGURATION DOCKER-COMPOSE ### Ajouter Loki pour l'AgrĂ©gation ```yaml # docker-compose.yml services: loki: image: grafana/loki:latest ports: - "3100:3100" command: -config.file=/etc/loki/local-config.yaml volumes: - loki-data:/loki grafana: image: grafana/grafana:latest ports: - "3001:3000" environment: - GF_SECURITY_ADMIN_PASSWORD=admin volumes: - grafana-data:/var/lib/grafana depends_on: - loki volumes: loki-data: grafana-data: ``` ### Configuration Backend Go ```env # .env LOG_AGGREGATION_ENABLED=true LOG_AGGREGATION_ENDPOINT=http://loki:3100/loki/api/v1/push LOG_AGGREGATION_BATCH_SIZE=100 LOG_AGGREGATION_FLUSH_INTERVAL=5s ``` --- ## 5. TESTS ### Test de CorrĂ©lation ```go // tests/integration/logging_correlation_test.go func TestRequestIDPropagation(t *testing.T) { // Faire une requĂȘte au backend req := httptest.NewRequest("POST", "/api/v1/tracks", body) req.Header.Set("X-Request-ID", "test-request-id-123") w := httptest.NewRecorder() router.ServeHTTP(w, req) // VĂ©rifier que le Request ID est propagĂ© assert.Equal(t, "test-request-id-123", w.Header().Get("X-Request-ID")) // VĂ©rifier que les logs contiennent le Request ID // (nĂ©cessite un logger de test qui capture les logs) } ``` --- **Fin des Exemples d'ImplĂ©mentation**