Caroline

Hébergé chez moi En prod depuis Février 2025 ± 5 services

Présentation

C'est une application de journalisation qui permet l'ingestion de journaux, mais aussi la consultation et la recherche.

Pourquoi ?

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.

Solution

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

Schéma d'architecture

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

Technologies utilisées

Base de données

Serveur Web

Programme d'ingestion

Client Web

Structure d'un journal

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
  • Nom de l'aplication
  • Nom du logger
  • Niveau de log
  • Timestamp en millisecondes
  • Nombre aléatoire
application character varying x Nom de l'application qui envoie le journal
niveau_log niveau x
  • TOUT
  • TRACE
  • DEBUG
  • INFO
  • WARN
  • ERROR
  • FATAL
TOUT est le niveau le plus bas.
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)
{
    "chemin": "/liens/cherche-titre",
    "pseudo": "natacha",
    "query_params": {},
    "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0"
}
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

Système de requêtage

Il y a deux types de requêtages possibles :

Requêtage simple

image

Il est possible de faire une requête basique par mots-clefs ou/et niveau.

Requêtage complexe

image

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.

Exemple

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
        
Avec les valeurs suivantes:
texte_1: "INFO"
texte_15: "rtl9"
texte_6: 1753999200000
texte_20: "toto"
texte_7: 1756677600000
texte_22: "pouet"
texte_11: "%m6%"
texte_26: "%tutu%"

Les alertes

Introduction

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.

Configuration d'une alerte

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.

Exemple de config de plusieurs alertes pour une application

                    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"

                

Délai de relance

Description

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.

Comment ça marche

À 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.

Par durée

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

Par intervalle

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

Évaluation d'une alerte

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"
)

Comment utiliser ce bidule ?

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.

Exemple de config pour le handler

                    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
                

Exemple d'utilisation des logs

                    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}",)
                

Quelques captures d'écrans du client Web

Page d'accueil