Aller au contenu principal

Alloy

· 9 minutes de lecture

Promtail, l'agent qui pousse les logs vers Loki, est en fin de vie depuis le 2 mars 2026 : il ne reçoit plus de correctifs. Un cluster qui collecte ses logs avec lui doit changer d'agent, et la migration ne doit casser ni les dashboards ni les requêtes qui reposent sur ses labels. Le même cluster lance souvent un agent par signal : Promtail pour les logs, des exporters pour les métriques, un OpenTelemetry Collector dès que des traces apparaissent. Grafana Alloy, le remplaçant désigné de Promtail, est une distribution de l'OpenTelemetry Collector qui collecte métriques, logs, traces et profils dans un seul processus. Cet article décrit son modèle de configuration, la collecte des logs des pods et des événements Kubernetes, et ce qu'il faut reprendre de Promtail pour une bascule sans rupture.

Un collecteur, plusieurs signaux​

Alloy succède à Grafana Agent et à Promtail. Il embarque les pipelines Prometheus (découverte de cibles, relabeling, scrape, remote write) et ceux d'OpenTelemetry (récepteurs OTLP, processeurs, exportateurs) dans un même binaire. Un seul DaemonSet peut ainsi lire les fichiers de logs, scraper des métriques et recevoir les traces des applications en OTLP. Installé d'abord pour remplacer Promtail, il peut ensuite recevoir la télémétrie OpenTelemetry sans qu'un autre agent soit déployé.

Des composants reliés​

La configuration s'écrit dans la syntaxe propre d'Alloy, proche de HCL. Elle déclare des composants, chacun identifié par un type et un label (loki.write "default"), avec des arguments en entrée et des exports en sortie. Un composant en référence un autre par ses exports : forward_to = [loki.write.default.receiver] branche la sortie d'un composant sur l'entrée d'un autre.

loki.source.file "pods" {
targets = local.file_match.pods.targets
forward_to = [loki.process.pods.receiver]
}

L'ensemble forme un graphe orienté, comparable à une installation de plomberie : des sources, des traitements, des sorties, reliés par des tuyaux nommés. Alloy évalue ce graphe en continu. Quand la découverte Kubernetes trouve un nouveau pod, l'export targets change et les composants qui en dépendent se mettent à jour, sans rechargement de la configuration.

Pour une configuration Promtail existante, alloy convert --source-format=promtail produit l'équivalent dans cette syntaxe. Le résultat sert de point de départ et reste à relire : il reproduit toutes les règles de l'original, y compris celles qui méritaient d'être revues.

Logs des pods : un DaemonSet​

