
🌐 Aussi en: English · Deutsch · Español
Deep Dive de la série autour de l’Ansible Tour. Niveau : administration Linux avancée. Le Tour explique les rôles ; les deep dives expliquent la technique en dessous – comment elle fonctionne, pourquoi elle est là, ce que cette installation en fait exactement, comment l’exploiter au quotidien et quelles autres options il y aurait eu.
Les métriques disent qu’il y a un problème : la mémoire est à 90 %, un conteneur a redémarré trois fois, le disque se remplit. Elles disent rarement pourquoi. Le pourquoi est dans les journaux – et sur un hôte K3s, ils sont répartis entre le journal systemd, un répertoire par pod sous /var/log/pods, le journal d’audit et quelques fichiers applicatifs. Jusqu’ici, trouver quelque chose voulait dire SSH, journalctl, kubectl logs et grep, une source après l’autre. Cet article explique comment Loki et Grafana Alloy en font un seul endroit consultable à côté des tableaux de bord, ce qui tourne exactement sur le blog depuis le 19 octobre 2026 – et ce que les nouvelles alertes ont trouvé dès leur première heure, avant que quiconque l’ait cherché.
🧭 Comment fonctionnent Loki et Alloy
L’idée : indexer les labels, pas les mots
Les systèmes classiques comme Elasticsearch construisent un index plein texte : chaque mot de chaque ligne devient consultable, et l’index est souvent plus gros que les journaux eux-mêmes. Loki fait l’inverse. Il n’indexe qu’un petit ensemble de labels par flux – par exemple job="journal", unit="sshd.service", namespace="wordpress" – et stocke les lignes compressées dans des chunks. Une requête choisit d’abord les flux par leurs labels (peu coûteux, via l’index) puis ne parcourt que les chunks de ces flux à la recherche du texte (force brute, mais sur peu de données). C’est le même modèle que Prometheus pour les métriques, d’où leur bonne entente.
Il en découle une règle qui décide si Loki reste rapide : les labels ne doivent avoir que peu de valeurs. Hôte, job, unité systemd, namespace, conteneur – quelques dizaines de valeurs chacun – sont de bons labels. Un nom d’utilisateur, une adresse IP, un identifiant de requête ne le sont pas : chaque nouvelle valeur crée un nouveau flux, et des milliers de petits flux grossissent l’index et rendent les chunks inutiles. Ces valeurs restent dans la ligne et sont filtrées au moment de la requête.
Flux, chunks, index, compacteur
- Flux (stream) : toutes les lignes ayant exactement le même ensemble de labels.
{job="journal", unit="k3s.service", level="info"}est un flux. - Chunk : un bloc compressé de lignes d’un flux, écrit sur le système de fichiers quand il est plein ou assez ancien.
- Index (TSDB, schéma v13) : associe les ensembles de labels aux chunks, un fichier d’index par jour.
- Compacteur : fusionne les fichiers d’index quotidiens et – c’est important – est le composant qui supprime réellement les données plus anciennes que la durée de conservation. Sans
retention_enabled, ce réglage ne fait que masquer les anciennes données dans les requêtes.
LogQL en deux étapes
Une requête LogQL commence toujours par un sélecteur de flux entre accolades, suivi de filtres optionnels. Avec une fenêtre de temps et une fonction autour, la même requête devient une métrique – c’est ce qu’utilisent les alertes.
# toutes les lignes de l'unité k3s contenant "error" (sans casse)
{job="journal", unit="k3s.service"} |~ "(?i)error"
# en nombre : combien de ces lignes dans les 15 dernières minutes ?
sum(count_over_time({job="journal", unit="k3s.service"} |~ "(?i)error" [15m]))|= signifie « contient », != « ne contient pas », |~ et !~ sont les variantes regex. Des analyseurs comme | json ou | logfmt transforment des parties d’une ligne en labels temporaires qui n’existent que pendant la requête – la façon économique de filtrer sur une valeur qui ne doit jamais devenir un vrai label.
Grafana Alloy : le collecteur
Alloy est l’agent de Grafana, successeur de Promtail et du Grafana Agent. Sa configuration est un pipeline de composants : des sources lisent les journaux (journal, fichiers), des processeurs les transforment (analyse, renommage des labels, horodatage, suppression) et des writers les envoient à Loki. Chaque composant indique où va sa sortie (forward_to), si bien que la configuration se lit comme un diagramme de flux. Alloy retient dans son propre répertoire de données jusqu’où il a lu chaque fichier et le journal : un redémarrage ne perd aucune ligne et n’en relit aucune.
🛡️ Pourquoi il est là
- Un seul endroit pour tous les journaux. Journal, chaque pod, le journal d’audit et fail2ban, consultables dans Grafana à côté des métriques, sur les 14 derniers jours – y compris pour des pods qui n’existent plus et dont
kubectl logsne peut plus rien montrer. - Des alertes sur ce que les métriques ne voient pas. Un refus SELinux, un bannissement fail2ban raté ou un OOM kill du noyau sont des lignes de journal, pas des nombres. Avant, on ne les trouvait que si quelqu’un regardait par hasard.
- Assez léger pour un petit serveur. Loki et Alloy ensemble demandent environ un demi-gigaoctet de RAM et quelques mégaoctets de disque par heure sur le blog – une pile plein texte en demanderait plusieurs fois plus.
🔧 Ce que fait ce dépôt
Le rôle common_loki s’exécute après common_grafana (dont il utilise le dépôt de paquets et l’API) et avant common_monit (qui surveille ensuite les deux services). Depuis le 19 octobre 2026, il tournait sur le blog ; les deux serveurs Nextcloud ont suivi le 20 octobre après une journée d’observation.
Paquets épinglés
Loki et Alloy sont des RPM du dépôt officiel de Grafana, le même que celui de Grafana. Les versions sont épinglées dans versions.yml comme tout autre composant (loki_version, alloy_version) ; le test de fumée en fin d’exécution lit leur métrique build_info et signale si la version en cours n’est pas celle épinglée.
Loki : un binaire, disque local, 14 jours
Le paquet fournit une configuration de démonstration qui stocke tout sous /tmp/loki – perdu à chaque redémarrage – avec des journaux en mode debug. Le rôle la remplace entièrement. Les parties importantes :
server:
http_listen_address: 127.0.0.1 # seul Grafana sur le même hôte lit
grpc_listen_address: 127.0.0.1
common:
path_prefix: /var/lib/loki
replication_factor: 1
ring:
kvstore:
store: inmemory # un processus, pas de cluster
schema_config:
configs:
- from: 2026-10-01
store: tsdb
object_store: filesystem
schema: v13
limits_config:
retention_period: 336h # 14 jours
compactor:
retention_enabled: true # la partie qui supprime vraiment
analytics:
reporting_enabled: falseLe modèle est vérifié avant de remplacer le vrai fichier : le validate d’Ansible lance loki -verify-config sur le candidat. Une faute de frappe fait donc échouer le playbook, pas le service au redémarrage.
Alloy : quatre sources, quelques labels
- Journal avec les labels
unit,levelettransport.transport="kernel"isole les messages du noyau – c’est là qu’écrit l’OOM killer. - Journaux des pods depuis
/var/log/pods/<namespace>_<pod>_<uid>/<conteneur>/N.log. Une règle de relabel extrait namespace, pod et conteneur du chemin ;stage.criretire le préfixe de containerd (heure, flux, drapeau de ligne partielle) et recolle les lignes coupées. - audit.log. Sur ces hôtes, les refus SELinux ne sont pas dans le journal – auditd possède le socket d’audit du noyau –, donc Alloy lit le fichier.
- fail2ban.log, car un bannissement raté n’est consigné que là.
Tous les flux reçoivent le label host ; le label automatique filename est supprimé, car il ne ferait que répéter des informations déjà présentes dans les autres labels.
Lire les journaux de root sans être root
Le paquet lance Alloy sous l’utilisateur alloy, membre des groupes adm et systemd-journal. Cela suffit pour le journal, pas pour audit.log (root, mode 0600), fail2ban.log et les journaux des pods (root, 0640). La solution paresseuse serait de lancer Alloy en root. Le rôle lui donne à la place une seule capability Linux, dans un drop-in systemd :
[Service]
AmbientCapabilities=CAP_DAC_READ_SEARCH
CapabilityBoundingSet=CAP_DAC_READ_SEARCH
NoNewPrivileges=yes
ProtectSystem=strict
ReadWritePaths=/var/lib/alloy
ProtectHome=yes
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
...CAP_DAC_READ_SEARCH contourne les contrôles de lecture sur les fichiers et répertoires – et rien d’autre. Alloy peut lire tous les journaux, mais ne peut pas écrire hors de son répertoire de données, ni changer de propriétaire, ni charger de modules. Loki reçoit un drop-in semblable sans aucune capability. systemd-analyze security note les deux unités 4.0 et 3.8 – « OK » sur son échelle, où un service non durci se situe vers 9.
Grafana : source de données, alertes et auto-test
Le rôle crée la source de données « Loki » via l’API de Grafana (vérifier d’abord, créer seulement si absente) et quatre règles d’alerte dans le dossier qui contient déjà les alertes sur les métriques :
| Alerte | LogQL (fenêtre de 15 minutes) | Pourquoi |
|---|---|---|
| SELinux a bloqué quelque chose | {job="audit"} |= "type=AVC" |= "permissive=0" | Un vrai refus. Les lignes permissive=1 des domaines encore en phase d’apprentissage ne comptent pas. |
| fail2ban signale des erreurs | {job="fail2ban"} |= "]: ERROR" != "KeyError('/var/log/pods/" | Une action de bannissement ou un filtre est cassé. Le KeyError exclu est inoffensif (voir plus bas). |
| OOM killer du noyau | {job="journal", transport="kernel"} |~ "(?i)out of memory: killed process" | Couvre aussi les processus de l’hôte, pas seulement les conteneurs. |
| Plus aucun journal | sum(count_over_time({job="journal"}[15m])) < 1 | Le pipeline lui-même est cassé. « Pas de données » déclenche l’alerte ici. |
Pour finir, le rôle teste toute la chaîne : il écrit un marqueur unique avec logger dans le journal et interroge Loki jusqu’à ce que le marqueur apparaisse. Si Alloy ou Loki est cassé, le playbook échoue précisément là.
🔍 La première heure : quatre découvertes que personne n’avait cherchées
Moins d’une heure après le premier déploiement, les nouvelles alertes avaient envoyé des e-mails. Aucun n’était du bruit – chacun pointait quelque chose de réel, deux d’entre eux mes propres erreurs.
1. Vieilles lignes, nouveaux horodatages
Les deux premières alertes se sont déclenchées aussitôt : un AVC et une erreur fail2ban. La ligne fail2ban datait de 11 h 13 le matin même. Explication : au premier démarrage, Alloy lit chaque fichier depuis le début et, sans autre instruction, horodate chaque ligne au moment où il la lit. Tout l’arriéré de audit.log et fail2ban.log est arrivé dans Loki comme « maintenant » – et les alertes, qui comptent les lignes des 15 dernières minutes, ont vu d’un coup des heures d’anciennes erreurs.
La correction consiste à prendre l’horodatage dans la ligne elle-même. Le journal d’audit porte le temps epoch dans msg=audit(1792426019.999:37103), fail2ban écrit l’heure locale sans fuseau :
loki.process "files" {
stage.match {
selector = "{job=\"audit\"}"
stage.regex { expression = "msg=audit\\((?P<ts>[0-9]+[.][0-9]+):" }
stage.timestamp { source = "ts" format = "Unix" }
}
stage.match {
selector = "{job=\"fail2ban\"}"
stage.regex { expression = "^(?P<ts>[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9:]{8},[0-9]{3})" }
stage.timestamp { source = "ts" format = "2006-01-02 15:04:05,000" location = "UTC" }
}
...
}Le fuseau n’est pas codé en dur : le rôle lit celui de l’hôte avec timedatectl et le transmet au modèle, de sorte que le même rôle reste juste sur un hôte en heure locale.
2. Le premier vrai AVC : un installateur de paquet
L’AVC lui-même était réel et tout neuf : pendant l’installation du RPM d’Alloy, useradd -m a voulu créer .bash_logout dans le nouveau répertoire personnel /var/lib/alloy. Ce répertoire a le type var_lib_t, et la politique n’autorise pas useradd_t à y créer des fichiers utilisateur. Inoffensif – Alloy n’a pas besoin de profil shell –, mais exactement le genre d’événement qui disparaissait autrefois dans un journal d’audit de 6 Mo.
3. Des erreurs fail2ban présentes depuis des jours
Les erreurs fail2ban dans Loki étaient de deux sortes. L’une inoffensive : KeyError('/var/log/pods/…'), le backend polling perd la trace d’un journal de pod supprimé lors d’un redémarrage ; la jail continue. L’autre non : les 13 et 14 octobre, Failed to execute ban jail 'nginx-k3s-bad-paths' action 'nftables…' – des bannissements qui n’ont pas eu lieu, pendant les jours où le rechargement du pare-feu était remanié. Personne ne les avait vus. L’alerte n’exclut désormais que le motif inoffensif connu.
4. Un AVC venant de l’outil suivant
Une heure plus tard, Trivy a été installé (son propre deep dive suit). Son unité de scan avait NoNewPrivileges=yes, et le script appelle k3s kubectl. Le binaire k3s passe dans le domaine container_runtime_t, et sous NoNewPrivileges SELinux refuse une telle transition si la politique ne l’autorise pas explicitement : avc: denied { nnp_transition }. L’alerte Loki s’est déclenchée quelques minutes après le premier scan. Ce n’était pas la première fois que ce mécanisme posait problème ici – MariaDB et Grafana s’y étaient déjà heurtés –, mais c’était la première fois qu’une machine le trouvait, plutôt qu’un service en panne.
🧰 Exploitation au quotidien
Recettes LogQL
# tout ce que SELinux a refusé aujourd'hui, sans les domaines en apprentissage
{job="audit"} |= "type=AVC" |= "permissive=0"
# ce qui a été banni dans l'heure, et pourquoi
{job="fail2ban"} |= "Ban "
# journaux d'un pod qui n'existe plus (namespace + conteneur sont des labels)
{namespace="wordpress", container="wordpress-fpm"} |= "Fatal"
# messages du noyau : OOM, segfaults, matériel
{job="journal", transport="kernel"}
# connexions SSH par clé, par utilisateur (analysé à la requête, pas un label)
{job="journal", unit="sshd.service"} |= "Accepted publickey" | regexp "for (?P<user>\\S+) from"
# quelles unités écrivent le plus ? (trouve les services bavards)
topk(10, sum by (unit) (count_over_time({job="journal"}[1h])))Tout arrive-t-il ?
# Loki prêt, Alloy prêt (tous deux seulement sur 127.0.0.1)
curl -s 127.0.0.1:3100/ready
curl -s 127.0.0.1:12345/-/ready
# lignes par source sur les 10 dernières minutes
for j in journal pods audit fail2ban; do
curl -sG 127.0.0.1:3100/loki/api/v1/query \
--data-urlencode "query=sum(count_over_time({job=\"$j\"}[10m]))"
done
# les namespaces connus de Loki
curl -s 127.0.0.1:3100/loki/api/v1/label/namespace/values
# la vue d'Alloy : avertissements de la dernière heure
journalctl -u alloy -p warning --since -1hDisque et mémoire
Après les deux premières heures sur le blog : Loki environ 240 Mo de RSS, Alloy environ 275 Mo, ensemble à peu près deux minutes de CPU, et 13 Mo dans /var/lib/loki – environ 150 Mo par jour, soit près de 2 Go sur les 14 jours. Les serveurs Nextcloud ont plus de pods et plus de trafic ; c’est pourquoi le rôle est d’abord resté une journée sur le blog, et les chiffres ont été comparés avant d’aller plus loin. Si le disque grossit plus vite que prévu, la première question est de savoir quel flux est gros – topk par unit ou namespace y répond –, la seconde s’il faut vraiment le conserver ; Alloy peut supprimer des lignes avec stage.drop avant qu’elles n’atteignent Loki.
Bon à savoir
- Fuseau horaire du navigateur. Grafana affiche les heures dans le fuseau du navigateur, les serveurs tournent en UTC. Une ligne « de 11 h 13 » dans un fichier apparaît à 13 h 13 dans Grafana en été.
- Les vieilles lignes sont rejetées.
reject_old_samples_max_age: 168h– un fichier dont les lignes ont plus d’une semaine (par exemple une sauvegarde restaurée) n’est pas importé. C’est voulu. - Le redémarrage est sans risque. Alloy garde ses positions de lecture dans
/var/lib/alloy/data. Supprimer ce répertoire lui fait tout relire – avec les bons horodatages désormais, mais en double.
🆘 Antisèche : symptôme → commande
La version courte de cet article, pour le moment où quelque chose ne va pas. À lancer sur l’hôte en root ; <…> sont des espaces réservés.
| Symptôme | Commande |
|---|---|
| Loki est-il prêt, Alloy est-il prêt ? | curl -s 127.0.0.1:3100/ready |
| Les journaux d’une source manquent | curl -sG 127.0.0.1:3100/loki/api/v1/query \ |
| Qu’a refusé SELinux ? | {job="audit"} |= "type=AVC" |= "permissive=0" |
| Journaux d’un pod qui n’existe plus | {namespace="<ns>", container="<container>"} |= "Fatal" |
| Noyau : OOM, segfaults, matériel | {job="journal", transport="kernel"} |
| Le disque grossit plus vite que prévu | du -sh /var/lib/loki |
| Les heures dans Grafana sont décalées de deux heures | # browser time zone vs. UTC on the servers - not a bug |
🔭 La suite
- Un domaine SELinux pour Loki et Alloy, comme la pile de supervision : aujourd’hui ils tournent en
unconfined_service_t, confinés seulement par systemd. - Journaux et métriques dans une même vue. Grafana peut afficher sous un panneau de métriques les lignes de journal correspondantes pour la même période – utile pour le tableau de bord des conteneurs.
- Moins de labels, mais meilleurs, pour les pods si le nombre de flux augmente : le nom du pod change à chaque redémarrage et pourrait devenir une métadonnée structurée plutôt qu’un label.
⚖️ Alternatives – et pourquoi Loki
| Option | Point fort | Pourquoi pas ici |
|---|---|---|
| journald + grep | Gratuit, toujours là | Un hôte, une source à la fois, pas d’alertes, les journaux des pods disparaissent avec le pod. |
| ELK / OpenSearch | Recherche plein texte sur tout, très puissant | Java, plusieurs Go de RAM pour un seul nœud – plus que toute la pile du blog. |
| Graylog | Bonne interface, flux et alertes intégrés | Testé auparavant en labo : nécessite MongoDB et OpenSearch – encore lourd pour un petit serveur. |
| Promtail + Loki | La combinaison classique | Promtail est en maintenance ; Alloy est son successeur et gère aussi métriques et traces. |
| Loki + Alloy | Index par labels comme Prometheus, léger, natif dans Grafana | Choisi : s’accorde avec Prometheus/Grafana déjà en place et avec le budget de RAM. |
Ce qui a décidé, ce ne sont pas les fonctionnalités mais l’adéquation. Prometheus et Grafana étaient déjà là, les e-mails d’alerte fonctionnaient, et le modèle de labels est le même que pour les métriques. Loki a ajouté la moitié manquante – le pourquoi derrière les chiffres – pour environ un demi-gigaoctet de RAM.


