OpenSearch et Vector¶
OpenSearch est le second puits de logs de PerfShop, en parallèle de Loki. Là où Loki indexe uniquement sur les labels et stocke le contenu en texte (modèle léger, requêtage par filtrage), OpenSearch indexe tous les champs en full-text et permet des agrégations et facets riches. Vector joue le rôle d'agent de collecte et de transformation entre les logs Docker et OpenSearch.
Source de vérité
Cette page est tirée de vector/vector.toml, opensearch/opensearch.yml, opensearch-seed/seed.py, et des blocs perfshop-opensearch, perfshop-opensearch-dashboards, perfshop-vector, perfshop-opensearch-seed des fichiers compose.
Pourquoi deux puits de logs ?¶
C'est une question légitime — collecter les mêmes logs deux fois ne paraît pas naturel. La réponse tient en trois points :
- Démonstration pédagogique : les étudiants doivent pouvoir comparer concrètement les modèles « index sur labels » (Loki) et « index full-text » (OpenSearch / Elasticsearch). Avoir les deux en parallèle permet d'illustrer en direct, sur les mêmes logs réels, les forces et limites de chaque approche.
- Cas d'usage différents : Loki est imbattable pour le filtrage rapide par container et niveau pendant un TP. OpenSearch est imbattable pour la recherche full-text exploratoire (« trouve toutes les exceptions où le mot
connectionapparaît, peu importe le service »). - Couplage Grafana / OpenSearch Dashboards : Loki est nativement intégré à Grafana ; OpenSearch a sa propre UI (OpenSearch Dashboards, fork de Kibana). Deux UIs, deux paradigmes — l'étudiant voit les deux mondes.
Versions pinned¶
PerfShop fige OpenSearch et OpenSearch Dashboards à 2.13.0, et Vector à 0.38.0-alpine. Les trois composants sont liés : Vector 0.38 utilise une syntaxe VRL stable, et OpenSearch 2.13 supporte les API utilisées par le seed Python (_index_template, saved_objects, _import).
Architecture¶
flowchart LR
SOCK["/var/run/docker.sock"]
VEC["perfshop-vector<br/>(timberio/vector:0.38.0-alpine)"]
OS["perfshop-opensearch<br/>(2.13.0)<br/>indexation full-text"]
OSD["perfshop-opensearch-dashboards<br/>(2.13.0)<br/>UI Kibana-compatible"]
SEED["perfshop-opensearch-seed<br/>(python:3.11-slim)<br/>one-shot"]
SOCK -->|"docker_logs source"| VEC
VEC -->|"transform VRL<br/>parse JSON +<br/>service_family routing"| VEC
VEC -->|"sink elasticsearch<br/>bulk.index = perfshop-{family}"| OS
OS --> OSD
SEED -.|"index templates +<br/>index patterns +<br/>dashboard import"| OS
SEED -.|"GET /api/status"| OSD
Vector — collecte et transformation¶
Vector est le composant le plus intéressant techniquement de cette stack. Il fonctionne en pipeline déclaratif TOML : sources → transforms → sinks.
Source — docker_logs¶
[sources.docker_logs]
type = "docker_logs"
docker_host = "unix:///var/run/docker.sock"
include_containers = [
"perfshop-app",
"perfshop-frontend",
"perfshop-db",
"perfshop-monitoring",
"perfshop-chaos-admin",
"perfshop-admin",
"perfshop-jmeter",
"perfshop-jmeter-ui",
"perfshop-loki",
"perfshop-promtail",
"perfshop-tempo",
"perfshop-pyroscope",
"perfshop-prometheus",
"perfshop-grafana",
"perfshop-testmgmt",
"perfshop-squash-db",
"perfshop-selenium",
"perfshop-test-runner",
"perfshop-orchestrator",
"perfshop-forgejo",
"perfshop-scripts-ui",
"perfshop-welcome",
"perfshop-docs",
]
Vector lit les logs via le socket Docker (monté en bind mount), exactement comme Promtail. Mais contrairement à Promtail qui ne couvre que 4 containers, Vector collecte 23 containers : tous les services applicatifs, observabilité et QA. Les services one-shot (*-seed) sont exclus parce qu'ils n'émettent que quelques lignes au démarrage.
Container hub jeux
Le hub de jeux est inclus dans les sources Vector parce que c'est techniquement un container nginx comme un autre. Aucune information sur son URL, son port ou son nom de service Docker n'apparaît dans la documentation utilisateur — seule la collecte technique des logs est mentionnée ici.
Transform — VRL (Vector Remap Language)¶
[transforms.enrich]
type = "remap"
inputs = ["docker_logs"]
source = '''
.container = replace(string!(.container_name), "/", "")
if exists(.timestamp) {
."@timestamp" = .timestamp
} else if exists(.time) {
."@timestamp" = .time
} else {
."@timestamp" = now()
}
parsed, err = parse_json(.message)
if err == null {
if exists(parsed.level) { .level = string!(parsed.level) }
if exists(parsed.logger_name) { .logger = string!(parsed.logger_name) }
if exists(parsed.message) { .msg = string!(parsed.message) }
if exists(parsed.chaos_family) { .chaos_family = string!(parsed.chaos_family) }
if exists(parsed.chaos_level) { .chaos_level = string!(parsed.chaos_level) }
if exists(parsed.scenario_id) { .scenario_id = string!(parsed.scenario_id) }
} else {
.msg = string!(.message)
.level = "INFO"
}
del(.label)
del(.labels)
del(.host)
del(.source_type)
c = .container
.service_family = if c == "perfshop-app" {
"spring"
} else if c == "perfshop-frontend" || c == "perfshop-admin" || c == "perfshop-chaos-admin" || c == "perfshop-monitoring" || c == "perfshop-scripts-ui" || c == "perfshop-welcome" || c == "perfshop-docs" {
"nginx"
} else if c == "perfshop-db" || c == "perfshop-squash-db" {
"mysql"
} else if c == "perfshop-jmeter" || c == "perfshop-jmeter-ui" {
"jmeter"
} else if c == "perfshop-testmgmt" || c == "perfshop-orchestrator" || c == "perfshop-selenium" || c == "perfshop-test-runner" {
"qa"
} else if c == "perfshop-forgejo" {
"forgejo"
} else {
"observability"
}
'''
C'est un véritable petit programme VRL qui fait cinq choses sur chaque événement :
1. Nettoyage du nom de container¶
Docker ajoute un préfixe / aux noms de containers (/perfshop-app). Le replace retire ce préfixe pour exposer un champ container=perfshop-app propre.
2. Mapping du timestamp¶
OpenSearch Dashboards exige un champ @timestamp pour l'index pattern temporel. Vector mappe timestamp → @timestamp (ou time → @timestamp, ou now() en dernier recours).
3. Parsing JSON conditionnel¶
Spring Boot avec logstash-logback-encoder produit des logs au format JSON :
{"@timestamp":"...","level":"ERROR","logger_name":"com.perfshop.controller.AuthController","message":"Login failed","chaos_family":"security","chaos_level":2,"scenario_id":"S6"}
Vector tente de parser le champ message comme JSON. Si ça réussit, il extrait six champs spécifiques (level, logger, msg, chaos_family, chaos_level, scenario_id) et les promeut comme champs top-level indexés par OpenSearch. Si le parse échoue (logs nginx, MySQL, etc. en texte brut), le message brut est mis dans msg et le level est forcé à INFO.
C'est ici que la valeur ajoutée d'OpenSearch sur Loki se voit : les champs chaos_family, chaos_level et scenario_id sont indexés en keyword, ce qui permet des agrégations du type « combien d'événements scenario_id=S6 dans la dernière heure ? » — impossible à faire efficacement en LogQL.
4. Suppression des champs Docker bruyants¶
Les labels Docker Compose contiennent des points (com.docker.compose.project) qui sont incompatibles avec les mappings OpenSearch (les points sont interprétés comme des nesting). Vector les supprime avant l'indexation.
5. Routage par famille de service¶
Chaque container est mappé à une famille (spring, nginx, mysql, jmeter, qa, forgejo, observability). Cette famille devient le suffixe de l'index OpenSearch cible — un seul container par famille n'est pas obligatoire.
Sink — elasticsearch (compatibilité ES)¶
[sinks.opensearch]
type = "elasticsearch"
inputs = ["enrich"]
endpoints = ["http://perfshop-opensearch:9200"]
mode = "bulk"
suppress_type_name = true
bulk.index = "perfshop-{{ service_family }}"
compression = "gzip"
request.retry_attempts = 10
healthcheck.enabled = true
Vector n'a pas (ou plus) de sink opensearch natif distinct — il utilise le sink elasticsearch standard, qui est compatible avec l'API REST OpenSearch (OpenSearch est un fork d'Elasticsearch).
| Paramètre | Effet |
|---|---|
mode = "bulk" |
Bulk insert pour réduire le nombre de requêtes HTTP |
suppress_type_name = true |
Supprime le champ _type (déprécié depuis ES 7+) |
bulk.index = "perfshop-{{ service_family }}" |
Templating : l'index cible est calculé dynamiquement à partir du champ service_family posé par le transform — un container Spring va dans perfshop-spring, un nginx va dans perfshop-nginx, etc. |
compression = "gzip" |
Compression réseau gzip |
request.retry_attempts = 10 |
10 essais avant abandon |
healthcheck.enabled = true |
Vérifie au démarrage que l'endpoint OpenSearch répond |
OpenSearch — configuration¶
Et côté variables d'environnement dans le compose :
environment:
- cluster.name=perfshop-logs
- node.name=perfshop-opensearch-node1
- discovery.type=single-node
- OPENSEARCH_JAVA_OPTS=${OPENSEARCH_JAVA_OPTS:--Xms512m -Xmx512m}
- DISABLE_SECURITY_PLUGIN=true
- DISABLE_PERFORMANCE_ANALYZER_AGENT_CLI=true
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536
hard: 65536
| Paramètre | Effet |
|---|---|
cluster.name=perfshop-logs |
Nom du cluster (un seul nœud) |
discovery.type=single-node |
Désactive le bootstrap multi-nœud (sinon OpenSearch refuse de démarrer en single-node sans config explicite) |
OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m |
Heap JVM 512 Mo (paramétrable via env) |
DISABLE_SECURITY_PLUGIN=true |
Plugin de sécurité désactivé — pas de TLS, pas d'auth, accessible librement depuis le réseau Docker interne. C'est volontaire pour le contexte pédagogique ; en production réelle, il faudrait l'activer. |
ulimits memlock=-1 |
Lock mémoire désactivé côté ulimit |
bootstrap.memory_lock: false |
Lock mémoire désactivé côté config |
nofile=65536 |
Cap fichiers ouverts élevé (OpenSearch en consomme beaucoup pour les segments Lucene) |
Healthcheck :
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:9200/_cluster/health | grep -qE '\"status\":\"(green|yellow)\"'"]
interval: 15s
timeout: 10s
retries: 12
start_period: 60s
Le healthcheck attend que le statut du cluster soit yellow ou green (single-node : pas de réplication possible, donc le statut maximal atteignable est yellow).
OpenSearch Dashboards¶
environment:
- OPENSEARCH_HOSTS=["http://perfshop-opensearch:9200"]
- DISABLE_SECURITY_DASHBOARDS_PLUGIN=true
volumes:
- ./opensearch/dashboards.yml:/usr/share/opensearch-dashboards/config/opensearch_dashboards.yml
OpenSearch Dashboards est un fork de Kibana — l'UI est familière à toute personne ayant déjà utilisé la stack ELK. Configuration minimale : pointe vers le cluster OpenSearch et désactive son propre plugin de sécurité.
Port hôte par défaut : 5601 (variable OPENSEARCH_HTTP_PORT).
Le seed opensearch-seed/seed.py¶
Le service perfshop-opensearch-seed (one-shot, restart: "no") effectue trois étapes au premier démarrage.
sequenceDiagram
autonumber
participant S as opensearch-seed
participant OS as perfshop-opensearch
participant OSD as perfshop-opensearch-dashboards
S->>OS: GET /_cluster/health<br/>(boucle 5s × 60)
OS-->>S: {"status":"yellow"} ✓
loop pour chaque famille (7)
S->>OS: PUT /_index_template/perfshop-{family}-template<br/>{ "index_patterns":["perfshop-{family}*"],<br/> "template":{"mappings":{...}} }
OS-->>S: 200 OK
end
S->>OSD: GET /api/status<br/>(boucle 5s × 90)
OSD-->>S: 200 OK
loop pour chaque pattern (8)
S->>OSD: POST /api/saved_objects/index-pattern/{pid}<br/>{"attributes":{"title":"perfshop-...*","timeFieldName":"@timestamp"}}
OSD-->>S: 200 ou 409 (déjà existant)
end
S->>OSD: POST /api/opensearch-dashboards/settings<br/>{"changes":{"defaultIndex":"perfshop-all"}}
OSD-->>S: 200 OK
S->>OSD: POST /api/saved_objects/_import?overwrite=true<br/>file: perfshop-all-logs.ndjson
OSD-->>S: 200 OK + successCount
Étape 1 — 7 index templates¶
Pour chaque famille (spring, nginx, mysql, jmeter, qa, forgejo, observability), le seed crée un template qui :
- Match les index
perfshop-{family}* - Définit
number_of_shards: 1,number_of_replicas: 0,index.refresh_interval: 5s - Pose un mapping explicite sur les champs :
@timestamp(date),ts(date),container(keyword),service_family(keyword),level(keyword),logger(keyword),msg(text + sub-fieldrawkeyword),message(text),stream(keyword),chaos_family(keyword),chaos_level(keyword),scenario_id(keyword),host(keyword)
Le mapping garantit que les agrégations sur chaos_family, scenario_id, etc. sont efficaces (champs keyword indexés en doc_values).
Étape 2 — 8 index patterns dans Dashboards¶
Le seed crée 8 index patterns dans OpenSearch Dashboards :
| Pattern | Cible |
|---|---|
perfshop-all |
perfshop-* |
perfshop-spring |
perfshop-spring* |
perfshop-nginx |
perfshop-nginx* |
perfshop-mysql |
perfshop-mysql* |
perfshop-jmeter |
perfshop-jmeter* |
perfshop-qa |
perfshop-qa* |
perfshop-forgejo |
perfshop-forgejo* |
perfshop-observability |
perfshop-observability* |
Et positionne perfshop-all comme index pattern par défaut (vue Discover).
Étape 3 — Import du dashboard PerfShop — All Logs¶
Le seed importe un dashboard NDJSON pré-construit (opensearch/dashboards/perfshop-all-logs.ndjson) via l'API POST /api/saved_objects/_import?overwrite=true. Si le fichier n'existe pas, l'étape est silencieusement ignorée.
Volumes¶
| Volume | Montage | Contenu |
|---|---|---|
opensearch-data (volume nommé) |
/usr/share/opensearch/data |
Données indexées par OpenSearch (segments Lucene, translog) |
./opensearch/opensearch.yml (bind mount) |
/usr/share/opensearch/config/opensearch.yml |
Config OpenSearch (lecture seule) |
./opensearch/dashboards.yml (bind mount) |
/usr/share/opensearch-dashboards/config/opensearch_dashboards.yml |
Config OpenSearch Dashboards |
./vector/vector.toml (bind mount) |
/etc/vector/vector.toml |
Pipeline Vector (lecture seule) |
/var/run/docker.sock (bind mount) |
/var/run/docker.sock |
Socket Docker pour la source docker_logs de Vector |
./opensearch-seed/seed.py (bind mount) |
/app/seed.py |
Script Python du seed (lecture seule) |
./opensearch/dashboards (bind mount) |
/app/dashboards |
Dashboards NDJSON pré-construits |
Ports¶
| Service | Port hôte | Port container | Variable d'env |
|---|---|---|---|
perfshop-opensearch |
9201 | 9200 | OPENSEARCH_API_PORT |
perfshop-opensearch-dashboards |
5601 | 5601 | OPENSEARCH_HTTP_PORT |
perfshop-vector |
(aucun) | (interne uniquement) | — |
Pour aller plus loin¶
- Vue d'ensemble — comparaison Loki vs OpenSearch
- Loki — l'autre puits de logs (modèle index-on-labels)
- Docker Compose — détail des services
perfshop-opensearch*etperfshop-vector