Aller au contenu

Loki et Promtail

Loki est l'un des deux puits de logs de PerfShop (l'autre étant OpenSearch — voir opensearch.md). Il fonctionne en mode single-node, indexe uniquement sur les labels (pas de full-text), et conserve les logs pendant 7 jours. Promtail est l'agent qui collecte les logs depuis le socket Docker et les pousse vers Loki.

Source de vérité

Cette page est tirée de loki/loki-config.yml et promtail/promtail-config.yml, et des bind mounts du service perfshop-promtail dans les fichiers compose.

Architecture du pipeline

flowchart LR
  subgraph sources["Sources de logs"]
    direction TB
    SOCK["/var/run/docker.sock<br/>(Docker Engine API)"]
    JLOG["./jmeter/logs/jmeter.log<br/>(bind mount RO)"]
    RFLOG["./test-runner/logs/*.log<br/>(bind mount RO)"]
  end

  PT["perfshop-promtail<br/>(grafana/promtail:latest)"]

  LOKI[("perfshop-loki<br/>(grafana/loki:latest)<br/>retention 168h")]

  GRAF["Grafana<br/>(datasource Loki)"]

  SOCK -->|docker SD| PT
  JLOG -->|file tail| PT
  RFLOG -->|file tail| PT

  PT -->|push API<br/>http://perfshop-loki:3100/loki/api/v1/push| LOKI
  LOKI --> GRAF

Loki — configuration

Mode et stockage

auth_enabled: false
target: all                  # mode single-node — pas de cluster, pas de ring distribué
Paramètre Valeur Effet
auth_enabled: false Aucune authentification — Loki est uniquement accessible depuis le réseau Docker interne
target: all Tous les modules (distributor, ingester, querier, query-frontend) tournent dans le même process

Stockage et schéma

common:
  instance_addr: 127.0.0.1
  path_prefix: /loki
  storage:
    filesystem:
      chunks_directory: /loki/chunks
      rules_directory: /loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory

schema_config:
  configs:
    - from: 2024-01-01
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h
Paramètre Valeur Effet
Backend de stockage filesystem Données stockées dans le volume nommé loki-data (/loki interne)
Schéma d'index v13 (TSDB) Format moderne, plus efficace que les schémas BoltDB historiques
Période d'index 24h Un nouvel index par jour, préfixe index_
Replication factor 1 Mode single-node, pas de réplication
Ring KV store inmemory Pas de Consul ni d'etcd

Limites et rétention

limits_config:
  retention_period: 168h            # 7 jours
  ingestion_rate_mb: 4              # 4 Mo/s par tenant
  ingestion_burst_size_mb: 8        # rafale tolérée
  max_query_series: 500             # cap sur le nombre de séries renvoyées par query

compactor:
  working_directory: /loki/compactor
  retention_enabled: true
  retention_delete_delay: 2h
  compaction_interval: 10m
  delete_request_store: filesystem
Paramètre Valeur Effet
retention_period 168h (7 jours) Suppression automatique par le compactor
ingestion_rate_mb 4 Mo/s Rate limit d'ingestion
ingestion_burst_size_mb 8 Mo Tolérance aux pics
max_query_series 500 Cap pour éviter les queries explosives
retention_enabled (compactor) true Active la purge automatique
compaction_interval 10m Le compactor passe toutes les 10 minutes
retention_delete_delay 2h Délai de grâce avant suppression effective

Cache de query

query_range:
  results_cache:
    cache:
      embedded_cache:
        enabled: true
        max_size_mb: 100

