Si vous avez déjà vu un pic de CPU à 02:17, suivi d’un 502 Bad Gateway nginx, puis plus rien d’exploitable dans vos logs, vous avez déjà ressenti le manque de métriques “corrélables”. Le but ici n’est pas d’empiler des graphes, mais de pouvoir répondre en 2 minutes à : “qu’est-ce qui a cassé, quand, et pourquoi WordPress 6.9.4 s’est mis à ramer ?”
Le besoin / Le problème serveur
Sur un serveur WordPress (6.9.4 en avril 2026) vous avez trois couches qui se renvoient la balle quand ça va mal : le système (CPU/RAM/disk I/O), le web (nginx/Apache), et l’app (PHP-FPM + MySQL/MariaDB + WordPress). Les logs seuls ne suffisent pas : ils sont verbeux, parfois incomplets, et surtout non agrégés.
À la fin, vous saurez :
- collecter des métriques système, nginx, PHP-FPM et MySQL via Prometheus,
- exposer des métriques WordPress ciblées (cron, requêtes, erreurs, cache) via un MU-plugin,
- déclencher des alertes automatiques (Alertmanager) avec anti-bruit (inhibition, fenêtres, seuils pertinents),
- corréler un incident WordPress avec des graphiques Grafana utiles (pas décoratifs),
- déployer en staging puis promouvoir en production sans “monitoring cassé”.
Résumé rapide
- Prometheus scrape : node_exporter + nginx exporter + php-fpm exporter + mysqld exporter + endpoint WordPress dédié.
- WordPress : MU-plugin qui expose
/metrics(ou/?bpcab_metrics=1) avec auth (token + allowlist IP) et métriques “actionnables”. - Alerting : règles Prometheus + Alertmanager (mail/Slack) avec inhibition (éviter “tout est rouge”).
- Grafana : dashboards orientés symptômes (latence, 5xx, saturation FPM, DB slow, WP-Cron en retard).
- Sécurité : endpoints métriques derrière firewall / basic auth / tokens, pas de fuite d’infos (versions, chemins, usernames).
Avant de commencer (prérequis)
Accès requis :
- SSH root ou sudo (installation services, systemd, firewall),
- WP-CLI sur le serveur (ou via SSH) pour valider WordPress,
- accès à la conf web (nginx/Apache) + PHP-FPM,
- accès MySQL (socket local ou user dédié).
Sauvegarde obligatoire (ne testez pas l’alerting en prod sans filet) :
- snapshot VM ou image disque, ou à minima backup
/etc+/var/libdes services, - dump DB avant toute création d’utilisateur / changement de conf :
# Dump rapide (à adapter) — conservez-le hors du serveur si possible
mysqldump --single-transaction --routines --triggers
-u root -p --all-databases | gzip > /root/backup-all-db-$(date +%F).sql.gz
Versions cibles :
- WordPress : 6.9.4 (stable actuelle),
- PHP : 8.1+ (8.2/8.3 très courant en 2026, adaptez vos exporters),
- MySQL 8.x ou MariaDB 10.6+ (les métriques diffèrent légèrement),
- nginx (recommandé ici) ou Apache (possible, mais exemples nginx fournis).
Hypothèse d’architecture : un serveur “monolithique” (WP + DB + monitoring). En production sérieuse, je sépare souvent Prometheus/Grafana sur une VM dédiée. Les configs restent identiques, seuls les endpoints changent.
Étape 1 : Installer Prometheus + exporters (node, nginx, php-fpm, mysql)
Je pars sur Debian/Ubuntu avec systemd. Sur RHEL/Alma, le principe est identique (packages et paths changent). L’idée : un exporter = une surface de métriques.
1) Créer un utilisateur système et dossiers
# Utilisateurs dédiés (bonnes pratiques)
sudo useradd --system --no-create-home --shell /usr/sbin/nologin prometheus || true
sudo useradd --system --no-create-home --shell /usr/sbin/nologin node_exporter || true
# Dossiers
sudo mkdir -p /etc/prometheus /var/lib/prometheus
sudo chown -R prometheus:prometheus /etc/prometheus /var/lib/prometheus
2) Installer Prometheus (binaire officiel) + service systemd
Les dépôts distro sont souvent en retard. Pour du monitoring, je préfère les releases officielles.
# Vérifiez la dernière version sur GitHub (exemple : 2.x)
# https://github.com/prometheus/prometheus/releases
PROM_VERSION="2.55.1"
cd /tmp
curl -fsSLO "https://github.com/prometheus/prometheus/releases/download/v${PROM_VERSION}/prometheus-${PROM_VERSION}.linux-amd64.tar.gz"
tar -xzf "prometheus-${PROM_VERSION}.linux-amd64.tar.gz"
sudo cp "prometheus-${PROM_VERSION}.linux-amd64/prometheus" /usr/local/bin/
sudo cp "prometheus-${PROM_VERSION}.linux-amd64/promtool" /usr/local/bin/
sudo mkdir -p /etc/prometheus/consoles /etc/prometheus/console_libraries
sudo cp -r "prometheus-${PROM_VERSION}.linux-amd64/consoles/"* /etc/prometheus/consoles/
sudo cp -r "prometheus-${PROM_VERSION}.linux-amd64/console_libraries/"* /etc/prometheus/console_libraries/
sudo chown -R prometheus:prometheus /etc/prometheus
sudo chmod 0755 /usr/local/bin/prometheus /usr/local/bin/promtool
3) node_exporter (métriques OS)
# https://github.com/prometheus/node_exporter/releases
NODE_VERSION="1.9.0"
cd /tmp
curl -fsSLO "https://github.com/prometheus/node_exporter/releases/download/v${NODE_VERSION}/node_exporter-${NODE_VERSION}.linux-amd64.tar.gz"
tar -xzf "node_exporter-${NODE_VERSION}.linux-amd64.tar.gz"
sudo cp "node_exporter-${NODE_VERSION}.linux-amd64/node_exporter" /usr/local/bin/
sudo chown node_exporter:node_exporter /usr/local/bin/node_exporter
sudo chmod 0755 /usr/local/bin/node_exporter
4) nginx exporter (métriques HTTP côté nginx)
Le point clé : activer stub_status en local, puis laisser l’exporter le convertir en métriques Prometheus.
# https://github.com/nginxinc/nginx-prometheus-exporter/releases
NGINX_EXP_VERSION="1.3.0"
cd /tmp
curl -fsSLO "https://github.com/nginxinc/nginx-prometheus-exporter/releases/download/v${NGINX_EXP_VERSION}/nginx-prometheus-exporter_${NGINX_EXP_VERSION}_linux_amd64.tar.gz"
tar -xzf "nginx-prometheus-exporter_${NGINX_EXP_VERSION}_linux_amd64.tar.gz"
sudo cp nginx-prometheus-exporter /usr/local/bin/nginx-prometheus-exporter
sudo chmod 0755 /usr/local/bin/nginx-prometheus-exporter
5) PHP-FPM exporter (saturation workers, queue, latence)
J’ai souvent vu des sites “lents” alors que le CPU est OK : en réalité FPM est saturé (pm.max_children trop bas, ou requêtes bloquées). Il faut activer pm.status_path.
Exporters possibles : plusieurs existent. Je donne un exemple avec un exporter PHP-FPM compatible “status page”. Adaptez la release selon votre choix (l’idée reste identique : lire /status et exposer /metrics).
# Exemple avec hipages/php-fpm_exporter (vérifiez la release actuelle)
# https://github.com/hipages/php-fpm_exporter/releases
FPM_EXP_VERSION="2.3.0"
cd /tmp
curl -fsSLO "https://github.com/hipages/php-fpm_exporter/releases/download/v${FPM_EXP_VERSION}/php-fpm_exporter_${FPM_EXP_VERSION}_linux_amd64.tar.gz"
tar -xzf "php-fpm_exporter_${FPM_EXP_VERSION}_linux_amd64.tar.gz"
sudo cp php-fpm_exporter /usr/local/bin/php-fpm_exporter
sudo chmod 0755 /usr/local/bin/php-fpm_exporter
6) MySQL exporter (DB)
# https://github.com/prometheus/mysqld_exporter/releases
MYSQL_EXP_VERSION="0.16.0"
cd /tmp
curl -fsSLO "https://github.com/prometheus/mysqld_exporter/releases/download/v${MYSQL_EXP_VERSION}/mysqld_exporter-${MYSQL_EXP_VERSION}.linux-amd64.tar.gz"
tar -xzf "mysqld_exporter-${MYSQL_EXP_VERSION}.linux-amd64.tar.gz"
sudo cp "mysqld_exporter-${MYSQL_EXP_VERSION}.linux-amd64/mysqld_exporter" /usr/local/bin/
sudo chmod 0755 /usr/local/bin/mysqld_exporter
7) Utilisateur MySQL dédié (lecture métriques)
Ne mettez pas root dans un exporter. Le moindre leak de .cnf devient critique.
sudo mysql -uroot -p <<'SQL'
CREATE USER IF NOT EXISTS 'exporter'@'localhost' IDENTIFIED BY 'CHANGEZ_MOI_MOT_DE_PASSE_LONG';
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'localhost';
FLUSH PRIVILEGES;
SQL
# Fichier de creds lisible uniquement par root (mysqld_exporter le lit)
sudo install -m 0600 -o root -g root /dev/null /etc/mysqld_exporter.cnf
sudo bash -c 'cat > /etc/mysqld_exporter.cnf' <<'EOF'
[client]
user=exporter
password=CHANGEZ_MOI_MOT_DE_PASSE_LONG
host=127.0.0.1
EOF
Étape 2 : Exposer des métriques WordPress (plugin MU + endpoint sécurisé)
Les métriques système ne suffisent pas. Vous voulez des signaux “WordPress” : erreurs PHP visibles, WP-Cron qui dérive, taux de réponses non cache, latence côté WP.
Je déconseille de rendre /metrics public. Même sans données perso, vous exposez des infos d’architecture (plugins, charge, timings). Je combine généralement :
- allowlist IP (Prometheus uniquement),
- token (header) pour éviter un scrape accidentel,
- désactivation sur front si besoin (ne pas impacter cache/page builder).
1) MU-plugin : endpoint de métriques
Créez wp-content/mu-plugins/bpcab-prom-metrics.php. MU-plugin = chargé tôt et fiable (moins de risques de désactivation “par erreur”).
<?php
/**
* Plugin Name: BPCAB Prometheus Metrics (MU)
* Description: Expose un endpoint Prometheus pour WordPress 6.9.4+ (sécurisé par IP + token).
* Author: BPCAB
* Version: 1.0.0
*
* Sécurité : ce fichier doit être déployé par SSH/CI, pas collé dans un plugin de snippets.
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Configuration minimale via constantes (à définir dans wp-config.php).
*
* BPCAB_METRICS_TOKEN : token long (au moins 32+ chars) envoyé en header.
* BPCAB_METRICS_ALLOW_IPS : liste CSV d'IPs autorisées (ex: "127.0.0.1,10.0.0.12").
*/
function bpcab_metrics_is_allowed(): bool {
$allow_csv = defined('BPCAB_METRICS_ALLOW_IPS') ? (string) BPCAB_METRICS_ALLOW_IPS : '127.0.0.1';
$allowed = array_filter(array_map('trim', explode(',', $allow_csv)));
$remote_ip = $_SERVER['REMOTE_ADDR'] ?? '';
if ($remote_ip === '' || !in_array($remote_ip, $allowed, true)) {
return false;
}
$expected = defined('BPCAB_METRICS_TOKEN') ? (string) BPCAB_METRICS_TOKEN : '';
if ($expected === '') {
// Si vous oubliez le token, on refuse par défaut.
return false;
}
// Header custom : X-Prometheus-Token
$got = $_SERVER['HTTP_X_PROMETHEUS_TOKEN'] ?? '';
return hash_equals($expected, (string) $got);
}
/**
* Endpoint : /?bpcab_metrics=1
* Avantage : ne dépend pas de rewrite, marche sur nginx/Apache.
* Inconvénient : si vous avez des caches agressifs, forcez no-cache ci-dessous.
*/
function bpcab_metrics_maybe_output() : void {
if (!isset($_GET['bpcab_metrics'])) {
return;
}
if (!bpcab_metrics_is_allowed()) {
status_header(403);
header('Content-Type: text/plain; charset=utf-8');
echo "forbiddenn";
exit;
}
// On évite les effets de bord (caches, compression, etc.)
if (!headers_sent()) {
header('Content-Type: text/plain; version=0.0.4; charset=utf-8');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
}
$start = microtime(true);
// Métriques simples et actionnables (évitez d'exposer des URLs, usernames, etc.)
$metrics = [];
// 1) Version WP (label) — attention à ne pas multiplier les labels (cardinalité faible)
$wp_version = get_bloginfo('version');
$metrics[] = '# HELP wp_info Informations WordPress (labels: version).';
$metrics[] = '# TYPE wp_info gauge';
$metrics[] = 'wp_info{version="' . esc_attr($wp_version) . '"} 1';
// 2) WP-Cron : timestamp du prochain événement (si DISABLE_WP_CRON, vous surveillerez plutôt le cron système)
$next = wp_next_scheduled('wp_version_check');
$metrics[] = '# HELP wp_next_core_check_timestamp Prochain check coeur WordPress (epoch).';
$metrics[] = '# TYPE wp_next_core_check_timestamp gauge';
$metrics[] = 'wp_next_core_check_timestamp ' . (is_int($next) ? $next : 0);
// 3) Erreurs PHP visibles via error_get_last() (signal faible, mais utile)
$last = error_get_last();
$metrics[] = '# HELP wp_php_last_error_present 1 si une erreur PHP est présente dans error_get_last().';
$metrics[] = '# TYPE wp_php_last_error_present gauge';
$metrics[] = 'wp_php_last_error_present ' . (!empty($last) ? 1 : 0);
// 4) Santé DB : latence d'un SELECT trivial (ms)
global $wpdb;
$t0 = microtime(true);
$wpdb->get_var('SELECT 1');
$db_ms = (microtime(true) - $t0) * 1000;
$metrics[] = '# HELP wp_db_select1_ms Latence SELECT 1 (ms).';
$metrics[] = '# TYPE wp_db_select1_ms gauge';
$metrics[] = 'wp_db_select1_ms ' . round($db_ms, 3);
// 5) Object cache (si présent)
$has_oc = wp_using_ext_object_cache() ? 1 : 0;
$metrics[] = '# HELP wp_ext_object_cache_enabled 1 si un object-cache externe est actif (Redis/Memcached).';
$metrics[] = '# TYPE wp_ext_object_cache_enabled gauge';
$metrics[] = 'wp_ext_object_cache_enabled ' . $has_oc;
// 6) Temps total génération endpoint (ms) — utile pour détecter un effet de bord
$total_ms = (microtime(true) - $start) * 1000;
$metrics[] = '# HELP wp_metrics_endpoint_ms Temps de génération des métriques (ms).';
$metrics[] = '# TYPE wp_metrics_endpoint_ms gauge';
$metrics[] = 'wp_metrics_endpoint_ms ' . round($total_ms, 3);
echo implode("n", $metrics) . "n";
exit;
}
add_action('init', 'bpcab_metrics_maybe_output', 0);
2) Définir le token + allowlist dans wp-config.php
Faites-le côté serveur, pas en base. Un token exposé en DB fuite souvent via backups/restores.
<?php
// wp-config.php (extrait)
// Token long, unique, non réutilisé ailleurs.
// Ne le commitez pas en clair si votre repo est partagé.
define('BPCAB_METRICS_TOKEN', 'COLLEZ_UN_TOKEN_64CHARS_MINIMUM_GENERE_PAR_OPENSSL');
// IP(s) Prometheus (ou localhost si Prometheus est sur la même machine)
define('BPCAB_METRICS_ALLOW_IPS', '127.0.0.1,10.0.0.50');
Générer un token propre :
openssl rand -hex 32
Étape 3 : Configurer Prometheus (scrape, labels, règles)
Le piège classique : vous mettez tout dans un seul job, puis vous ne savez plus “quel site” ou “quelle couche” a déclenché l’alerte. Ajoutez des labels stables (instance, role, site) sans exploser la cardinalité.
1) Fichier prometheus.yml (scrape configs)
On va scraper :
- node_exporter :
:9100 - nginx exporter :
:9113(souvent) - php-fpm exporter :
:9253(selon exporter) - mysqld exporter :
:9104 - WordPress endpoint : via nginx location interne (recommandé) ou direct
Je recommande de proxyfier l’endpoint WP via nginx en local, pour injecter le header token côté serveur (Prometheus n’a pas besoin de connaître le token). Ça évite aussi qu’un collègue copie-colle l’URL et se prenne un 403 incompréhensible.
2) Règles d’alerting (PromQL) orientées incidents WordPress
Des alertes utiles :
- saturation mémoire / disque,
- hausse 5xx nginx,
- FPM “max children reached”,
- DB trop lente,
- endpoint WP metrics qui devient lent (souvent signe de DB ou autoload options).
Étape 4 : Alerting automatique avec Alertmanager (mail/Slack) + anti-bruit
J’ai souvent vu des setups où chaque symptôme envoie une alerte, ce qui finit en “mute permanent”. L’anti-bruit se fait à deux niveaux :
- dans Prometheus :
for:(éviter les spikes), seuils réalistes, - dans Alertmanager : grouping, inhibit rules (si le serveur est down, inutile d’alerter “nginx 5xx”).
Grouping et inhibition
Exemple : si InstanceDown est en firing, inhibez NginxHigh5xx, PhpFpmSaturated, etc.
Étape 5 : Grafana (datasource, dashboards, panels utiles WordPress)
Grafana sert à réduire le temps de diagnostic. Je vise des dashboards par “symptôme” :
- Latence perçue : p95/p99, temps de réponse nginx,
- Erreurs : 4xx/5xx, upstream errors,
- Capacité : CPU steal (VM), RAM, disk IO wait,
- PHP-FPM : active/idle, queue, max children reached,
- MySQL : threads_running, slow queries, buffer pool,
- WordPress : db_select1_ms, endpoint_ms, object cache actif, cron drift (selon métriques).
PromQL de base (exemples de panels)
# CPU (utilisation non idle)
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
# RAM dispo (%)
(node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100
# Nginx 5xx rate (selon métriques exporter/logs)
# Si vous utilisez nginx-prometheus-exporter basé sur stub_status, vous n'avez pas les codes HTTP.
# Pour les codes, il faut souvent un exporter basé sur logs (ex: mtail) ou NGINX Plus.
# Ici, on suppose des métriques http_requests_total{status="500"} (à adapter).
sum by (instance) (rate(http_requests_total{status=~"5.."}[5m]))
# PHP-FPM saturation (exemple générique, dépend de l'exporter)
phpfpm_active_processes / phpfpm_max_children * 100
# MySQL threads running
mysql_global_status_threads_running
# WordPress DB ping (ms)
wp_db_select1_ms
Note franche : nginx stub_status ne donne pas les codes HTTP. Si votre priorité est le 5xx rate, vous avez trois options réalistes :
- NGINX Plus (métriques riches),
- instrumenter via logs (mtail, vector + exporter, ou Loki + alerting Grafana),
- exposer le 5xx côté app (WordPress/PHP) via logs structurés (plus complexe).
Dans la pratique, sur des stacks WordPress, je combine Prometheus pour infra + Loki pour logs nginx/PHP. Ici je reste volontairement sur Prometheus/Alertmanager, mais gardez cette limite en tête.
Étape 6 : Déploiement staging → production et rollback
Ne déployez pas le MU-plugin métriques directement en prod sans test. Le risque principal : vous déclenchez du code trop tôt (hook inadapté) et vous cassez une route, ou vous introduisez une latence DB supplémentaire.
Stratégie simple
- staging : activer endpoint + scrape toutes les 30s,
- vérifier cardinalité et perf,
- prod : activer scrape toutes les 15s si nécessaire, mais évitez 1s (inutile et coûteux),
- rollback : supprimer le MU-plugin (ou le renommer) + reload nginx + reload Prometheus.
WP-CLI pour valider côté WordPress
# Vérifier version et santé
wp core version
wp core verify-checksums
# Vérifier que MU-plugins est bien chargé
wp mu-plugin list
Fichiers de configuration complets
Cette section est volontairement “copier-coller”. Adaptez les ports si vous avez déjà des services.
1) nginx : stub_status + proxy endpoint WordPress metrics
Exemple de server block (extrait). Placez-le dans votre vhost (ou un include). Le point clé : stub_status en local et endpoint /metrics/wp qui ajoute le header token.
# /etc/nginx/snippets/monitoring.conf
# À inclure dans votre server { } WordPress
# 1) Nginx stub_status (local uniquement)
location = /nginx_status {
stub_status;
allow 127.0.0.1;
deny all;
}
# 2) Proxy local vers WordPress metrics
# On évite d'exposer le token à Prometheus (il scrape localhost).
location = /metrics/wp {
allow 127.0.0.1;
deny all;
# Votre site WordPress doit répondre en local (fastcgi/php).
# Ici on passe par HTTP local (si vous avez un upstream local).
# Si votre WordPress n'écoute pas en HTTP local, adaptez en proxy_pass vers 127.0.0.1:8080 par ex.
proxy_set_header X-Prometheus-Token "COLLEZ_ICI_LE_MEME_TOKEN_QUE_WP-CONFIG";
proxy_set_header Host $host;
proxy_pass http://127.0.0.1/?bpcab_metrics=1;
# Important : pas de cache
add_header Cache-Control "no-store";
}
Activez l’include dans votre vhost :
# Exemple vhost : /etc/nginx/sites-available/example.conf
# server { ... include /etc/nginx/snippets/monitoring.conf; ... }
sudo nginx -t
sudo systemctl reload nginx
2) PHP-FPM : activer status path
Exemple pool www. Sur Debian, c’est souvent /etc/php/8.2/fpm/pool.d/www.conf. Ajustez la version.
# Éditez le pool FPM
sudo sed -i 's|^;pm.status_path =.*|pm.status_path = /fpm-status|g' /etc/php/8.2/fpm/pool.d/www.conf
# Optionnel : ping path pour vérifier que FPM répond
grep -q '^ping.path' /etc/php/8.2/fpm/pool.d/www.conf || echo 'ping.path = /fpm-ping' | sudo tee -a /etc/php/8.2/fpm/pool.d/www.conf
sudo systemctl restart php8.2-fpm
Exposez /fpm-status uniquement en local via nginx :
# /etc/nginx/snippets/php-fpm-status.conf
location ~ ^/(fpm-status|fpm-ping)$ {
allow 127.0.0.1;
deny all;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
3) systemd : services exporters + Prometheus
node_exporter.service
sudo bash -c 'cat > /etc/systemd/system/node_exporter.service' <<'EOF'
[Unit]
Description=Prometheus Node Exporter
Wants=network-online.target
After=network-online.target
[Service]
User=node_exporter
Group=node_exporter
Type=simple
ExecStart=/usr/local/bin/node_exporter
--web.listen-address=127.0.0.1:9100
Restart=on-failure
RestartSec=2s
# Sécurité systemd (durcissement basique)
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now node_exporter
nginx-prometheus-exporter.service
sudo bash -c 'cat > /etc/systemd/system/nginx-prometheus-exporter.service' <<'EOF'
[Unit]
Description=Nginx Prometheus Exporter
Wants=network-online.target
After=network-online.target nginx.service
[Service]
Type=simple
ExecStart=/usr/local/bin/nginx-prometheus-exporter
-nginx.scrape-uri http://127.0.0.1/nginx_status
-web.listen-address 127.0.0.1:9113
Restart=on-failure
RestartSec=2s
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now nginx-prometheus-exporter
php-fpm_exporter.service
sudo bash -c 'cat > /etc/systemd/system/php-fpm_exporter.service' <<'EOF'
[Unit]
Description=PHP-FPM Prometheus Exporter
Wants=network-online.target
After=network-online.target php8.2-fpm.service nginx.service
[Service]
Type=simple
# L'exporter lit la page status en local
ExecStart=/usr/local/bin/php-fpm_exporter
--phpfpm.scrape-uri "http://127.0.0.1/fpm-status?full"
--web.listen-address 127.0.0.1:9253
Restart=on-failure
RestartSec=2s
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now php-fpm_exporter
mysqld_exporter.service
sudo bash -c 'cat > /etc/systemd/system/mysqld_exporter.service' <<'EOF'
[Unit]
Description=Prometheus MySQL Exporter
Wants=network-online.target
After=network-online.target mysql.service
[Service]
Type=simple
Environment="DATA_SOURCE_NAME=exporter:CHANGEZ_MOI_MOT_DE_PASSE_LONG@(127.0.0.1:3306)/"
ExecStart=/usr/local/bin/mysqld_exporter
--web.listen-address=127.0.0.1:9104
--config.my-cnf=/etc/mysqld_exporter.cnf
Restart=on-failure
RestartSec=2s
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now mysqld_exporter
prometheus.service
sudo bash -c 'cat > /etc/systemd/system/prometheus.service' <<'EOF'
[Unit]
Description=Prometheus
Wants=network-online.target
After=network-online.target
[Service]
User=prometheus
Group=prometheus
Type=simple
ExecStart=/usr/local/bin/prometheus
--config.file=/etc/prometheus/prometheus.yml
--storage.tsdb.path=/var/lib/prometheus
--web.listen-address=127.0.0.1:9090
--web.enable-lifecycle
Restart=on-failure
RestartSec=2s
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ReadWritePaths=/var/lib/prometheus
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now prometheus
4) Prometheus : config + règles
sudo bash -c 'cat > /etc/prometheus/prometheus.yml' <<'EOF'
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- /etc/prometheus/rules/wordpress.rules.yml
scrape_configs:
- job_name: "node"
static_configs:
- targets: ["127.0.0.1:9100"]
labels:
role: "server"
site: "example"
- job_name: "nginx"
static_configs:
- targets: ["127.0.0.1:9113"]
labels:
role: "web"
site: "example"
- job_name: "php-fpm"
static_configs:
- targets: ["127.0.0.1:9253"]
labels:
role: "app"
site: "example"
- job_name: "mysql"
static_configs:
- targets: ["127.0.0.1:9104"]
labels:
role: "db"
site: "example"
- job_name: "wordpress"
metrics_path: /metrics/wp
static_configs:
- targets: ["127.0.0.1"]
labels:
role: "wp"
site: "example"
EOF
sudo mkdir -p /etc/prometheus/rules
sudo chown -R prometheus:prometheus /etc/prometheus
sudo promtool check config /etc/prometheus/prometheus.yml
Règles (exemple) :
sudo bash -c 'cat > /etc/prometheus/rules/wordpress.rules.yml' <<'EOF'
groups:
- name: wordpress-server-alerts
rules:
- alert: InstanceDown
expr: up == 0
for: 2m
labels:
severity: critical
annotations:
summary: "Target Prometheus down ({{ $labels.job }})"
description: "Le scrape échoue depuis 2 minutes sur {{ $labels.instance }} (job={{ $labels.job }}, site={{ $labels.site }})."
- alert: LowDiskSpace
expr: (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay"}) < 0.10
for: 10m
labels:
severity: warning
annotations:
summary: "Espace disque faible (<10%)"
description: "Risque d'échec MySQL / logs. Instance={{ $labels.instance }} mount={{ $labels.mountpoint }}."
- alert: HighLoad15m
expr: node_load15 > (count by (instance) (node_cpu_seconds_total{mode="system"}) * 1.5)
for: 15m
labels:
severity: warning
annotations:
summary: "Load élevé"
description: "Load15 élevé vs CPU. Possible saturation PHP-FPM, backups, ou attaques."
- name: wordpress-app-alerts
rules:
- alert: WordPressDbSelectSlow
expr: wp_db_select1_ms > 50
for: 5m
labels:
severity: warning
annotations:
summary: "DB lente (SELECT 1 > 50ms)"
description: "Latence DB anormale sur {{ $labels.site }}. Vérifiez MySQL (IO, locks, slow queries)."
- alert: WordPressMetricsEndpointSlow
expr: wp_metrics_endpoint_ms > 200
for: 10m
labels:
severity: warning
annotations:
summary: "Endpoint métriques WP lent (>200ms)"
description: "Souvent corrélé à DB lente, autoload options volumineuses, ou saturation FPM."
- alert: PhpFpmSaturated
# Cette métrique dépend de l'exporter. Adaptez à vos noms réels.
expr: (phpfpm_active_processes / phpfpm_max_children) > 0.90
for: 5m
labels:
severity: critical
annotations:
summary: "PHP-FPM saturé (>90%)"
description: "Augmentez pm.max_children, optimisez les requêtes, ou ajoutez du cache. Site={{ $labels.site }}."
EOF
sudo chown -R prometheus:prometheus /etc/prometheus/rules
sudo promtool check rules /etc/prometheus/rules/wordpress.rules.yml
# Reload Prometheus (web.enable-lifecycle activé)
curl -fsS -X POST http://127.0.0.1:9090/-/reload
5) php.ini (petit réglage utile pour incidents)
Vous voulez des erreurs exploitables. En prod, pas d’affichage, mais logs propres.
# Exemple : /etc/php/8.2/fpm/conf.d/99-monitoring.ini
sudo bash -c 'cat > /etc/php/8.2/fpm/conf.d/99-monitoring.ini' <<'EOF'
; Logs PHP en production (ne pas afficher aux visiteurs)
display_errors=0
log_errors=1
error_log=/var/log/php/php-fpm-error.log
; Limiter les surprises lors de pics
max_execution_time=60
memory_limit=256M
EOF
sudo mkdir -p /var/log/php
sudo chown -R www-data:www-data /var/log/php
sudo systemctl restart php8.2-fpm
Vérification
Vérifiez chaque brique en local. Tant que curl ne marche pas, Prometheus ne marchera pas.
1) Exporters
curl -fsS http://127.0.0.1:9100/metrics | head
curl -fsS http://127.0.0.1:9113/metrics | head
curl -fsS http://127.0.0.1:9253/metrics | head
curl -fsS http://127.0.0.1:9104/metrics | head
2) Endpoint WordPress metrics (via nginx proxy)
curl -fsS http://127.0.0.1/metrics/wp | sed -n '1,40p'
Vous devez voir des lignes type :
# HELP wp_info Informations WordPress (labels: version).
# TYPE wp_info gauge
wp_info{version="6.9.4"} 1
wp_db_select1_ms 1.234
3) Prometheus targets + query
# Targets (API)
curl -fsS "http://127.0.0.1:9090/api/v1/targets" | head -c 400; echo
# Query simple
curl -G -fsS "http://127.0.0.1:9090/api/v1/query" --data-urlencode 'query=up' | head -c 400; echo
4) WP-CLI : test basique app
wp eval 'echo "WP OKn";'
wp option get siteurl
wp cron event list --fields=hook,next_run_relative --format=table | head -n 20
Si ça ne marche pas
Diagnostic par étapes. Ne sautez pas directement à Grafana : le problème est souvent un endpoint local cassé.
Tableau de diagnostic
| Symptôme | Cause probable | Vérification | Solution |
|---|---|---|---|
| Prometheus target “DOWN” | Service exporter non démarré / écoute sur mauvaise IP | systemctl status node_exporter + ss -lntp | grep 9100 |
Corriger --web.listen-address, redémarrer service |
| WordPress metrics = 403 | IP non allowlistée ou token manquant | curl -i http://127.0.0.1/?bpcab_metrics=1 |
Vérifier BPCAB_METRICS_ALLOW_IPS et injection header nginx |
| Endpoint WP metrics très lent | DB lente / locks / autoload options énormes | mysqladmin processlist + métrique wp_db_select1_ms |
Analyser slow query log, optimiser options autoload, index |
| PHP-FPM exporter ne remonte rien | pm.status_path non activé ou nginx bloque l’URL |
curl -i http://127.0.0.1/fpm-status |
Activer pm.status_path, ajouter location nginx allow localhost |
| MySQL exporter renvoie “access denied” | user exporter mal créé / mauvais mot de passe | mysql -uexporter -p -h127.0.0.1 -e "SELECT 1" |
Recréer user + corriger /etc/mysqld_exporter.cnf |
Logs à consulter
# Prometheus
journalctl -u prometheus -n 200 --no-pager
# Exporters
journalctl -u node_exporter -n 100 --no-pager
journalctl -u nginx-prometheus-exporter -n 100 --no-pager
journalctl -u php-fpm_exporter -n 100 --no-pager
journalctl -u mysqld_exporter -n 100 --no-pager
# nginx / PHP
tail -n 200 /var/log/nginx/error.log
tail -n 200 /var/log/php/php-fpm-error.log
Erreurs réalistes que je vois souvent
- Copier le MU-plugin au mauvais endroit : il doit être dans
wp-content/mu-plugins/(pas dansplugins/). - Oublier un point-virgule dans
wp-config.php: vous cassez tout le site (white screen). Testez d’abord en staging. - Hook inadapté : si vous tentez d’exposer les métriques sur
plugins_loadedet que vous faites du DB trop tôt, vous pouvez provoquer des effets de bord. Ici,initpriorité 0 est un compromis. - Conflit cache : certains reverse proxies/caches peuvent mettre en cache
/?bpcab_metrics=1. D’où les headersno-storeet le proxy nginx local.
Pièges et erreurs courantes
| Erreur | Cause | Solution |
|---|---|---|
Vous scrapez /?bpcab_metrics=1 depuis l’extérieur |
Endpoint exposé publiquement (risque fuite d’infos) | Allowlist IP + token + proxy local nginx + firewall |
| Vous mettez des labels dynamiques (URL, user, plugin) | Explosion de cardinalité Prometheus | Labels stables uniquement (site, role, instance). Jamais de valeurs uniques par requête. |
| Prometheus consomme trop de disque | Rétention trop longue / scrape trop fréquent | --storage.tsdb.retention.time=15d (ex) + scrape 15s/30s |
| Alertes “flapping” | Seuils trop agressifs, pas de for: |
Ajoutez for: 5m / 10m, utilisez des moyennes (rate/avg) |
| Exporter inaccessible | Écoute sur 0.0.0.0 bloqué par firewall ou au contraire exposé | Écoutez sur 127.0.0.1 et scrapez localement |
| Snippet cassé via plugin de snippets | Chargement ordre imprévisible / erreurs fatales | MU-plugin versionné, déployé via Git/SSH, tests staging |
| PHP trop ancien | Serveur legacy, exporters/WordPress non compatibles | Montez à PHP 8.1+ (WordPress 6.9.4), sinon vous perdez du temps sur des bugs fantômes |
Sécurité serveur
Le monitoring augmente la surface d’attaque. Traitez-le comme une API interne.
- Écoute locale : exporters et Prometheus sur
127.0.0.1quand tout est sur la même machine. - Firewall : si Prometheus est sur une VM dédiée, n’ouvrez que les ports nécessaires depuis l’IP Prometheus.
- Tokens et allowlist pour l’endpoint WordPress.
- Pas de secrets en clair dans un repo : token WP et creds MySQL exporter via secrets manager/Ansible vault si possible.
- Durcissement systemd (déjà esquissé) :
NoNewPrivileges,ProtectSystem,PrivateTmp. - HTTPS : si vous exposez Grafana/Prometheus, mettez-les derrière nginx avec TLS + auth (basic auth, SSO, ou mTLS).
Headers HTTP (si vous exposez Grafana via nginx) : je pose systématiquement au moins X-Frame-Options, X-Content-Type-Options, Referrer-Policy. Ce n’est pas spécifique WordPress, mais ça évite des surprises.
Ressources
- Prometheus – Documentation officielle
- Alertmanager – Documentation officielle
- Grafana – Documentation officielle
- WordPress Developer Resources – Plugins
- WordPress Developer Resources – Hooks (actions & filters)
- WordPress.org – Documentation
- WordPress Core Trac (suivi des tickets)
- GitHub – wordpress-develop (source)
- PHP Manual (8.1+)
FAQ
Est-ce que je peux faire ça sur un hébergement mutualisé ?
Pas proprement. Sans systemd/SSH root vous ne pouvez pas lancer exporters ni Prometheus. Dans ce cas, externalisez : un Prometheus ailleurs qui scrape un endpoint WordPress très verrouillé, ou passez par une solution SaaS.
Pourquoi un MU-plugin plutôt qu’un plugin classique ?
Parce que je vois régulièrement des plugins désactivés “pour tester”, ou cassés par une mise à jour. Le MU-plugin est chargé automatiquement et réduit le risque de monitoring silencieusement désactivé.
Pourquoi ne pas exposer /metrics en clair et mettre un simple mot de passe ?
Parce que ça finit souvent indexé, scanné, ou leaké. L’allowlist IP + token est basique mais efficace. Si Prometheus est sur une VM dédiée, ajoutez un firewall réseau et idéalement mTLS.
Le endpoint métriques peut-il casser Elementor/Divi/Avada ?
Non, si vous restez sur une query var dédiée et que vous exit immédiatement. Le code ci-dessus ne charge pas de templates et n’interagit pas avec les builders. Le vrai risque vient d’un hook trop tardif (qui laisse WordPress générer la page) ou d’un plugin de cache qui intercepte l’URL.
Comment monitorer le “vrai TTFB” côté visiteur ?
Prometheus ici mesure l’infra. Pour le ressenti utilisateur, ajoutez du RUM (Real User Monitoring) ou au minimum des sondes externes (blackbox exporter) qui mesurent un endpoint public.
Dois-je activer DISABLE_WP_CRON ?
Sur des sites à trafic irrégulier, oui : cron système toutes les minutes, plus fiable. Dans ce cas, surveillez plutôt la dérive via vos logs cron et des métriques système. Le MU-plugin peut aussi exposer un compteur d’exécutions si vous l’ajoutez.
Pourquoi mes métriques nginx n’ont pas de codes HTTP ?
Parce que stub_status ne fournit pas cette dimension. Pour les codes, vous devez passer par NGINX Plus, ou analyser les logs (mtail / vector / Loki). C’est une limite fréquente.
Quelle rétention Prometheus pour un WordPress “standard” ?
15 jours est un bon départ sur un petit serveur. Si vous voulez 90 jours, prévoyez du disque et surveillez la croissance TSDB. Ajustez surtout la fréquence de scrape et évitez les labels à haute cardinalité.
Comment tester une alerte sans casser la prod ?
Le plus simple : baisser temporairement un seuil (en staging), ou simuler un up == 0 en stoppant un exporter. N’allez pas “remplir le disque” en prod juste pour voir une alerte.
Que faire si l’endpoint WordPress metrics renvoie parfois 500 ?
Regardez d’abord /var/log/nginx/error.log et /var/log/php/php-fpm-error.log. J’ai souvent vu : DB down, memory_limit trop bas, ou un autoload options énorme qui rend init instable. Réduisez les appels DB dans l’endpoint et gardez-le minimal.