- Archiver 131 .md dans docs/archive/root-md/ - Archiver 22 .json dans docs/archive/root-json/ - Conserver 7 .md utiles (README, CONTRIBUTING, CHANGELOG, etc.) - Conserver package.json, package-lock.json, turbo.json - Ajouter README d'index dans chaque archive
634 lines
15 KiB
Markdown
634 lines
15 KiB
Markdown
# 💻 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<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`
|
|
```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<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
|
|
```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**
|
|
|