Aller au contenu principal

Logging

Configuration du logging et bonnes pratiques pour MyTelevision API.

Vue d'ensemble

MyTelevision API utilise le logging JSON structure pour toute la sortie de logs, permettant un parsing facile par les systemes d'agregation de logs comme Loki, ELK Stack ou Datadog.

Format des logs

Tous les logs sont emis au format JSON :

{
"timestamp": "2025-01-15T10:30:00.123Z",
"level": "info",
"context": "AuthService",
"message": "User logged in successfully",
"traceId": "abc123-def456",
"userId": "user-uuid",
"metadata": {
"ip": "192.168.1.100",
"userAgent": "MyTV/2.1.0"
}
}

Niveaux de log

NiveauUsageExemple
errorErreurs applicatives, exceptionsDatabase connection failed
warnConditions d'alerteRate limit approaching
infoOperations normalesUser logged in
debugInformations de debug detailleesQuery parameters
verboseDebug tres detailleFull request/response

Configuration par environnement

# Variable d'environnement
LOG_LEVEL=info # production
LOG_LEVEL=debug # staging
LOG_LEVEL=verbose # development

Configuration du Logger NestJS

Module Logger

// src/infrastructure/logger/logger.module.ts
import { Module, Global } from '@nestjs/common';
import { WinstonModule } from 'nest-winston';
import * as winston from 'winston';

@Global()
@Module({
imports: [
WinstonModule.forRoot({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
),
defaultMeta: { service: 'mytv-api' },
transports: [
new winston.transports.Console({
format:
process.env.NODE_ENV === 'production'
? winston.format.json()
: winston.format.combine(
winston.format.colorize(),
winston.format.simple(),
),
}),
],
}),
],
exports: [WinstonModule],
})
export class LoggerModule {}

Utilisation dans les services

import { Injectable, Logger } from '@nestjs/common';

@Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name);

async login(dto: LoginDto) {
this.logger.log('Login attempt', { email: dto.email });

try {
const user = await this.validateCredentials(dto);
this.logger.log('Login successful', {
userId: user.id,
email: user.email,
});
return user;
} catch (error) {
this.logger.warn('Login failed', {
email: dto.email,
reason: error.message,
});
throw error;
}
}
}

Logging des requetes

Middleware Request Logger

// src/infrastructure/middleware/request-logger.middleware.ts
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { v4 as uuidv4 } from 'uuid';

@Injectable()
export class RequestLoggerMiddleware implements NestMiddleware {
private readonly logger = new Logger('HTTP');

use(req: Request, res: Response, next: NextFunction) {
const traceId = (req.headers['x-trace-id'] as string) || uuidv4();
const startTime = Date.now();

// Attacher le trace ID a la requete
req['traceId'] = traceId;
res.setHeader('X-Trace-ID', traceId);

// Logger la requete
this.logger.log('Incoming request', {
traceId,
method: req.method,
path: req.path,
query: req.query,
ip: req.ip,
userAgent: req.headers['user-agent'],
});

// Logger la reponse
res.on('finish', () => {
const duration = Date.now() - startTime;
const logMethod = res.statusCode >= 400 ? 'warn' : 'log';

this.logger[logMethod]('Request completed', {
traceId,
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration,
contentLength: res.get('Content-Length'),
});
});

next();
}
}

Masquage des donnees sensibles

Sanitisation des logs

// src/infrastructure/logger/log-sanitizer.ts
const SENSITIVE_FIELDS = [
'password',
'token',
'accessToken',
'refreshToken',
'secret',
'apiKey',
'creditCard',
'cvv',
];

export function sanitizeLogData(data: any): any {
if (typeof data !== 'object' || data === null) {
return data;
}

const sanitized = { ...data };

for (const key of Object.keys(sanitized)) {
if (SENSITIVE_FIELDS.some((f) => key.toLowerCase().includes(f))) {
sanitized[key] = '[REDACTED]';
} else if (typeof sanitized[key] === 'object') {
sanitized[key] = sanitizeLogData(sanitized[key]);
}
}

return sanitized;
}

// Masquer un email
export function maskEmail(email: string): string {
const [local, domain] = email.split('@');
return `${local.charAt(0)}***@${domain}`;
}

// Masquer une IP
export function maskIp(ip: string): string {
const parts = ip.split('.');
if (parts.length === 4) {
return `${parts[0]}.${parts[1]}.xxx.xxx`;
}
return ip.replace(/:[^:]+$/, ':xxxx');
}

Agregation de logs

Loki + Promtail

