Scripts externes pour les alertes

Le canal « Processus » de la livraison externe des alertes lance sur le nœud votre programme ou votre script à chaque événement d”alerte : le déclenchement d’un incident et sa résolution. Les alertes parviennent ainsi à tout système qu’un script peut appeler : passerelle SMS, messagerie instantanée, webhook, supervision maison ou système de tickets. Cette page décrit comment le nœud lance la commande, ce qu’elle reçoit et comment le résultat est traité, et fournit deux modèles prêts à l’emploi : en /bin/sh et en Python 3.

Le modèle général de la livraison externe et les deux autres canaux, Telegram et e-mail, sont décrits dans Livraison externe des alertes, la fenêtre de configuration dans Destinations d’alerte.

Configuration du canal

Le canal se configure dans l’onglet « Processus » de la fenêtre « Destinations d’alerte ». La fenêtre s’ouvre avec le bouton « Destinations d’alerte… » dans les paramètres de l’alerteur (Destinations d’alerte). Les mêmes valeurs se trouvent dans la section alerter du fichier de configuration.

Champ

Clé

Rôle

« Activer la destination processus »

process-enable

Active le canal. Désactivé par défaut.

« Commande »

process-command

Ligne de commande : le programme et ses arguments. Vide par défaut ; un canal activé avec une commande vide ne lance rien.

« Sévérité minimale »

process-min-severity

Seuil inférieur de gravité de l’événement : Info (3), Warning (4, par défaut), Error (5), Critical (6), AdminAction (7).

« Délai d’expiration, ms »

process-timeout-ms

Nombre de millisecondes accordées à un lancement : de 100 à 600000, 5000 par défaut.

Les quatre valeurs sont lues à chaque événement : une modification prend donc effet dès l’alerte suivante, sans redémarrage du service.

Lancement de la commande

Sans shell. La ligne « Commande » est découpée en mots selon les règles du shell, mais aucun shell n’est lancé. Les mots sont séparés par des espaces ; le texte entre apostrophes est pris tel quel ; entre guillemets doubles, la barre oblique inverse n’échappe que " et \ ; hors guillemets, la barre oblique inverse échappe le caractère suivant ; une paire d’apostrophes vide '' donne un argument vide. Le premier mot est le programme, les suivants sont ses arguments. Les pipelines |, les redirections >, la substitution de variables $VAR, ~ et les motifs de noms de fichiers ne fonctionnent pas : ils sont transmis au programme comme du texte ordinaire. Tout cela se fait à l’intérieur du script.

Recherche du programme. Si le premier mot contient /, il s’agit d’un chemin de fichier : le fichier doit exister et être exécutable, sinon chaque événement se termine par l’erreur process-command not executable. Un nom sans / est recherché dans le PATH du service. Il est plus sûr de toujours indiquer le chemin complet.

Environnement d’exécution.

  • Utilisateur et groupe : les mêmes que ceux du service, pss.

  • Répertoire de travail : la racine / ; indiquez des chemins complets dans le script.

  • Les variables d’environnement sont héritées du service (y compris son PATH) ; les variables PSS_ALERT_* s’y ajoutent.

  • Les fichiers et sockets ouverts du service ne sont pas transmis au script : il ne dispose que de l’entrée standard, de la sortie standard et de la sortie d’erreur standard.

  • La sortie standard du script va dans le journal système du service, c’est-à-dire dans la sortie de journalctl -u pss (Journal du service). La sortie d’erreur standard est lue par le nœud lui-même, après la fin du script (Résultat, délai d’expiration et journal).

Ce que reçoit le script

Chaque événement est transmis de deux manières à la fois : champ par champ dans des variables d’environnement et en entier au format JSON sur l’entrée standard. Le nœud ne passe aucun argument : il n’y a que ceux inscrits dans la « Commande ».

Variables d’environnement

Variable

Valeur

PSS_ALERT_ID

Identifiant stable de l’incident, identique pour son déclenchement et sa résolution, par exemple 2:12:3.

PSS_ALERT_STATE

raised : l’incident s’est déclenché, cleared : il est résolu.

PSS_ALERT_SEVERITY

