Serveur interne
OxPHP exécute un serveur HTTP distinct sur un port dédié pour les contrôles de santé, les métriques Prometheus et l'inspection de la configuration en direct. Il reste totalement isolé de l'écouteur de l'application principale : pas de TLS, pas de limitation de débit, pas d'ID de requête et aucun traitement d'événements.
Fonctionnement
Définissez INTERNAL_ADDR avec une adresse d'écoute (par exemple 127.0.0.1:9090) et OxPHP démarre un second écouteur HTTP sur cette adresse. Le serveur interne expose les endpoints intégrés ci-dessous :
/health— statut JSON agrégé/health/liveness,/healthz(alias) — sonde de vivacité/health/readiness,/readyz(alias) — sonde de disponibilité/health/startup,/startupz(alias) — sonde de démarrage/metrics— exposition Prometheus/config— configuration active du serveur au format JSON
Les plugins peuvent enregistrer des endpoints supplémentaires sous le préfixe /__<plugin>/. Pendant l'arrêt gracieux, le serveur interne reste disponible jusqu'à ce que le serveur principal ait terminé de vider ses connexions.
Le serveur interne ne démarre que lorsque INTERNAL_ADDR est explicitement défini. Sans cette variable, les endpoints de santé, de métriques et de configuration ne sont pas accessibles via HTTP.
Configuration
| Variable | Valeur par défaut | Description |
|---|---|---|
INTERNAL_ADDR |
(non défini) | Adresse du serveur interne. Non démarré lorsqu'il n'est pas défini. Une valeur ne comportant qu'un port (:9090) se lie à 127.0.0.1 ; utilisez explicitement 0.0.0.0:9090 pour l'exposer hors de l'hôte. Exemple : 127.0.0.1:9090 |
INTERNAL_ALLOW_IPS |
(non défini) | Liste d'autorisation CIDR/IP séparée par des virgules. Les pairs en dehors de la liste reçoivent 403 sur les chemins /metrics, /config et /__<plugin>/ ; les sondes de santé restent toujours accessibles. Non défini/vide = tout autoriser. Le loopback n'est pas implicite — indiquez 127.0.0.1/32 pour conserver l'accès depuis localhost. Une liste mal formée interrompt le démarrage |
Endpoints
GET /health
Renvoie un statut de santé au format JSON, destiné aux tableaux de bord et aux contrôles de santé des conteneurs. Pour Kubernetes, utilisez plutôt les endpoints de sonde dédiés (voir Intégration Kubernetes) : /health rend compte de tous les sous-systèmes à la fois, si bien que, branché sur la sonde de vivacité, la défaillance de n'importe lequel d'entre eux redémarrerait le pod.
200 OK — tous les systèmes fonctionnent :
{
"active_connections": 7,
"executor_healthy": true,
"plugins": {
"otel": "ok"
},
"pool_stalled": false,
"status": "ok",
"total_requests": 48203,
"uptime_secs": 3612
}503 Service Unavailable — un plugin a signalé une défaillance :
{
"active_connections": 7,
"executor_healthy": true,
"plugins": {
"otel": "failed"
},
"pool_stalled": false,
"status": "degraded",
"total_requests": 48203,
"uptime_secs": 3612
}L'objet plugins liste chaque plugin chargé avec son statut de santé : "ok", "degraded" ou "failed". L'endpoint renvoie 503 lorsqu'un plugin signale "failed", lorsqu'aucun thread worker PHP n'est plus en cours d'exécution (executor_healthy: false) ou lorsque le pool est bloqué (pool_stalled: true, en mode worker uniquement ; voir Contrôles de santé). Les plugins "degraded" apparaissent dans le corps JSON mais le statut HTTP reste 200. La charge n'est pas non plus une défaillance : un serveur qui rejette des requêtes avec 529 en situation de surcharge signale toujours "ok".
GET /metrics
Renvoie des métriques compatibles Prometheus au format texte (text/plain; version=0.0.4; charset=utf-8). Renvoie toujours 200.
curl http://localhost:9090/metrics# HELP oxphp_requests_total Total HTTP requests
# TYPE oxphp_requests_total counter
oxphp_requests_total 48203
# HELP oxphp_active_connections Current open connections
# TYPE oxphp_active_connections gauge
oxphp_active_connections 7
...Pour la liste complète des métriques disponibles, voir Métriques.
GET /config
Renvoie la configuration active du serveur au format JSON. Renvoie toujours 200.
curl -s http://localhost:9090/config | jq .{
"access_log": "all",
"async_in_flight_cap": 0,
"async_max_fibers": 256,
"async_queue_capacity": 0,
"async_workers": 0,
"brotli_level": 5,
"deny_file": {
"allow": 1,
"deny": 3,
"entry": 1,
"fallback": "404",
"loaded": true,
"rules": 5
},
"document_root": "/var/www/html/public",
"drain_timeout_seconds": 25,
"entry_file": "/var/www/html/public/index.php",
"executor_type": "sapi",
"gzip_level": 6,
"header_timeout_seconds": 5,
"listen_addr": "0.0.0.0:443",
"log_level": "info",
"max_connections": 10000,
"max_query_body": 524288,
"php_workers": "8",
"plugins": {},
"queue_capacity": 1024,
"queue_max_waiting": 1024,
"queue_max_waiting_bytes": 67108864,
"queue_wait_timeout_ms": 1000,
"rate_limit": 100,
"rate_window_seconds": 60,
"runtime_hooks": ["sleep", "streams", "net"],
"static_max_age": 2592000,
"static_revalidate": false,
"superglobals_enabled": true,
"tls_enabled": true,
"tls_min_version": "1.2",
"tokio_workers": 4,
"trace_context": false,
"trusted_proxies": false,
"worker_max_memory_mib": 0,
"worker_mode_enabled": false,
"zstd_level": 6
}runtime_hooks liste les catégories de hooks d'exécution que le processus a installées au démarrage. Il s'agit d'un ensemble analysé plutôt que de la valeur brute de RUNTIME_HOOKS : all, 1 et sleep, streams, net se lisent tous comme ["sleep", "streams", "net"], et un collecteur n'a pas besoin de réimplémenter cette grammaire. Une liste vide signifie qu'aucun hook n'est installé, ce que produit aussi un nom de variable mal orthographié. Il indique ce qui a été installé, pas ce qui est effectif : là où il n'y a aucune fibre, les hooks délèguent aux fonctions natives qu'ils ont remplacées, si bien qu'un serveur en mode traditionnel liste les catégories qu'il a mises en place tout en se comportant nativement. Voir Hooks d'exécution.
deny_file résume le fichier .oxphpdeny lu depuis DOCUMENT_ROOT au démarrage : le nombre de règles, ventilé par action — deny, entry pour les règles >, allow pour les règles ! — et ce que répond une règle de refus, soit le code de statut PHP_DENY_FALLBACK, soit "script" (sans le chemin du script). Les règles elles-mêmes sont omises : sur un serveur interne accessible depuis l'extérieur, elles constitueraient une carte exacte des chemins que le fichier protège. Pour voir comment chaque règle a été lue, démarrez le serveur avec LOG_LEVEL=debug et cherchez les lignes oxphpdeny rule. Sans le fichier, l'entrée vaut {"loaded": false}.
Les chemins des fichiers de certificat et de clé TLS ne sont jamais émis (seul le booléen tls_enabled est exposé), et internal_addr ainsi que error_pages_dir sont expurgés de la réponse servie — topologie de déploiement et chemins du système de fichiers qui aideraient un attaquant et dont les collecteurs n'ont pas besoin.
Endpoints de plugins
Les plugins peuvent enregistrer des endpoints personnalisés sous le préfixe /__<plugin_name>/. Par exemple, un plugin nommé otel pourrait exposer /__otel/status. Ces endpoints ne sont disponibles que si le plugin correspondant est chargé et a enregistré un gestionnaire.
Tout chemin ne correspondant ni à un endpoint intégré ni à un endpoint de plugin renvoie 404 Not Found.
Intégration Kubernetes
Branchez chaque sonde sur l'endpoint conçu pour elle plutôt que de pointer les trois vers /health. Cet endpoint agrège tous les sous-systèmes en un seul statut : sur la sonde de vivacité, il répondrait donc à une défaillance de plugin ou à un pool bloqué par un redémarrage du pod.
Sonde de disponibilité
/health/readiness détermine si Kubernetes achemine le trafic vers le pod :
readinessProbe:
httpGet:
path: /health/readiness
port: 9090
initialDelaySeconds: 2
periodSeconds: 5Lorsqu'il renvoie 503, Kubernetes retire le pod de la liste des endpoints du Service ; le trafic reprend lorsqu'il renvoie de nouveau 200. Il signale les défaillances et non la charge, si bien qu'un pod surchargé reste en rotation. Voir Contrôles de santé.
Sonde de vivacité
/health/liveness répond 200 dès lors que le processus est capable de servir une requête HTTP. C'est ce que la vivacité doit signifier, car un redémarrage ne peut pas remédier à un pool surchargé et emporte avec lui le travail en cours :
livenessProbe:
httpGet:
path: /health/liveness
port: 9090
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3Sonde de démarrage
Pour les applications au démarrage lent (frameworks volumineux, autoloaders lourds) :
startupProbe:
httpGet:
path: /health/startup
port: 9090
initialDelaySeconds: 1
periodSeconds: 2
failureThreshold: 15Sécurité
Le serveur interne n'utilise aucune authentification par jeton bearer ; l'accès est contrôlé par l'adresse sur laquelle il se lie et par INTERNAL_ALLOW_IPS. Pour le protéger :
- Liez-le à localhost — un
INTERNAL_ADDRne comportant qu'un port (:9090) se lie déjà à127.0.0.1; un127.0.0.1:9090explicite maintient le port accessible uniquement depuis l'intérieur du conteneur ou de l'hôte - Définissez
INTERNAL_ALLOW_IPSlorsque l'écouteur doit être accessible hors de l'hôte — une liste d'autorisation CIDR/IP qui renvoie403sur/metrics,/configet les chemins de plugins aux pairs qui n'en font pas partie, tandis que les sondes de santé restent accessibles. Le loopback n'est pas implicite, incluez donc127.0.0.1/32si vous avez encore besoin d'un accès depuis localhost. Le serveur émet un avertissement au démarrage si l'écouteur est exposé hors de l'hôte sans liste d'autorisation définie - Ne l'exposez pas en tant que Service Kubernetes — déclarez le port comme un
containerPortmais ne créez pas de Service pour lui. Les sondes Kubernetes accèdent directement aux ports du conteneur - Utilisez des network policies — restreignez l'accès au niveau réseau si le port doit être exposé
L'endpoint /config révèle des détails opérationnels (document root, limites de débit, nombre de workers, valeurs des délais d'expiration). Les chemins TLS, internal_addr et error_pages_dir sont expurgés, mais demandez-vous si le reste devrait être accessible depuis l'extérieur du pod.
Exemple Docker
Pour un aperçu rapide, vous pouvez publier le port interne ; en production, liez le serveur interne à localhost, ou, sous Kubernetes, dont les sondes httpGet n'atteignent pas un écouteur loopback, liez-le à 0.0.0.0:9090 et définissez INTERNAL_ALLOW_IPS.
services:
app:
image: ghcr.io/oxphp/oxphp:0.13.0
ports:
- "80:80"
- "9090:9090"
environment:
- DOCUMENT_ROOT=/var/www/html/public
- INTERNAL_ADDR=0.0.0.0:9090services:
app:
image: ghcr.io/oxphp/oxphp:0.13.0
ports:
- "80:80"
environment:
- DOCUMENT_ROOT=/var/www/html/public
- INTERNAL_ADDR=127.0.0.1:9090Dépannage
Les endpoints de santé, de métriques et de configuration ne sont pas accessibles
INTERNAL_ADDR n'est pas défini.
Correction : Ajoutez la variable d'environnement :
INTERNAL_ADDR=0.0.0.0:9090/health renvoie 503 mais l'application fonctionne
Un plugin chargé signale PluginHealth::Failed ou, plus rarement, executor_healthy vaut false ou pool_stalled vaut true. Vérifiez l'objet plugins dans la réponse de santé pour identifier le plugin défaillant :
curl -s http://localhost:9090/health | jq '.plugins'Impossible d'atteindre le serveur interne depuis l'extérieur du conteneur
Le serveur est lié à 127.0.0.1, qui n'est accessible que depuis l'intérieur du conteneur.
Correction : Passez à 0.0.0.0 pour un accès externe et définissez INTERNAL_ALLOW_IPS. Les sondes Kubernetes httpGet nécessitent la même modification, car elles n'atteignent pas un écouteur loopback.
Les métriques n'affichent pas le mode worker ou les métriques async
Les métriques du mode worker n'apparaissent que lorsque le mode worker est activé (WORKER_MODE_ENABLED=true). Les métriques async n'apparaissent que lorsque ASYNC_WORKERS > 0 et qu'au moins une tâche a été distribuée ou rejetée.
Voir aussi
- Métriques — référence complète des métriques Prometheus
- Contrôles de santé — comportement détaillé des contrôles de santé
- Référence de configuration — toutes les variables d'environnement
- Arrêt gracieux — séquence d'arrêt et cycle de vie du serveur interne