Le schéma reprend celui de Promtail : un pod Alloy par nœud lit les fichiers /var/log/pods/<namespace>_<pod>_<uid>/<conteneur>/*.log des pods de ce nœud. La chaîne compte cinq composants.

  1. discovery.kubernetes liste les pods. Un sélecteur de champ spec.nodeName égal au nom du nœud, injecté par le chart dans la variable HOSTNAME, limite la liste aux pods locaux. Sans ce filtre, chaque instance surveillerait tous les pods du cluster et l'API server servirait autant de listes complètes qu'il y a de nœuds.
  2. discovery.relabel construit les labels (namespace, pod, container, app…) à partir des métadonnées __meta_kubernetes_*, et calcule le chemin __path__ du fichier à partir de l'UID du pod et du nom du conteneur.
  3. local.file_match résout ce motif de chemin en fichiers réels.
  4. loki.source.file lit les fichiers et retient la position atteinte dans chacun.
  5. loki.process applique stage.cri, qui décode le format écrit par containerd (horodatage, flux stdout ou stderr, lignes longues découpées en plusieurs morceaux), puis loki.write pousse vers Loki.

Une alternative, loki.source.kubernetes, lit les logs par l'API Kubernetes au lieu des fichiers. Elle évite de monter un répertoire de l'hôte, mais fait transiter tout le volume de logs par l'API server et le kubelet ; la lecture des fichiers reste la méthode la moins coûteuse pour un cluster entier.

Piège : les pods statiques du control plane​

Les composants du control plane déployés par kubeadm (kube-apiserver, etcd, kube-scheduler) sont des pods statiques, lancés par le kubelet à partir de fichiers locaux. L'UID visible dans l'API est celui du mirror pod, pas celui du dossier de logs : le kubelet nomme ce dossier d'après le hash de configuration du pod, exposé dans l'annotation kubernetes.io/config.hash. Une règle qui construit __path__ uniquement à partir de l'UID ne trouve donc aucun fichier pour ces pods, et leurs logs manquent sans aucun message d'erreur. Une seconde règle, appliquée quand l'annotation est présente, construit le chemin à partir du hash.

Pour que le DaemonSet ait aussi un pod sur les nœuds de control plane, il doit tolérer leur taint node-role.kubernetes.io/control-plane:NoSchedule.

Migrer depuis Promtail sans rupture​

Trois éléments décident si la bascule passe inaperçue pour les utilisateurs des logs.

Les labels. Dashboards, alertes et requêtes enregistrées sélectionnent les flux par leurs labels. Reprendre exactement les règles de relabeling de Promtail (mêmes noms, mêmes valeurs, même construction de job en <namespace>/<app>) garde ces requêtes valides. La migration n'est pas le bon moment pour renommer des labels : les deux changements se mélangeraient dans les régressions.

Les positions de lecture. Un agent qui démarre sans positions enregistrées relit chaque fichier depuis le début : tout le contenu encore présent dans /var/log/pods est renvoyé une seconde fois, sous forme de doublons dans les flux déjà alimentés par l'ancien agent. L'argument legacy_positions_file de loki.source.file reprend le fichier de positions de Grafana Agent, au même format que celui de Promtail. Il n'est lu qu'au premier démarrage, tant qu'Alloy n'a pas encore son propre fichier de positions.

La persistance de ces positions. Alloy les range dans son répertoire de données (--storage.path). Dans un pod, ce répertoire disparaît avec lui, sauf s'il est placé sur un hostPath : le même problème de relecture se reposerait alors à chaque redémarrage du pod.

Si Loki change aussi de version majeure, faire tourner l'ancienne et la nouvelle chaîne en parallèle pendant quelques jours permet de comparer les volumes ingérés par conteneur avant de retirer l'ancienne. Pour ne pas toucher aux clients (datasource Grafana, sondes, outils qui lisent Loki), un Service supplémentaire qui garde l'ancien nom et sélectionne les pods du nouveau Loki rend la bascule invisible.

Événements Kubernetes​

Les événements Kubernetes (image introuvable, sonde en échec, pod non planifiable, éviction) sont souvent la première piste d'un incident. L'API ne les garde qu'une heure par défaut : un problème survenu pendant la nuit n'en laisse plus aucune trace le matin. Le composant loki.source.kubernetes_events lit ces événements par l'API et les convertit en lignes de logs, au format logfmt ou JSON.

Ce composant ne se déploie pas comme la collecte des fichiers. Chaque instance lit les événements de tout le cluster, si bien que le DaemonSet enverrait chaque événement une fois par nœud. Il tourne donc à part, dans un Deployment à un réplica, avec la stratégie de mise à jour Recreate pour que l'ancien et le nouveau pod ne coexistent jamais pendant un déploiement. Le mode clustering d'Alloy permet aussi de répartir les namespaces entre plusieurs instances, mais à l'échelle d'un petit cluster, une instance suffit.

kind, reason et type (Normal ou Warning) ont peu de valeurs possibles et peuvent devenir des labels. Extraits de la ligne par stage.logfmt puis promus par stage.labels, ils rendent la requête utile la plus directe :

{job="kubernetes-events", namespace="production", type="Warning"}

Le composant retient l'horodatage du dernier événement lu dans son répertoire de données. Si ce répertoire est éphémère, un pod recréé relit les événements encore présents dans l'API, soit au plus une heure. Loki écarte une ligne reçue deux fois dans un même flux avec le même horodatage et le même contenu, ce qui limite l'effet de cette relecture. Côté droits, lire les événements de tous les namespaces demande un ClusterRole limité à get, list et watch sur les ressources events.

Superviser le collecteur​

Un collecteur en panne ne produit aucune alerte de lui-même : les logs cessent simplement d'arriver. Alloy expose sur son port 12345 deux outils pour éviter ce cas.

  • L'interface web affiche le graphe des composants, l'état de santé de chacun et la valeur courante de ses arguments et exports : la liste des cibles découvertes, les fichiers suivis, les erreurs d'envoi. Un kubectl port-forward vers ce port suffit pour diagnostiquer une source muette.
  • L'endpoint /metrics, scrapé par Prometheus, fournit les compteurs de lignes lues, d'entrées envoyées et d'échecs d'envoi vers Loki. Une alerte sur des échecs d'envoi persistants, ou sur un volume ingéré tombé à zéro pour un nœud, détecte la panne silencieuse.

Le chart crée par défaut des droits larges sur l'API. La collecte des fichiers n'a besoin que de lire les pods et les namespaces, et celle des événements que de lire les événements ; restreindre les règles RBAC du chart à ces ressources limite ce qu'un collecteur compromis peut lire.

Vers les traces​

Le même binaire accepte des composants OpenTelemetry : otelcol.receiver.otlp reçoit les traces, métriques et logs qu'une application instrumentée envoie en OTLP, et des exportateurs les transmettent à Tempo, Prometheus ou Loki. Le DaemonSet déployé pour les logs devient alors le point de réception de la télémétrie applicative, avec quelques composants de plus dans son graphe.

Limites​

  • Syntaxe propre : la configuration n'est ni du YAML ni la configuration standard de l'OpenTelemetry Collector. Elle demande un apprentissage, et les exemples de la communauté OpenTelemetry doivent être traduits.
  • Accès à l'hôte : la collecte par fichiers demande de monter /var/log/pods et un répertoire de données sur chaque nœud, ce qui expose les logs de tous les pods du nœud au collecteur.
  • Sorties standard uniquement : comme avec Promtail, une application qui écrit dans un fichier à l'intérieur de son conteneur échappe à la collecte.
  • Instance unique pour les événements : sans clustering, la collecte des événements s'arrête pendant que son pod redémarre ; la relecture de la dernière heure ne couvre que des interruptions courtes.

Conclusion​

Alloy remplace Promtail sans changer le principe de la collecte. Un DaemonSet lit les fichiers des pods de son nœud, des règles de relabeling construisent les labels, un pipeline décode le format CRI et pousse vers Loki. Pour une bascule sans rupture, il faut reprendre les labels à l'identique, reprendre et persister les positions de lecture, et ne pas oublier la règle qui couvre les pods statiques. Les événements Kubernetes, qui expirent au bout d'une heure dans l'API, demandent une instance unique à part. Au-delà des logs, le modèle de composants et les récepteurs OTLP font du même collecteur la porte d'entrée de la télémétrie applicative.

Ressources utiles​

Application / Projet lié​

Mis en pratique dans le projet2024 → aujourd'huiCluster Kubernetes interne DiscoDiodeRemplacement de Promtail par Alloy lors du passage de Loki 2.6 à Loki 3 : DaemonSet aux labels repris de Promtail, reprise de ses positions de lecture, événements Kubernetes conservés 30 jours dans Loki par une instance dédiée, deux chaînes en parallèle avant la bascule.KuberneteskubeadmCalicoTailscaleTraefikHelmGitHub ActionsPrometheusGrafana