Gravité sous forme de nombre : 3–7 (voir « Sévérité minimale » ci-dessus).

PSS_ALERT_SEVERITY_STR

Gravité sous forme de mot : Info, Warning, Error, Critical, AdminAction.

PSS_ALERT_CODE

Code du type d’incident sous forme de nombre (Catalogue des codes d’alerte).

PSS_ALERT_CODE_STR

Le même code sous forme de désignation textuelle, par exemple no-data, io-error.

PSS_ALERT_SRC_TYPE

Type de source : System, Stream, Cluster, EPG, Mosaic, DVB ou CS.

PSS_ALERT_SRC_ID

Identifiant de la source, par exemple le numéro du flux.

PSS_ALERT_NODE

Nom du nœud sur lequel l’incident s’est déclenché. Pour une alerte reçue d’un voisin du domaine Meshwork, c’est le nom de ce voisin.

PSS_ALERT_DISPLAY_NAME

Nom de l’objet lisible par un humain, par exemple le nom du flux.

PSS_ALERT_MESSAGE

Texte de l’alerte.

PSS_ALERT_TS

Heure de cet événement, en secondes Unix (UTC).

PSS_ALERT_RAISED_TS

Heure du déclenchement de l’incident.

PSS_ALERT_CLEARED_TS

Heure de la résolution ; 0 tant que l’incident est actif.

Les variables sont pratiques en /bin/sh : un texte contenant des espaces et des sauts de ligne y est transmis sans altération, et elles n’apparaissent pas dans la liste des processus (ps).

JSON sur l’entrée standard. Une seule ligne sans saut de ligne final, après quoi l’entrée est fermée. Le JSON contient les mêmes informations, mais la gravité et le type de source n’y figurent que sous forme de nombres (type de source : 1 — System, 2 — Stream, 3 — Cluster, 4 — EPG, 5 — Mosaic, 6 — DVB, 7 — CS), et les désignations textuelles du code et de la gravité en sont absentes. Champs :

  • toujours : id, state, ts, raised_ts, cleared_ts, severity, src_type, src_id, node, display_name, code, message ;

  • pour les incidents d’une entrée ou d’une sortie particulière : io_id et io_kind (input ou output) ;

  • pour les alertes AdminAction : admin_action_src ;

  • pour un incident « en battement » : flapping (true) et flap_cycles, son nombre de cycles (Quand le script est lancé).

Le script n’est pas tenu de lire l’entrée standard : tout ce qu’il lui faut se trouve aussi dans les variables. S’il la lit, il doit la lire jusqu’au bout, et non une seule ligne.

Exemple : sur le nœud edge-1, le flux Sport HD (numéro 12) ne reçoit plus de données depuis 15 secondes. Déclenchement :

{"id":"2:12:3","state":"raised","ts":1761134400,"raised_ts":1761134400,
 "cleared_ts":0,"severity":4,"src_type":2,"src_id":12,"node":"edge-1",
 "display_name":"Sport HD","code":3,
 "message":"[Sport HD] no data 15s (no-data threshold 15s)"}

Résolution du même incident deux minutes plus tard :

{"id":"2:12:3","state":"cleared","ts":1761134520,"raised_ts":1761134400,
 "cleared_ts":1761134520,"severity":4,"src_type":2,"src_id":12,"node":"edge-1",
 "display_name":"Sport HD","code":3,
 "message":"[Sport HD] no data 15s (no-data threshold 15s)"}

Ici, le JSON est réparti sur plusieurs lignes pour plus de lisibilité ; le nœud le transmet en une seule ligne. La résolution conserve la gravité et le texte de l’incident : il n’y a pas de texte « rétabli », c’est le champ state qui signale la résolution. Variables d’environnement du même déclenchement :

PSS_ALERT_ID=2:12:3
PSS_ALERT_STATE=raised
PSS_ALERT_SEVERITY=4
PSS_ALERT_SEVERITY_STR=Warning
PSS_ALERT_CODE=3
PSS_ALERT_CODE_STR=no-data
PSS_ALERT_SRC_TYPE=Stream
PSS_ALERT_SRC_ID=12
PSS_ALERT_NODE=edge-1
PSS_ALERT_DISPLAY_NAME=Sport HD
PSS_ALERT_MESSAGE=[Sport HD] no data 15s (no-data threshold 15s)
PSS_ALERT_TS=1761134400
PSS_ALERT_RAISED_TS=1761134400
PSS_ALERT_CLEARED_TS=0

