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
| Niveau | Usage | Exemple |
|---|---|---|
error | Erreurs applicatives, exceptions | Database connection failed |
warn | Conditions d'alerte | Rate limit approaching |
info | Operations normales | User logged in |
debug | Informations de debug detaillees | Query parameters |
verbose | Debug tres detaille | Full 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 Type | Description |
|---|---|
AUTH_LOGIN | Tentative de connexion utilisateur |
AUTH_LOGOUT | Deconnexion utilisateur |
AUTH_TOKEN_REFRESH | Rafraichissement de token |
USER_CREATE | Utilisateur cree |
USER_UPDATE | Utilisateur modifie |
USER_DELETE | Utilisateur supprime |
PERMISSION_CHANGE | Permissions modifiees |
SUBSCRIPTION_CHANGE | Abonnement modifie |
ADMIN_ACTION | Action 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
| Environnement | Retention | Stockage |
|---|---|---|
| Development | 7 jours | Local |
| Staging | 30 jours | Loki |
| Production | 90 jours | Loki + S3 archive |
| Audit logs | 7 ans | PostgreSQL + 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