Devops
Difficulte: Intermédiaire
20 min de lecture

Prometheus & Grafana : Monitoring complet de son infrastructure Linux | Morgann Riu

Guide complet Prometheus & Grafana 2026 : Docker Compose, PromQL, AlertManager, dashboards, exporters, Thanos HA. Surveillez toute votre infra Linux.

Retour aux tutoriels
Prérequis
Docker et Docker Compose installés sur votre serveur. Connaissances de base en administration Linux (systemd, fichiers de configuration YAML). Pour l'installation de Docker, consultez le guide d'installation Docker.

Pourquoi Prometheus ? Pull vs Push, architecture réelle

La surveillance d'une infrastructure Linux se résumait longtemps à Nagios et ses plugins, ou à Zabbix et son agent. Ces outils fonctionnent en modèle push : chaque agent envoie ses données vers un collecteur central. Prometheus inverse ce paradigme avec le modèle pull : c'est le serveur Prometheus qui interroge périodiquement chaque cible sur son endpoint /metrics.

Ce renversement n'est pas cosmétique. Il change fondamentalement la manière dont le monitoring est opéré :

  • Détection des instances mortes : si Prometheus ne peut pas scraper une cible, la métrique up passe à 0. Avec le push, une instance silencieuse est indétectable.
  • Configuration centralisée : toutes les cibles sont définies dans Prometheus, pas dispersées sur chaque agent. Un seul fichier YAML pour voir toute l'infrastructure.
  • Debug simple : l'endpoint /metrics est du texte brut lisible avec curl, sans agent intermédiaire à débugger.
  • Écosystème standardisé : le format d'exposition Prometheus est devenu un standard OpenMetrics adopté par des centaines d'applications (Nginx, PostgreSQL, Redis, Kubernetes, etc.).

Les quatre composants de la stack


┌─────────────────┐   scrape /metrics   ┌──────────────────┐   alertes    ┌─────────────────┐
│  Node Exporter  │ ◄─────────────────── │   Prometheus     │ ──────────►  │  AlertManager   │
│  (port 9100)    │                      │   (port 9090)    │              │  (port 9093)    │
│  Nginx Exporter │ ◄─────────────────── │   TSDB locale    │              │  Slack / Email  │
│  (port 9113)    │                      │   PromQL         │              │  PagerDuty      │
│  Blackbox Exp.  │ ◄─────────────────── │   Alerting rules │              └─────────────────┘
│  (port 9115)    │                      └──────────────────┘
└─────────────────┘                               │
                                                  │ datasource
                                                  ▼
                                       ┌──────────────────┐
                                       │     Grafana       │
                                       │   (port 3000)    │
                                       │   Dashboards     │
                                       │   Alerting       │
                                       └──────────────────┘
  • Prometheus : collecte (scrape) les métriques, les stocke dans une base de données temporelle locale (TSDB), évalue les règles d'alertes toutes les 15 secondes.
  • Exporters : traduisent les métriques système ou applicatives au format Prometheus. Node Exporter pour Linux, Nginx Exporter pour Nginx, Blackbox Exporter pour les sondes HTTP/SSL.
  • AlertManager : reçoit les alertes de Prometheus, les déduplique, les groupe, et les route vers les bons canaux avec gestion des silences et des inhibitions.
  • Grafana : interface de visualisation qui interroge Prometheus via PromQL pour afficher des dashboards interactifs. Ne stocke rien, ne collecte rien.

Installation : Docker Compose stack complète

Déployer la stack complète avec Docker Compose est la méthode la plus reproductible. Un seul fichier décrit toute l'infrastructure de monitoring.

Structure des fichiers

mkdir -p ~/monitoring/{prometheus,grafana,alertmanager}
mkdir -p ~/monitoring/prometheus/rules
mkdir -p ~/monitoring/grafana/{provisioning/datasources,provisioning/dashboards,dashboards}
cd ~/monitoring

Docker Compose complet

# ~/monitoring/docker-compose.yml
version: '3.8'

networks:
  monitoring:
    driver: bridge

volumes:
  prometheus_data:
    driver: local
  grafana_data:
    driver: local
  alertmanager_data:
    driver: local