Quand le script est lancé

  • Déclenchement et résolution. Le script est lancé une fois au déclenchement de l’incident et une fois à sa résolution. Les mises à jour intermédiaires d’un incident actif (répétition, nouveau texte, changement de gravité) ne lui sont pas transmises.

  • Seuil de gravité. La « Sévérité minimale » s’applique à l’incident, et non à chaque événement. Un incident inférieur au seuil n’est pas transmis. Si sa gravité atteint plus tard le seuil, le script reçoit une seule fois raised, au moment de cette hausse. cleared arrive si et seulement si raised a été transmis, quelle que soit la gravité au moment de la résolution. Un événement est considéré comme transmis dès qu’il a été remis au canal : si la livraison a échoué, l’incident est tout de même considéré comme ouvert, et sa résolution sera transmise.

  • Interrupteur principal. Lorsque l’alerteur du nœud est désactivé (Alerteur), le script n’est pas lancé du tout, y compris pour la résolution d’un incident déjà signalé.

  • Alertes du domaine. Lorsque les alertes sont répliquées sur le domaine Meshwork, le script reçoit aussi les alertes des voisins ; le nœud source est indiqué dans PSS_ALERT_NODE (Réplication des alertes à l’échelle du domaine). Si le canal est activé sur plusieurs nœuds du domaine, chacun d’eux signalera le même incident. Pour éviter les doublons, activez le canal sur un seul nœud.

  • Incidents en battement. Si un incident se déclenche et se résout encore et encore (par défaut, trois cycles consécutifs séparés d’au plus 600 secondes), le nœud cesse de le résoudre à chaque cycle : l’incident reste actif, et une seule résolution arrive lorsque la condition est restée rétablie pendant au moins 600 secondes. Le script reçoit un seul déclenchement et une seule résolution au lieu d’une paire par cycle. Ces valeurs sont définies par les clés flap-suppress, flap-cycles, flap-window-sec et flap-settle-sec de la section alerter du fichier de configuration (Le fichier de configuration pss.json).

  • Les alertes persistantes, qui ne se résolvent que manuellement, envoient leur résolution lorsque l’opérateur résout l’alerte (Consultation et résolution des alertes).

  • Redémarrage du service. La liste des incidents actifs est conservée en mémoire et se perd au redémarrage du service, sans aucune résolution. Une condition toujours présente se déclenche à nouveau, et le script reçoit un second raised avec le même PSS_ALERT_ID.

Il vaut donc mieux traiter l’événement d’après PSS_ALERT_ID et PSS_ALERT_STATE : un raised répété met à jour un incident connu au lieu d’en ouvrir un nouveau.

File d’attente et ordre

  • Le script est lancé strictement une instance à la fois : le lancement suivant commence après la fin du précédent, et les événements sont traités dans leur ordre d’arrivée.

  • Les lancements sont gérés par un fil d’exécution d’arrière-plan dédié au canal. Un script lent ne retarde ni les autres canaux ni le fonctionnement du nœud, mais il retarde les événements suivants de ce même canal.

  • Pendant que le script s’exécute, les nouveaux événements attendent dans la file d’attente du canal, d’une capacité de 512 événements. Lorsque la file est pleine, les événements les plus récents sont rejetés, et delivery queue full, dropped alert(s) est écrit dans le journal du service.

  • À l’arrêt et au redémarrage du service, les événements de la file sont rejetés ; le service n’attend que le lancement déjà en cours.

Faites le calcul : lors d’une tempête d’une centaine d’événements, un script qui s’exécute en deux secondes prend plus de trois minutes de retard sur les événements. Placez les livraisons longues en arrière-plan (voir la section suivante).

