Tom Daluzeau
← Parcours Python · système
Python · système · leçon 4 / 8Ch. 4 · Python et le système12 minTerminée

Bilan du parc : log.log, max(key=) et code de sortie

Journaliser chaque serveur au niveau qui correspond à son état avec log.log, trouver le pire état du parc avec max et key, et en déduire le code de sortie du programme : 0, 1 ou 2.

Mis à jour le

À recouper avec le cours. Cette leçon a été écrite à partir du plan du chapitre, avant relecture avec les supports. En cas de doute, le cours fait foi.

Pourquoi on voit ça

Ton outil a vérifié chaque serveur et obtenu un état pour chacun. Il reste à rendre le verdict, pour deux publics :

  • un humain, qui lit les messages et veut voir tout de suite ce qui va mal ;
  • un programme (cron, pipeline CI, supervision), qui ne lit pas tes messages. Il ne regarde qu’une chose : le code de sortie.

Cette leçon assemble trois petites pièces : journaliser selon la gravité, trouver le pire état, et en faire un code de sortie. C’est la fin du chapitre, et ce qui rend ton outil utilisable dans une chaîne automatisée.

Journaliser selon la gravité : log.log

Le chapitre 3 a présenté logging et ses niveaux. Chaque niveau est en réalité un nombre :

Niveau Valeur Méthode habituelle
logging.DEBUG 10 log.debug(...)
logging.INFO 20 log.info(...)
logging.WARNING 30 log.warning(...)
logging.ERROR 40 log.error(...)

Dans le fil rouge, un serveur a trois états possibles : ok, degraded (il répond, mais mal) ou down (en panne). On veut un message INFO pour ok, WARNING pour degraded, ERROR pour down.

Première idée : un if / elif / else avec trois appels différents. Ça marche, mais c’est long. Mieux : log.log(niveau, message), qui prend le niveau en argument. Le niveau devient une donnée, qu’on range dans un dictionnaire.

import logging

logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
log = logging.getLogger("fleetcheck")

NIVEAUX = {
    "ok": logging.INFO,
    "degraded": logging.WARNING,
    "down": logging.ERROR,
}

resultats = [
    {"name": "web-1", "status": "ok"},
    {"name": "db-1", "status": "down"},
    {"name": "cache-1", "status": "degraded"},
]

for r in resultats:
    log.log(NIVEAUX[r["status"]], "%s : %s", r["name"], r["status"])
INFO web-1 : ok
ERROR db-1 : down
WARNING cache-1 : degraded

log.log(logging.ERROR, "...") fait exactement la même chose que log.error("..."). Les %s du message sont remplis, dans l’ordre, par les arguments suivants.

Le seuil filtre toujours

Le seuil de basicConfig s’applique comme d’habitude. Sans aucune configuration, il vaut WARNING : la ligne de web-1 disparaît, et seuls les serveurs à problème s’affichent.

db-1 : down
cache-1 : degraded

C’est tout l’intérêt de choisir le bon niveau : en usage normal, on ne voit que ce qui va mal ; avec -v, on voit tout.

état NIVEAUX[état] niveau = nombre "down" ERROR (40) "degraded" WARNING (30) seuil par défaut (WARNING) "ok" INFO (20) avec basicConfig(level=INFO) DEBUG (10) au-dessus de la ligne : affiché ; en dessous : ignoré
Le dictionnaire choisit le niveau ; le seuil décide ensuite si le message s'affiche.

Trouver le pire état : max avec key

Le bilan du parc tient en une question : quel est le pire état rencontré ? Tu connais max sur des nombres. Sur des chaînes, il a un piège :

etats = ["ok", "down", "degraded"]
print(max(etats))
ok

max compare les chaînes par ordre alphabétique, et « ok » vient après « down ». Il ne sait pas que down est plus grave. Il faut le lui dire, avec un dictionnaire qui donne une note de gravité à chaque état, et le paramètre key.

GRAVITE = {"ok": 0, "degraded": 1, "down": 2}

etats = ["ok", "down", "degraded"]
pire = max(etats, key=lambda e: GRAVITE[e])
print(pire)
down

