15 KiB
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