services:

  # ─── Prometheus ────────────────────────────────────────────────
  prometheus:
    image: prom/prometheus:v2.53.0
    container_name: prometheus
    restart: unless-stopped
    user: "65534:65534"           # nobody:nobody, pas de root
    ports:
      - "127.0.0.1:9090:9090"    # Écoute uniquement sur localhost
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ./prometheus/rules:/etc/prometheus/rules:ro
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention.time=30d'
      - '--storage.tsdb.retention.size=10GB'
      - '--web.enable-lifecycle'
      - '--web.enable-admin-api'
      - '--web.console.libraries=/usr/share/prometheus/console_libraries'
      - '--web.console.templates=/usr/share/prometheus/consoles'
    networks:
      - monitoring
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:9090/-/healthy"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s

  # ─── Node Exporter ─────────────────────────────────────────────
  node-exporter:
    image: prom/node-exporter:v1.8.2
    container_name: node-exporter
    restart: unless-stopped
    pid: host                    # Accès aux métriques de l'hôte
    ports:
      - "127.0.0.1:9100:9100"
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.rootfs=/rootfs'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
      - '--collector.systemd'
    networks:
      - monitoring

  # ─── AlertManager ──────────────────────────────────────────────
  alertmanager:
    image: prom/alertmanager:v0.27.0
    container_name: alertmanager
    restart: unless-stopped
    user: "65534:65534"
    ports:
      - "127.0.0.1:9093:9093"
    volumes:
      - ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
      - alertmanager_data:/alertmanager
    command:
      - '--config.file=/etc/alertmanager/alertmanager.yml'
      - '--storage.path=/alertmanager'
      - '--web.external-url=http://localhost:9093'
      - '--cluster.listen-address='      # Désactive le clustering si instance unique
    networks:
      - monitoring
    depends_on:
      - prometheus

  # ─── Grafana ───────────────────────────────────────────────────
  grafana:
    image: grafana/grafana:11.1.0
    container_name: grafana
    restart: unless-stopped
    user: "472:472"
    ports:
      - "127.0.0.1:3000:3000"
    environment:
      GF_SECURITY_ADMIN_USER: admin
      GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-changeme}
      GF_USERS_ALLOW_SIGN_UP: "false"
      GF_ANALYTICS_REPORTING_ENABLED: "false"
      GF_SERVER_ROOT_URL: https://grafana.example.com
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning:ro
      - ./grafana/dashboards:/var/lib/grafana/dashboards:ro
    networks:
      - monitoring
    depends_on:
      - prometheus
    healthcheck:
      test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://localhost:3000/api/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

  # ─── Blackbox Exporter ─────────────────────────────────────────
  blackbox-exporter:
    image: prom/blackbox-exporter:v0.25.0
    container_name: blackbox-exporter
    restart: unless-stopped
    ports:
      - "127.0.0.1:9115:9115"
    volumes:
      - ./prometheus/blackbox.yml:/etc/blackbox_exporter/config.yml:ro
    networks:
      - monitoring
Sécurité réseau
Notez que tous les ports sont liés à 127.0.0.1 uniquement. Ne jamais exposer Prometheus, AlertManager ou Node Exporter directement sur Internet. Utilisez un reverse proxy Nginx avec TLS pour accéder à Grafana depuis l'extérieur.

Configuration Prometheus

prometheus.yml : scrape_configs et service discovery

# ~/monitoring/prometheus/prometheus.yml
global:
  scrape_interval: 15s          # Collecte toutes les 15 secondes
  evaluation_interval: 15s      # Évaluation des règles toutes les 15s
  scrape_timeout: 10s           # Timeout par scrape

  # Labels ajoutés à toutes les métriques de cette instance
  external_labels:
    datacenter: 'paris-1'
    environment: 'production'

# Chargement des règles d'alertes
rule_files:
  - "/etc/prometheus/rules/*.yml"

# Configuration AlertManager
alerting:
  alertmanagers:
    - static_configs:
        - targets: ["alertmanager:9093"]
      timeout: 10s