key reçoit une fonction. max l’appelle sur chaque élément, compare les notes obtenues, puis renvoie l’élément qui a la meilleure note. Il renvoie "down", pas 2. C’est le même key que celui de sorted, vu au chapitre 1.

éléments "ok" "down" "degraded" key = GRAVITE[e] une note par élément notes 0 2 1 la plus haute note gagne résultat pire = "down" sans key : "ok" (l'alphabet) max renvoie l'élément, pas 2
key ne sert qu'à comparer : max renvoie l'élément dont la note est la plus haute.

Ça marche aussi sur la liste de dictionnaires :

pire = max(resultats, key=lambda r: GRAVITE[r["status"]])
print(pire["name"], pire["status"])
db-1 down

Ici pire est le dictionnaire entier du serveur le plus mal en point.

Le code de sortie : 0, 1 ou 2

Quand un programme se termine, il laisse un nombre au shell. echo $? l’affiche. La convention du cours :

Code Sens Exemples
0 tout va bien tous les serveurs sont ok
1 l’outil a tourné et a détecté un problème un serveur degraded ou down
2 erreur d’utilisation ou de configuration inventaire introuvable, argument invalide, variable obligatoire absente

La différence entre 1 et 2 est le point à ne pas rater :

  • 1 : l’outil a fait son travail. C’est le parc qui a un problème.
  • 2 : l’outil n’a pas pu faire son travail. C’est la façon de le lancer qu’il faut corriger. argparse renvoie déjà 2 tout seul quand un argument est mauvais.

Un pipeline peut ainsi réagir différemment : alerter l’équipe d’astreinte pour un 1, prévenir celui qui a écrit la configuration pour un 2.

tout va bien pire état : ok 0 le pipeline continue problème détecté pire état : degraded ou down 1 alerter l'astreinte outil empêché inventaire absent, argument faux 2 corriger le lancement de l'outil
Le code de sortie dit au pipeline qui doit agir : personne, l'astreinte, ou celui qui a configuré l'outil.

Du pire état au code

def code_sortie(etats):
    pire = max(etats, key=lambda e: GRAVITE[e])
    if pire == "ok":
        return 0
    return 1

print(code_sortie(["ok", "ok"]))
print(code_sortie(["ok", "degraded"]))
print(code_sortie(["down", "ok"]))
0
1
1

Un seul serveur dégradé suffit à faire passer le code à 1.

sys.exit : rendre le code au shell

sys.exit(n) termine le programme immédiatement avec le code n. Rien de ce qui suit n’est exécuté.

import sys

print("début")
sys.exit(1)
print("fin")
début

Puis, dans le shell :

echo $?
1

La forme habituelle : main() renvoie le code avec return, et une seule ligne, tout en bas, le transmet au shell.

import sys

def main():
    etats = ["ok", "degraded", "ok"]
    pire = max(etats, key=lambda e: GRAVITE[e])
    print("pire état :", pire)
    if pire == "ok":
        return 0
    return 1

if __name__ == "__main__":
    sys.exit(main())
pire état : degraded

Le code de sortie est 1. Un programme qui se termine sans appeler sys.exit, et sans exception, renvoie 0.

À retenir

  • log.log(niveau, message, ...) journalise à un niveau passé en argument : on le lit dans un dictionnaire état → niveau.
  • Les niveaux sont des nombres : DEBUG 10, INFO 20, WARNING 30, ERROR 40. Sans configuration, le seuil est WARNING.
  • max(etats) sans key compare par ordre alphabétique : il renvoie "ok".
  • max(etats, key=lambda e: GRAVITE[e]) compare les notes, mais renvoie l’élément.
  • Code de sortie : 0 tout va bien, 1 problème détecté dans le parc, 2 erreur d’utilisation ou de configuration.
  • sys.exit(n) arrête le programme tout de suite avec le code n ; forme habituelle : sys.exit(main()).

Entraînement

12 questions, dont 4 à compléter. Tu réponds, on passe à la suivante. La note arrive à la fin.

Cartes · 7

Réviser les cartes ici

Espace : retourner