Résultat, délai d’expiration et journal

  • Code de sortie 0 : l’événement est livré.

  • Code non nul : échec de la livraison. Une ligne de la forme [alert:process] deliver failed: process-command '/usr/local/bin/pss-alert.sh' exit 1: <texte de stderr>, avec les 4 derniers Ko de la sortie d’erreur, est écrite dans le journal du service (Journal du service).

  • La sortie d’erreur est lue par le nœud pendant que le script s’exécute et n’est affichée qu’avec un code non nul. Une sortie sur stderr avec le code 0 n’est pas une erreur et n’a aucun effet ; une sortie abondante sur stderr ne ralentit pas le script et ne provoque pas d’expiration du délai.

  • Délai d’expiration. Si le script ne s’est pas terminé dans le « Délai d’expiration, ms », le nœud l’arrête de force (SIGKILL) et écrit timed out after <N> ms. Seul le script lui-même est arrêté ; les processus qu’il a lancés continuent de tourner.

  • Pas de nouvelle tentative. Un événement en échec n’est pas renvoyé ; le canal passe au suivant.

  • Limitation de la fréquence des messages. La première erreur du canal est écrite immédiatement, les suivantes au plus une fois par minute, avec le nombre d’erreurs survenues pendant ce temps en fin de ligne ((x12)).

Note

Processus en arrière-plan. Un processus laissé en arrière-plan par le script ne retarde pas les événements suivants du canal, même s’il a hérité de la sortie d’erreur. Dans les versions antérieures à 2.0.2.415, un tel processus bloquait le canal jusqu’à sa fin ; aussi, sur les nœuds où des versions antérieures peuvent fonctionner, et de manière générale par précaution, lancez les tâches en arrière-plan avec l’entrée et les sorties fermées :

some-command </dev/null >/dev/null 2>&1 &

ou avec une redirection vers votre propre fichier journal :

setsid some-command </dev/null >>/var/log/pss-alerts/bg.log 2>&1 &

Sécurité

  • Le script s’exécute avec les droits du service, ceux de l’utilisateur pss. Faites de root le propriétaire du fichier du script et n’accordez à l’utilisateur pss que la lecture et l’exécution (mode 750, groupe pss) : le service ne peut alors pas modifier le script qu’il lance lui-même.

  • Ne transmettez pas de secrets dans les arguments de la « Commande ». Les arguments sont visibles par tous les utilisateurs du système dans la liste des processus (ps), sont stockés dans le fichier de configuration et apparaissent dans l’interface web pour le rôle Admin (pour les rôles Restricted admin et Viewer, la ligne de commande est masquée). Conservez les jetons et les mots de passe dans le script lui-même ou dans un fichier séparé lisible uniquement par l’utilisateur pss.

  • Le texte de l’alerte (nom du flux, message) provient de la configuration et des sources. Ne l’insérez pas sans guillemets dans des commandes shell et ne le passez pas à eval. Les modèles ci-dessous ne transmettent les valeurs qu’entre guillemets.

Modèle en /bin/sh

Le script écrit une ligne dans le journal pour chaque événement et se termine avec le code 0. L’emplacement de la vraie livraison est signalé par un commentaire.

#!/bin/sh
# Récepteur d'alertes Perfect Streamer (canal « Processus »).
# Le nœud lance le script à chaque déclenchement et à chaque résolution d'incident.
# Champs de l'événement dans les variables PSS_ALERT_*, JSON complet sur l'entrée standard.

LOG=/var/log/pss-alerts/alerts.log

# JSON de l'entrée standard : une seule ligne sans saut de ligne final,
# d'où la lecture par cat et non par read.
json=$(cat)

# Une ligne dans le journal. Erreur d'écriture = code non nul : le nœud l'écrira
# dans son propre journal avec le texte de stderr.
printf '%s %s %s %s [%s] %s: %s\n' \
    "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$PSS_ALERT_STATE" "$PSS_ALERT_SEVERITY_STR" \
    "$PSS_ALERT_NODE" "$PSS_ALERT_CODE_STR" "$PSS_ALERT_DISPLAY_NAME" \
    "$PSS_ALERT_MESSAGE" >> "$LOG" || exit 1

