Aller au contenu
medium

05, Loki : centralisation des logs

27 min de lecture

Les métriques vous disent quoi (combien de requêtes, quelle latence). Les logs vous disent pourquoi (le message d'erreur exact, la stack trace). Après avoir configuré Prometheus pour les métriques et Alertmanager pour les notifications, vous allez maintenant ajouter la gestion des logs centralisés.

Loki est souvent appelé le "Prometheus des logs". Comme Prometheus, il indexe par labels plutôt que par contenu complet. Cela le rend léger, économique et rapide, parfait pour Kubernetes.

À la fin de ce module, vous saurez :

  1. Expliquer l'architecture Loki/Promtail
  2. Installer Loki et Promtail sur Kubernetes
  3. Requêter les logs avec LogQL
  4. Parser les logs JSON pour extraire des champs
  5. Générer des métriques à partir des logs
  6. Créer des dashboards et alertes sur les logs
  1. Prometheus et Grafana fonctionnent

    Fenêtre de terminal
    kubectl get pods -n observability -l "app.kubernetes.io/name in (prometheus,grafana)"

    Ce que vous devez voir : Les pods en Running.

  2. L'application OpenTelemetry Demo génère du trafic

    Fenêtre de terminal
    kubectl get pods -n otel-demo | head -5

    Ce que vous devez voir : Plusieurs pods en Running.

  3. Le repo Helm Grafana est disponible

    Fenêtre de terminal
    helm repo list | grep grafana

    Ce que vous devez voir : La ligne grafana.

    Si absent :

    Fenêtre de terminal
    helm repo add grafana https://grafana.github.io/helm-charts
    helm repo update
  4. Vous êtes dans le répertoire du lab

    Fenêtre de terminal
    cd ~/lab-observability

Loki se distingue des moteurs de recherche classiques par ce qu'il indexe : seulement les labels, jamais le contenu des lignes. Cette décision explique à la fois sa légèreté et la façon dont il faut écrire les requêtes.

La chaîne comporte quatre maillons, du producteur de logs jusqu'à la consultation. Retenez surtout la place de Promtail : c'est lui qui lit les fichiers de logs sur chaque node et attache les labels Kubernetes (namespace, pod, container). Si un label vous manque plus tard dans une requête, c'est la configuration de Promtail qu'il faut regarder, pas Loki.

Architecture Loki

ComposantRôle
PodsÉcrivent leurs logs sur stdout/stderr
PromtailCollecte les logs et les enrichit avec les labels Kubernetes
LokiStocke les logs, indexe par labels
GrafanaInterface pour chercher et visualiser

L'approche traditionnelle (ELK/Splunk) :

  • Indexe tout le contenu de chaque log
  • Recherche full-text puissante
  • Stockage coûteux (chaque mot est indexé)
  • Ingestion lourde

L'approche Loki :

  • Indexe seulement les labels (namespace, pod, container...)
  • Stockage économique (les logs bruts sont compressés)
  • Recherche par labels d'abord, puis filtrage de contenu
  • Ingestion légère
CritèreLokiELK/Splunk
IndexationLabels seulementContenu complet
Coût stockage✅ Faible❌ Élevé
Recherche full-text⚠️ Séquentielle✅ Indexée
Intégration Grafana✅ Native⚠️ Plugin
Syntaxe requêteLogQL (≈ PromQL)Propre à chaque outil
Complexité déploiement✅ Simple⚠️ Complexe

Conclusion : Loki est le choix recommandé pour la majorité des cas DevOps. Réservez ELK/Splunk aux besoins de recherche full-text intensive (logs d'application avec messages très variés, analyse forensique).

L'installation se fait par Helm, avec un chart qui déploie les deux composants d'un coup. Les valeurs par défaut conviennent à un lab ; les ajustements de rétention et de stockage viennent plus loin.

Le chart loki-stack de Grafana installe Loki et Promtail ensemble. C'est la méthode la plus simple pour commencer.

Examinez la configuration que nous allons utiliser :

Fenêtre de terminal
cat 05-loki/helm-values/loki-stack.yaml

Contenu :

# Loki - serveur de logs
loki:
enabled: true
persistence:
enabled: true
size: 10Gi
config:
limits_config:
retention_period: 168h # 7 jours
reject_old_samples: true
reject_old_samples_max_age: 168h
chunk_store_config:
max_look_back_period: 0s
table_manager:
retention_deletes_enabled: true
retention_period: 168h
# Promtail - collecteur de logs
promtail:
enabled: true
config:
clients:
- url: http://loki:3100/loki/api/v1/push
snippets:
pipelineStages:
# Parser les logs JSON automatiquement
- cri: {}
- json:
expressions:
level: level
msg: msg
trace_id: trace_id
span_id: span_id
- labels:
level:
trace_id:
span_id:

Ce que fait cette configuration :

SectionEffet
loki.persistenceLes logs sont stockés sur disque (pas perdus au redémarrage)
loki.config.limits_config.retention_periodLes logs plus vieux que 7 jours sont supprimés
promtail.config.snippets.pipelineStagesLes logs JSON sont parsés et le niveau extrait comme label
  1. Installez loki-stack

    Fenêtre de terminal
    helm upgrade --install loki grafana/loki-stack \
    -n observability \
    -f 05-loki/helm-values/loki-stack.yaml \
    --wait

    Ce que vous devez voir :

    Release "loki" does not exist. Installing it now.
    level=WARN msg="this chart is deprecated"
    NAME: loki
    LAST DEPLOYED: [date]
    NAMESPACE: observability
    STATUS: deployed

    Patientez 1-2 minutes que les pods démarrent.

  2. Vérifiez que Loki tourne

    Fenêtre de terminal
    kubectl get pods -n observability -l app=loki

    Ce que vous devez voir :

    NAME READY STATUS RESTARTS AGE
    loki-0 1/1 Running 0 2m

    Le pod loki-0 est en Running.

  3. Vérifiez que Promtail tourne

    Fenêtre de terminal
    kubectl get pods -n observability -l app.kubernetes.io/name=promtail

    Ce que vous devez voir :

    NAME READY STATUS RESTARTS AGE
    loki-promtail-xxxxx 1/1 Running 0 2m

    Promtail est un DaemonSet : il y a un pod par node de votre cluster. Si vous avez 3 nodes, vous verrez 3 pods.

  4. Vérifiez que Promtail collecte des logs

    Fenêtre de terminal
    kubectl logs -n observability -l app.kubernetes.io/name=promtail --tail=10

    Ce que vous devez voir : Des lignes mentionnant les fichiers de logs découverts et les streams créés. Par exemple :

    level=info msg="Adding target" ...
    level=info msg="start" ...

    Si vous voyez des erreurs de connexion à Loki, attendez quelques secondes et réessayez.

Grafana doit savoir où trouver Loki pour pouvoir l'interroger.

  1. Ouvrez Grafana

    Grâce au NodePort configuré dans le module 03, Grafana est accessible directement :

    Fenêtre de terminal
    minikube service grafana -n observability --url

    Ou accédez directement à http://<MINIKUBE_IP>:30030

    Connectez-vous avec admin / admin.

  2. Allez dans Connections → Data sources

  3. Cliquez sur "Add data source"

  4. Cherchez et sélectionnez "Loki"

  5. Configurez la connexion

    ChampValeur
    NameLoki
    URLhttp://loki:3100

    Laissez les autres champs par défaut.

  6. Cliquez sur "Save & test"

    Ce que vous devez voir : Un message vert "Data source successfully connected".

LogQL ressemble à PromQL mais s'applique à des lignes de log. Une requête se construit toujours en deux temps : d'abord un sélecteur de labels qui réduit le volume, ensuite un filtre sur le contenu. Inverser cet ordre est le premier réflexe à perdre.

Explore est le mode de Grafana conçu pour l'itération : vous tapez une requête, vous voyez le résultat, vous ajustez, sans passer par la sauvegarde d'un dashboard. C'est là que vous mettez au point vos requêtes LogQL avant de les figer dans un panneau. Sélectionnez bien la datasource Loki en haut, sinon l'éditeur attend du PromQL.

  1. Dans Grafana, cliquez sur l'icône boussole (Explore) dans le menu latéral

  2. En haut, sélectionnez "Loki" dans le dropdown des datasources

  3. Vous voyez : Un éditeur de requête et un sélecteur de plage temporelle

Tapez cette requête dans l'éditeur :

{namespace="otel-demo"}

Cliquez sur Run query (ou Shift+Enter).

Ce que vous devez voir :

  • Une liste de logs avec des timestamps
  • Chaque log affiche ses labels (namespace, pod, container)
  • Les logs les plus récents en haut

Les labels disponibles dépendent de ce que Promtail a collecté. Les labels Kubernetes courants sont :

LabelDescriptionExemple
namespaceLe namespace Kubernetesotel-demo
podLe nom du podotel-demo-frontend-xyz
containerLe nom du conteneurfrontend
appLe label app du podfrontend
jobLe job de scraping Promtailkubernetes-pods

Exemples de sélecteurs :

# Logs d'un namespace spécifique
{namespace="otel-demo"}
# Logs d'un pod spécifique
{namespace="otel-demo", pod="otel-demo-frontend-abc123"}
# Logs de tous les pods dont le nom commence par "frontend"
{namespace="otel-demo", pod=~".*frontend.*"}
# Logs de tous les namespaces sauf kube-system
{namespace!="kube-system"}

Les opérateurs de sélection :

OpérateurSignificationExemple
=Égal exactement{namespace="otel-demo"}
!=Différent de{namespace!="kube-system"}
=~Correspond au regex{pod=~".*frontend.*"}
!~Ne correspond pas au regex{pod!~".*test.*"}

Après avoir sélectionné les streams, vous pouvez filtrer les lignes par leur contenu :

{namespace="otel-demo"} |= "error"

Cette requête retourne les logs du namespace otel-demo qui contiennent le mot "error".

Les opérateurs de filtre de ligne :

OpérateurSignificationExemple
|=Contient{app="frontend"} |= "error"
!=Ne contient pas{app="frontend"} != "health"
|~Correspond au regex{app="frontend"} |~ "status=(4|5)[0-9]{2}"
!~Ne correspond pas au regex{app="frontend"} !~ "DEBUG"

Exemples pratiques :

# Logs contenant "error" (case-insensitive avec regex)
{namespace="otel-demo"} |~ "(?i)error"
# Logs contenant "error" mais pas "healthcheck"
{namespace="otel-demo"} |= "error" != "healthcheck"
# Logs avec un code HTTP 4xx ou 5xx
{namespace="otel-demo"} |~ "HTTP/(4|5)[0-9]{2}"
# Logs contenant une trace_id (pour la corrélation)
{namespace="otel-demo"} |~ "trace_id"

La plupart des applications modernes génèrent des logs en JSON. LogQL peut les parser.

Exemple de log JSON :

{"level":"error","msg":"Failed to process order","order_id":"12345","trace_id":"abc123"}

Parser avec | json :

{namespace="otel-demo"} | json

Après | json, les champs JSON deviennent des labels que vous pouvez utiliser :

# Filtrer par niveau d'erreur
{namespace="otel-demo"} | json | level="error"
# Filtrer par code HTTP
{namespace="otel-demo"} | json | status_code >= 400
# Combiner plusieurs filtres
{namespace="otel-demo"} | json | level="error" | service_name="checkout"

Vous pouvez modifier l'affichage des logs :

# Afficher seulement certains champs
{namespace="otel-demo"} | json | line_format "{{.level}} - {{.msg}}"
# Format plus lisible
{namespace="otel-demo"} | json | line_format "[{{.level}}] {{.service_name}}: {{.msg}}"

Exercice 1 : Explorer les logs de l'application démo

Section intitulée « Exercice 1 : Explorer les logs de l'application démo »
  1. Listez tous les logs du namespace otel-demo

    {namespace="otel-demo"}

    Observation : Beaucoup de logs ! Réduisez la plage temporelle à 15 minutes.

  2. Filtrez sur les erreurs

    {namespace="otel-demo"} |= "error"

    Combien de logs d'erreur voyez-vous ?

  3. Parsez le JSON et filtrez par niveau

    {namespace="otel-demo"} | json | level="error"

    Question : Y a-t-il une différence avec la requête précédente ?

    (Oui, la précédente cherche le texte "error" n'importe où ; celle-ci cherche le champ JSON level égal à "error")

  4. Identifiez les pods les plus bavards

    {namespace="otel-demo"} | json

    Regardez le panneau latéral (si disponible) pour voir la distribution par pod.

  5. Cherchez les logs avec un trace_id

    {namespace="otel-demo"} | json | trace_id != ""

    Ces logs pourront être corrélés avec les traces Tempo (module 07).

LogQL peut compter et mesurer les logs, transformant des logs en métriques.

Logs par seconde :

rate({namespace="otel-demo"}[5m])

Cette requête retourne le taux de logs par seconde sur les 5 dernières minutes, groupé par stream.

Logs par seconde, agrégés par pod :

sum by (pod) (rate({namespace="otel-demo"}[5m]))

Logs par seconde, agrégés par niveau :

sum by (level) (rate({namespace="otel-demo"} | json [5m]))

Là où rate donne une vitesse, count_over_time renvoie un volume absolu sur la fenêtre indiquée. C'est la fonction à utiliser pour répondre à « combien d'erreurs cette nuit », pas pour alerter.

# Nombre total de logs d'erreur sur la dernière heure
count_over_time({namespace="otel-demo"} |= "error" [1h])
# Par pod
sum by (pod) (count_over_time({namespace="otel-demo"} |= "error" [1h]))

Un nombre d'erreurs seul ne dit rien sans son volume de référence : cent erreurs sur un million de requêtes n'ont pas le même sens que cent sur mille. Le ratio se calcule en divisant deux rate calculés sur la même fenêtre.

# Ratio erreurs / total logs par service
(
sum by (service_name) (
rate({namespace="otel-demo"} | json | level="error" [5m])
)
)
/
(
sum by (service_name) (
rate({namespace="otel-demo"} | json [5m])
)
)
  1. Comptez les logs par seconde de chaque pod

    sum by (pod) (rate({namespace="otel-demo"}[5m]))

    Ce que vous devez voir : Un graphique avec une ligne par pod.

  2. Identifiez le top 5 des pods les plus bavards

    topk(5, sum by (pod) (rate({namespace="otel-demo"}[5m])))
  3. Comptez les erreurs par niveau de log

    sum by (level) (rate({namespace="otel-demo"} | json [5m]))
  4. Alertez si plus de 10 erreurs par minute

    D'abord, vérifiez le taux d'erreurs :

    sum(rate({namespace="otel-demo"} | json | level="error" [1m])) * 60

    Cette valeur (erreurs par minute) peut être utilisée pour une alerte Grafana.

Promtail fait plus que collecter les logs. Il peut les transformer.

Les "stages" sont des étapes de traitement appliquées à chaque log :

pipeline_stages:
# 1. Parser le format CRI (Container Runtime Interface)
- cri: {}
# 2. Parser le JSON
- json:
expressions:
level: level
message: msg
trace_id: trace_id
# 3. Ajouter des labels
- labels:
level:
trace_id:
# 4. Filtrer (drop) les logs inutiles
- match:
selector: '{app="frontend"}'
stages:
- drop:
expression: "healthcheck"
# 5. Modifier le contenu
- replace:
expression: "password=.*"
replace: "password=***"

Explication de chaque stage :

StageRôleExemple
criParse le format des logs Kubernetes (CRI)Extrait le timestamp
jsonParse le contenu JSONExtrait level, msg, etc.
labelsAjoute des labels à partir des champs extraitslevel devient un label filtrable
dropSupprime les logs correspondantsIgnore les healthchecks
replaceModifie le contenuMasque les mots de passe

Les stack traces Java ou Python s'étendent sur plusieurs lignes :

2024-01-15 10:30:45 ERROR Something went wrong
java.lang.NullPointerException
at com.example.MyClass.method(MyClass.java:42)
at com.example.Main.main(Main.java:10)

Configuration pour les regrouper :

pipeline_stages:
- multiline:
firstline: '^\d{4}-\d{2}-\d{2}' # Nouvelle entrée = commence par une date
max_wait_time: 3s

Les requêtes mises au point dans Explore se figent ensuite dans un dashboard, ce qui évite de les réécrire à chaque incident. Deux types de panneaux suffisent : le flux de logs brut et la courbe de volume.

Ce premier panneau affiche le flux de logs brut, celui que vous lisez pendant un incident pour voir les messages exacts. La visualisation Logs de Grafana conserve l'horodatage et les labels ; activez Wrap lines pour ne pas tronquer les longues lignes JSON.

  1. Créez un nouveau dashboard ou ouvrez un existant

  2. Add panel → Visualization: Logs

  3. Configurez la requête

    • Data source : Loki
    • Query : {namespace="otel-demo"} | json
  4. Dans Panel options

    OptionValeur recommandée
    TimeShow
    Unique labelsShow
    Common labelsHide
    Wrap linesOn
  5. Sauvegardez le panel

Ici on ne lit plus les lignes, on les compte. La visualisation Time series trace une courbe de volume par niveau de log, ce qui rend visible une montée d'erreurs bien avant de plonger dans les messages. La requête réutilise rate() et | json vus plus haut.

  1. Add panel → Visualization: Time series

  2. Configurez la requête

    sum by (level) (rate({namespace="otel-demo"} | json [5m]))
  3. Dans Panel options

    • Title : "Log rate par niveau"
    • Legend : Table, valeurs
  4. Sauvegardez

Ce dernier panneau répond à une question de priorisation : quel service génère le plus d'erreurs sur la dernière heure. La visualisation Bar gauge avec des seuils colorés (vert, jaune, rouge) transforme le classement en signal visuel immédiat pour l'astreinte.

  1. Add panel → Visualization: Bar gauge

  2. Configurez la requête

    sum by (service_name) (
    count_over_time({namespace="otel-demo"} | json | level="error" [1h])
    )
  3. Dans Panel options

    • Title : "Erreurs par service (dernière heure)"
    • Thresholds : 0=vert, 10=jaune, 50=rouge
  4. Sauvegardez

Grafana peut créer des alertes basées sur des requêtes Loki.

  1. Allez dans Alerting → Alert rules

  2. Cliquez sur "New alert rule"

  3. Configurez la requête

    • Data source : Loki

    • Query :

      sum(count_over_time({namespace="otel-demo"} | json | level="error" [5m]))
  4. Configurez la condition

    • Expression : count > 10
    • For : 5 minutes
  5. Configurez les labels et annotations

    Summary: Plus de 10 erreurs en 5 minutes dans otel-demo
  6. Sauvegardez

C'est le réglage qui décide de la facture. Loki ne purge rien par défaut dans une installation de lab : sans politique explicite, le volume croît jusqu'à saturer le disque.

Loki compresse bien les logs (ratio ~10:1 typique).

Formule d'estimation :

Espace = (logs_bruts_par_jour_GB / 10) × jours_rétention

Exemple :

  • 50 GB de logs bruts par jour
  • Rétention 7 jours
  • Espace nécessaire ≈ (50 / 10) × 7 = 35 GB

Dans les values Helm :

loki:
config:
limits_config:
retention_period: 168h # 7 jours
reject_old_samples: true
reject_old_samples_max_age: 168h
table_manager:
retention_deletes_enabled: true
retention_period: 168h

Les pannes de Loki se ressemblent : Grafana répond, mais aucune ligne n'apparaît. La cause se situe presque toujours en amont, du côté de la collecte ou des labels, plutôt que dans la requête elle-même.

Le tableau ci-dessous se lit de haut en bas comme un arbre de décision : chaque étape isole un maillon de la chaîne de collecte. Ne sautez pas d'étape, car un pod Running ne garantit pas que Promtail pousse effectivement vers Loki. La première ligne qui échoue désigne le composant à corriger.

ÉtapeCommande/ActionCe que vous cherchez
1. Promtail tourne ?kubectl get pods -n observability -l app.kubernetes.io/name=promtailPods Running
2. Loki tourne ?kubectl get pods -n observability -l app=lokiPod Running
3. Logs Promtailkubectl logs -n observability -l app.kubernetes.io/name=promtail --tail=50Pas d'erreurs de connexion
4. Les pods cibles ont des logs ?kubectl logs -n otel-demo <pod> --tail=5Des logs s'affichent
5. Datasource Loki OK ?Grafana → Connections → Data sources → Loki → Save & test"Successfully connected"

Loki est surchargé.

CauseSolution
Trop de requêtes simultanéesRéduisez les dashboards actifs
Requêtes trop largesAjoutez des filtres de labels
Ressources insuffisantesAugmentez memory/CPU de Loki

Une requête LogQL lente vient presque toujours d'un scan trop large : Loki lit les logs bruts de tous les streams sélectionnés avant de filtrer. La parade tient en un principe, filtrer par labels d'abord pour réduire le volume, puis seulement par contenu et par | json.

CauseSolution
Plage temporelle trop largeRéduisez à 1h ou moins
Pas de filtre de labelsAjoutez {namespace="..."} au minimum
Trop de parsing JSONFiltrez par texte d'abord (|= "error"), puis | json

Un log coupé net ou une stack trace éclatée en dizaines d'entrées séparées signalent deux réglages distincts de Promtail : une ligne trop longue rejetée par une limite de taille, ou des lignes multiples qu'aucune stage multiline ne regroupe. Le symptôme visuel diffère, la correction se fait dans les deux cas côté collecteur.

CauseSolution
Limite de taille de ligneAugmentez max_line_size dans Promtail
Logs multilignes non groupésAjoutez une stage multiline
  1. Loki et Promtail sont déployés

    Fenêtre de terminal
    kubectl get pods -n observability -l "app in (loki)" && \
    kubectl get pods -n observability -l app.kubernetes.io/name=promtail

    Les pods sont en Running.

  2. La datasource Loki fonctionne

    Dans Grafana → Connections → Data sources → Loki : "Successfully connected"

  3. Les logs sont collectés

    Dans Grafana → Explore → Loki :

    {namespace="otel-demo"}

    Des logs s'affichent.

  4. Le parsing JSON fonctionne

    {namespace="otel-demo"} | json | level="error"

    Des logs filtrés s'affichent.

  5. Vous savez générer des métriques

    sum by (pod) (rate({namespace="otel-demo"}[5m]))

    Un graphique s'affiche.

Ce module a ajouté le deuxième pilier de l'observabilité, les logs centralisés, à côté des métriques. Voici les points à retenir avant de passer aux traces.

  1. Loki indexe les labels, pas le contenu, ce qui le rend léger et économique
  2. Promtail collecte les logs des pods et les enrichit avec les labels Kubernetes
  3. LogQL ressemble à PromQL : sélecteurs de stream {} + filtres |= |~ + transformations | json
  4. Parser JSON permet de filtrer sur les champs : | json | level="error"
  5. Les fonctions rate() et count_over_time() transforment les logs en métriques
  6. Grafana Alerting peut créer des alertes basées sur des requêtes Loki

Vous avez maintenant les métriques (Prometheus) et les logs (Loki). Il vous manque le troisième pilier de l'observabilité : les traces distribuées. Dans le prochain module, nous ajoutons Tempo pour suivre les requêtes à travers tous vos services.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn