Aller au contenu

Monitoring de flotte

Un device silencieux est un device suspect — la supervision de flotte transforme l'absence de signal en alerte actionnable avant que le problème ne devienne critique.


Le problème du device silencieux

Le mode de défaillance le plus fréquent dans une flotte IoT n'est pas le crash visible — c'est le silence. Un device qui cesse de reporter ses données sans générer d'erreur explicite. La batterie est à plat. La carte SIM a expiré. Le câble RS-485 s'est décroché. Le service systemd est tombé en boucle de redémarrage et ne remonte plus rien. La passerelle réseau a changé d'adresse IP.

Sans supervision active, ces défaillances passent inaperçues jusqu'à ce qu'un opérateur de terrain constate physiquement l'absence de données dans l'interface. Sur des installations distantes ou difficiles d'accès, cette découverte peut prendre des jours.

Le monitoring de flotte inverse ce paradigme : c'est le système qui détecte et alerte, pas l'opérateur qui surveille.


Le heartbeat comme signal de vie

Le heartbeat est le mécanisme fondamental de détection de devices silencieux. Chaque device envoie périodiquement un message de présence, indépendamment de ses données métier.

Implémentation côté device

# heartbeat.py — service de heartbeat sur gateway Linux
import time
import json
import socket
import subprocess
import paho.mqtt.client as mqtt
from pathlib import Path

DEVICE_ID    = open("/etc/device-id").read().strip()
MQTT_HOST    = "mqtt-prod.industrial.local"
MQTT_PORT    = 8883
HB_INTERVAL  = 60  # secondes — configurable via desired state

def collect_heartbeat() -> dict:
    """Collecte les métriques système pour le heartbeat."""
    # Uptime
    with open("/proc/uptime") as f:
        uptime_s = float(f.read().split()[0])

    # Charge CPU (moyenne 1 min)
    with open("/proc/loadavg") as f:
        load_1m = float(f.read().split()[0])

    # Mémoire disponible
    mem_info = {}
    with open("/proc/meminfo") as f:
        for line in f:
            key, val = line.split(":")
            mem_info[key.strip()] = int(val.strip().split()[0])  # kB

    # Température CPU (Raspberry Pi)
    try:
        temp_raw = Path("/sys/class/thermal/thermal_zone0/temp").read_text()
        cpu_temp_c = int(temp_raw.strip()) / 1000.0
    except FileNotFoundError:
        cpu_temp_c = None

    # Espace disque disponible
    stat = subprocess.run(["df", "-BM", "/"], capture_output=True, text=True)
    disk_free_mb = int(stat.stdout.splitlines()[1].split()[3].rstrip("M"))

    # Compteur de services en échec
    failed = subprocess.run(
        ["systemctl", "list-units", "--state=failed", "--no-legend"],
        capture_output=True, text=True
    )
    failed_services = len(failed.stdout.strip().splitlines()) if failed.stdout.strip() else 0

    return {
        "device_id":       DEVICE_ID,
        "timestamp":       time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        "firmware_version": "2.4.0",
        "uptime_s":        int(uptime_s),
        "cpu_load_1m":     load_1m,
        "cpu_temp_c":      cpu_temp_c,
        "mem_free_mb":     mem_info.get("MemAvailable", 0) // 1024,
        "disk_free_mb":    disk_free_mb,
        "failed_services": failed_services,
        "connected_zigbee_devices": _count_zigbee_devices(),
    }

def _count_zigbee_devices() -> int:
    try:
        result = subprocess.run(
            ["mosquitto_sub", "-h", "127.0.0.1", "-t", "zigbee2mqtt/bridge/devices",
             "-C", "1", "-W", "2"],
            capture_output=True, text=True
        )
        devices = json.loads(result.stdout)
        return len([d for d in devices if d.get("interview_completed")])
    except Exception:
        return -1  # Erreur de collecte

