Grafana Alloy — remplacer Promtail (et tout le reste)
Promtail est en fin de vie. Grafana Agent aussi. Alloy est le successeur officiel depuis avril 2024 — un collecteur universel qui gère logs, métriques et traces dans un seul binaire avec un langage de config déclaratif. Alloy complète la stack LGTM en unifiant la collecte des données.
La syntaxe change complètement, mais la migration est assistée.
Concepts clés
Alloy fonctionne par composants connectés entre eux. Chaque composant a un type et un nom, accepte des arguments, expose des exports.
// type.nom { ... }
loki.source.file "app_logs" {
targets = local.file_match.app.targets
forward_to = [loki.write.default.receiver]
}
On connecte les composants en passant les exports d'un composant comme argument d'un autre — loki.write.default.receiver est l'export receiver du composant loki.write nommé default.
L'UI de debug est dispo sur http://localhost:12345 — elle affiche le graphe des composants et leur état en temps réel.
Installation
Debian / Ubuntu
# Import GPG + repo
wget -q -O - https://apt.grafana.com/gpg.key | gpg --dearmor > /etc/apt/keyrings/grafana.gpg
echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" \
> /etc/apt/sources.list.d/grafana.list
apt-get update && apt-get install -y alloy
Docker
docker run -d \
-v /etc/alloy/config.alloy:/etc/alloy/config.alloy \
-p 12345:12345 \
grafana/alloy:latest \
run /etc/alloy/config.alloy
Kubernetes (Helm)
helm repo add grafana https://grafana.github.io/helm-charts
helm upgrade --install alloy grafana/alloy \
--namespace monitoring \
-f values.yaml
Le fichier de config par défaut : /etc/alloy/config.alloy
# Valider la config avant de reload
alloy fmt /etc/alloy/config.alloy
alloy validate /etc/alloy/config.alloy
# Reload à chaud
systemctl reload alloy
# ou via API
curl -X POST http://localhost:12345/-/reload
Migration depuis Promtail
Alloy embarque un convertisseur automatique pour faciliter la transition. Pour configurer les alertes basées sur les logs collectés, voir l'alerting avec Loki.
Le résultat est fonctionnel mais souvent verbeux — ça vaut le coup de simplifier à la main ensuite. La commande supporte aussi --source-format=static (Grafana Agent static mode).
Pipelines
Logs → Loki
Pipeline classique : découverte de fichiers + envoi vers Loki.
local.file_match "app" {
path_targets = [{"__path__" = "/var/log/app/*.log", "job" = "app"}]
}
loki.source.file "app_logs" {
targets = local.file_match.app.targets
forward_to = [loki.process.add_labels.receiver]
}
loki.process "add_labels" {
forward_to = [loki.write.default.receiver]
stage.static_labels {
values = {
env = "prod",
}
}
}
loki.write "default" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}
Logs Kubernetes
Pour collecter les logs des pods K8s, on passe par loki.source.kubernetes :
discovery.kubernetes "pods" {
role = "pod"
}
discovery.relabel "pods" {
targets = discovery.kubernetes.pods.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"
}
}
loki.source.kubernetes "pods" {
targets = discovery.relabel.pods.output
forward_to = [loki.write.default.receiver]
}
loki.write "default" {
endpoint {
url = "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/push"
}
}
Métriques → Prometheus / Mimir
prometheus.scrape "node" {
targets = [{"__address__" = "localhost:9100"}]
forward_to = [prometheus.remote_write.mimir.receiver]
}
prometheus.remote_write "mimir" {
endpoint {
url = "http://mimir:9009/api/v1/push"
}
}
Pour scraper la discovery K8s :
discovery.kubernetes "services" {
role = "service"
}
prometheus.scrape "k8s_services" {
targets = discovery.kubernetes.services.targets
forward_to = [prometheus.remote_write.mimir.receiver]
}
Traces → Tempo
otelcol.receiver.otlp "default" {
grpc { endpoint = "0.0.0.0:4317" }
http { endpoint = "0.0.0.0:4318" }
output {
traces = [otelcol.exporter.otlp.tempo.input]
}
}
otelcol.exporter.otlp "tempo" {
client {
endpoint = "http://tempo:4317"
tls {
insecure = true
}
}
}
Multiline parsing
Par défaut, Alloy envoie une entrée Loki par ligne de log. Pour les stack traces et les exceptions, c'est la galère — chaque ligne arrive séparément, impossible de corréler.
stage.multiline agrège les lignes jusqu'à ce que firstline matche à nouveau (ou que max_wait_time expire).
loki.process "multiline_app" {
forward_to = [loki.write.default.receiver]
stage.multiline {
firstline = "^\\d{4}-\\d{2}-\\d{2}" // ligne qui commence par une date
max_wait_time = "3s"
max_lines = 128
}
}
firstline: regex qui identifie le début d'un nouvel événementmax_wait_time: flush forcé si aucune nouvelle ligne après ce délai (défaut :5s)max_lines: flush forcé au-delà de N lignes (garde-fou contre les dumps infinis)
Java stack traces
2024-01-15 10:23:45.123 ERROR [main] c.example.MyService - Something went wrong
java.lang.NullPointerException: Cannot invoke method foo()
at com.example.MyService.doSomething(MyService.java:42)
at com.example.MyController.handle(MyController.java:18)
Les lignes de continuation commencent par une tab ou Caused by: — le firstline matche le timestamp.
loki.process "java_logs" {
forward_to = [loki.write.default.receiver]
stage.multiline {
firstline = "^\\d{4}-\\d{2}-\\d{2}\\s\\d{2}:\\d{2}:\\d{2}"
max_wait_time = "3s"
max_lines = 256
}
// Extraire level et logger depuis la première ligne
stage.regex {
expression = "^\\d{4}-\\d{2}-\\d{2}\\s\\d{2}:\\d{2}:\\d{2}\\.\\d+\\s(?P<level>\\w+)\\s\\[(?P<thread>[^\\]]+)\\]"
}
stage.labels {
values = {
level = "",
}
}
}
Python tracebacks
Traceback (most recent call last):
File "/app/main.py", line 42, in handle
result = process(data)
ValueError: invalid data
2 approches : matcher ^Traceback comme firstline (on groupe à partir de l'exception), ou matcher les lignes qui ne commencent pas par un espace (plus générique).
loki.process "python_logs" {
forward_to = [loki.write.default.receiver]
// Toute ligne qui ne commence pas par un espace = nouveau log
stage.multiline {
firstline = "^[^\\s]"
max_wait_time = "3s"
max_lines = 128
}
}
Go panics
goroutine 1 [running]:
main.(*Server).handleRequest(...)
/app/server.go:87 +0x1a4
panic: runtime error: index out of range [3] with length 3
Les panics Go commencent par goroutine ou panic: — on matche les deux.
loki.process "go_logs" {
forward_to = [loki.write.default.receiver]
stage.multiline {
firstline = "^(goroutine|panic:|\\d{4}/\\d{2}/\\d{2})"
max_wait_time = "3s"
max_lines = 64
}
}
JSON avec stack trace dans un champ
Pattern fréquent avec les frameworks modernes (Logback JSON, structlog...) — le log est une ligne JSON mais le champ stack_trace contient un multiline.
{"time":"2024-01-15T10:23:45Z","level":"error","msg":"boom","stack_trace":"goroutine 1...\n\tat main.go:42"}
Dans ce cas, pas besoin de multiline — chaque ligne est déjà un événement complet. On parse le JSON directement :
loki.process "json_logs" {
forward_to = [loki.write.default.receiver]
stage.json {
expressions = {
level = "level",
stack_trace = "stack_trace",
}
}
stage.labels {
values = {
level = "",
}
}
}
Logs systemd (journal)
Sur du bare metal ou des VMs, les logs applicatifs passent souvent par journald plutôt que des fichiers. loki.source.journal lit directement le journal systemd — pas besoin de local.file_match.
loki.source.journal "systemd" {
forward_to = [loki.process.journal.receiver]
relabel_rules = discovery.relabel.journal.rules
labels = {job = "systemd"}
}
discovery.relabel "journal" {
targets = []
// Unité systemd comme label (sshd, nginx, docker...)
rule {
source_labels = ["__journal__systemd_unit"]
target_label = "unit"
}
// Priorité syslog comme label (err, warning, info...)
rule {
source_labels = ["__journal_priority_keyword"]
target_label = "level"
}
// Hostname
rule {
source_labels = ["__journal__hostname"]
target_label = "host"
}
}
loki.process "journal" {
forward_to = [loki.write.default.receiver]
// Ignorer les logs debug et info trop verbeux
stage.drop {
expression = ".*systemd.*Started.*"
}
}
loki.write "default" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}
Les labels disponibles depuis __journal_* correspondent aux champs journald standards — _SYSTEMD_UNIT, _HOSTNAME, PRIORITY, _COMM, etc. On les liste avec :
Par défaut loki.source.journal démarre depuis la position courante. Pour rejouer depuis le début (migration initiale) :
loki.source.journal "systemd" {
forward_to = [loki.write.default.receiver]
path = "/var/log/journal"
max_age = "24h" // ne pas remonter plus loin que 24h
}
Permissions
Alloy doit appartenir au groupe systemd-journal pour lire le journal :
Helm values K8s (DaemonSet)
Configuration minimale pour déployer Alloy en DaemonSet sur K8s — collecte des logs de tous les pods :
alloy:
configMap:
content: |
discovery.kubernetes "pods" {
role = "pod"
}
discovery.relabel "pods" {
targets = discovery.kubernetes.pods.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"
}
// Exclure les namespaces système
rule {
source_labels = ["__meta_kubernetes_namespace"]
regex = "kube-system"
action = "drop"
}
}
loki.source.kubernetes "pods" {
targets = discovery.relabel.pods.output
forward_to = [loki.write.default.receiver]
}
loki.write "default" {
endpoint {
url = env("LOKI_URL")
}
}
controller:
type: daemonset
rbac:
create: true
serviceAccount:
create: true
Les variables d'environnement sont injectables directement dans la config via env("VAR").
Troubleshooting
# Logs du service
journalctl -u alloy -f
# État des composants via API
curl -s http://localhost:12345/api/v0/web/components | jq '.[] | {name, health}'
# Métriques Alloy lui-même
curl http://localhost:12345/metrics | grep alloy_
# Debug d'un composant spécifique dans l'UI
open http://localhost:12345/component/loki.source.file.app_logs
Les erreurs courantes :
component evaluation failed→ vérifier que les exports référencés existent bien (typo dans le nom du composant)failed to read targets→ permission sur les fichiers de log (Alloy tourne souvent enalloyuser)429 Too Many Requests→ rate limiting Loki, ajouter unloki.processavecstage.limit
Voir aussi
- Netdata, Prometheus et Grafana : une stack de monitoring simple et puissante — architecture et composants LGTM
- Générer des alertes depuis Loki — alerting basé sur les logs collectés
- Ecrire une métrique custom pour node_exporter — métriques complémentaires via textfile
- 2-3 tips pour la stack LGTM — API calls pratiques
- Commandes utiles pour K8S — kubectl pour déboguer les pods Alloy en DaemonSet