# docker-compose.logging.yml
services:
loki:
image: grafana/loki:latest
ports:
- '3100:3100'
command: -config.file=/etc/loki/loki-config.yaml
volumes:
- ./monitoring/loki/loki-config.yaml:/etc/loki/loki-config.yaml
- loki-data:/loki

promtail:
image: grafana/promtail:latest
volumes:
- /var/log:/var/log
- ./monitoring/promtail/promtail-config.yaml:/etc/promtail/promtail-config.yaml
command: -config.file=/etc/promtail/promtail-config.yaml

volumes:
loki-data:

Configuration Promtail

# monitoring/promtail/promtail-config.yaml
server:
http_listen_port: 9080
grpc_listen_port: 0

positions:
filename: /tmp/positions.yaml

clients:
- url: http://loki:3100/loki/api/v1/push

scrape_configs:
- job_name: mytv-api
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
- source_labels: ['__meta_docker_container_name']
regex: '/(.*)'
target_label: 'container'
pipeline_stages:
- json:
expressions:
level: level
context: context
traceId: traceId
- labels:
level:
context:

Requetes Grafana (Loki)

{container="mytv-api"} | json | level="error"

Audit Logging

Service d'audit

// src/infrastructure/logger/audit.service.ts
@Injectable()
export class AuditService {
private readonly logger = new Logger('AUDIT');

async log(event: AuditEvent) {
this.logger.log('Audit event', {
type: 'AUDIT',
eventType: event.type,
actorId: event.actorId,
actorType: event.actorType,
resourceType: event.resourceType,
resourceId: event.resourceId,
action: event.action,
changes: event.changes,
ip: event.ip,
userAgent: event.userAgent,
timestamp: new Date().toISOString(),
});

// Persister egalement en base de donnees pour la conformite
await this.prisma.auditLog.create({
data: event,
});
}
}

Types d'evenements d'audit

Event TypeDescription
AUTH_LOGINTentative de connexion utilisateur
AUTH_LOGOUTDeconnexion utilisateur
AUTH_TOKEN_REFRESHRafraichissement de token
USER_CREATEUtilisateur cree
USER_UPDATEUtilisateur modifie
USER_DELETEUtilisateur supprime
PERMISSION_CHANGEPermissions modifiees
SUBSCRIPTION_CHANGEAbonnement modifie
ADMIN_ACTIONAction administrateur

Logging des erreurs

Exception Filter

// src/infrastructure/filters/exception.filter.ts
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger('ExceptionFilter');

catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const request = ctx.getRequest();
const response = ctx.getResponse();

const status =
exception instanceof HttpException ? exception.getStatus() : 500;

const errorResponse = {
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: exception instanceof Error ? exception.message : 'Unknown error',
};

// Logger l'erreur avec le contexte
this.logger.error('Unhandled exception', {
traceId: request.traceId,
error:
exception instanceof Error
? {
name: exception.name,
message: exception.message,
stack: exception.stack,
}
: exception,
request: {
method: request.method,
url: request.url,
userId: request.user?.id,
},
});

response.status(status).json(errorResponse);
}
}

Performance Logging

Slow Query Logging

// Prisma middleware pour les requetes lentes
prisma.$use(async (params, next) => {
const start = Date.now();
const result = await next(params);
const duration = Date.now() - start;

if (duration > 100) {
// Logger les requetes > 100ms
logger.warn('Slow query detected', {
model: params.model,
action: params.action,
duration,
args: JSON.stringify(params.args).substring(0, 200),
});
}

return result;
});

Retention des logs

EnvironnementRetentionStockage
Development7 joursLocal
Staging30 joursLoki
Production90 joursLoki + S3 archive
Audit logs7 ansPostgreSQL + S3

Exemples de recherche de logs

Trouver des erreurs par Trace ID

# Requete Loki
{container="mytv-api"} | json | traceId="abc123"

# Grep dans le container
docker logs mytv-api 2>&1 | grep "abc123"

Trouver toutes les erreurs de la derniere heure

# Requete Loki
{container="mytv-api"} | json | level="error" | line_format "{{.timestamp}} {{.message}}"

Trouver les echecs de connexion

# Requete Loki
{container="mytv-api"} | json | context="AuthService" |= "Login failed"

Bonnes pratiques

A faire

  • Utiliser le logging JSON structure
  • Inclure les trace IDs pour la correlation
  • Logger au niveau de log appropriate
  • Masquer les donnees sensibles
  • Inclure le contexte pertinent
  • Utiliser des noms de champs coherents

A ne pas faire

  • Logger des mots de passe ou tokens
  • Logger les corps complets de requete/reponse
  • Logger des donnees personnelles inutilement
  • Utiliser console.log en production
  • Logger au niveau debug en production
  • Creer un volume de logs excessif