def run():
    client = mqtt.Client(client_id=f"{DEVICE_ID}-heartbeat", protocol=mqtt.MQTTv5)
    # Configurer TLS (omis ici pour la lisibilité)
    client.connect(MQTT_HOST, MQTT_PORT)
    client.loop_start()

    while True:
        payload = collect_heartbeat()
        client.publish(
            f"fleet/{DEVICE_ID}/heartbeat",
            json.dumps(payload),
            qos=1,
            retain=True   # Le dernier heartbeat reste disponible pour les late subscribers
        )
        time.sleep(HB_INTERVAL)

if __name__ == "__main__":
    run()

Détection de silence côté serveur

Le serveur fleet manager calcule l'âge du dernier heartbeat reçu. Si cet âge dépasse HB_INTERVAL × 3 (3 intervalles manqués consécutifs), l'alerte "device silencieux" est déclenchée.

-- Requête TimescaleDB / PostgreSQL pour détecter les devices silencieux
SELECT
    device_id,
    MAX(timestamp)           AS last_seen,
    NOW() - MAX(timestamp)   AS silence_duration,
    EXTRACT(EPOCH FROM (NOW() - MAX(timestamp))) AS silence_s
FROM fleet_heartbeats
GROUP BY device_id
HAVING EXTRACT(EPOCH FROM (NOW() - MAX(timestamp))) > 180   -- > 3 × 60 s
ORDER BY silence_s DESC;

Métriques clés par catégorie de device

Chaque type de device a ses métriques spécifiques. Le tableau suivant liste les métriques importantes pour les types de devices les plus courants.

Catégorie Métrique Seuil warning Seuil critical Unité
Toutes Âge dernier heartbeat > 3 min > 10 min secondes
Toutes Failed services systemd ≥ 1 ≥ 3 count
Gateway Linux CPU température > 70 °C > 80 °C °C
Gateway Linux Mémoire libre < 300 MB < 100 MB MB
Gateway Linux Disque libre < 5 GB < 1 GB MB
Gateway Linux Charge CPU (1 min) > 2,0 > 3,5 load
Gateway Linux Devices Zigbee connectés < 80 % attendus < 50 % attendus %
Capteur BLE/LoRa Niveau batterie < 20 % < 10 % %
Capteur BLE/LoRa RSSI < -80 dBm < -95 dBm dBm
Capteur LoRa SNR < 5 dB < 0 dB dB
Mobile terrain Batterie < 25 % < 10 % %
Mobile terrain Signal cellulaire < -100 dBm < -110 dBm dBm
Mobile terrain GPS précision > 10 m > 50 m mètres

Architecture de supervision

