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

  1. Alerter sur les symptômes, pas sur les causes
  2. Utiliser les SLOs pour prioriser les actions
  3. Logs structurés en JSON avec un traceId pour la corrélation
  4. Dashboards par service avec les 4 golden signals : latence, trafic, erreurs, saturation
  5. Runbooks associés à chaque alerte
  6. On-call rotation avec des escalations claires