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.

Note

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 :

json
{ "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 :

json
{ "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.

bash
curl http://localhost:9090/metrics
text
# 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.

bash
curl -s http://localhost:9090/config | jq .
json
{ "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 :

yaml
readinessProbe: httpGet: path: /health/readiness port: 9090 initialDelaySeconds: 2 periodSeconds: 5

Lorsqu'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 :

yaml
livenessProbe: httpGet: path: /health/liveness port: 9090 initialDelaySeconds: 5 periodSeconds: 10 failureThreshold: 3

Sonde de démarrage

Pour les applications au démarrage lent (frameworks volumineux, autoloaders lourds) :

yaml
startupProbe: httpGet: path: /health/startup port: 9090 initialDelaySeconds: 1 periodSeconds: 2 failureThreshold: 15

Sé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_ADDR ne comportant qu'un port (:9090) se lie déjà à 127.0.0.1 ; un 127.0.0.1:9090 explicite maintient le port accessible uniquement depuis l'intérieur du conteneur ou de l'hôte
  • Définissez INTERNAL_ALLOW_IPS lorsque l'écouteur doit être accessible hors de l'hôte — une liste d'autorisation CIDR/IP qui renvoie 403 sur /metrics, /config et 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 donc 127.0.0.1/32 si 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 containerPort mais 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é
Warning

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.

compose.yaml
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:9090

Dé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 :

bash
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 :

bash
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

Une erreur ? Signalez-la →