01 — Logs structurés : JSON, corrélation, niveaux
Ce que tu vas apprendre
- Pourquoi les logs texte sont impossibles à exploiter à l'échelle
- Le format JSON structuré et ses champs obligatoires
- Les niveaux de log et quand utiliser chacun
- Le correlation ID : relier les logs d'une même requête dans un système distribué
- Implémentation complète avec pino (TypeScript) et structlog (Python)
Prérequis
Logs texte vs logs structurés
Un log texte classique :
[2026-06-01 10:23:41] ERROR Payment failed for order abc-123: card declined after 3 attempts
Lisible par un humain. Inutilisable par une machine. Pour extraire l'orderId, le reason ou le nombre de tentatives, il faut un parser regex — fragile, coûteux, et différent pour chaque service.
Un log structuré en JSON :
json{
"timestamp": "2026-06-01T10:23:41.123Z",
"level": "error",
"event": "payment.failed",
"orderId": "abc-123",
"reason": "card_declined",
"attempts": 3,
"service": "payment-service",
"requestId": "req-789xyz"
}
Chaque champ est requêtable directement. Dans Kibana, Loki ou CloudWatch : orderId = "abc-123" AND level = "error". Pas de regex.
Un log structuré est un événement, pas un message.
La distinction compte : un message s'adresse à un humain, un événement s'adresse à une machine qui l'agrège, le filtre et le corrèle.
Les champs obligatoires
Tout log structuré doit contenir au minimum :
typescriptinterface LogEntry {
timestamp: string; // ISO 8601 — "2026-06-01T10:23:41.123Z"
level: "debug" | "info" | "warn" | "error" | "fatal";
event: string; // Nom de l'événement — "order.created", "payment.failed"
service: string; // Nom du service — "order-api", "payment-worker"
requestId?: string; // Correlation ID — relier tous les logs d'une même requête
}
Et selon le contexte, des champs domaine :
json{
"timestamp": "2026-06-01T10:23:41.123Z",
"level": "info",
"event": "order.created",
"service": "order-api",
"requestId": "req-abc123",
"orderId": "ord-456",
"customerId": "cust-789",
"totalCents": 4990,
"durationMs": 45
}
Les niveaux de log
| Niveau | Quand l'utiliser | Exemple |
|---|---|---|
debug |
Informations de développement — désactivé en prod | Paramètres d'une requête SQL |
info |
Événements métier normaux | Commande créée, paiement reçu |
warn |
Situation anormale mais récupérée | Retry réussi, fallback utilisé |
error |
Erreur qui affecte l'utilisateur mais pas le service | Paiement refusé, validation échouée |
fatal |
Erreur qui arrête le service | Connexion BDD perdue, port déjà utilisé |
Règle clé : ne pas logger en error ce qui est un comportement attendu. Un paiement refusé par la banque est un info ou warn — c'est une réponse normale, pas une erreur système. Un timeout vers la BDD est un error.
Implémentation TypeScript avec pino
pino est le logger Node.js le plus rapide — il sérialise en JSON nativement.
bashnpm install pino pino-http
typescript// logger.ts
import pino from "pino";
export const logger = pino({
level: process.env.LOG_LEVEL || "info",
base: {
service: "order-api",
version: process.env.APP_VERSION || "unknown",
},
timestamp: pino.stdTimeFunctions.isoTime,
// En développement, activer pretty-print avec pino-pretty
// En production : JSON brut (plus rapide, agrégé par Loki/ELK)
});
Utilisation dans un handler :
typescript// order.handler.ts
import { logger } from "./logger";
import type { Request, Response } from "express";
export async function createOrder(req: Request, res: Response): Promise<void> {
const requestLogger = logger.child({
requestId: req.headers["x-request-id"] ?? crypto.randomUUID(),
userId: req.user?.id,
});
requestLogger.info({ event: "order.create.start", body: req.body });
try {
const order = await orderService.create(req.body);
requestLogger.info({
event: "order.created",
orderId: order.id,
totalCents: order.totalCents,
durationMs: Date.now() - req.startTime,
});
res.status(201).json(order);
} catch (err) {
requestLogger.error({
event: "order.create.failed",
error: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
});
res.status(500).json({ error: "Internal server error" });
}
}
Middleware HTTP pour pino
typescriptimport pinoHttp from "pino-http";
app.use(pinoHttp({
logger,
customProps: (req) => ({
requestId: req.headers["x-request-id"] ?? crypto.randomUUID(),
}),
customSuccessMessage: (req, res) =>
`${req.method} ${req.url} → ${res.statusCode}`,
customErrorMessage: (req, res, err) =>
`${req.method} ${req.url} → ${res.statusCode} — ${err.message}`,
}));
Résultat pour chaque requête HTTP :
json{
"timestamp": "2026-06-01T10:23:41.123Z",
"level": "info",
"requestId": "req-abc123",
"method": "POST",
"url": "/orders",
"statusCode": 201,
"durationMs": 45,
"service": "order-api"
}
Implémentation Python avec structlog
bashpip install structlog
python# logger.py
import logging
import structlog
import sys
def configure_logging(level: str = "INFO") -> None:
logging.basicConfig(
format="%(message)s",
stream=sys.stdout,
level=getattr(logging, level.upper()),
)
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars, # Injecte le contexte partagé
structlog.processors.add_log_level, # Ajoute "level"
structlog.processors.TimeStamper(fmt="iso"), # Ajoute "timestamp"
structlog.processors.StackInfoRenderer(),
structlog.processors.JSONRenderer(), # Sérialise en JSON
],
wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
context_class=dict,
logger_factory=structlog.PrintLoggerFactory(),
)
logger = structlog.get_logger().bind(service="order-api")
Utilisation :
python# order_handler.py
import uuid
import structlog
from structlog.contextvars import bind_contextvars, clear_contextvars
logger = structlog.get_logger()
async def create_order(request: Request) -> Response:
# Initialiser le contexte de la requête — propagé à tous les logs du handler
clear_contextvars()
bind_contextvars(
request_id=request.headers.get("x-request-id", str(uuid.uuid4())),
user_id=request.user.id if request.user else None,
)
log = logger.bind(event="order.create")
log.info("start")
try:
order = await order_service.create(request.json())
log.info(
"success",
order_id=order.id,
total_cents=order.total_cents,
)
return JSONResponse(order.to_dict(), status_code=201)
except Exception as exc:
log.error("failed", error=str(exc), exc_info=True)
return JSONResponse({"error": "Internal server error"}, status_code=500)
Résultat JSON :
json{
"event": "order.create",
"level": "info",
"timestamp": "2026-06-01T10:23:41.123456Z",
"service": "order-api",
"request_id": "req-abc123",
"user_id": "user-789",
"order_id": "ord-456",
"total_cents": 4990
}
Le correlation ID : relier les logs entre services
Dans un système distribué, une requête passe par plusieurs services. Sans corrélation, impossible de retrouver les logs d'une même requête.
Le pattern :
- Le premier service (API Gateway ou service d'entrée) génère un
requestIdunique - Il le transmet aux services appelés via un header HTTP :
X-Request-ID: req-abc123 - Chaque service loggue avec ce
requestIddans ses logs
typescript// Middleware : propager le correlation ID
app.use((req, res, next) => {
const requestId = req.headers["x-request-id"] as string ?? crypto.randomUUID();
req.requestId = requestId;
res.setHeader("X-Request-ID", requestId);
next();
});
// Appel vers un service downstream
async function callPaymentService(orderId: string, requestId: string): Promise<void> {
await fetch("http://payment-service/charge", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Request-ID": requestId, // Propagation
},
body: JSON.stringify({ orderId }),
});
}
Résultat : tous les logs avec requestId = "req-abc123" peuvent être filtrés ensemble dans Kibana ou Loki pour reconstituer le parcours complet d'une requête.
Ce qu'il ne faut pas logger
- Données personnelles (RGPD) : email, nom, adresse, numéro de carte — utiliser un masque ou un hash
- Secrets : mots de passe, tokens, clés API — jamais dans les logs
- Objets entiers non maîtrisés :
logger.info(req.body)peut exposer des données sensibles
typescript// ❌ Dangereux
logger.info({ body: req.body }); // Peut contenir un mot de passe
// ✅ Sûr
logger.info({
event: "user.login",
email: maskEmail(req.body.email), // "a***@exemple.fr"
ip: req.ip,
});
function maskEmail(email: string): string {
const [user, domain] = email.split("@");
return `${user[0]}***@${domain}`;
}
Résumé
| Concept | Implémentation |
|---|---|
| Format | JSON structuré — pas de texte libre |
| Champs obligatoires | timestamp, level, event, service |
| Niveaux | debug / info / warn / error / fatal — utiliser le bon |
| Corrélation | requestId propagé via X-Request-ID entre services |
| TypeScript | pino + pino-http |
| Python | structlog + bind_contextvars |
Étape suivante : 02 — Métriques — compter, mesurer et alerter sur des données agrégées.
Sources
- Majors, C., Fong-Jones, L., & Miranda, G. (2022). Observability Engineering, Chapitre 3 "Logs". O'Reilly.
- pino Documentation. getpino.io/
- structlog Documentation. www.structlog.org/en/stable/
- W3C. Trace Context. w3.org/TR/trace-context/ (Standard corrélation ID)