veza/LOGGING_IMPLEMENTATION_EXAMPLES.md

15 KiB

💻 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)

// 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é

// 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

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

// Après création du logger
logger = WrapLoggerWithSecretFilter(logger)

1.3 Remplacer fmt.Println

Code Actuel (Problématique)

// internal/core/auth/service.go:233
fmt.Printf(">>> ERROR STRING: %s\n", result.Error.Error())

Code Corrigé

// 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)

// internal/handlers/auth.go:150-167
logger.Info("=== REGISTER HANDLER CALLED ===", ...)
logger.Info("=== BEFORE BindAndValidateJSON ===")
logger.Info("=== AFTER BindAndValidateJSON SUCCESS ===")

Code Corrigé

// 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

// 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é)

// 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é

// 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

// 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

// 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

// Dans les handlers
use axum::extract::Extension;

async fn send_message(
    Extension(request_id): Extension<String>,
    // ... autres paramètres
) -> Result<Json<ApiResponse<Message>>, 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

/**
 * 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<string, unknown>;
}

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<string, unknown>
  ) {
    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<void> {
    // 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<string, unknown>) {
    if (import.meta.env.DEV) {
      this.log('debug', message, context);
    }
  }

  info(message: string, context?: Record<string, unknown>) {
    this.log('info', message, context);
  }

  warn(message: string, context?: Record<string, unknown>) {
    this.log('warn', message, context);
  }

  error(message: string, context?: Record<string, unknown>) {
    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

// 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

// apps/web/src/services/api/auth.ts
console.log('Login attempt', { email });

Code Corrigé

// 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

# 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
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

// 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