Aller au contenu

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 :

  1. 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.
  2. 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 connection apparaît, peu importe le service »).
  3. 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

del(.label)
del(.labels)
del(.host)
del(.source_type)

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

# opensearch.yml
network.host: 0.0.0.0
plugins.security.disabled: true
bootstrap.memory_lock: false

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-field raw keyword), 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

ndjson_path = "/app/dashboards/perfshop-all-logs.ndjson"

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* et perfshop-vector