Cache embarqué de 100 Mo pour accélérer les queries répétées (utile pour Grafana qui réinterroge les mêmes plages de temps lors d'un refresh).

Ports

Port interne Usage
3100 HTTP listen — API push, query, admin (http_listen_port: 3100)
9096 gRPC listen — communication interne, inutilisée en single-node (grpc_listen_port: 9096)

Le port hôte par défaut est 19100 (variable LOKI_HTTP_PORT). Le port interne du conteneur reste 3100 — seul le mapping vers le host change.

Promtail — configuration

Cible et endpoint

clients:
  - url: http://perfshop-loki:3100/loki/api/v1/push

Promtail pousse les logs vers Loki via l'API push standard, en utilisant le DNS Docker interne.

Trois jobs de scrape

PerfShop déclare trois jobs Promtail distincts.

flowchart TB
  subgraph p["Promtail"]
    J1["Job perfshop-containers<br/>(docker SD)"]
    J2["Job jmeter-log<br/>(file tail)"]
    J3["Job rf-runner-log<br/>(file tail)"]
  end

  SOCK["/var/run/docker.sock"] --> J1
  J1 -.filtre.-> KEEP["perfshop-app<br/>perfshop-frontend<br/>perfshop-db<br/>perfshop-jmeter-ui"]

  JFILE["/jmeter-logs/jmeter.log"] --> J2
  RFFILES["/rf-logs/*.log"] --> J3

  J1 --> LOKI[("Loki")]
  J2 --> LOKI
  J3 --> LOKI

Job 1 — perfshop-containers (Docker SD)

- job_name: perfshop-containers
  docker_sd_configs:
    - host: unix:///var/run/docker.sock
      refresh_interval: 5s
      filters:
        - name: name
          values:
            - perfshop-app
            - perfshop-frontend
            - perfshop-db
            - perfshop-jmeter-ui

Promtail interroge l'API Docker via le socket Unix monté en bind mount toutes les 5 secondes. Le filtre name n'inclut que les containers dont le nom matche l'une des valeurs listées. Quatre containers seulement sont collectés via Docker SD : le backend, le frontend, la BDD MySQL, et perfshop-jmeter-ui.

Les autres services (Grafana, Tempo, Squash TM, Forgejo, etc.) ne sont pas collectés par Loki — leurs logs vont uniquement dans OpenSearch via Vector. C'est volontaire : Loki est dimensionné pour les logs « chauds » qui servent aux démos pédagogiques ; OpenSearch est le puits exhaustif pour la recherche full-text.

Relabeling :

relabel_configs:
  - source_labels: [__meta_docker_container_name]
    regex: /(.*)
    target_label: container
  - source_labels: [__meta_docker_container_name]
    regex: /(.*)
    target_label: job
  - source_labels: [container]
    regex: "perfshop-app|perfshop-frontend|perfshop-db|perfshop-jmeter-ui"
    action: keep

Le préfixe / que Docker ajoute aux noms de container (/perfshop-app) est nettoyé pour exposer les labels container=perfshop-app et job=perfshop-app directement dans Loki.

Pipeline stages :

pipeline_stages:
  - docker: {}
  - match:
      selector: '{container="perfshop-app"}'
      stages:
        - regex:
            expression: '(?P<level>ERROR|WARN|INFO|DEBUG)'
        - labels:
            level:
  - match:
      selector: '{container="perfshop-app"}'
      stages:
        - multiline:
            firstline: '^\d{4}-\d{2}-\d{2}'
            max_wait_time: 3s

Trois étapes :

  1. docker: {} — parse le format JSON des logs Docker et extrait time, stream, attrs, et log (le texte effectif).
  2. Extraction du level pour perfshop-app — regex ERROR|WARN|INFO|DEBUG qui extrait le niveau et le pose comme label Loki. C'est ce label qui permet la requête {container="perfshop-app"} | level="ERROR".
  3. Multiline pour les stack traces Java — toutes les lignes qui ne commencent pas par un timestamp (^\d{4}-\d{2}-\d{2}) sont concaténées à l'événement précédent. Une stack trace Java reste donc un seul événement Loki, ce qui rend la lecture beaucoup plus naturelle dans Grafana.

Job 2 — jmeter-log (file tail)

- job_name: jmeter-log
  static_configs:
    - targets: [localhost]
      labels:
        job: perfshop-jmeter
        container: perfshop-jmeter
        __path__: /jmeter-logs/jmeter.log

Pourquoi un job séparé ? Le container perfshop-jmeter est en tail -f /dev/null (idle). Aucun log n'est émis sur sa sortie standard, donc docker logs perfshop-jmeter est vide et le Docker SD du job 1 ne récupère rien. Pendant un tir JMeter, le moteur écrit dans /jmeter-logs/jmeter.log à l'intérieur du container, qui est le bind mount ./jmeter/logs:/jmeter-logs. Promtail lit ce fichier directement depuis le filesystem hôte (le bind mount est aussi monté dans perfshop-promtail en lecture seule).

Le pipeline applique le même parsing level + multiline que le job 1, adapté au format JMeter.

Job 3 — rf-runner-log (file tail)

- job_name: rf-runner-log
  static_configs:
    - targets: [localhost]
      labels:
        job: perfshop-test-runner
        container: perfshop-test-runner
        __path__: /rf-logs/*.log

Même mécanisme pour Robot Framework et pytest, qui écrivent dans /rf-logs/ (bind mount ./test-runner/logs). Le glob *.log collecte tous les fichiers de logs produits par les exécutions.

Le pipeline ajoute PASS et FAIL aux niveaux extraits, en plus de ERROR|WARN|INFO|DEBUG, parce que Robot Framework utilise ces tags pour les résultats de test.

Bind mounts du container Promtail

volumes:
  - ./promtail/promtail-config.yml:/etc/promtail/config.yml:ro
  - /var/run/docker.sock:/var/run/docker.sock:ro
  - ./jmeter/logs:/jmeter-logs:ro
  - ./test-runner/logs:/rf-logs:ro

Tous les mounts sont en lecture seule côté Promtail. Le socket Docker est monté pour permettre la découverte SD ; les deux dossiers de logs sont les mêmes que ceux écrits par les containers JMeter et Test Runner respectivement.

Exemples LogQL

Toutes les requêtes ci-dessous sont extraites des dashboards Grafana réellement livrés dans grafana/dashboards/{eleves,formateurs}/dashboard-logs-*.json.

Tous les logs du backend

{container="perfshop-app"} | logfmt

Le parser | logfmt est utilisé par le dashboard Logs Formateur. Spring Boot émet des logs au format clé=valeur quand l'encodeur logback logstash-logback-encoder est actif, ce qui permet d'extraire level, logger_name, message, etc. comme champs requêtables.

Logs de niveau ERROR uniquement

{container="perfshop-app"} != "[BusinessChaos]" != "[BackendChaos]" != "[SecurityChaos]" != "[ChaosInterceptor]" != "[FrontendChaos]" != "[ChaosScripting]" |= "ERROR"

C'est la requête utilisée par le dashboard Logs Élève (panel Erreurs backend uniquement). Les != excluent les logs internes du moteur chaos pour ne pas spoiler l'étudiant ; le |= ne garde que les lignes contenant le mot ERROR.

Logs d'une famille de chaos spécifique

{container="perfshop-app"} |= "[BusinessChaos]"

Utilisé par le dashboard Logs Formateur. Chaque famille de chaos préfixe ses logs par un tag entre crochets, ce qui rend le filtrage trivial.

Volume de logs par niveau (timeseries)

sum by (level) (count_over_time({container="perfshop-app"} | logfmt | level="ERROR" [1m]))

Combine count_over_time (équivalent LogQL de rate pour les logs) avec un parser logfmt qui extrait dynamiquement le label level. Le sum by (level) permet de superposer ERROR, WARN et INFO dans le même panel.

Logs nginx avec erreurs HTTP 4xx ou 5xx

count_over_time({container="perfshop-frontend"} |= " 4" [1m])
count_over_time({container="perfshop-frontend"} |= " 5" [1m])

Approche simple : nginx log les codes HTTP avec un espace devant (HTTP/1.1" 404), donc |= " 4" matche les 4xx. Pas de parser dédié — c'est suffisant pour les besoins pédagogiques.

Logs MySQL avec exclusion des notes

{container="perfshop-db"} != "[note]"

MySQL 8 log énormément de lignes [note] au démarrage et lors des opérations normales. Le != les exclut pour ne garder que les [error] et [warning].

Volumes

Volume Montage Contenu
loki-data (volume nommé) /loki Chunks, index, compactor working dir, WAL
./loki/loki-config.yml (bind mount) /etc/loki/local-config.yaml Configuration Loki (lecture seule)

Ports

Service Port hôte Port container Variable d'env
perfshop-loki 19100 3100 LOKI_HTTP_PORT
perfshop-promtail (aucun) (interne uniquement)

Promtail n'expose aucun port à l'hôte : il pousse vers Loki et n'a pas besoin d'être accessible de l'extérieur. Son port HTTP interne 9080 est uniquement pour les métriques internes Promtail (non scrapées par PerfShop).

Pour aller plus loin