Stack d'observabilité Docker : OTel Collector + ClickHouse + Prometheus + Grafana
Setup complet d'une stack d'observabilité Docker avec OpenTelemetry Collector, ClickHouse pour les logs, Prometheus pour les métriques, et Grafana pour la visualisation. Philosophie, configuration, docker-compose.
Cet article fait suite à Gestion des logs Docker : enjeux, solutions et tendances — Guide complet. L’article précédent couvrait le panorama complet des solutions ; celui-ci détaille le setup concret d’une stack choisie.
Le choix s’est porté sur OTel Collector + ClickHouse + Prometheus + Grafana. Pourquoi cette combinaison :
- ClickHouse gère la haute cardinalité nativement (filtrer par IP, par requête, par user agent — pas de problème)
- SQL pour requêter les logs (pas de nouveau langage à apprendre)
- Compression 15-20x (stockage très économique)
- OpenTelemetry est le standard CNCF, vendor-neutral — on peut changer de backend sans tout refaire
- Prometheus est le standard de facto pour les métriques conteneur (67% d’adoption en production)
- Grafana unifie logs et métriques dans une seule interface
Voici l’architecture complète :
┌──────────────────────────────────────────────────────────────────┐
│ Hôte Docker │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Astro 1 │ │ Astro 2 │ │ Astro N │ ... │ Caddy │ │
│ │ (nginx) │ │ (nginx) │ │ (nginx) │ │ (proxy) │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │ │
│ └────────────┴───────┬───┴───────────────────┘ │
│ │ STDOUT/STDERR → JSON files │
│ ▼ │
│ /var/lib/docker/containers/*/*.log │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ OTel Collector │ │
│ │ (contrib) │ │
│ │ │ │
│ │ filelog receiver │──── parse, filter, │
│ │ OTLP receiver │ enrich, batch │
│ └──────┬──────┬──────┘ │
│ │ │ │
│ logs ─────┘ └───── metrics │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────┐ ┌──────────────┐ ┌──────────┐ │
│ │ ClickHouse │ │ Prometheus │◄──│ cAdvisor │ │
│ │ (logs) │ │ (métriques) │ │ │ │
│ └──────┬─────┘ └──────┬───────┘ └──────────┘ │
│ │ │ │
│ └─────────┬─────────┘ │
│ ▼ │
│ ┌──────────┐ │
│ │ Grafana │ :3000 │
│ └──────────┘ │
└──────────────────────────────────────────────────────────────────┘
1. OpenTelemetry Collector — le routeur universel
Philosophie
L’OpenTelemetry Collector est un pipeline de données télémétriques vendor-neutral. Il reçoit des logs, métriques et traces depuis n’importe quelle source, les transforme, et les exporte vers n’importe quel backend. C’est le deuxième projet le plus actif de la CNCF après Kubernetes.
Le principe : découpler la collecte du stockage. Si demain on veut remplacer ClickHouse par autre chose, on change l’exporter — pas le reste de l’infrastructure.
Core vs Contrib
Il existe deux distributions :
| Distribution | Image Docker | Contenu |
|---|---|---|
| Core | otel/opentelemetry-collector | Composants de base uniquement |
| Contrib | otel/opentelemetry-collector-contrib | Tous les composants communautaires |
Pour cette stack, il faut Contrib — c’est la seule qui inclut le ClickHouse exporter, le filelog receiver, le Docker stats receiver, et le resource detection processor.
docker pull otel/opentelemetry-collector-contrib:0.117.0
Source : OTel Collector Docker Install
Architecture : Receivers → Processors → Exporters
La configuration a quatre sections qui se câblent dans service.pipelines :
receivers: # Comment les données entrent
processors: # Comment elles sont transformées
exporters: # Où elles sont envoyées
service:
pipelines: # Ce qui relie le tout
Principe fondamental : configurer un receiver ou un exporter ne l’active pas. Il doit être référencé dans un pipeline sous service pour être actif. Source : OTel Collector Configuration
Comment collecter les logs Docker
Trois approches existent. Voici le comparatif, puis le détail de l’approche recommandée.
| Aspect | Filelog receiver | Fluentd driver | OTLP direct |
|---|---|---|---|
docker logs fonctionne | Oui | Non | Non |
| Modification de daemon.json | Non | Oui | Non |
| Composant supplémentaire | Non | Optionnel (Fluent Bit) | Non |
| Backpressure | Filesystem (naturel) | Mémoire (risque de perte) | gRPC (backpressure native) |
| Métadonnées conteneur | Via Docker API | Via tag template | Natif (SDK) |
| Applicable aux sites statiques | Oui | Oui | Non (pas d’app à instrumenter) |
Recommandation : filelog receiver (Option A). Pas de changement de configuration Docker, docker logs continue de fonctionner, le filesystem sert de buffer naturel.
Filelog receiver : configuration
Le filelog receiver lit les fichiers de log JSON de Docker situés dans /var/lib/docker/containers/<container-id>/<container-id>-json.log. Chaque ligne a ce format :
{
"log": "192.168.1.1 - - [03/Mar/2026] \"GET / HTTP/1.1\" 200 1234\n",
"stream": "stdout",
"time": "2026-03-03T10:15:30.123456789Z"
}
L’opérateur container de type docker parse automatiquement ce format — il remplace environ 69 lignes de configuration manuelle par 5 lignes :
receivers:
filelog:
include:
- /var/lib/docker/containers/*/*-json.log
exclude:
# Exclure les logs du collector lui-même pour éviter les boucles
- /var/lib/docker/containers/*otel*/*-json.log
include_file_path: true
start_at: end # Ne pas relire l'historique au démarrage
operators:
- id: container-parser
type: container
format: docker
Les volumes à monter :
volumes:
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
[Sources : OTel Container Log Parser, Filelog Receiver README]
Processors : l’ordre compte
L’ordre recommandé dans le pipeline : memory_limiter (premier) → resourcedetection → transform → filter → batch (dernier).
memory_limiter — empêche les OOM en surveillant la consommation mémoire. Doit être le premier processor de chaque pipeline :
processors:
memory_limiter:
check_interval: 5s
limit_mib: 1500 # Limite dure
spike_limit_mib: 300 # Limite souple = limit - spike
Source : Memory Limiter Processor
resourcedetection — détecte automatiquement les métadonnées de l’hôte et du conteneur :
processors:
resourcedetection:
detectors: [env, docker, system]
timeout: 2s
Le détecteur env lit la variable OTEL_RESOURCE_ATTRIBUTES (utile pour injecter deployment.environment=production). Le détecteur docker ajoute les métadonnées du conteneur du collector. Le détecteur system ajoute host.name, os.type. Source : Resource Detection Processor
transform — utilise OTTL (OpenTelemetry Transformation Language) pour extraire les champs des access logs JSON de Caddy :
processors:
transform/access-logs:
error_mode: ignore
log_statements:
- context: log
statements:
# Parser le body JSON (les access logs de Caddy sont du JSON)
- set(body, ParseJSON(body)) where IsString(body)
# Extraire les champs utiles en attributs
- set(attributes["http.method"], body["request"]["method"])
where body["request"]["method"] != nil
- set(attributes["http.status_code"], body["status"])
where body["status"] != nil
- set(attributes["http.host"], body["request"]["host"])
where body["request"]["host"] != nil
- set(attributes["http.path"], body["request"]["uri"])
where body["request"]["uri"] != nil
- set(attributes["net.peer.ip"], body["request"]["remote_ip"])
where body["request"]["remote_ip"] != nil
- set(attributes["http.duration_s"], body["duration"])
where body["duration"] != nil
# Assigner la sévérité selon le status code
- set(severity_number, SEVERITY_NUMBER_INFO)
where body["status"] != nil and body["status"] < 400
- set(severity_number, SEVERITY_NUMBER_WARN)
where body["status"] != nil and body["status"] >= 400
and body["status"] < 500
- set(severity_number, SEVERITY_NUMBER_ERROR)
where body["status"] != nil and body["status"] >= 500
filter — supprime les logs à faible valeur. Pour des sites statiques, 80-90% des logs sont du bruit (assets, health checks) :
processors:
filter/drop-noise:
error_mode: ignore
logs:
log_record:
# Assets statiques
- 'IsMatch(attributes["http.path"], ".*\\.(css|js|png|jpg|jpeg|gif|svg|ico|woff2?)$")'
# Health checks
- 'attributes["http.path"] == "/health"'
- 'attributes["http.path"] == "/healthz"'
# 304 Not Modified (le navigateur a le cache)
- 'attributes["http.status_code"] == 304'
batch — regroupe les données avant export. ClickHouse recommande des inserts d’au moins 1 000 lignes. Doit être le dernier processor :
processors:
batch:
send_batch_size: 5000
timeout: 5s
Exporter ClickHouse
exporters:
clickhouse:
endpoint: tcp://clickhouse:9000?dial_timeout=10s&compress=lz4&async_insert=1
database: otel
username: default
password: "${CLICKHOUSE_PASSWORD}"
logs_table_name: otel_logs
traces_table_name: otel_traces
ttl: 720h # 30 jours
create_schema: true # Créer automatiquement les tables
sending_queue:
enabled: true
num_consumers: 10
queue_size: 1000
retry_on_failure:
enabled: true
initial_interval: 5s
max_interval: 30s
max_elapsed_time: 300s
Les paramètres clés :
async_insert=1: ClickHouse bufferise les insertions avant de les flusher — réduit la chargecompress=lz4: compression sur le réseau entre le collector et ClickHousettl: 720h: rétention de 30 jours (modifiable aussi côté ClickHouse)create_schema: true: crée automatiquement la base de données et les tables au premier démarrage
Source : ClickHouse Exporter README
Exporter Prometheus
Le collector expose un endpoint /metrics que Prometheus viendra scraper (modèle pull) :
exporters:
prometheus:
endpoint: 0.0.0.0:8889
resource_to_telemetry_conversion:
enabled: true
resource_to_telemetry_conversion convertit les attributs de resource en labels Prometheus — utile pour avoir service_name, host_name etc. comme labels. Source : Prometheus and OTel: Better Together
Configuration complète du collector
extensions:
health_check:
endpoint: 0.0.0.0:13133
receivers:
# Lecture des fichiers de log Docker
filelog:
include:
- /var/lib/docker/containers/*/*-json.log
exclude:
- /var/lib/docker/containers/*otel*/*-json.log
include_file_path: true
start_at: end
operators:
- id: container-parser
type: container
format: docker
# Réception OTLP (pour les apps instrumentées)
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
memory_limiter:
check_interval: 5s
limit_mib: 1500
spike_limit_mib: 300
resourcedetection:
detectors: [env, docker, system]
timeout: 2s
transform/access-logs:
error_mode: ignore
log_statements:
- context: log
statements:
- set(body, ParseJSON(body)) where IsString(body)
- set(attributes["http.method"], body["request"]["method"])
where body["request"]["method"] != nil
- set(attributes["http.status_code"], body["status"])
where body["status"] != nil
- set(attributes["http.host"], body["request"]["host"])
where body["request"]["host"] != nil
- set(attributes["http.path"], body["request"]["uri"])
where body["request"]["uri"] != nil
- set(attributes["net.peer.ip"], body["request"]["remote_ip"])
where body["request"]["remote_ip"] != nil
- set(attributes["http.duration_s"], body["duration"])
where body["duration"] != nil
- set(severity_number, SEVERITY_NUMBER_INFO)
where body["status"] != nil and body["status"] < 400
- set(severity_number, SEVERITY_NUMBER_WARN)
where body["status"] != nil and body["status"] >= 400
and body["status"] < 500
- set(severity_number, SEVERITY_NUMBER_ERROR)
where body["status"] != nil and body["status"] >= 500
filter/drop-noise:
error_mode: ignore
logs:
log_record:
- 'IsMatch(attributes["http.path"], ".*\\.(css|js|png|jpg|jpeg|gif|svg|ico|woff2?)$")'
- 'attributes["http.path"] == "/health"'
- 'attributes["http.path"] == "/healthz"'
- 'attributes["http.status_code"] == 304'
batch:
send_batch_size: 5000
timeout: 5s
exporters:
clickhouse:
endpoint: tcp://clickhouse:9000?dial_timeout=10s&compress=lz4&async_insert=1
database: otel
username: default
password: "${CLICKHOUSE_PASSWORD}"
ttl: 720h
create_schema: true
sending_queue:
enabled: true
num_consumers: 10
queue_size: 1000
retry_on_failure:
enabled: true
initial_interval: 5s
max_interval: 30s
max_elapsed_time: 300s
prometheus:
endpoint: 0.0.0.0:8889
resource_to_telemetry_conversion:
enabled: true
service:
extensions: [health_check]
pipelines:
logs:
receivers: [filelog, otlp]
processors:
[
memory_limiter,
resourcedetection,
transform/access-logs,
filter/drop-noise,
batch,
]
exporters: [clickhouse]
metrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [prometheus]
traces:
receivers: [otlp]
processors: [memory_limiter, resourcedetection, batch]
exporters: [clickhouse]
2. ClickHouse — le moteur de stockage des logs
Philosophie
ClickHouse est une base de données OLAP columnaire écrite en C++. Contrairement à Elasticsearch qui indexe chaque mot de chaque document (index inversé), ClickHouse stocke les données par colonne et les compresse. Résultat : des requêtes analytiques 5-30x plus rapides qu’Elasticsearch, avec une compression 15-20x. Source : ClickHouse vs Elasticsearch: The Billion-Row Matchup
Pour les logs, ça signifie qu’on peut filtrer par IP (haute cardinalité), par plage horaire, par service — le tout en SQL standard avec des temps de réponse sub-seconde.
Image Docker et ports
docker pull clickhouse/clickhouse-server:24.12
| Port | Protocole | Usage |
|---|---|---|
| 8123 | HTTP | Interface REST, requêtes via curl |
| 9000 | TCP natif | Client natif (clickhouse-client), connexion Grafana |
| 9009 | Interserver | Réplication multi-nœud (pas nécessaire en single-node) |
Source : ClickHouse Docker Install
Schéma auto-créé par l’exporter OTel
Quand create_schema: true, le ClickHouse exporter crée automatiquement les tables. Voici la table otel_logs :
CREATE TABLE otel.otel_logs
(
Timestamp DateTime64(9) CODEC(Delta(8), ZSTD(1)),
TraceId String CODEC(ZSTD(1)),
SpanId String CODEC(ZSTD(1)),
TraceFlags UInt32 CODEC(ZSTD(1)),
SeverityText LowCardinality(String) CODEC(ZSTD(1)),
SeverityNumber Int32 CODEC(ZSTD(1)),
ServiceName LowCardinality(String) CODEC(ZSTD(1)),
Body String CODEC(ZSTD(1)),
ResourceAttributes Map(LowCardinality(String), String) CODEC(ZSTD(1)),
ScopeAttributes Map(LowCardinality(String), String) CODEC(ZSTD(1)),
LogAttributes Map(LowCardinality(String), String) CODEC(ZSTD(1)),
-- Index bloom filter pour recherche rapide par TraceId
INDEX idx_trace_id TraceId TYPE bloom_filter(0.001) GRANULARITY 1,
-- Index pour recherche dans les clés des attributs
INDEX idx_log_attr_key mapKeys(LogAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
-- Index tokenisé pour recherche dans le Body
INDEX idx_body Body TYPE tokenbf_v1(32768, 3, 0) GRANULARITY 1
)
ENGINE = MergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (ServiceName, SeverityText, toUnixTimestamp(Timestamp), TraceId)
TTL toDateTime(Timestamp) + toIntervalDay(30)
SETTINGS ttl_only_drop_parts = 1;
Points clés du schéma :
PARTITION BY toDate(Timestamp): une partition par jour. Quand le TTL expire, ClickHouse supprime des partitions entières (très efficace) plutôt que des lignes individuelles.ORDER BY (ServiceName, SeverityText, toUnixTimestamp(Timestamp), TraceId): suit le principe de cardinalité croissante — le champ le plus sélectif (ServiceName) en premier. Cet ORDER BY optimise les requêtes qui filtrent par service puis par temps.LowCardinality(String): optimisation pour les colonnes avec peu de valeurs distinctes (noms de service, niveaux de sévérité). Gains de stockage significatifs.Map(LowCardinality(String), String): stockage flexible clé-valeur pour les attributs — évite l’explosion de colonnes. C’est là que les champs extraits par le transform processor (http.method, http.host, net.peer.ip…) sont stockés.CODEC(Delta(8), ZSTD(1)): Delta encoding sur les timestamps pour une compression additionnelle. ZSTD(1) sur toutes les autres colonnes (bon ratio compression/vitesse).
L’exporter crée aussi otel_traces (même structure avec SpanName, Duration, StatusCode, Events, Links) et les tables de métriques (otel_metrics_gauge, otel_metrics_sum, otel_metrics_histogram, etc.).
[Sources : ClickHouse OTel Integration, Storing Log Data in ClickHouse]
TTL et rétention
Le TTL peut être configuré soit dans l’exporter OTel (ttl: 720h), soit directement dans la DDL ClickHouse pour plus de granularité :
-- TTL simple : supprimer après 30 jours
ALTER TABLE otel.otel_logs
MODIFY TTL toDateTime(Timestamp) + INTERVAL 30 DAY DELETE;
-- TTL par sévérité : rétention différenciée
ALTER TABLE otel.otel_logs
MODIFY TTL
toDateTime(Timestamp) + INTERVAL 7 DAY DELETE
WHERE SeverityText IN ('TRACE', 'DEBUG'),
toDateTime(Timestamp) + INTERVAL 30 DAY DELETE
WHERE SeverityText = 'INFO',
toDateTime(Timestamp) + INTERVAL 365 DAY DELETE
WHERE SeverityText IN ('WARN', 'ERROR');
Le setting ttl_only_drop_parts = 1 supprime les parts entières quand elles expirent plutôt que de réécrire les données — bien plus efficace. Source : ClickHouse TTL
Requêtes SQL utiles
Logs par plage horaire (que s’est-il passé à 14h05 ?) :
SELECT
Timestamp,
SeverityText,
ServiceName,
Body,
LogAttributes['http.status_code'] AS status,
LogAttributes['http.host'] AS host
FROM otel.otel_logs
WHERE Timestamp BETWEEN '2026-03-03 14:05:00' AND '2026-03-03 14:06:00'
ORDER BY Timestamp
LIMIT 100;
Suivre un utilisateur par IP :
SELECT
Timestamp,
ServiceName,
LogAttributes['http.method'] AS method,
LogAttributes['http.path'] AS path,
LogAttributes['http.status_code'] AS status,
LogAttributes['http.host'] AS host,
LogAttributes['http.duration_s'] AS duration
FROM otel.otel_logs
WHERE LogAttributes['net.peer.ip'] = '192.168.1.42'
AND Timestamp > now() - INTERVAL 1 HOUR
ORDER BY Timestamp;
Logs d’un service spécifique :
SELECT Timestamp, SeverityText, Body
FROM otel.otel_logs
WHERE ServiceName = 'caddy'
AND SeverityText IN ('WARN', 'ERROR')
AND Timestamp > now() - INTERVAL 24 HOUR
ORDER BY Timestamp DESC
LIMIT 50;
Top 10 des erreurs :
SELECT
LogAttributes['http.host'] AS site,
LogAttributes['http.path'] AS path,
LogAttributes['http.status_code'] AS status,
count() AS occurrences
FROM otel.otel_logs
WHERE SeverityText = 'ERROR'
AND Timestamp > now() - INTERVAL 24 HOUR
GROUP BY site, path, status
ORDER BY occurrences DESC
LIMIT 10;
Volume de logs par service (pour identifier les conteneurs bruyants) :
SELECT
ServiceName,
formatReadableSize(sum(length(Body))) AS volume,
count() AS lines
FROM otel.otel_logs
WHERE Timestamp > now() - INTERVAL 1 HOUR
GROUP BY ServiceName
ORDER BY lines DESC;
Dimensionnement single-node
| Charge | CPU | RAM | Disque |
|---|---|---|---|
| Dev/staging | 2 cores | 4 Go | 20 Go SSD |
| Production (~200 conteneurs) | 4 cores | 8-16 Go | SSD, taille selon rétention |
| Production haute charge | 8+ cores | 32+ Go | NVMe SSD |
Le file descriptor limit est essentiel — ClickHouse ouvre des milliers de fichiers simultanément pendant les requêtes. Configurer ulimits.nofile: 262144.
3. Prometheus — les métriques conteneur
Philosophie
Prometheus est un système de monitoring et d’alerting basé sur un modèle pull : il scrape des endpoints HTTP /metrics à intervalles réguliers pour collecter des métriques numériques (time-series). C’est le premier projet CNCF a avoir atteint le statut Graduated (2018, avant même Fluentd).
Prometheus n’est pas un outil de logs. Il ne stocke que des métriques numériques. Mais combiné à ClickHouse (logs) et Grafana (visualisation), il couvre le pilier métriques de l’observabilité : CPU, mémoire, réseau, taux de requêtes, latence.
prometheus.yml
global:
scrape_interval: 15s # Intervalle par défaut
evaluation_interval: 15s # Évaluation des alertes
scrape_configs:
# Métriques exposées par l'OTel Collector
- job_name: otel-collector
scrape_interval: 10s
static_configs:
- targets: ["otel-collector:8889"]
# Métriques conteneur via cAdvisor
- job_name: cadvisor
scrape_interval: 15s
static_configs:
- targets: ["cadvisor:8080"]
# Prometheus scrape lui-même
- job_name: prometheus
static_configs:
- targets: ["localhost:9090"]
cAdvisor : métriques conteneur
cAdvisor (Container Advisor) est un agent Google qui expose les métriques de ressources de chaque conteneur Docker. C’est la source standard pour les métriques conteneur dans l’écosystème Prometheus. Source : Prometheus — Monitoring Docker with cAdvisor
Métriques clés pour 200+ sites statiques :
| Métrique | Description |
|---|---|
container_cpu_usage_seconds_total | Utilisation CPU cumulée |
container_memory_working_set_bytes | Mémoire effective (meilleur indicateur d’OOM) |
container_memory_cache | Cache de pages (important pour du file serving) |
container_network_receive_bytes_total | Trafic réseau entrant |
container_network_transmit_bytes_total | Trafic réseau sortant |
container_start_time_seconds | Timestamp de démarrage (détecter les restarts) |
Docker daemon metrics (optionnel)
Pour exposer les métriques du démon Docker lui-même, ajouter dans /etc/docker/daemon.json :
{
"metrics-addr": "0.0.0.0:9323"
}
Puis redémarrer Docker. Vérifier avec curl localhost:9323/metrics. Ajouter dans prometheus.yml :
- job_name: docker-daemon
static_configs:
- targets: ["host.docker.internal:9323"]
Source : Docker Daemon Prometheus Metrics
Rétention et stockage
La rétention se configure via des flags de ligne de commande (pas dans le fichier de config) :
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--storage.tsdb.retention.time=15d" # Garder 15 jours
- "--storage.tsdb.retention.size=10GB" # Ou max 10 Go
- "--web.enable-lifecycle" # API de reload
Si les deux limites (temps et taille) sont configurées, c’est la première atteinte qui déclenche la suppression. Source : Prometheus Storage
Requêtes PromQL utiles
CPU par conteneur (top 10) :
topk(10,
rate(container_cpu_usage_seconds_total{name!=""}[5m])
)
Mémoire par conteneur :
container_memory_working_set_bytes{name!=""} / 1024 / 1024
Conteneurs qui ont redémarré (dans les dernières 24h) :
changes(container_start_time_seconds{name!=""}[24h]) > 0
Trafic réseau par conteneur (Mo/s) :
rate(container_network_receive_bytes_total{name!=""}[5m]) / 1024 / 1024
4. Grafana — la visualisation unifiée
Philosophie
Grafana est un dashboard universel d’observabilité qui sait parler à 100+ sources de données. Dans cette stack, il unifie les logs (ClickHouse) et les métriques (Prometheus) dans une seule interface.
Plugin ClickHouse
Le plugin grafana-clickhouse-datasource (par Grafana Labs) connecte Grafana à ClickHouse. Il supporte le mapping OTel pour naviguer dans otel_logs et otel_traces avec des fonctionnalités d’exploration intégrées.
Installation via variable d’environnement :
environment:
- GF_INSTALL_PLUGINS=grafana-clickhouse-datasource
[Sources : Grafana ClickHouse Plugin, ClickHouse Grafana Docs]
Provisioning automatique
Au lieu de configurer manuellement via l’interface web, on provisionne les data sources et dashboards via des fichiers YAML montés dans le conteneur.
grafana/provisioning/datasources/datasources.yaml :
apiVersion: 1
datasources:
# Prometheus — métriques conteneur
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false
# ClickHouse — logs et traces
- name: ClickHouse
type: grafana-clickhouse-datasource
access: proxy
editable: false
jsonData:
host: clickhouse
port: 9000
protocol: native
defaultDatabase: otel
username: default
# Activer le mapping OTel pour l'exploration de logs/traces
otelEnabled: true
otelVersion: latest
logs:
defaultDatabase: otel
defaultTable: otel_logs
otelEnabled: true
otelVersion: latest
timeColumn: Timestamp
logLevelColumn: SeverityText
logMessageColumn: Body
traces:
defaultDatabase: otel
defaultTable: otel_traces
otelEnabled: true
otelVersion: latest
secureJsonData:
password: "${CLICKHOUSE_PASSWORD}"
grafana/provisioning/dashboards/dashboards.yaml :
apiVersion: 1
providers:
- name: Default
orgId: 1
folder: Observability
type: file
disableDeletion: false
updateIntervalSeconds: 30
allowUiUpdates: true
options:
path: /var/lib/grafana/dashboards
Les fichiers JSON de dashboards placés dans ./grafana/dashboards/ sont automatiquement importés. Des dashboards communautaires sont disponibles sur grafana.com/grafana/dashboards (ex : Docker Dashboard ID 10585).
Source : Grafana Provisioning Docs
Explorer les logs
Avec le mapping OTel activé, l’Explore mode de Grafana permet de naviguer dans les logs ClickHouse :
- Sélectionner la data source ClickHouse
- Passer en mode “Logs”
- Filtrer par ServiceName, SeverityText, ou rechercher dans le Body
- Cliquer sur un log pour voir tous ses attributs (http.host, net.peer.ip, etc.)
Pour les requêtes avancées, utiliser le query editor SQL directement dans Grafana.
5. Caddy : logging structuré JSON
Pour 200+ sites derrière un reverse proxy, le JSON structuré est essentiel. Le Common Log Format (CLF) ne contient pas le header Host — impossible de distinguer quel site a reçu quelle requête.
Caddy émet du JSON structuré par défaut via la bibliothèque Zap (zero-allocation). Configuration minimale dans le Caddyfile :
{
log {
output stdout
format json {
time_format iso8601
}
# Désactiver les logs des health checks
exclude net/http.request.uri "/health" "/healthz"
}
}
Un log d’accès Caddy contient nativement :
{
"level": "info",
"ts": "2026-03-03T14:05:23.456Z",
"msg": "handled request",
"request": {
"remote_ip": "192.168.1.42",
"remote_port": "54321",
"client_ip": "192.168.1.42",
"method": "GET",
"host": "mon-site.example.com",
"uri": "/blog/article",
"headers": { "User-Agent": ["..."] }
},
"status": 200,
"duration": 0.001234,
"size": 15678
}
Le champ request.host permet de filtrer les logs par site — exactement ce qu’il faut pour 200+ sites.
Filtrage pour la vie privée (masquer les IPs, supprimer les cookies) :
log {
output stdout
format filter {
fields {
request>headers>Cookie delete
request>headers>Authorization delete
request>remote_ip ip_mask {
ipv4 16
ipv6 32
}
}
wrap json
}
}
[Sources : Caddy Logging Docs, Better Stack — Logging in Caddy]
Alternatives : pour Nginx, configurer un
log_format json_accessavecescape=jsonet les variables nécessaires ($host,$remote_addr,$request_time…). Pour Traefik, activeraccessLog.format=jsonavec filtrage par status codes et contrôle des champs/headers. [Sources : Nginx ngx_http_log_module, Traefik Access Logs]
6. Le docker-compose.yml complet
docker-compose.yml
services:
# ============================================
# ClickHouse — stockage des logs et traces
# ============================================
clickhouse:
image: clickhouse/clickhouse-server:24.12
container_name: clickhouse
ports:
- "8123:8123" # HTTP
- "9000:9000" # Native TCP
volumes:
- clickhouse_data:/var/lib/clickhouse
- clickhouse_logs:/var/log/clickhouse-server
environment:
CLICKHOUSE_DB: otel
CLICKHOUSE_USER: default
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-changeme}
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
ulimits:
nofile:
soft: 262144
hard: 262144
deploy:
resources:
limits:
memory: 4G
reservations:
memory: 2G
healthcheck:
test: ["CMD", "clickhouse-client", "--query", "SELECT 1"]
interval: 10s
timeout: 5s
retries: 5
networks:
- observability
restart: unless-stopped
# ============================================
# OpenTelemetry Collector — collecte et routage
# ============================================
otel-collector:
image: otel/opentelemetry-collector-contrib:0.117.0
container_name: otel-collector
command: ["--config=/etc/otelcol/config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otelcol/config.yaml:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
- "8889:8889" # Prometheus exporter
environment:
- CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-changeme}
- OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production
depends_on:
clickhouse:
condition: service_healthy
networks:
- observability
restart: unless-stopped
# ============================================
# cAdvisor — métriques conteneur
# ============================================
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.49.1
container_name: cadvisor
ports:
- "8080:8080"
volumes:
- /:/rootfs:ro
- /var/run:/var/run:rw
- /sys:/sys:ro
- /var/lib/docker:/var/lib/docker:ro
- /dev/disk:/dev/disk:ro
privileged: true
networks:
- observability
restart: unless-stopped
# ============================================
# Prometheus — stockage des métriques
# ============================================
prometheus:
image: prom/prometheus:v2.50.0
container_name: prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--storage.tsdb.retention.time=15d"
- "--web.enable-lifecycle"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
ports:
- "9090:9090"
depends_on:
- otel-collector
- cadvisor
networks:
- observability
restart: unless-stopped
# ============================================
# Grafana — visualisation
# ============================================
grafana:
image: grafana/grafana:11.4.0
container_name: grafana
environment:
- GF_INSTALL_PLUGINS=grafana-clickhouse-datasource
- GF_SECURITY_ADMIN_USER=${GRAFANA_USER:-admin}
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-admin}
- CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-changeme}
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
depends_on:
- prometheus
- clickhouse
networks:
- observability
restart: unless-stopped
networks:
observability:
driver: bridge
volumes:
clickhouse_data:
clickhouse_logs:
prometheus_data:
grafana_data:
Arborescence des fichiers
observability/
├── docker-compose.yml
├── .env # CLICKHOUSE_PASSWORD, GRAFANA_PASSWORD
├── otel-collector-config.yaml # Config complète (voir section 1)
├── prometheus.yml # Config Prometheus (voir section 3)
└── grafana/
├── provisioning/
│ ├── datasources/
│ │ └── datasources.yaml # ClickHouse + Prometheus (voir section 4)
│ └── dashboards/
│ └── dashboards.yaml # Provider de dashboards (voir section 4)
└── dashboards/
└── *.json # Fichiers JSON de dashboards
Démarrage
# Créer le .env
echo "CLICKHOUSE_PASSWORD=un_mot_de_passe_fort" > .env
echo "GRAFANA_USER=admin" >> .env
echo "GRAFANA_PASSWORD=un_autre_mot_de_passe" >> .env
# Démarrer la stack
docker compose up -d
# Vérifier que tout tourne
docker compose ps
# Accéder à Grafana
# http://localhost:3000
7. Réduction du bruit
Pour des sites statiques, la majorité des logs sont des accès aux assets (CSS, JS, images, fonts) et des health checks. Filtrer ce bruit est essentiel pour réduire les coûts de stockage et rendre les logs exploitables.
Au niveau du collecteur (filter processor)
Le filter processor configuré plus haut supprime les logs avant qu’ils n’atteignent ClickHouse :
- Assets statiques (
*.css,*.js,*.png,*.jpg,*.svg,*.woff2…) - Health checks (
/health,/healthz) - 304 Not Modified (le navigateur avait déjà la ressource en cache)
Au niveau de Caddy
On peut aussi exclure directement dans le Caddyfile pour ne même pas émettre ces logs :
log {
output stdout
format json
# Ne pas logger les requêtes sur les assets
exclude net/http.request.uri_path "/assets/*" "*.css" "*.js" "*.png" "*.jpg" "*.svg" "*.woff2"
}
Au niveau de ClickHouse (TTL par sévérité)
Pour les logs qui arrivent quand même, la rétention différenciée par sévérité permet de garder les erreurs longtemps et de supprimer rapidement les logs informatifs :
ALTER TABLE otel.otel_logs
MODIFY TTL
toDateTime(Timestamp) + INTERVAL 3 DAY DELETE
WHERE SeverityText IN ('TRACE', 'DEBUG'),
toDateTime(Timestamp) + INTERVAL 14 DAY DELETE
WHERE SeverityText = 'INFO',
toDateTime(Timestamp) + INTERVAL 90 DAY DELETE
WHERE SeverityText IN ('WARN', 'ERROR');
8. Aller plus loin
ClickStack
Si la configuration manuelle semble trop lourde, ClickStack est une stack d’observabilité tout-en-un basée sur ClickHouse, lancée en mai 2025. Elle inclut ClickHouse + OTel Collector pré-configuré + HyperDX (UI) + MongoDB (état applicatif). C’est un “Datadog self-hosted” clé en main. Le tradeoff : on remplace Grafana par HyperDX et on ajoute une dépendance MongoDB. Source : ClickStack Architecture
Alerting Grafana
Grafana supporte l’alerting unifié sur les deux sources de données. Exemples :
- Alerte CPU :
rate(container_cpu_usage_seconds_total[5m]) > 0.8→ notification Slack - Alerte erreurs : requête SQL sur
otel.otel_logscomptant les erreurs 5xx dans les 5 dernières minutes - Alerte conteneur down :
absent(container_start_time_seconds{name="caddy"})→ le conteneur critique n’est plus là
Traces distribuées
Pour les applications que vous contrôlez (pas les sites statiques, mais des APIs, des backends), l’OTel Collector est déjà configuré pour recevoir des traces via OTLP (port 4317/4318) et les exporter vers ClickHouse. Il suffit d’instrumenter l’application avec un SDK OpenTelemetry et les traces apparaîtront dans Grafana avec corrélation automatique logs ↔ traces via le TraceId.
Scaling
Pour dépasser les capacités d’un single-node ClickHouse :
- Passer de
MergeTreeàReplicatedMergeTreedans le schéma - Ajouter
cluster_namedans la config de l’exporter OTel - Déployer plusieurs nœuds ClickHouse avec ClickHouse Keeper (remplaçant de ZooKeeper)
- Pour Prometheus : considérer Mimir (stockage long-terme distribué) ou Thanos
Sources
OpenTelemetry
- OTel Collector Configuration
- OTel Collector Docker Install
- OTel Container Log Parser
- Prometheus and OTel: Better Together
- ClickHouse Exporter README
- Filelog Receiver README
- Memory Limiter Processor
- Batch Processor
- Resource Detection Processor
- Transform Processor (OTTL)
- Filter Processor
ClickHouse
- ClickHouse Docker Install
- ClickHouse OTel Integration
- ClickHouse and OpenTelemetry
- Storing Log Data in ClickHouse
- ClickHouse TTL
- ClickHouse vs Elasticsearch: The Billion-Row Matchup
- ClickStack Architecture
Prometheus
Grafana
- Grafana Provisioning
- Grafana Docker Configuration
- Grafana ClickHouse Plugin
- ClickHouse Grafana Integration