Monitoring et Observabilité en Production
Monitoring et Observabilité en Production
Les trois piliers de l'observabilité
L'observabilité repose sur trois types de données complémentaires :
graph TB
subgraph "Les 3 piliers"
M[Métriques<br/>Données numériques agrégées]
L[Logs<br/>Événements textuels horodatés]
T[Traces<br/>Parcours des requêtes]
end
subgraph "Outils"
M --> PROM[Prometheus / Grafana]
L --> ELK[ELK Stack / Loki]
T --> JAE[Jaeger / Tempo]
end
subgraph "Résultat"
PROM --> A[Alerting]
ELK --> D[Debugging]
JAE --> P[Performance]
end
Prometheus : collecte de métriques
Instrumentation d'une application
import { Counter, Histogram, Registry } from 'prom-client';
const register = new Registry();
// Compteur de requêtes HTTP
const httpRequestsTotal = new Counter({
name: 'http_requests_total',
help: 'Nombre total de requêtes HTTP',
labelNames: ['method', 'route', 'status'],
registers: [register],
});
// Histogramme de latence
const httpRequestDuration = new Histogram({
name: 'http_request_duration_seconds',
help: 'Durée des requêtes HTTP en secondes',
labelNames: ['method', 'route'],
buckets: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
registers: [register],
});
// Middleware Express
app.use((req, res, next) => {
const end = httpRequestDuration.startTimer({
method: req.method,
route: req.route?.path || req.path,
});
res.on('finish', () => {
httpRequestsTotal.inc({
method: req.method,
route: req.route?.path || req.path,
status: res.statusCode.toString(),
});
end();
});
next();
});
// Endpoint /metrics
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});
Configuration Prometheus
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "alerts/*.yml"
scrape_configs:
- job_name: 'api'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
target_label: __address__
regex: (.+)
replacement: $1
Règles d'alerting
# alerts/api.yml
groups:
- name: api-alerts
rules:
- alert: HighErrorRate
expr: |
rate(http_requests_total{status=~"5.."}[5m])
/ rate(http_requests_total[5m]) > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "Taux d'erreur HTTP > 5%"
description: "{{ $labels.instance }} a un taux d'erreur de {{ $value | humanizePercentage }}"
- alert: HighLatency
expr: |
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 2
for: 10m
labels:
severity: warning
annotations:
summary: "P95 de latence > 2s"
- alert: PodCrashLooping
expr: |
rate(kube_pod_container_status_restarts_total[15m]) > 0
for: 5m
labels:
severity: critical
annotations:
summary: "Pod {{ $labels.pod }} en crash loop"
Grafana : visualisation et dashboards
Dashboard en JSON (provisionné par code)
{
"dashboard": {
"title": "API Overview",
"panels": [
{
"title": "Requêtes par seconde",
"type": "timeseries",
"targets": [
{
"expr": "sum(rate(http_requests_total[5m])) by (status)",
"legendFormat": "HTTP {{ status }}"
}
]
},
{
"title": "Latence P50 / P95 / P99",
"type": "timeseries",
"targets": [
{
"expr": "histogram_quantile(0.50, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "P50"
},
{
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "P95"
},
{
"expr": "histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "P99"
}
]
}
]
}
}
Logging structuré
Logs JSON avec contexte
import { createLogger, format, transports } from 'winston';
const logger = createLogger({
level: 'info',
format: format.combine(
format.timestamp(),
format.json(),
),
defaultMeta: {
service: 'api',
version: process.env.APP_VERSION,
environment: process.env.NODE_ENV,
},
transports: [
new transports.Console(),
],
});
// Utilisation avec contexte
logger.info('Commande traitée', {
orderId: order.id,
userId: user.id,
amount: order.total,
duration: elapsed,
traceId: req.headers['x-trace-id'],
});
Agrégation avec Loki + Grafana
# Loki - promtail config
scrape_configs:
- job_name: kubernetes
kubernetes_sd_configs:
- role: pod
pipeline_stages:
- json:
expressions:
level: level
service: service
traceId: traceId
- labels:
level:
service:
- timestamp:
source: timestamp
format: RFC3339
Requêtes LogQL dans Grafana :
# Toutes les erreurs de l'API
{service="api"} | json | level="error"
# Requêtes lentes (> 2s)
{service="api"} | json | duration > 2000
# Erreurs groupées par message
sum by (message) (count_over_time({service="api"} | json | level="error" [1h]))
Distributed Tracing
OpenTelemetry
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
import { PgInstrumentation } from '@opentelemetry/instrumentation-pg';
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: 'http://tempo:4318/v1/traces',
}),
instrumentations: [
new HttpInstrumentation(),
new ExpressInstrumentation(),
new PgInstrumentation(),
],
});
sdk.start();
SLOs et Error Budgets
Définir des SLOs
| Service | SLI | SLO | Error Budget (30j) |
|---|---|---|---|
| API | Disponibilité | 99.9% | 43.2 min |
| API | Latence P95 | < 500ms | 5% de requêtes lentes |
| Paiement | Taux de succès | 99.95% | 21.6 min |
Prometheus pour les SLOs
# SLO de disponibilité 99.9%
- record: slo:api:availability:ratio
expr: |
1 - (
sum(rate(http_requests_total{status=~"5.."}[30d]))
/ sum(rate(http_requests_total[30d]))
)
- alert: SLOBudgetBurning
expr: slo:api:availability:ratio < 0.999
for: 1h
labels:
severity: critical
annotations:
summary: "Budget d'erreur épuisé pour l'API"
Bonnes pratiques
- Alerter sur les symptômes, pas sur les causes
- Utiliser les SLOs pour prioriser les actions
- Logs structurés en JSON avec un
traceIdpour la corrélation - Dashboards par service avec les 4 golden signals : latence, trafic, erreurs, saturation
- Runbooks associés à chaque alerte
- On-call rotation avec des escalations claires