# Branchez ici la vraie livraison, par exemple un webhook.
# Un appel synchrone doit tenir dans le « Délai d'expiration, ms » (5 s par défaut) :
#
# printf '%s' "$json" | curl -sS -m 3 -X POST \
#     -H 'Content-Type: application/json' --data-binary @- \
#     https://hooks.example.com/pss >/dev/null || exit 1
#
# Livraison longue : uniquement en arrière-plan, entrée et sorties fermées,
# sinon le canal se bloque (voir « Résultat, délai d'expiration et journal ») :
#
# ( printf '%s' "$json" | curl -sS -m 30 -X POST \
#     -H 'Content-Type: application/json' --data-binary @- \
#     https://hooks.example.com/pss ) </dev/null >/dev/null 2>&1 &

exit 0

Modèle en Python 3

La même chose en Python 3, avec la seule bibliothèque standard (Python 3.6 ou plus récent).

#!/usr/bin/env python3
# Récepteur d'alertes Perfect Streamer (canal « Processus »).
# Le nœud lance le script à chaque déclenchement et à chaque résolution d'incident.
# Champs de l'événement dans les variables PSS_ALERT_*, JSON complet sur l'entrée standard.

import json
import os
import sys
import time

LOG = "/var/log/pss-alerts/alerts.log"


def main():
    # Toute l'entrée standard : une ligne JSON sans saut de ligne final.
    raw = sys.stdin.read()
    alert = json.loads(raw) if raw else {}
    env = os.environ

    line = "{} {} {} {} [{}] {}: {}\n".format(
        time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        alert.get("state", env.get("PSS_ALERT_STATE", "")),
        env.get("PSS_ALERT_SEVERITY_STR", ""),
        alert.get("node", ""),
        env.get("PSS_ALERT_CODE_STR", ""),
        alert.get("display_name", ""),
        alert.get("message", ""),
    )
    # Une erreur d'écriture termine le script avec un code non nul, et le texte
    # de l'erreur part sur stderr : le nœud l'écrit dans son propre journal.
    with open(LOG, "a", encoding="utf-8") as f:
        f.write(line)

    # Branchez ici la vraie livraison, par exemple un webhook.
    # L'appel doit tenir dans le « Délai d'expiration, ms » (5 s par défaut) :
    #
    # import urllib.request
    # req = urllib.request.Request(
    #     "https://hooks.example.com/pss",
    #     data=raw.encode("utf-8"),
    #     headers={"Content-Type": "application/json"},
    # )
    # urllib.request.urlopen(req, timeout=3).close()
    #
    # Lancez les processus d'arrière-plan (subprocess.Popen) avec
    # stdin/stdout/stderr=subprocess.DEVNULL et start_new_session=True
    # (voir « Résultat, délai d'expiration et journal »).

    return 0


if __name__ == "__main__":
    sys.exit(main())

Installation et vérification

  1. Créez le répertoire du journal, accessible en écriture à l’utilisateur pss :

    sudo install -d -o pss -g pss -m 750 /var/log/pss-alerts
    
  2. Enregistrez le modèle, par exemple sous /usr/local/bin/pss-alert.sh (ou pss-alert.py), puis définissez le propriétaire et les droits :

    sudo chown root:pss /usr/local/bin/pss-alert.sh
    sudo chmod 750 /usr/local/bin/pss-alert.sh
    
  3. Vérifiez le script manuellement en tant qu’utilisateur pss avec un exemple d’événement :

    echo '{"id":"2:12:3","state":"raised","severity":4,"node":"edge-1","display_name":"Sport HD","code":3,"message":"test"}' | \
      sudo -u pss env PSS_ALERT_ID=2:12:3 PSS_ALERT_STATE=raised \
        PSS_ALERT_SEVERITY=4 PSS_ALERT_SEVERITY_STR=Warning \
        PSS_ALERT_CODE=3 PSS_ALERT_CODE_STR=no-data \
        PSS_ALERT_NODE=edge-1 PSS_ALERT_DISPLAY_NAME='Sport HD' \
        PSS_ALERT_MESSAGE=test \
        /usr/local/bin/pss-alert.sh
    echo "exit=$?"
    tail -n 1 /var/log/pss-alerts/alerts.log
    

    Le résultat attendu est exit=0 et une nouvelle ligne dans le journal. Mesurez au passage la durée d’exécution : elle doit être nettement inférieure au « Délai d’expiration, ms ».

  4. Dans l’onglet « Processus » de la fenêtre « Destinations d’alerte », indiquez dans « Commande » le chemin complet /usr/local/bin/pss-alert.sh, définissez la « Sévérité minimale », activez la destination et enregistrez.

  5. Vérifiez la livraison sur un flux de test : interrompez l’arrivée des données sur son entrée (par exemple, arrêtez l’émetteur). Après le délai d’absence de données (15 secondes par défaut, Paramètres de l’alerter), une alerte no-data de gravité Warning se déclenche, puis elle est résolue à la reprise des données. Les lignes raised puis cleared doivent apparaître dans le journal du script.

