La collecte des journaux : Loki et Alloy, pour qu'un message survive à son pod (#40)
Helm Charts / Detect changed charts (push) Successful in 22s
Helm Charts / Library charts tool (push) Skipped
Helm Charts / Application charts crowdsec (push) Skipped
Helm Charts / Application charts hashicorp-vault (push) Skipped
Helm Charts / Application charts minio (push) Skipped
Helm Charts / Application charts pgbouncer (push) Skipped
Helm Charts / Application charts pgcat (push) Skipped
Helm Charts / Application charts prometheus (push) Skipped
Helm Charts / Application charts redis (push) Skipped
Helm Charts / Application charts loki (push) Successful in 14s
Helm Charts / Application charts chart (push) Successful in 34s
Helm Charts / Application charts grafana (push) Failing after 49s
Helm Charts / Application charts alloy (push) Successful in 52s
Helm Charts / Detect changed charts (push) Successful in 22s
Helm Charts / Library charts tool (push) Skipped
Helm Charts / Application charts crowdsec (push) Skipped
Helm Charts / Application charts hashicorp-vault (push) Skipped
Helm Charts / Application charts minio (push) Skipped
Helm Charts / Application charts pgbouncer (push) Skipped
Helm Charts / Application charts pgcat (push) Skipped
Helm Charts / Application charts prometheus (push) Skipped
Helm Charts / Application charts redis (push) Skipped
Helm Charts / Application charts loki (push) Successful in 14s
Helm Charts / Application charts chart (push) Successful in 34s
Helm Charts / Application charts grafana (push) Failing after 49s
Helm Charts / Application charts alloy (push) Successful in 52s
Alloy plutôt que Promtail, que l'amont a marqué déprécié. Rétention 30 jours, calée sur 305 Mio/jour mesurés — et la mesure a révélé pire que ce que l'issue supposait : traefik ne gardait que 10 minutes d'histoire, clickhouse 2. La propriété tient, éprouvée : jeton écrit, pod supprimé, `kubectl logs` ne répond plus, Loki rend encore les lignes. Ferme #38. Co-Authored-By: Claude Opus 5 <[email protected]> Co-authored-by: Gabriel Radureau <[email protected]>
This commit was merged in pull request #40.
This commit is contained in:
@@ -336,9 +336,9 @@ jobs:
|
||||
needs: [filter-chart,library-charts]
|
||||
strategy:
|
||||
matrix:
|
||||
# ⚠ Les NEUF charts applicatifs du dépôt — voir l'avertissement du job
|
||||
# ⚠ Les ONZE charts applicatifs du dépôt — voir l'avertissement du job
|
||||
# `library-charts` ci-dessus. Un chart absent de cette liste n'est
|
||||
# JAMAIS construit, et sa PR est verte quand même.
|
||||
# chart: ${{ fromJson(needs.filter-chart.outputs.application_charts) }}
|
||||
chart: [chart, crowdsec, grafana, hashicorp-vault, minio, pgbouncer, pgcat, prometheus, redis]
|
||||
chart: [alloy, chart, crowdsec, grafana, hashicorp-vault, loki, minio, pgbouncer, pgcat, prometheus, redis]
|
||||
type: [application]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
@@ -0,0 +1,16 @@
|
||||
apiVersion: v2
|
||||
name: alloy
|
||||
description: >-
|
||||
L'agent qui ramasse les journaux de chaque nœud et les pousse vers Loki.
|
||||
Un DaemonSet — donc trois pods, un par Raspberry Pi.
|
||||
|
||||
# Même raison que pour `loki/` : aucune dépendance au registre Helm interne,
|
||||
# que le runner de CI ne résout pas (relevé du 2026-09-07). Voir loki/Chart.yaml.
|
||||
dependencies:
|
||||
- name: alloy
|
||||
version: 1.12.1
|
||||
repository: https://grafana.github.io/helm-charts
|
||||
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "v1.19.2"
|
||||
@@ -0,0 +1,275 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Grafana Alloy — l'agent de collecte, un pod par nœud.
|
||||
# -----------------------------------------------------------------------------
|
||||
# POURQUOI ALLOY ET PAS PROMTAIL (l'arbitrage de tools#38, mesuré le 2026-09-07)
|
||||
#
|
||||
# Relevé sur l'index du dépôt Helm de Grafana lui-même
|
||||
# (`curl https://grafana.github.io/helm-charts/index.yaml`) :
|
||||
#
|
||||
# chart versions dernière publication `deprecated:` dans l'index
|
||||
# promtail 141 2025-10-31 true
|
||||
# alloy 57 2026-08-27 (absent)
|
||||
# loki 344 2026-08-10 (absent)
|
||||
#
|
||||
# Promtail n'est pas « en fin de vie bientôt » : son chart porte le drapeau
|
||||
# `deprecated: true` dans l'index, et sa dernière publication a plus de dix mois.
|
||||
# Déployer AUJOURD'HUI, sur une installation NEUVE, un agent que l'amont a déjà
|
||||
# rangé, c'est se donner une dette à échéance connue pour économiser de la
|
||||
# mémoire — et l'économie, elle, est à vérifier, pas à supposer.
|
||||
#
|
||||
# Le coût réel d'Alloy sur CE matériel est mesuré après déploiement et reporté
|
||||
# dans la PR ; c'est ce que le plafond `resources` ci-dessous encadre.
|
||||
#
|
||||
# ARM64 : vérifié au registre, manifeste multi-plateforme (jeton anonyme
|
||||
# Docker Hub) — `grafana/alloy:v1.19.2` publie linux/amd64, **linux/arm64**,
|
||||
# ppc64le, s390x. `grafana/loki:3.6.12` publie amd64, **arm64**, arm/v7.
|
||||
# -----------------------------------------------------------------------------
|
||||
alloy:
|
||||
controller:
|
||||
# C'est déjà le défaut du chart ; on l'écrit parce que c'est LA décision
|
||||
# structurante — un agent par nœud, donc trois fois le coût mémoire.
|
||||
type: daemonset
|
||||
|
||||
# Le volume que consomme le montage `alloy.mounts.extra` ci-dessous : la
|
||||
# racine Docker de ces machines, où aboutissent les liens de /var/log/pods.
|
||||
volumes:
|
||||
extra:
|
||||
- name: docker-containers
|
||||
hostPath:
|
||||
path: /mnt/arcodange/docker/containers
|
||||
# `Directory` (et non `DirectoryOrCreate`) EXPRÈS : si un jour un
|
||||
# nœud n'a pas ce chemin, on veut que son agent refuse de démarrer
|
||||
# et le DISE, plutôt que de monter un dossier vide et de rapporter
|
||||
# zéro journal en se déclarant sain — le défaut exact qu'on vient
|
||||
# de traverser.
|
||||
type: Directory
|
||||
|
||||
alloy:
|
||||
# -- Le montage de /var/log : c'est par là que l'agent lit les journaux.
|
||||
# On tail des FICHIERS plutôt que d'interroger l'API du kubelet : sur trois
|
||||
# Pi, faire transiter tous les journaux par l'API du cluster ajouterait au
|
||||
# serveur d'API une charge qu'il n'a pas à porter.
|
||||
mounts:
|
||||
varlog: true
|
||||
# ⚠⚠ CE CLUSTER TOURNE SOUS DOCKER, PAS SOUS CONTAINERD — et son magasin
|
||||
# d'images n'est pas là où le chart le cherche. Les deux faits ensemble
|
||||
# rendent ce drapeau inutilisable.
|
||||
#
|
||||
# Mesuré le 2026-09-07 (`kubectl get nodes -o …containerRuntimeVersion`) :
|
||||
# pi1 docker://29.1.3
|
||||
# pi2 docker://28.4.0
|
||||
# pi3 docker://27.4.0
|
||||
#
|
||||
# Et sous /var/log/pods, les journaux ne sont PAS des fichiers : ce sont
|
||||
# des liens symboliques vers la racine Docker, DÉPLACÉE sur un disque
|
||||
# externe (identique sur pi1 et pi3, vérifié pod par pod) :
|
||||
#
|
||||
# /var/log/pods/<ns>_<pod>_<uid>/<conteneur>/0.log
|
||||
# -> /mnt/arcodange/docker/containers/<id>/<id>-json.log
|
||||
#
|
||||
# `dockercontainers: true` monterait `/var/lib/docker/containers`, qui
|
||||
# n'existe pas ici. On monte donc la vraie racine, plus bas, en `extra`.
|
||||
dockercontainers: false
|
||||
|
||||
# ⚠ SANS CE MONTAGE, LA COLLECTE EST SILENCIEUSEMENT VIDE — et c'est le
|
||||
# mode d'échec le plus traître de ce lot, parce que TOUT A L'AIR SAIN :
|
||||
# les six composants d'Alloy se déclarent `healthy`, aucune erreur de
|
||||
# poussée, Loki répond `ready`. Le seul relevé qui le dit est
|
||||
# `loki_source_file_files_active_total = 0`, et l'export vide de
|
||||
# `local.file_match`. `ls` montre les liens, `stat` échoue sur la cible.
|
||||
extra:
|
||||
- name: docker-containers
|
||||
mountPath: /mnt/arcodange/docker/containers
|
||||
readOnly: true
|
||||
|
||||
# ⚠ LA MÉMOIRE EST LA RESSOURCE RARE, ET UN DAEMONSET LA PREND TROIS FOIS.
|
||||
# Mesuré le 2026-09-07 avant tout déploiement : pi1 79 %, pi2 109 %,
|
||||
# pi3 58 % de leur mémoire ALLOUABLE. pi2 est en surengagement — c'est
|
||||
# précisément le nœud où un agent de trop bascule la machine.
|
||||
#
|
||||
# Le plafond n'est donc pas une formalité : il est ce qui garantit que
|
||||
# l'agent meurt AVANT le service qu'il observe. Un OOM d'Alloy est un
|
||||
# incident de collecte ; un OOM de MinIO ou de pgbouncer est un incident
|
||||
# tout court.
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
# Pas de limite CPU (l'étranglement d'un agent lui fait perdre des
|
||||
# lignes en silence, ce qui est le défaut qu'on essaie de supprimer).
|
||||
#
|
||||
# ⚠ 384 Mi, ET C'EST UN CHIFFRE MESURÉ, PAS UN CHIFFRE ROND.
|
||||
# Le premier jet plafonnait à 256 Mi. Relevé sur la pile qui tourne,
|
||||
# pendant la reprise de l'historique (`kubectl top pods --containers`) :
|
||||
#
|
||||
# agent de pi1 214 Mi ← 84 % du plafond de 256 Mi
|
||||
# agent de pi2 98 Mi
|
||||
# agent de pi3 83 Mi
|
||||
#
|
||||
# Aucun OOMKill constaté (0 redémarrage sur les trois), mais 84 % n'est
|
||||
# pas une marge : un agent tué perd sa position de lecture, et un trou
|
||||
# dans la collecte est exactement ce que ce lot existe pour supprimer.
|
||||
#
|
||||
# ⚠ Relever un PLAFOND ne consomme rien : il borne le pire cas. C'est la
|
||||
# REQUÊTE (128 Mi, inchangée) que le planificateur réserve.
|
||||
#
|
||||
# ⚠ L'écart entre les trois nœuds n'est pas du bruit : il suit le nombre
|
||||
# de fichiers suivis, donc le nombre de conteneurs du nœud. Un nœud qui
|
||||
# se remplit fera monter son agent.
|
||||
memory: 384Mi
|
||||
|
||||
# Pas de grappe : chaque agent lit SON nœud, il n'a rien à coordonner.
|
||||
clustering:
|
||||
enabled: false
|
||||
|
||||
configMap:
|
||||
create: true
|
||||
# -----------------------------------------------------------------------
|
||||
# La configuration Alloy. Trois étages : découvrir, lire, pousser.
|
||||
# -----------------------------------------------------------------------
|
||||
content: |-
|
||||
// ÉTAGE 1 — découvrir les pods de CE nœud, et de lui seul.
|
||||
//
|
||||
// ⚠ Sans le filtre par nœud, chacun des trois agents découvrirait les
|
||||
// pods des TROIS nœuds, chercherait leurs fichiers en local, n'en
|
||||
// trouverait aucun pour les deux autres, et referait ce travail en
|
||||
// boucle. Le filtre se pose côté serveur d'API (`field`), pas en
|
||||
// relabel : ce qui n'est pas envoyé ne coûte rien.
|
||||
discovery.kubernetes "pods_du_noeud" {
|
||||
role = "pod"
|
||||
|
||||
selectors {
|
||||
role = "pod"
|
||||
field = "spec.nodeName=" + sys.env("HOSTNAME")
|
||||
}
|
||||
}
|
||||
|
||||
// ÉTAGE 2 — traduire les métadonnées Kubernetes en étiquettes Loki,
|
||||
// et fabriquer le chemin du fichier journal.
|
||||
//
|
||||
// ⚠ LES ÉTIQUETTES SONT LE SEUL INDEX DE LOKI, ET CHAQUE COMBINAISON
|
||||
// DISTINCTE EST UN FLUX. On garde ce par quoi on cherche vraiment
|
||||
// (namespace, pod, conteneur, nœud) et RIEN de ce qui varie sans
|
||||
// qu'on l'interroge : mettre ici une étiquette à forte cardinalité
|
||||
// (un identifiant de requête, un horodatage) est la façon classique
|
||||
// de faire tomber un Loki. Le reste du contenu reste cherchable en
|
||||
// plein texte, simplement non indexé.
|
||||
discovery.relabel "journaux_de_pods" {
|
||||
targets = discovery.kubernetes.pods_du_noeud.targets
|
||||
|
||||
rule {
|
||||
source_labels = ["__meta_kubernetes_namespace"]
|
||||
target_label = "namespace"
|
||||
}
|
||||
rule {
|
||||
source_labels = ["__meta_kubernetes_pod_name"]
|
||||
target_label = "pod"
|
||||
}
|
||||
rule {
|
||||
source_labels = ["__meta_kubernetes_pod_container_name"]
|
||||
target_label = "container"
|
||||
}
|
||||
// ⚠ `__meta_kubernetes_POD_node_name`, PAS `__meta_kubernetes_node_name`.
|
||||
// Le second n'existe QUE pour `role = "node"` ; sous `role = "pod"` il
|
||||
// est vide, et un relabel dont la source est vide ne pose pas
|
||||
// l'étiquette — SANS erreur, sans avertissement, sans rien. Mesuré :
|
||||
// l'étiquette `node` était simplement absente de Loki, sur une
|
||||
// collecte par ailleurs saine. On ne s'en aperçoit qu'en DEMANDANT
|
||||
// les valeurs de l'étiquette et en trouvant la liste vide.
|
||||
rule {
|
||||
source_labels = ["__meta_kubernetes_pod_node_name"]
|
||||
target_label = "node"
|
||||
}
|
||||
rule {
|
||||
source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_name"]
|
||||
target_label = "app"
|
||||
}
|
||||
|
||||
// Le chemin réel posé par le kubelet :
|
||||
// /var/log/pods/<ns>_<pod>_<uid>/<conteneur>/0.log
|
||||
// On vise par l'UID (unique) et le nom du conteneur.
|
||||
rule {
|
||||
source_labels = ["__meta_kubernetes_pod_uid", "__meta_kubernetes_pod_container_name"]
|
||||
separator = "/"
|
||||
action = "replace"
|
||||
replacement = "/var/log/pods/*$1/*.log"
|
||||
target_label = "__path__"
|
||||
}
|
||||
}
|
||||
|
||||
// ÉTAGE 3 — lire les fichiers, décoder le format CRI, pousser.
|
||||
local.file_match "journaux_de_pods" {
|
||||
path_targets = discovery.relabel.journaux_de_pods.output
|
||||
}
|
||||
|
||||
loki.source.file "journaux_de_pods" {
|
||||
targets = local.file_match.journaux_de_pods.targets
|
||||
forward_to = [loki.process.journaux_de_pods.receiver]
|
||||
}
|
||||
|
||||
// ⚠⚠ `stage.docker`, PAS `stage.cri` — ces nœuds tournent sous Docker
|
||||
// (mesuré : docker://29.1.3, 28.4.0, 27.4.0), donc chaque ligne est un
|
||||
// objet JSON {"log":…,"stream":…,"time":…} et non le format CRI
|
||||
// « <horodatage> <flux> <F|P> <message> ».
|
||||
//
|
||||
// Ce n'est pas cosmétique : sans le bon décodeur, l'horodatage stocké
|
||||
// serait celui de la LECTURE et non celui de l'écriture, et les lignes
|
||||
// arriveraient encore encapsulées dans leur JSON. Or dater une panne à
|
||||
// la minute est PRÉCISÉMENT l'usage qui motive ce lot (tools#36).
|
||||
loki.process "journaux_de_pods" {
|
||||
stage.docker {}
|
||||
|
||||
forward_to = [loki.write.loki.receiver]
|
||||
}
|
||||
|
||||
loki.write "loki" {
|
||||
endpoint {
|
||||
url = "http://loki.tools.svc.cluster.local:3100/loki/api/v1/push"
|
||||
}
|
||||
}
|
||||
|
||||
# L'agent a besoin de LIRE les pods du serveur d'API pour les découvrir.
|
||||
# Le chart pose un ClusterRole en lecture seule (pods, nodes, services,
|
||||
# endpoints) — rien qui écrive.
|
||||
rbac:
|
||||
create: true
|
||||
|
||||
serviceAccount:
|
||||
create: true
|
||||
|
||||
# Pas de collecte de métriques d'Alloy par Prometheus pour l'instant : le
|
||||
# ServiceMonitor suppose l'opérateur Prometheus, que ce cluster n'a pas
|
||||
# (le chart `prometheus` d'ici est le chart communautaire, pas l'opérateur).
|
||||
serviceMonitor:
|
||||
enabled: false
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
|
||||
# ⚠⚠ LE MÊME PIÈGE QUE LE SIDE-CAR DE LOKI, DEUXIÈME OCCURRENCE — et celle-ci
|
||||
# est plus sournoise, parce qu'elle RESSEMBLE à un plafond.
|
||||
#
|
||||
# Le chart ajoute au pod un second conteneur, `config-reloader`, qui recharge
|
||||
# Alloy quand sa ConfigMap change. Mesuré sur le premier déploiement, ses
|
||||
# ressources amont sont :
|
||||
#
|
||||
# resources:
|
||||
# requests: { cpu: 10m, memory: 50Mi }
|
||||
# ← et RIEN d'autre
|
||||
#
|
||||
# Une requête SANS limite n'est pas un plafond : c'est une réservation. Le
|
||||
# conteneur peut croître jusqu'à épuiser le nœud, et comme il partage le
|
||||
# cgroup du pod, il emporte Alloy avec lui. Sur un DaemonSet, cela vaut sur
|
||||
# les TROIS nœuds — pi2 compris, mesuré à 108 % de son allouable.
|
||||
#
|
||||
# ⚠ On ne peut pas l'éteindre : sans lui, un changement de configuration
|
||||
# n'atteindrait l'agent qu'au prochain redémarrage du pod. On le PLAFONNE.
|
||||
configReloader:
|
||||
enabled: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
memory: 64Mi
|
||||
@@ -10,3 +10,9 @@ tools:
|
||||
plausible: {}
|
||||
prometheus: {}
|
||||
minio: {}
|
||||
# La collecte de journaux (tools#38). Deux applications parce que ce sont
|
||||
# deux charts amont, et parce qu'on redémarre l'agent sans toucher au
|
||||
# magasin — mais elles ne valent rien l'une sans l'autre : `alloy` pousse
|
||||
# vers le service que `loki` publie.
|
||||
loki: {}
|
||||
alloy: {}
|
||||
|
||||
@@ -573,6 +573,21 @@ grafana: &grafana_config
|
||||
url: http://prometheus-server.tools.svc.cluster.local
|
||||
access: proxy
|
||||
isDefault: true
|
||||
# Les journaux du cluster (tools#38). Le chart `loki` d'à côté déploie
|
||||
# le binaire unique ; `alloy` y pousse ce que disent les pods.
|
||||
# ⚠ `loki.auth_enabled: false` côté serveur : aucun en-tête
|
||||
# X-Scope-OrgID à porter ici. Si l'authentification multi-locataire
|
||||
# était rallumée, cette source de données cesserait de répondre et il
|
||||
# faudrait lui ajouter `httpHeaderName1: X-Scope-OrgID`.
|
||||
- name: Loki
|
||||
type: loki
|
||||
url: http://loki.tools.svc.cluster.local:3100
|
||||
access: proxy
|
||||
isDefault: false
|
||||
jsonData:
|
||||
# La rétention posée dans loki/values.yaml. Grafana s'en sert pour
|
||||
# ne pas proposer une fenêtre que le magasin ne peut plus servir.
|
||||
maxLines: 1000
|
||||
# - name: CloudWatch
|
||||
# type: cloudwatch
|
||||
# access: proxy
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
@@ -0,0 +1,35 @@
|
||||
apiVersion: v2
|
||||
name: loki
|
||||
description: >-
|
||||
Le garde-mémoire des journaux du cluster — Loki en binaire unique, stockage
|
||||
fichier sur Longhorn. Ce qu'un pod a dit continue de se lire après sa mort.
|
||||
|
||||
# ⚠ PAS de dépendance au registre Helm interne, CONTRAIREMENT à grafana et
|
||||
# prometheus — et c'est délibéré.
|
||||
#
|
||||
# La bibliothèque `tool` ne sert QUE via les gabarits `templates/helm-chart*.yaml`,
|
||||
# eux-mêmes gardés par `{{- if eq .Values.tool.kind "HelmChart" -}}`. Avec
|
||||
# `kind: SubChart` — le mode de grafana, de prometheus, et le nôtre — ces
|
||||
# gabarits ne rendent RIEN : la bibliothèque est un poids mort.
|
||||
#
|
||||
# Or ce poids mort a un coût mesuré : le runner de CI ne résout pas
|
||||
# `gitea.arcodange.lab` (relevé du 2026-09-07, run 6728, `curl` code 6). Un
|
||||
# repli sur `GITHUB_SERVER_URL` a été posé dans `helmcharts.yaml`, mais il reste
|
||||
# un point de panne EN AMONT du `helm template`. Si la dépendance ne se
|
||||
# télécharge pas, l'étape qui éprouve VRAIMENT ce chart ne s'exécute jamais et
|
||||
# le job meurt sur une cause étrangère au chart.
|
||||
#
|
||||
# On ne dépend donc que de `grafana.github.io`, dont le même relevé a constaté
|
||||
# qu'il répond depuis ce runner. Le jour où l'on veut basculer en
|
||||
# `kind: HelmChart` (CRD k3s), on rajoute la dépendance ET les deux gabarits.
|
||||
dependencies:
|
||||
- name: loki
|
||||
version: 7.3.0
|
||||
repository: https://grafana.github.io/helm-charts
|
||||
|
||||
type: application
|
||||
version: 0.1.0
|
||||
# ⚠ 3.6.11, PAS 3.6.12. L'index Helm annonce `appVersion: 3.6.12` pour le chart
|
||||
# 7.3.0, mais le chart épingle `loki.image.tag: 3.6.11` dans ses valeurs — et
|
||||
# c'est le tag qui est tiré. Vérifié sur le pod déployé.
|
||||
appVersion: "3.6.11"
|
||||
@@ -0,0 +1,283 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Loki — binaire unique, stockage fichier, sur trois Raspberry Pi.
|
||||
# -----------------------------------------------------------------------------
|
||||
# ⚠⚠ LES DÉFAUTS AMONT SONT ÉCRITS POUR UN CLUSTER DE SERVEURS, PAS POUR CECI.
|
||||
# Mesurés sur le chart 7.3.0 (`helm show values grafana/loki --version 7.3.0`) :
|
||||
#
|
||||
# deploymentMode SimpleScalable → read + write + backend, 3 jeux
|
||||
# chunksCache.enabled true → un memcached à …
|
||||
# chunksCache.allocatedMemory 8192 → … 8 Gio, sur un nœud qui en
|
||||
# alloue 5,6 (pi2)
|
||||
# resultsCache.enabled true → un second memcached
|
||||
# gateway.enabled true → un nginx de plus
|
||||
# lokiCanary.enabled true → un DaemonSet de plus
|
||||
# test.enabled true → un pod de test
|
||||
# loki.commonConfig.replication_factor 3 → 3 exemplaires de chaque flux
|
||||
# loki.storage.type s3 → un magasin objet qu'on n'a pas
|
||||
# loki.auth_enabled true → multi-locataire (en-tête
|
||||
# X-Scope-OrgID exigé à chaque appel)
|
||||
#
|
||||
# Laisser ces défauts, c'est exactement la deuxième propriété de tools#38
|
||||
# violée : « la collecte ne doit pas faire tomber ce qu'elle observe ». Chaque
|
||||
# `false` ci-dessous est donc une décision, pas une omission.
|
||||
# -----------------------------------------------------------------------------
|
||||
loki:
|
||||
# -- Un seul processus, une seule réplique. Le reste des modes est éteint plus bas.
|
||||
deploymentMode: SingleBinary
|
||||
|
||||
loki:
|
||||
# Locataire unique : personne d'autre que le homelab n'écrit ici. Laisser
|
||||
# `true` obligerait chaque requête (et chaque source de données Grafana) à
|
||||
# porter un en-tête X-Scope-OrgID, pour aucun bénéfice.
|
||||
auth_enabled: false
|
||||
|
||||
commonConfig:
|
||||
path_prefix: /var/loki
|
||||
# -- UNE réplique de chaque flux : il n'y a qu'un ingesteur.
|
||||
# Le défaut amont (3) ferait refuser toute écriture, faute de quorum.
|
||||
replication_factor: 1
|
||||
|
||||
storage:
|
||||
type: filesystem
|
||||
filesystem:
|
||||
chunks_directory: /var/loki/chunks
|
||||
rules_directory: /var/loki/rules
|
||||
# ⚠ PAS de `admin_api_directory` ici. Le chart l'accepte (il est dans
|
||||
# ses valeurs par défaut), mais le binaire OSS le REFUSE :
|
||||
# failed parsing config: field admin_api_directory not found in
|
||||
# type common.FilesystemConfig
|
||||
# C'est un champ de l'édition entreprise (GEL). Mesuré : le pod part en
|
||||
# CrashLoopBackOff, 3 redémarrages en 4 min. Le chart ne valide pas la
|
||||
# config qu'il écrit — seul le démarrage réel le dit.
|
||||
|
||||
# ⚠ Le chart n'écrit AUCUN schéma par défaut (`schemaConfig: {}`) et refuse
|
||||
# de démarrer sans. tsdb + v13 est le couple courant de Loki 3.x.
|
||||
#
|
||||
# ⚠⚠ ET LA DATE `from` NE SE MET PAS AU JOUR DE L'INSTALLATION.
|
||||
# C'est la faute qu'a faite le premier jet, et elle a coûté cher à
|
||||
# diagnostiquer parce qu'elle ne ressemble à rien de ce qu'elle est.
|
||||
#
|
||||
# `from` dit « à partir de quand ce schéma s'applique », pas « depuis quand
|
||||
# on garde ». Posée à la date du jour, Loki n'a AUCUN schéma pour les
|
||||
# horodatages antérieurs et refuse la requête d'écriture ENTIÈRE :
|
||||
#
|
||||
# POST /loki/api/v1/push (500)
|
||||
# failed to create stream: no schema config found for time 1786622254
|
||||
#
|
||||
# Or l'agent lit aussi les fichiers ROTATÉS encore présents sur le disque
|
||||
# (4.log, 9.log, 10.log…), qui contiennent des lignes vieilles de plusieurs
|
||||
# mois — une remontait à 2026-04-13. ⚠ Et comme une poussée est un LOT, le
|
||||
# 500 emporte les lignes FRAÎCHES qui voyageaient avec les vieilles.
|
||||
#
|
||||
# Symptôme observé : `kubectl logs` montrait 19 lignes du pod témoin, Loki
|
||||
# en montrait 0, l'agent se déclarait sain, ses compteurs `dropped` étaient
|
||||
# tous à zéro — et `loki_write_sent_entries_total` valait 0. Rien ne
|
||||
# DÉSIGNAIT la cause côté agent : elle n'était lisible que dans le journal
|
||||
# de Loki. (Journal qu'on ne pouvait lire que parce que le pod n'avait pas
|
||||
# redémarré. L'ironie est le sujet même de l'issue #38.)
|
||||
#
|
||||
# La date est donc largement antérieure à tout ce que le cluster peut
|
||||
# porter. Ce qui borne la conservation, c'est `retention_period` +
|
||||
# le compacteur, pas ceci.
|
||||
schemaConfig:
|
||||
configs:
|
||||
- from: "2020-01-01"
|
||||
store: tsdb
|
||||
object_store: filesystem
|
||||
schema: v13
|
||||
index:
|
||||
prefix: loki_index_
|
||||
period: 24h
|
||||
|
||||
# ⚠⚠ UNE RÉTENTION QUI NE SUPPRIME RIEN EST UNE RÉTENTION QUI MENT.
|
||||
# `limits_config.retention_period` seul ne fait RIEN : c'est le compacteur
|
||||
# qui efface, et il ne le fait que si `retention_enabled` est vrai. Le
|
||||
# couple est indissociable — l'un sans l'autre laisse le volume grossir
|
||||
# jusqu'à saturation, en silence.
|
||||
compactor:
|
||||
working_directory: /var/loki/compactor
|
||||
retention_enabled: true
|
||||
delete_request_store: filesystem
|
||||
|
||||
limits_config:
|
||||
# -- 30 jours. Voir `RETENTION` en fin de fichier pour la mesure.
|
||||
retention_period: 720h
|
||||
# ⚠⚠ CE N'EST PAS UN RÉGLAGE DE CONSERVATION, ET LE CONFONDRE BLOQUE TOUT.
|
||||
#
|
||||
# Posé à 720 h pour « coller » à la rétention, il a produit un second
|
||||
# blocage complet, juste après celui du schéma : l'agent lit aussi les
|
||||
# fichiers rotatés présents sur le disque, dont certains remontent à
|
||||
# AVRIL. Loki refusait alors chaque lot —
|
||||
#
|
||||
# entry ... has timestamp too old: 2026-04-13T13:47:05Z,
|
||||
# oldest acceptable timestamp is: 2026-08-08T15:41:50Z
|
||||
#
|
||||
# — et comme l'agent REJOUE un lot refusé, la file ne se vidait jamais :
|
||||
# `loki_write_sent_entries_total` restait à 0 pour toujours. Un lot
|
||||
# définitivement refusé bloque la file derrière lui.
|
||||
#
|
||||
# Ce garde-fou existe contre les horloges DÉCALÉES, pas pour borner la
|
||||
# conservation. On l'ouvre donc largement au-delà de ce que le disque
|
||||
# peut porter ; ce qui borne vraiment, c'est `retention_period` + le
|
||||
# compacteur, plus bas.
|
||||
#
|
||||
# ⚠ Conséquence assumée : au tout premier démarrage, l'agent remonte
|
||||
# l'historique encore sur le disque, et le compacteur supprimera dans
|
||||
# l'heure ce qui dépasse 30 jours. On paie une ingestion inutile UNE
|
||||
# fois, en échange d'une file qui se vide. Le bénéfice de bord est
|
||||
# qu'on récupère au passage l'histoire déjà écrite.
|
||||
reject_old_samples: true
|
||||
reject_old_samples_max_age: 8760h
|
||||
max_cache_freshness_per_query: 10m
|
||||
split_queries_by_interval: 15m
|
||||
query_timeout: 300s
|
||||
volume_enabled: true
|
||||
# Débit mesuré du cluster : ~305 Mio/jour TOUS pods confondus, soit
|
||||
# ~3,6 Kio/s. Les bornes amont (4 Mio/s par flux) sont hors sujet ici ;
|
||||
# on les laisse, elles ne mordent pas.
|
||||
allow_structured_metadata: true
|
||||
|
||||
# Pas de vitrine : personne d'extérieur n'interroge ce Loki.
|
||||
analytics:
|
||||
reporting_enabled: false
|
||||
|
||||
singleBinary:
|
||||
replicas: 1
|
||||
persistence:
|
||||
enabled: true
|
||||
# ⚠ DEUX StorageClass portent le drapeau `(default)` sur ce cluster
|
||||
# (`local-path` ET `longhorn`, mesuré le 2026-09-07). Un PVC sans classe
|
||||
# explicite est donc un tirage au sort — et `local-path` ne survit pas au
|
||||
# déplacement du pod, ce qui viderait la mémoire qu'on vient de bâtir.
|
||||
# On NOMME la classe.
|
||||
storageClass: longhorn
|
||||
# 30 j × 305 Mio/j = 8,9 Gio BRUTS ; Loki compresse (facteur 5 à 10 sur
|
||||
# du journal), donc 0,9 à 1,8 Gio attendus. 10 Gi laisse 5 à 10× de marge
|
||||
# pour la croissance de kadans, sans immobiliser le disque.
|
||||
size: 10Gi
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
# Pas de limite CPU : l'étranglement d'un compacteur est pire que sa
|
||||
# lenteur. La mémoire, elle, est PLAFONNÉE — c'est la ressource rare
|
||||
# (pi2 mesuré à 109 % de son allouable le 2026-09-07).
|
||||
memory: 512Mi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tout ce qui suit existe pour un déploiement distribué. Ici, chacun de ces
|
||||
# blocs est du poids mort sur des Raspberry Pi — on les éteint NOMMÉMENT
|
||||
# plutôt que par un `deploymentMode` qui « devrait » suffire.
|
||||
# ---------------------------------------------------------------------------
|
||||
write: { replicas: 0 }
|
||||
read: { replicas: 0 }
|
||||
backend: { replicas: 0 }
|
||||
ingester: { replicas: 0 }
|
||||
distributor: { replicas: 0 }
|
||||
querier: { replicas: 0 }
|
||||
queryFrontend: { replicas: 0 }
|
||||
queryScheduler: { replicas: 0 }
|
||||
indexGateway: { replicas: 0 }
|
||||
compactor: { replicas: 0 }
|
||||
bloomPlanner: { replicas: 0 }
|
||||
bloomBuilder: { replicas: 0 }
|
||||
bloomGateway: { replicas: 0 }
|
||||
patternIngester: { replicas: 0 }
|
||||
ruler: { replicas: 0 }
|
||||
overridesExporter: { replicas: 0 }
|
||||
|
||||
# Les deux memcached. Le premier réclame 8 Gio à lui seul par défaut.
|
||||
chunksCache:
|
||||
enabled: false
|
||||
resultsCache:
|
||||
enabled: false
|
||||
|
||||
# Un nginx devant un service que seul Grafana interroge, depuis le cluster.
|
||||
gateway:
|
||||
enabled: false
|
||||
|
||||
# Un DaemonSet qui écrit des journaux pour vérifier qu'on lit les journaux :
|
||||
# trois pods de plus sur trois nœuds, pour une propriété que le sabotage de
|
||||
# la PR démontre mieux (écrire une ligne, tuer le pod, la relire).
|
||||
lokiCanary:
|
||||
enabled: false
|
||||
|
||||
test:
|
||||
enabled: false
|
||||
|
||||
# Le chart sait déployer SON PROPRE MinIO. On en a déjà un, et on n'en veut
|
||||
# pas ici : le stockage est le disque.
|
||||
minio:
|
||||
enabled: false
|
||||
|
||||
monitoring:
|
||||
dashboards:
|
||||
enabled: false
|
||||
rules:
|
||||
enabled: false
|
||||
serviceMonitor:
|
||||
enabled: false
|
||||
metricsInstance:
|
||||
enabled: false
|
||||
selfMonitoring:
|
||||
enabled: false
|
||||
grafanaAgent:
|
||||
installOperator: false
|
||||
|
||||
# Pas d'opérateur de déploiement progressif pour une réplique unique.
|
||||
rollout_operator:
|
||||
enabled: false
|
||||
|
||||
# ⚠ UN SECOND CONTENEUR SANS PLAFOND ANNULE LE PLAFOND DU PREMIER.
|
||||
# Le chart ajoute au pod un side-car `loki-sc-rules` (kiwigrid/k8s-sidecar)
|
||||
# qui surveille des ConfigMaps de règles d'alerte. Mesuré sur le premier
|
||||
# déploiement : il arrive avec `resources: {}` — ni requête ni limite — dans
|
||||
# un pod posé sur pi2, le nœud à 108 % de son allouable. On l'éteint : le
|
||||
# `ruler` est à 0 réplique, il n'y a aucune règle à ingérer, donc ce side-car
|
||||
# surveille des ConfigMaps qui n'existent pas.
|
||||
sidecar:
|
||||
rules:
|
||||
enabled: false
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# RETENTION — l'arbitrage, et la mesure qui le porte
|
||||
# -----------------------------------------------------------------------------
|
||||
# Mesuré le 2026-09-07 sur ce cluster, par l'API (donc ce que Loki ingérerait) :
|
||||
#
|
||||
# fenêtre de 10 min, tous pods : 2 182 015 octets
|
||||
# fenêtre de 1 h, tous pods : 7 897 505 octets
|
||||
#
|
||||
# Les deux instruments ne s'accordent pas (l'heure ne vaut que 0,60 fois six
|
||||
# fois les dix minutes) — et le désaccord est LE résultat. Par conteneur, le
|
||||
# rapport 1 h / 10 min vaut ~6 partout (débit stable) SAUF :
|
||||
#
|
||||
# kube-system/traefik ratio 1,18 → la fenêtre d'une heure est TRONQUÉE
|
||||
# tools/clickhouse-0 ratio 3,98 → tronquée aussi
|
||||
#
|
||||
# Vérifié directement, en demandant à chaque journal jusqu'où il remonte :
|
||||
#
|
||||
# traefik pod démarré il y a 12 j — journal remontant à 10 MINUTES
|
||||
# clickhouse pod démarré il y a 8 j — journal remontant à 2 MINUTES
|
||||
#
|
||||
# Autrement dit, les deux composants les plus bavards du cluster gardent entre
|
||||
# deux et dix minutes d'histoire. C'est l'issue tools#38 en un chiffre.
|
||||
#
|
||||
# Estimation corrigée (max(1 h, 10 min × 6) par conteneur, pour ne pas hériter
|
||||
# de la troncature) : 13 333 645 octets/h = 305 Mio/jour bruts.
|
||||
#
|
||||
# 30 j bruts 8,94 Gio
|
||||
# 30 j compressés (facteur 5) 1,79 Gio
|
||||
# 30 j compressés (facteur 10) 0,89 Gio
|
||||
#
|
||||
# POURQUOI 30 JOURS et pas 7 : les deux pannes qui motivent ce lot ont duré des
|
||||
# SEMAINES avant d'être vues (la mise à l'abri de kadans#1033) ou trois jours
|
||||
# (tools#36). Une rétention de 7 jours aurait perdu le début des deux. 30 jours
|
||||
# couvre le délai réel entre « ça casse » et « quelqu'un s'en aperçoit », et
|
||||
# coûte 1 à 2 Gio — sur 99 Gio libres au plus serré des trois nœuds Longhorn.
|
||||
#
|
||||
# ⚠ CE QUI RENDRAIT CE CHIFFRE FAUX : la mesure est une photo d'un mardi
|
||||
# après-midi. Elle ne dit rien d'un pod qui part en boucle d'erreur, cas où le
|
||||
# débit d'un seul conteneur peut dépasser à lui seul tout le reste du cluster.
|
||||
# La marge de 5 à 10× est là pour ça, pas pour la croissance ordinaire.
|
||||
# -----------------------------------------------------------------------------
|
||||
Reference in New Issue
Block a user