Hébergé chez moi En prod depuis Février 2025 ± 5 services
C'est une application de journalisation qui permet l'ingestion de journaux, mais aussi la consultation et la recherche.
Il y a beaucoup de services hébergés avec chacun leurs fichiers de journalisation.
Cela peut vite être compliqué de se connecter au bon serveur, trouver le bon fichier de journaux, effectuer la recherche…
Une alerte par mail, ne serait pas du luxe en cas de problème.
Récupérer les journaux, les stocker, déclencher des alertes et effectuer des recherches.
Il faut donc deux services, un service web pour la recherche et un service qui s'occupe de l'ingestion et des alertes.
Programme d'ingestion
Pour encaisser une grosse charge, ce programme n'est pas appelé en direct.
Serveur Web
flowchart LR
subgraph redis [Redis]
canal@{shape: das, label: "IDs logs"}
hash@{shape: cyl, label: "Donnée logs"}
end
subgraph caroline [Caroline]
journal@{shape: rect, label: "Journal\n(ingestion)"}
serveur@{shape: rect, label: "Serveur\n(requêtage)"}
end
super-tv([Super TV])
framboise([Framboise])
postgres@{ shape: cyl, label: "Postgres" }
nginx@{ shape: hex, label: "Nginx" }
sakura@{shape: rounded, label: "Sakura\n(courriel)"}
utilisateur@{shape: circle, label: "Admin"}
super-tv e1@--> canal
e1@{animate: true}
framboise e2@--> canal
e2@{animate: true}
super-tv --> hash
framboise --> hash
canal e3@--> journal
e3@{animate: true}
hash <--> journal
journal -- alerte --> sakura
journal --> postgres
utilisateur <--> nginx
nginx <--> serveur
serveur <--> postgres
La structure a été construite à partir du logger python.
| Nom | Type base de donnée | Requis | Description |
|---|---|---|---|
| id | character varying | x | Identifiant unique du journal en base. Par exemple : framboise-framboise-ERROR-1771248714768-826314
|
| application | character varying | x | Nom de l'application qui envoie le journal |
| niveau_log | niveau | x |
|
| logger | character varying | x | Le nom du logger |
| date_log | bigint | x | La date du journal en millisecondes, timestamp + millisecondes |
| message | character varying | Le message du journal
<POST|/liens/cherche-titre> Problème
|
|
| fichier | character varying | Nom du fichier du journal. Par exemple : application.py |
|
| chemin | character varying | Chemin complet du fichier d'où le journal provient. Par exemple : framboise/application.py |
|
| ligne | int4 | x | Ligne du fichier où le journal apparaît |
| extra | jsonb | Par défaut ('{}'::jsonb) Un objet en json, en clef(str) -> valeur(Any)
|
|
| trace | character varying | La trace de l'erreur, par exemple :
Traceback (most recent call last):
File "/****/framboise/venv/lib/python3.13/site-packages/flask/app.py", line 1484, in full_dispatch_request
rv = self.dispatch_request()
File "/****/framboise/venv/lib/python3.13/site-packages/flask/app.py", line 1469, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
File "/****/framboise/framboise/securite.py", line 66, in wrapper
|
Il est possible de faire une requête basique par mots-clefs ou/et niveau.
Dans le cas d'une requête complexe, une expression basée sur la grammaire Lucie est demandée. Seule cette requête sera utilisée, le reste du formulaire sera ignoré.
C'est le sous-module Cécile qui s'occupe de la transformation en SQL, le module après analyse syntaxique et sémantique générera la partie se trouvant après le where.
La requête ci-dessous :
$niveau > "INFO"
et $date entre "08/2025" "09/2025"
et ($message contient "m6" ou $message=="rtl9")
et $extra->"toto" == "pouet"
et $message contient "tutu"
Sera transformée en SQL comme ci-dessous :
niveau_journal > @texte_1
and (date_journal > @texte_6 and date_journal < @texte_7)
and (unaccent(message) ilike @texte_11 or message = @texte_15)
and extra->>@texte_20 = @texte_22
and unaccent(message) ilike @texte_26
Après l'ajout du journal dans la base de données, le système d'alerte va être activé si l'application contient des alertes et des utilisateurs à alerter.
Dans ce cas, le système va scanner chaque alerte de l'application. Pour chaque alerte selectionnée, le système enverra un événement à Sakura (système de notification) qui enverra un courriel à chaque utilisateur. Ce courriel contiendra diverses informations.
| Nom | Requis | Description |
|---|---|---|
| nom | x | Nom de l'alerte |
| description | x | Description de l'alerte |
| requête | x | Requête qui retourne vrai si le journal correspond à l'alerte |
| délai de relance | Permet d'avoir une attente avant d'envoyer le prochain courriel si l'alerte se reproduit. Si aucune valeur n'est présente, envoi un courriel à chaque alerte. |
nom: Super Tv
identifiant: tv-dev
jeton: super-tv-dev
duree_retention: 31
utilisateurs:
- johann
- toto
alertes:
- nom: Mise à jour foiré
description: Erreur de mise à jour
requete: $niveau == "INFO" et $logger == "maj"
delai_relance: ~j
- nom: Détail d'un programme
description: Détail d'un programme
requete: $niveau == "ERROR" et $logger == "detail"
delai_relance: 6h
- nom: pouet
description: pouet
requete: $niveau == "FATAL"
Pour éviter d'avoir un envoi de courriel à chaque alerte, il est possible de metter un délai de relance. Il y a deux types de délai, par durée et par intervalle.
À chaque alerte avant d'envoyer un courriel, si un délai de relance est présent : le programme vérifie s'il y a un cadenas de relance dans Redis. Si un cadenas est présent, aucun courriel n'est envoyé, sinon il est envoyé.
Le TTL de la valeur dans Redis est calculé par rapport à la date de l'alerte, le cadenas sera automatiquement détruit par Redis quand le TTL de la valeur sera à zéro.
Dans ce cas le délai de relance est une valeur fixe.
| Nom | Unité | Valeur en secondes | Exemple |
|---|---|---|---|
| Minute | m | duree × 60 | 12m -» 12 minutes |
| Heure | h | duree × 60 × 60 | 6h -» 6 heures |
| Jour | j | duree × 60 × 60 × 24 | 5j -» 5 jours |
| Semaine | s | duree × 60 × 60 × 24 × 7 | 3s -» 3 semaines |
| Mois | o | duree × 60 × 60 × 24 × 31 | 2o -» 2 mois |
| Année | a | duree × 60 × 60 × 24 × 365 | 1a -» 1 année |
Dans ce cas, le délai de relance est variable et calculé en fonction de la date de l'alerte.
Le délai de relance sera égal à date prochain envoi mail - date de l'alerte.
Pour une meilleure explication, prenons une alerte qui est arrivée le
Samedi 11 juillet à 19h35m23s.
Voici un tableau résumant les configurations, et les prochains envoi de mail pour la même alerte.
| Nom | Unité | Exemple | Prochain envoi de mail possible |
|---|---|---|---|
| Heure | h | ~h | Samedi 11 juillet à 20h00 |
| Jour | j | ~j | Dimanche 12 juillet à 00h00 |
| Semaine | s | ~s | Lundi 13 juillet à 00h00 |
| Mois | o | ~o | Samedi 1 août à 00h00 |
La requête configurée dans l'alerte doit aussi respecter la grammaire Lucie,
mais cette fois-ci, c'est le sous-module Capucine qui sera utilisé pour transformer
la requête en expression Go.
Le journal reçu sera transformé en variable et donné à une expression.
Si cette expression renvoie vrai alors un courriel sera potentiellement envoyé.
Capucine va générer un arbre à partir de la requête.
La requête ci-dessous va générer cet arbre :
$niveau > "INFO"
ou $message contient "pouet"
et $message contient "toto"
flowchart TB
A[et] --> B[ou]
B --> C["`niveau **>** #quot;INFO#quot;`"]
B --> D["`message
**contient**
#quot;pouet#quot;`"]
A --> E["`message
**contient**
#quot;toto#quot;`"]
Par simplicité, les opérateurs et et ou ont la même priorité.
L'expression à gauche sera plus prioritaire que l'expression à droite.
Pour résoudre ce problème, il suffit de rajouter des parenthèses, comme ci-dessous :
$niveau > "INFO"
ou (
$message contient "pouet"
et $message contient "toto"
)
Pour l'instant, c'est seulement utilisé pour les programmes en Python, un handler spécifique a été créé pour s'intégrer au logger Python.
journalisation:
gestionnaires:
caroline:
classe: caroline.CarolineHandler
formateur: caroline
params:
chemin_execution: /opt/******/framboise
token: framboise-dev
redis_hote: natacha.*****.fr
canal_journaux: caroline:journaux
chemin_base_journaux: caroline:journaux
journaux:
requete:
gestionnaires:
- caroline
niveau: DEBUG
logger_requete = logging.getLogger("requete")
logger_requete.info(
"<%s> %s -> %s",
requete_info.method,
requete_info.remote_addr,
requete_info.path,
)
logger_framboise = logging.getLogger("framboise")
logger_framboise.warning(
erreur.message,
extra={
"pseudo": requete_info.pseudo,
"methode": requete_info.method,
"chemin": requete_info.path,
},
exc_info=erreur,
)
logger = logging.getLogger("ajout-serie")
logger.info(f"Ajout épisode -> {episode.numero} - {episode.titre}",)