Problèmes courants

  • Le script n’est jamais lancé. Vérifiez l’interrupteur principal de l’alerteur et « Activer la destination processus », que la « Commande » n’est pas vide et que la gravité de l’événement n’est pas inférieure au seuil. Par défaut, le seuil est Warning : les événements Info ne sortent pas du nœud.

  • Dans le journal du service : process-command not executable. Le fichier n’existe pas au chemin indiqué, ou l’utilisateur pss n’a pas le droit de l’exécuter. Vérifiez avec ls -l, ainsi que les droits sur tous les répertoires du chemin.

  • Dans le journal du service : posix_spawnp failed. Le programme n’a pas pu être lancé : le plus souvent, le nom est indiqué sans chemin et introuvable dans le PATH du service, ou la première ligne du script (#!) est incorrecte. Indiquez le chemin complet.

  • Dans le journal du service : exit <N>. Le script s’est terminé en erreur ; le texte de l’erreur issu de stderr figure sur la même ligne. Cause fréquente : pas de droit d’écriture sur le répertoire du journal ou sur les fichiers de travail, car le script s’exécute en tant que pss et non root, et dans le répertoire /.

  • Dans le journal du service : timed out after <N> ms. Le script a dépassé le délai : accélérez la livraison (délai de l’appel réseau inférieur au « Délai d’expiration, ms »), augmentez le « Délai d’expiration, ms » ou placez la partie longue en arrière-plan, sortie fermée.

  • Les alertes arrivent avec un grand retard ou se perdent ; dans le journal : delivery queue full. Le script est trop lent pour le rythme des événements : la file d’attente du canal déborde. Accélérez le script ou relevez la « Sévérité minimale ».

  • Le canal s’est tu sans la moindre erreur dans le journal (versions antérieures à 2.0.2.415). Le script a très probablement laissé un processus en arrière-plan qui a hérité de la sortie d’erreur, et le nœud attend sa fin. Trouvez ce processus (ps -u pss -f), arrêtez-le et corrigez le lancement en arrière-plan : avec </dev/null >/dev/null 2>&1 ou avec une redirection vers un fichier. Depuis la version 2.0.2.415, un tel processus ne bloque plus le canal.

  • Le script fonctionne à la main, mais pas depuis le nœud. La cause est l’environnement : autre utilisateur (pss), répertoire de travail /, autre PATH, absence des variables de votre session. Vérifiez avec la commande de l’étape 3 de la section Installation et vérification, via sudo -u pss.

  • Un deuxième raised est arrivé avec le même PSS_ALERT_ID. Le service a redémarré, et l’incident actif a été détecté à nouveau (voir Quand le script est lancé).

  • Pas de cleared. L’alerteur du nœud a été désactivé pendant que l’incident était actif, ou le service a redémarré avant la résolution (voir Quand le script est lancé).

  • La même alerte arrive de plusieurs nœuds. Le canal est activé sur plusieurs nœuds du domaine qui répliquent les alertes. Ne le laissez que sur un seul.

  • La sortie du script est invisible. La sortie standard va dans journalctl -u pss, et la sortie d’erreur n’arrive dans le journal du service qu’avec un code de sortie non nul. Écrivez vos diagnostics dans votre propre fichier journal.