scrape_configs:
  # ── Prometheus lui-même ────────────────────────────────────────
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  # ── Node Exporter ──────────────────────────────────────────────
  - job_name: 'node'
    static_configs:
      - targets: ['node-exporter:9100']
        labels:
          hostname: 'monitoring-server'
          role: 'monitoring'

  # ── Serveurs distants (targets statiques) ──────────────────────
  - job_name: 'servers'
    scrape_interval: 30s        # Override par job
    static_configs:
      - targets:
          - '192.168.1.10:9100'
          - '192.168.1.11:9100'
          - '192.168.1.12:9100'
        labels:
          environment: 'production'
          role: 'web'
      - targets:
          - '192.168.1.20:9100'
          - '192.168.1.21:9100'
        labels:
          environment: 'production'
          role: 'database'

  # ── Blackbox Exporter (sondes HTTP) ───────────────────────────
  - job_name: 'blackbox-http'
    metrics_path: /probe
    params:
      module: [http_2xx]
    static_configs:
      - targets:
          - 'https://exemple.com'
          - 'https://api.exemple.com/health'
          - 'https://grafana.exemple.com'
    relabel_configs:
      # Copie l'URL cible dans le paramètre ?target=
      - source_labels: [__address__]
        target_label: __param_target
      # Utilise l'URL comme label "instance"
      - source_labels: [__param_target]
        target_label: instance
      # Redirige le scrape vers le Blackbox Exporter
      - target_label: __address__
        replacement: blackbox-exporter:9115

  # ── Blackbox Exporter (expiration SSL) ────────────────────────
  - job_name: 'blackbox-ssl'
    metrics_path: /probe
    params:
      module: [ssl_expiry]
    static_configs:
      - targets:
          - 'exemple.com:443'
          - 'api.exemple.com:443'
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: blackbox-exporter:9115

  # ── Service discovery via fichier (pour infra dynamique) ──────
  - job_name: 'dynamic-servers'
    file_sd_configs:
      - files:
          - /etc/prometheus/targets/*.json
        refresh_interval: 1m    # Recharge la liste chaque minute

Configuration Blackbox Exporter

# ~/monitoring/prometheus/blackbox.yml
modules:
  # Vérification HTTP avec code 200
  http_2xx:
    prober: http
    timeout: 5s
    http:
      valid_http_versions: ["HTTP/1.1", "HTTP/2.0"]
      valid_status_codes: [200, 201, 204]
      method: GET
      follow_redirects: true
      preferred_ip_protocol: "ip4"
      tls_config:
        insecure_skip_verify: false

  # Vérification expiration SSL uniquement
  ssl_expiry:
    prober: http
    timeout: 5s
    http:
      method: HEAD
      fail_if_not_ssl: true
      tls_config:
        insecure_skip_verify: false

  # Ping ICMP
  icmp:
    prober: icmp
    timeout: 5s
    icmp:
      preferred_ip_protocol: "ip4"

  # Vérification port TCP
  tcp_connect:
    prober: tcp
    timeout: 5s

PromQL : requêtes essentielles commentées

PromQL opère sur deux types de sélecteurs : les instant vectors (valeur instantanée) et les range vectors (ensemble de valeurs sur une fenêtre, notés [5m]). La règle d'or : utilisez toujours rate() ou increase() sur les compteurs (métriques suffixées _total), jamais de valeur brute.

5 requêtes essentielles pour surveiller un serveur Linux

# ─── 1. Utilisation CPU en pourcentage ────────────────────────────────────────
# Principe : 100% - % de temps passé en mode "idle"
# rate() calcule le taux de variation/seconde sur 5 minutes (lisse les pics)
# avg by (instance) agrège tous les cœurs CPU par serveur
100 - (
  avg by (instance) (
    rate(node_cpu_seconds_total{mode="idle"}[5m])
  ) * 100
)

# ─── 2. Mémoire utilisée en pourcentage ───────────────────────────────────────
# MemAvailable inclut la mémoire récupérable (caches) = mémoire réellement libre
# Plus fiable que (MemTotal - MemFree) qui ignore les caches Linux
(1 - (
  node_memory_MemAvailable_bytes /
  node_memory_MemTotal_bytes
)) * 100

# ─── 3. Espace disque utilisé en pourcentage ──────────────────────────────────
# Filtré sur le filesystem racine, à adapter selon vos points de montage
# node_filesystem_avail_bytes = espace disponible pour les utilisateurs non-root
(1 - (
  node_filesystem_avail_bytes{mountpoint="/", fstype!="tmpfs"}
  /
  node_filesystem_size_bytes{mountpoint="/", fstype!="tmpfs"}
)) * 100

# ─── 4. Trafic réseau entrant (bits/s) ────────────────────────────────────────
# irate() utilise les deux derniers points pour détecter les pics instantanés
# Multiplication par 8 pour convertir octets → bits
# Adapter "eth0" à votre interface réseau principale
irate(node_network_receive_bytes_total{device="eth0"}[5m]) * 8

# ─── 5. Percentile 95 de la durée des requêtes HTTP ──────────────────────────
# histogram_quantile reconstruit les percentiles depuis les buckets
# Nécessite un exporter qui expose des histogrammes (nginx, traefik, etc.)
# le "le" label signifie "less than or equal to" (borne supérieure du bucket)
histogram_quantile(0.95,
  sum by (le, job) (
    rate(http_request_duration_seconds_bucket[5m])
  )
)

Requêtes avancées : topk, prédiction, taux d'erreur

# Top 5 des processus par consommation CPU
topk(5,
  sum by (groupname) (
    rate(namedprocess_namegroup_cpu_seconds_total[5m])
  )
)

# Prédiction : disk full dans combien d'heures ?
# predict_linear projette la tendance des 6 dernières heures
# Alerte si remplissage prévu dans moins de 48h
predict_linear(
  node_filesystem_avail_bytes{mountpoint="/"}[6h],
  48 * 3600
) < 0

# Taux d'erreurs HTTP (4xx + 5xx) en pourcentage
(
  sum(rate(nginx_http_requests_total{status=~"[45].."}[5m]))
  /
  sum(rate(nginx_http_requests_total[5m]))
) * 100

# Disponibilité sur 24h (pour SLO dashboard)
avg_over_time(up{job="servers"}[24h]) * 100

Règles d'alerte Prometheus

# ~/monitoring/prometheus/rules/node_alerts.yml
groups:
  - name: instance_availability
    interval: 15s              # Override de l'evaluation_interval global
    rules:
      # ── Instance inaccessible ────────────────────────────────────────────────
      - alert: InstanceDown
        expr: up == 0
        for: 2m                # Déclenche seulement si DOWN depuis 2 minutes
        labels:
          severity: critical
        annotations:
          summary: "Instance {{ $labels.instance }} inaccessible"
          description: "Le job {{ $labels.job }} sur {{ $labels.instance }} ne répond plus depuis 2 minutes. Vérifiez l'état du serveur et du service."
          runbook: "https://wiki.exemple.com/runbooks/instance-down"

  - name: system_resources
    rules:
      # ── CPU élevé ────────────────────────────────────────────────────────────
      - alert: HighCpuUsage
        expr: |
          100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 90
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "CPU critique sur {{ $labels.instance }}"
          description: "Utilisation CPU de {{ printf \"%.1f\" $value }}% sur {{ $labels.instance }} depuis 5 minutes."

      - alert: HighCpuUsageWarning
        expr: |
          100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 75
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "CPU élevé sur {{ $labels.instance }}"
          description: "Utilisation CPU de {{ printf \"%.1f\" $value }}% sur {{ $labels.instance }} depuis 10 minutes."

      # ── Mémoire critique ─────────────────────────────────────────────────────
      - alert: HighMemoryUsage
        expr: |
          (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 90
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Mémoire critique sur {{ $labels.instance }}"
          description: "{{ printf \"%.1f\" $value }}% de mémoire utilisée sur {{ $labels.instance }}."

      # ── Disque > 85% ──────────────────────────────────────────────────────────
      - alert: DiskSpaceWarning
        expr: |
          (1 - node_filesystem_avail_bytes{mountpoint="/", fstype!="tmpfs"} /
               node_filesystem_size_bytes{mountpoint="/", fstype!="tmpfs"}) * 100 > 85
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "Espace disque faible sur {{ $labels.instance }}"
          description: "{{ printf \"%.1f\" $value }}% du disque utilisé sur {{ $labels.instance }} (partition /)."

      - alert: DiskSpaceCritical
        expr: |
          (1 - node_filesystem_avail_bytes{mountpoint="/", fstype!="tmpfs"} /
               node_filesystem_size_bytes{mountpoint="/", fstype!="tmpfs"}) * 100 > 95
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Disque presque plein sur {{ $labels.instance }}"
          description: "{{ printf \"%.1f\" $value }}% du disque utilisé sur {{ $labels.instance }}. Intervention urgente requise."

      # ── Disque saturé dans 48h ───────────────────────────────────────────────
      - alert: DiskWillFillIn48h
        expr: |
          predict_linear(node_filesystem_avail_bytes{mountpoint="/", fstype!="tmpfs"}[6h], 48 * 3600) < 0
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: "Disque plein prévu dans 48h sur {{ $labels.instance }}"
          description: "Selon la tendance des 6 dernières heures, le disque de {{ $labels.instance }} sera plein dans moins de 48 heures."

  - name: ssl_certificates
    rules:
      # ── Certificat SSL expirant dans 30 jours ────────────────────────────────
      - alert: SslCertificateExpiringSoon
        expr: |
          (probe_ssl_earliest_cert_expiry - time()) / 86400 < 30
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: "Certificat SSL expire bientôt pour {{ $labels.instance }}"
          description: "Le certificat SSL de {{ $labels.instance }} expire dans {{ printf \"%.0f\" $value }} jours."

      - alert: SslCertificateExpired
        expr: |
          (probe_ssl_earliest_cert_expiry - time()) / 86400 < 7
        for: 1h
        labels:
          severity: critical
        annotations:
          summary: "Certificat SSL critique pour {{ $labels.instance }}"
          description: "Le certificat SSL de {{ $labels.instance }} expire dans {{ printf \"%.0f\" $value }} jours. Action immédiate requise."

  - name: http_availability
    rules:
      # ── Endpoint HTTP inaccessible ───────────────────────────────────────────
      - alert: HttpEndpointDown
        expr: probe_success == 0
        for: 3m
        labels:
          severity: critical
        annotations:
          summary: "Endpoint HTTP inaccessible : {{ $labels.instance }}"
          description: "L'URL {{ $labels.instance }} ne répond plus depuis 3 minutes."

AlertManager : routes, receivers, inhibitions

# ~/monitoring/alertmanager/alertmanager.yml
global:
  resolve_timeout: 5m

  # Configuration SMTP globale
  smtp_smarthost: 'smtp.gmail.com:587'
  smtp_from: 'alertmanager@exemple.com'
  smtp_auth_username: 'alertmanager@exemple.com'
  smtp_auth_password: 'votre-app-password'
  smtp_require_tls: true

# ─── Arbre de routage ─────────────────────────────────────────────────────────
route:
  # Groupement par alertname + datacenter pour éviter les doublons
  group_by: ['alertname', 'datacenter', 'job']
  group_wait: 30s              # Attendre 30s pour grouper les alertes initiales
  group_interval: 5m           # Intervalle entre notifications pour un groupe actif
  repeat_interval: 4h          # Répétition si l'alerte persiste
  receiver: 'email-ops'        # Receiver par défaut

  routes:
    # Alertes critiques → Slack en priorité + répétition fréquente
    - match:
        severity: critical
      receiver: 'slack-critical'
      group_wait: 10s           # Moins d'attente pour les critiques
      repeat_interval: 30m
      continue: true            # Continue vers les autres routes (email aussi)

    # Alertes critiques → email aussi
    - match:
        severity: critical
      receiver: 'email-ops'

    # Alertes warning → Slack canal séparé
    - match:
        severity: warning
      receiver: 'slack-warning'
      repeat_interval: 6h

# ─── Receivers ────────────────────────────────────────────────────────────────
receivers:
  - name: 'email-ops'
    email_configs:
      - to: 'ops-team@exemple.com'
        subject: '[{{ .Status | toUpper }}] {{ .CommonLabels.alertname }} - {{ .CommonAnnotations.summary }}'
        body: |
          {{ range .Alerts }}
          Alerte : {{ .Annotations.summary }}
          Description : {{ .Annotations.description }}
          Sévérité : {{ .Labels.severity }}
          Instance : {{ .Labels.instance }}
          Début : {{ .StartsAt.Format "2006-01-02 15:04:05" }}
          {{ end }}
        send_resolved: true
        headers:
          X-Priority: '1'

  - name: 'slack-critical'
    slack_configs:
      - api_url: 'https://hooks.slack.com/services/TXXXXXXXX/BXXXXXXXX/xxxxxxxxxxxxxxxxxxxxxxxx'
        channel: '#alertes-critiques'
        username: 'AlertManager'
        icon_emoji: ':rotating_light:'
        title: '[{{ .Status | toUpper }}] {{ .CommonLabels.alertname }}'
        text: |
          {{ range .Alerts }}
          *{{ .Annotations.summary }}*
          {{ .Annotations.description }}
          *Instance :* {{ .Labels.instance }}
          *Datacenter :* {{ .Labels.datacenter }}
          {{ end }}
        color: '{{ if eq .Status "firing" }}danger{{ else }}good{{ end }}'
        send_resolved: true

  - name: 'slack-warning'
    slack_configs:
      - api_url: 'https://hooks.slack.com/services/TXXXXXXXX/BXXXXXXXX/xxxxxxxxxxxxxxxxxxxxxxxx'
        channel: '#alertes-monitoring'
        username: 'AlertManager'
        icon_emoji: ':warning:'
        title: '[WARNING] {{ .CommonLabels.alertname }}'
        text: '{{ range .Alerts }}{{ .Annotations.summary }}\n{{ .Annotations.description }}{{ end }}'
        send_resolved: true

# ─── Inhibitions ──────────────────────────────────────────────────────────────
inhibit_rules:
  # Si l'instance est DOWN, supprimer les alertes CPU/RAM/Disk de la même instance
  - source_match:
      alertname: 'InstanceDown'
    target_match_re:
      alertname: '(HighCpuUsage|HighMemoryUsage|DiskSpaceWarning|DiskSpaceCritical)'
    equal: ['instance']

  # Si une alerte critique existe, supprimer les warnings du même alertname
  - source_match:
      severity: 'critical'
    target_match:
      severity: 'warning'
    equal: ['alertname', 'instance']

Grafana : dashboards, variables et provisioning as code

Datasource provisionnée automatiquement

Le provisioning Grafana permet de déployer les datasources et dashboards sans passer par l'interface web. Configuration as code, versionnée en Git.

# ~/monitoring/grafana/provisioning/datasources/prometheus.yml
apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
    uid: prometheus-uid         # UID fixe pour référencer dans les dashboards
    editable: false
    jsonData:
      httpMethod: POST
      prometheusType: Prometheus
      prometheusVersion: 2.53.0
      timeInterval: 15s
      queryTimeout: 60s

Dashboard provisioning

# ~/monitoring/grafana/provisioning/dashboards/default.yml
apiVersion: 1

providers:
  - name: 'default'
    orgId: 1
    folder: 'Production'
    type: file
    disableDeletion: true       # Empêche la suppression via l'UI
    updateIntervalSeconds: 30   # Recharge les fichiers toutes les 30s
    allowUiUpdates: false       # Les modifications UI ne persistent pas
    options:
      path: /var/lib/grafana/dashboards

Dashboard as code : Node Exporter simplifié

{
  "title": "Infrastructure Overview",
  "uid": "infra-overview",
  "tags": ["production", "node-exporter"],
  "refresh": "30s",
  "time": { "from": "now-3h", "to": "now" },
  "templating": {
    "list": [
      {
        "name": "instance",
        "type": "query",
        "label": "Serveur",
        "datasource": "Prometheus",
        "query": "label_values(node_uname_info, instance)",
        "refresh": 2,
        "multi": false,
        "includeAll": true,
        "allValue": ".*"
      }
    ]
  },
  "panels": [
    {
      "type": "stat",
      "title": "CPU Usage",
      "gridPos": { "x": 0, "y": 0, "w": 6, "h": 4 },
      "targets": [{
        "expr": "100 - (avg by (instance) (rate(node_cpu_seconds_total{mode='idle', instance=~'$instance'}[5m])) * 100)",
        "legendFormat": "{{ instance }}"
      }],
      "options": { "reduceOptions": { "calcs": ["lastNotNull"] } },
      "fieldConfig": {
        "defaults": {
          "unit": "percent",
          "thresholds": {
            "mode": "absolute",
            "steps": [
              { "color": "green", "value": null },
              { "color": "yellow", "value": 75 },
              { "color": "red", "value": 90 }
            ]
          }
        }
      }
    }
  ]
}

Import du dashboard Node Exporter Full (ID 1860)

Grafana dispose d'un catalogue de dashboards communautaires. Le dashboard 1860 (Node Exporter Full) couvre toutes les métriques système en un seul import :

  1. Allez dans Dashboards > New > Import
  2. Entrez l'ID 1860 dans le champ "Import via grafana.com"
  3. Sélectionnez votre datasource Prometheus dans le menu déroulant
  4. Cliquez sur Import

Autres dashboards utiles : 13978 (Node Exporter Quickstart), 7587 (Docker monitoring), 9614 (Nginx), 9628 (PostgreSQL).

Variables de dashboard

Les variables transforment un dashboard statique en outil interactif multi-serveurs. Créez une variable instance via Dashboard Settings > Variables > New variable :

  • Type : Query
  • Query : label_values(node_uname_info, instance)
  • Refresh : On time range change
  • Multi-value : activé pour comparer plusieurs serveurs

Utilisez ensuite $instance dans toutes vos requêtes : node_cpu_seconds_total{instance=~"$instance"}. Le filtre s'applique à tous les panneaux simultanément.

Annotations automatiques depuis AlertManager

Les annotations Grafana permettent de superposer les événements d'alerte sur les graphiques. Configurez une annotation automatique dans les paramètres du dashboard :

  • Source : Prometheus
  • Query : ALERTS{alertstate="firing"}
  • Title : {{alertname}}
  • Tags : {{severity}}

Exporters supplémentaires

Nginx Exporter

# Activer stub_status dans Nginx
# /etc/nginx/conf.d/stub_status.conf
server {
    listen 127.0.0.1:8080;
    location /nginx_status {
        stub_status on;
        access_log off;
        allow 127.0.0.1;
        deny all;
    }
}
# Ajout dans docker-compose.yml
  nginx-exporter:
    image: nginx/nginx-prometheus-exporter:1.1.0
    container_name: nginx-exporter
    restart: unless-stopped
    ports:
      - "127.0.0.1:9113:9113"
    command:
      - -nginx.scrape-uri=http://host-gateway:8080/nginx_status
    networks:
      - monitoring
    extra_hosts:
      - "host-gateway:host-gateway"

PostgreSQL Exporter

# Ajout dans docker-compose.yml
  postgres-exporter:
    image: prometheuscommunity/postgres-exporter:v0.15.0
    container_name: postgres-exporter
    restart: unless-stopped
    environment:
      DATA_SOURCE_NAME: "postgresql://exporter:motdepasse@postgres:5432/postgres?sslmode=disable"
    ports:
      - "127.0.0.1:9187:9187"
    networks:
      - monitoring
-- Créer l'utilisateur PostgreSQL pour l'exporter
CREATE USER exporter WITH PASSWORD 'motdepasse';
ALTER USER exporter SET SEARCH_PATH TO exporter,pg_catalog;
GRANT CONNECT ON DATABASE postgres TO exporter;
GRANT pg_monitor TO exporter;

Ajout des exporters dans Prometheus

# Ajout dans prometheus.yml / scrape_configs
  - job_name: 'nginx'
    static_configs:
      - targets: ['nginx-exporter:9113']

  - job_name: 'postgresql'
    static_configs:
      - targets: ['postgres-exporter:9187']

Démarrage et opérations

# Démarrer toute la stack
cd ~/monitoring
docker compose up -d

# Vérifier l'état de tous les services
docker compose ps

# Voir les logs en temps réel
docker compose logs -f prometheus

# Valider la configuration Prometheus avant reload
docker compose exec prometheus promtool check config /etc/prometheus/prometheus.yml

# Valider les règles d'alertes
docker compose exec prometheus promtool check rules /etc/prometheus/rules/node_alerts.yml

# Recharger Prometheus sans redémarrer (hot-reload)
curl -X POST http://localhost:9090/-/reload

# Vérifier les targets actifs
curl -s http://localhost:9090/api/v1/targets | python3 -m json.tool | grep -E "health|job|instance"

# Lister les alertes actives dans AlertManager
curl -s http://localhost:9093/api/v2/alerts | python3 -m json.tool

Haute disponibilité : Thanos ou Grafana Mimir

Prometheus seul présente deux limites en production critique : une seule instance (SPOF), et une rétention limitée par la capacité disque locale (15-30 jours recommandés). Thanos et Grafana Mimir résolvent ces deux problèmes.

Architecture Thanos


Instance Prometheus 1 ──── Thanos Sidecar ────┐
                                               │
Instance Prometheus 2 ──── Thanos Sidecar ────┤──► Thanos Query ──► Grafana
                                               │       (dédup)
Prometheus HA pair         S3 / Minio ◄────────┘
                           (rétention longue)      Thanos Store
                                                   (query S3)

Déploiement Thanos Sidecar

# Extension docker-compose pour Thanos
  thanos-sidecar:
    image: thanosio/thanos:v0.35.0
    container_name: thanos-sidecar
    command:
      - sidecar
      - --prometheus.url=http://prometheus:9090
      - --tsdb.path=/prometheus
      - --grpc-address=0.0.0.0:10901
      - --http-address=0.0.0.0:10902
      - --objstore.config-file=/etc/thanos/s3.yml
    volumes:
      - prometheus_data:/prometheus
      - ./thanos/s3.yml:/etc/thanos/s3.yml:ro
    networks:
      - monitoring

  thanos-query:
    image: thanosio/thanos:v0.35.0
    container_name: thanos-query
    command:
      - query
      - --http-address=0.0.0.0:9091
      - --endpoint=thanos-sidecar:10901
      - --query.replica-label=replica
    ports:
      - "127.0.0.1:9091:9091"
    networks:
      - monitoring
# ~/monitoring/thanos/s3.yml
type: S3
config:
  bucket: monitoring-thanos
  endpoint: s3.eu-west-3.amazonaws.com
  region: eu-west-3
  access_key: AKIAXXXXXXXXXXXXXXXX
  secret_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Grafana Mimir : l'alternative simplifiée
Grafana Mimir offre les mêmes fonctionnalités que Thanos (HA, stockage S3, rétention illimitée) avec une architecture monolithe plus simple à déployer. Il accepte nativement les remote_write de Prometheus via le protocole Prometheus. Idéal si vous débutez avec la HA Prometheus.

Rétention et sizing

Cibles Métriques/instance RAM recommandée Disque (30j)
5 serveurs~1 000512 Mo5 Go
20 serveurs~1 0002 Go20 Go
100 serveurs~1 0008 Go100 Go
1 000 serveurs~1 00032 Go→ Thanos/Mimir

Troubleshooting

Prometheus ne scrape pas une cible

# Lister les targets et leur état
curl -s http://localhost:9090/api/v1/targets | \
  python3 -c "import sys,json; [print(t['scrapeUrl'], t['health'], t.get('lastError','')) for t in json.load(sys.stdin)['data']['activeTargets']]"

# Tester manuellement l'endpoint de la cible
curl -v http://192.168.1.10:9100/metrics | head -20

# Vérifier les logs Prometheus
docker compose logs prometheus --tail=50 | grep -i error

# Tester la connectivité depuis le conteneur Prometheus
docker compose exec prometheus wget -qO- http://node-exporter:9100/metrics | head -5

Grafana ne charge pas les données

# Vérifier que Prometheus répond
curl -s http://localhost:9090/api/v1/query?query=up | python3 -m json.tool

# Tester la datasource depuis Grafana UI
# Configuration > Data Sources > Prometheus > Save & Test

# Vérifier les logs Grafana
docker compose logs grafana --tail=50 | grep -i error

AlertManager ne reçoit pas les alertes

# Vérifier la connexion Prometheus -> AlertManager
curl -s http://localhost:9090/api/v1/alertmanagers | python3 -m json.tool

# Voir les alertes en attente dans Prometheus
curl -s http://localhost:9090/api/v1/alerts | python3 -m json.tool

# Vérifier la config AlertManager
docker compose exec alertmanager amtool check-config /etc/alertmanager/alertmanager.yml

# Tester l'envoi d'une alerte manuelle
curl -XPOST http://localhost:9093/api/v2/alerts -H "Content-Type: application/json" -d '[{
  "labels": {"alertname": "TestAlert", "severity": "warning"},
  "annotations": {"summary": "Test depuis curl"}
}]'
Stack opérationnelle
Votre infrastructure de monitoring est complète. Commencez par importer le dashboard 1860 dans Grafana pour avoir immédiatement une vue exhaustive de vos serveurs Linux. Ajoutez ensuite progressivement vos règles d'alertes et vos exporters métier selon vos besoins.

Conclusion

Vous disposez maintenant d'une stack de monitoring production-grade qui couvre l'ensemble du cycle observabilité :

  • Prometheus collecte et stocke les métriques de tous vos serveurs via un modèle pull fiable, avec détection automatique des instances tombées.
  • Node Exporter + Blackbox Exporter exposent les métriques système et surveillent vos endpoints HTTP et certificats SSL.
  • PromQL permet des requêtes expressives pour calculer CPU, RAM, disk, taux d'erreur et percentiles de latence.
  • Grafana visualise tout en dashboards interactifs avec variables, annotations et alerting natif. Le dashboard 1860 couvre 95% des besoins dès l'import.
  • AlertManager route les alertes intelligemment avec groupement, inhibitions et silences pour éviter les tempêtes d'alertes.
  • Thanos ou Mimir étendent la stack pour la haute disponibilité et la rétention longue durée quand l'infrastructure grandit.

Pour aller plus loin, explorez Loki (centralisation des logs, même stack Grafana), Tempo (tracing distribué) et Pyroscope (profiling continu) pour compléter les trois piliers de l'observabilité : métriques, logs et traces.

Morgann Riu

Écrit par

Morgann Riu

Expert en cybersécurité et administration Linux. Je partage mes connaissances à travers des tutoriels gratuits et des formations pour aider les administrateurs systèmes et développeurs à sécuriser leurs infrastructures.

Partager ce tutoriel

Cet article vous a plu ?

Commentaires

Checklist Sécurité Linux

30 points essentiels pour sécuriser un serveur Linux. Recevez aussi les nouveaux tutoriels par email.

Pas de spam. Désabonnement en 1 clic.