graph TB
    subgraph DEVICES ["Devices terrain"]
        D1[Gateway #1\nheartbeat toutes les 60 s]
        D2[Gateway #2\nheartbeat toutes les 60 s]
        Dn[Gateway #N\nheartbeat toutes les 60 s]
        C1[Capteurs IoT\nmétriques embarquées]
    end

    subgraph INGEST ["Couche d'ingestion"]
        MQTT[Broker MQTT\nMosquitto / EMQX]
        TELE[Télémétrie collector\nTelegraf / Vector]
    end

    subgraph STORE ["Stockage"]
        TSDB[Time-series DB\nInfluxDB v3 / TimescaleDB]
        ALERT_DB[État des alertes\nPostgreSQL]
    end

    subgraph PROCESS ["Traitement et alerting"]
        RULES[Moteur de règles\nCapacitor / Alertmanager]
        SILENCE[Détection silence\nCron 30 s]
    end

    subgraph NOTIFY ["Notification"]
        PD[PagerDuty\nOn-call rotation]
        SLACK[Slack / Teams\ncanal #iot-alertes]
        SMS[SMS / Appel\nurgences critiques]
        TICKET[Ticketing\nJira / Zammad]
    end

    subgraph VIZ ["Visualisation"]
        GRAF[Grafana\nDashboards]
        MOBILE_APP[App mobile\nopérateurs terrain]
    end

    D1 & D2 & Dn & C1 --> MQTT
    MQTT --> TELE
    TELE --> TSDB
    TSDB --> RULES
    TSDB --> SILENCE
    RULES --> ALERT_DB
    SILENCE --> ALERT_DB
    ALERT_DB --> PD & SLACK & SMS & TICKET
    TSDB --> GRAF
    ALERT_DB --> GRAF
    GRAF --> MOBILE_APP

Configuration des alertes dans Grafana / Alertmanager

Règle Prometheus — heartbeat manquant

# prometheus/alerts/fleet.yml
groups:
  - name: fleet_health
    interval: 1m
    rules:
      # Device silencieux depuis > 3 minutes
      - alert: DeviceSilent
        expr: |
          (time() - fleet_heartbeat_last_seen_timestamp) > 180
        for: 0m
        labels:
          severity: warning
        annotations:
          summary: "Device silencieux : {{ $labels.device_id }}"
          description: >
            Le device {{ $labels.device_id }} (site: {{ $labels.site }})
            n'a pas envoyé de heartbeat depuis
            {{ $value | humanizeDuration }}.

      # Batterie critique
      - alert: LowBattery
        expr: fleet_device_battery_percent < 10
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Batterie critique : {{ $labels.device_id }}"
          description: >
            Batterie à {{ $value }}% sur {{ $labels.device_id }}.
            Intervention terrain requise.

      # Température CPU gateway élevée
      - alert: GatewayCpuOverheat
        expr: fleet_device_cpu_temp_celsius > 80
        for: 3m
        labels:
          severity: critical
        annotations:
          summary: "Surchauffe gateway : {{ $labels.device_id }}"

      # Pourcentage de devices actifs trop bas
      - alert: FleetHealthDegraded
        expr: |
          (count(fleet_heartbeat_last_seen_timestamp > time() - 300)
           / count(fleet_heartbeat_last_seen_timestamp)) < 0.95
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Santé flotte dégradée  {{ $value | humanizePercentage }} de devices actifs"

Dashboards terrain

Un dashboard de flotte efficace répond en moins de 5 secondes à la question de l'opérateur de permanence : "Est-ce que tout va bien en ce moment ?"

Vue de synthèse — niveau top

Panneau Contenu Actualisation
Santé globale % devices actifs (objectif : ≥ 99 %) 30 s
Carte géographique Devices verts / orange / rouge sur carte 30 s
Alertes actives Compteur par sévérité Temps réel
Dernières 24 h Graphe nombre de devices actifs vs temps
Batteries critiques Liste devices < 15 % 5 min
File d'attente OTA Devices en cours de mise à jour 1 min

Vue détail — device individuel

Accessible en cliquant sur un device dans la carte ou la liste :

  • Historique des heartbeats sur 7 jours (timeline verte = actif, rouge = absent)
  • Graphes CPU, mémoire, température sur 24 h
  • Log des 10 derniers événements (connexions, déconnexions, alertes, mises à jour)
  • État du desired state vs reported state
  • Boutons d'action : redémarrer le service, déclencher une mise à jour, wipe

SLA et KPI de flotte

Pour un contrat de service ou un engagement interne, définir les KPI de flotte permet de mesurer objectivement la qualité opérationnelle.

KPI Formule Objectif typique
Disponibilité flotte Devices actifs / Total × 100 ≥ 99,0 %
MTTD (détection) Temps moyen entre panne et alerte < 5 min
MTTR (restauration) Temps moyen entre alerte et résolution < 4 h
Couverture firmware Devices à jour / Total × 100 100 % sous 30 j
Taux d'alerte fausse Faux positifs / Total alertes < 5 %
Données manquées Mesures perdues / Attendues < 0,1 %

Ce qu'il faut retenir

  • Le heartbeat toutes les 60 secondes (3 intervalles manqués = alerte) est le mécanisme universel de détection de devices silencieux.
  • Les métriques à superviser varient selon le type de device : température CPU et disque pour les gateways, batterie et RSSI pour les capteurs sans fil.
  • L'architecture Prometheus + Alertmanager + Grafana couvre l'ensemble des besoins de supervision avec des outils open-source matures.
  • Les KPI de flotte (disponibilité, MTTD, MTTR) permettent de mesurer et d'engager la qualité de service opérationnelle.

Section suivante : Architecture cloud IoT — de la gateway au cloud : ingestion à grande échelle, pipelines de données et services managés.