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¶
| 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¶
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¶
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 :
docker: {}— parse le format JSON des logs Docker et extraittime,stream,attrs, etlog(le texte effectif).- Extraction du level pour
perfshop-app— regexERROR|WARN|INFO|DEBUGqui extrait le niveau et le pose comme label Loki. C'est ce label qui permet la requête{container="perfshop-app"} | level="ERROR". - 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¶
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¶
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)¶
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¶
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¶
- Vue d'ensemble — flux global d'observabilité, comparaison Loki vs OpenSearch
- Grafana — datasource Loki et corrélation
tracesToLogsV2 - Dashboards livrés — panels logs Élève et Formateur
- OpenSearch et Vector — l'autre puits de logs (full-text)