Métriques Prometheus
OxPHP expose des métriques compatibles Prometheus au format d'exposition texte via GET /metrics sur le serveur interne. Elles couvrent le débit des requêtes, les temps de réponse, l'état des connexions, la santé du pool de workers, la mise en cache des fichiers statiques, l'efficacité de la compression et les performances du mode worker.
Activation des métriques
Définissez INTERNAL_ADDR pour démarrer le serveur interne :
INTERNAL_ADDR=127.0.0.1:9090Ensuite, effectuez le scraping depuis Prometheus ou tout collecteur compatible :
curl http://localhost:9090/metricsMétriques du serveur
| Métrique | Type | Description |
|---|---|---|
oxphp_uptime_seconds |
gauge | Secondes écoulées depuis le démarrage du processus serveur |
oxphp_requests_total |
counter | Total des requêtes HTTP reçues sur le port principal |
Métriques de requêtes
| Métrique | Type | Description |
|---|---|---|
oxphp_requests_by_method_total |
counter | Requêtes par méthode HTTP. Label : method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT, QUERY, OTHER) |
oxphp_responses_by_status_total |
counter | Réponses par classe de statut. Label : status (1xx, 2xx, 3xx, 4xx, 5xx) |
oxphp_request_bytes_total |
counter | Total des octets de corps de requête reçus |
oxphp_response_bytes_total |
counter | Total des octets de corps de réponse envoyés |
oxphp_request_cancelled_total |
counter | Requêtes annulées par raison. Label : reason (client_abort, timeout, shutdown). Comptée une fois pour chaque requête que le serveur distribue (pas seulement celles qui atteignent PHP), au moment où cette requête reçoit sa réponse. client_abort couvre donc un client qui a raccroché pendant que sa requête était encore en file (le cas courant) aussi bien qu'un client qui a raccroché pendant l'exécution de son gestionnaire ; un client qui raccroche une fois que l'envoi de la réponse a commencé a dépassé ce point et n'est pas compté. Toujours émise |
Seules les méthodes et classes de statut ayant au moins un événement enregistré sont émises. Les labels à valeur nulle sont omis.
Histogramme de durée de requête
| Métrique | Type | Description |
|---|---|---|
oxphp_request_duration_us |
histogram | Durée de requête de bout en bout en microsecondes pour toutes les requêtes (fichiers statiques et PHP) |
Limites des buckets (microsecondes) : 100, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000, 250000, 500000, 1000000, +Inf.
Utilisez cet histogramme pour suivre la latence globale, identifier les endpoints lents et mesurer les percentiles de latence de queue.
Métriques de connexion
| Métrique | Type | Description |
|---|---|---|
oxphp_active_connections |
gauge | Connexions TCP actuellement ouvertes sur le port principal |
oxphp_accept_stalled |
gauge | 1 tant que la boucle d'acceptation est mise en attente d'un permis MAX_CONNECTIONS libre. Les connexions sont acceptées par le noyau mais plus rien de nouveau n'est servi, si bien que, du point de vue d'un client, le serveur a cessé de répondre. Alertez directement sur oxphp_accept_stalled == 1 ; aucune comparaison avec le budget configuré n'est nécessaire |
oxphp_accept_stalls_total |
counter | Connexions qui ont dû attendre un permis MAX_CONNECTIONS après avoir été acceptées. Bouge même quand un blocage commence et se termine entre deux scrapings, ce que la jauge seule manquerait : rate(oxphp_accept_stalls_total[5m]) > 0 signifie que le budget s'épuise, si brièvement que ce soit. Relevez MAX_CONNECTIONS ou réduisez l'arriéré PHP (QUEUE_CAPACITY + QUEUE_MAX_WAITING) ; voir Garder de la marge pour la boucle d'acceptation |
oxphp_pending_requests |
gauge | Requêtes PHP acceptées mais pas encore répondues — en attente d'une place dans la file, en file, ou en cours d'exécution. Uniquement les requêtes routées vers PHP : un fichier statique, une 404 ou un chemin refusé reçoit sa réponse sans passer par la file et n'apparaît jamais ici |
oxphp_dropped_requests_total |
counter | Requêtes pour lesquelles le worker PHP a échoué après avoir accepté la requête |
oxphp_admission_refused_total |
counter | Requêtes répondues sans atteindre un worker. Label : reason, avec les valeurs wait_timeout (a épuisé tout son budget d'attente, qui vaut QUEUE_WAIT_TIMEOUT_MS ou moins lorsque oxphp_admission_wait_budget_us montre que le serveur l'a raccourci. Donnez plus de marge au pool, mais lisez d'abord oxphp_admission_wait_wasted_total : si le pool n'a rien commencé pendant ces attentes, le remède est l'inverse. Une requête dont le client était déjà parti quand le budget s'est épuisé n'est pas comptée ici : elle reçoit un 499 et est comptée dans oxphp_request_cancelled_total{reason="client_abort"}, si bien que cette série reste un constat sur le pool), waiting_full (déjà QUEUE_MAX_WAITING requêtes en attente, relevez-le ou relevez MAX_CONNECTIONS), waiting_bytes (les corps déjà stationnés remplissent QUEUE_MAX_WAITING_BYTES, le corps de cette requête n'avait donc nulle part où se poser ; relevez-le, ou abaissez ce qu'un client peut téléverser), queue_full (QUEUE_WAIT_TIMEOUT_MS=0, l'attente est désactivée), shutting_down (le délai de drainage a expiré pendant que la requête attendait encore son admission), pool_unavailable (il ne reste aucun thread worker à qui remettre la requête : le pool a disparu, il n'est pas simplement occupé). Seules les quatre premières relèvent de la surcharge et répondent 529 ; shutting_down répond 503 comme le reste du drainage progressif, et pool_unavailable répond 500. Alertez sur la surcharge avec ces quatre-là précisément : la métrique dans son ensemble bouge aussi lors d'un redémarrage. Exclues de oxphp_queue_wait_us |
oxphp_admission_wait_wasted_total |
counter | Requêtes qui ont épuisé tout leur budget d'attente pendant que le pool ne commençait aucune requête. Sous-ensemble de oxphp_admission_refused_total{reason="wait_timeout"}, qui distingue une attente perdue face à d'autres requêtes en concurrence pour un worker d'une attente pendant laquelle le pool n'a rien démarré. Les requêtes dont le client était déjà parti sont exclues, comme ci-dessus. Ne compte que tant que oxphp_admission_wait_budget_us est à son plafond. Une valeur non nulle soutenue signifie qu'attendre ne rapporte rien : raccourcissez QUEUE_WAIT_TIMEOUT_MS. Voir Attentes gaspillées |
oxphp_queue_depth |
gauge | Requêtes présentes dans la file des workers à cet instant : admises, pas encore prises en charge par un worker. Une entrée ne quitte la file que lors d'une prise en charge, si bien que ce nombre compte les requêtes déjà répondues à leur échéance (529, ou 499 si le client était parti) à côté de celles qui attendent encore ; une profondeur supérieure à oxphp_pending_requests correspond à cette différence. Présente uniquement pour l'exécuteur SAPI, le seul à avoir une file |
oxphp_queue_capacity |
gauge | Nombre total de places dans la file : QUEUE_CAPACITY, la borne à laquelle se lit oxphp_queue_depth |
oxphp_admission_slots_available |
gauge | Permis d'admission que personne ne détient. Une requête en prend un avant d'entrer dans la file et le rend quand un worker la prend en charge ; c'est donc la capacité libre qu'une nouvelle arrivée peut réclamer sans attendre. Pendant un drainage progressif, la porte est fermée et n'admet rien, quelle que soit la valeur affichée |
oxphp_admission_wait_budget_us |
gauge | Durée pendant laquelle une requête arrivant maintenant peut attendre un worker, en microsecondes (l'unité de oxphp_queue_wait_us, qu'elle borne). QUEUE_WAIT_TIMEOUT_MS est le plafond de cette valeur : le serveur la divise par deux tant que le travail admis par l'attente s'achève pour des clients déjà partis, et la fait remonter progressivement une fois la file vidée et quand moins d'un quart de ce que le pool démarre va à des clients partis. En dessous du plafond, cela signifie qu'on propose au pool plus qu'il ne peut servir, pour des clients moins patients que le plafond. Rester au plancher (un soixante-quatrième du plafond, porté à 10 ms sauf si le plafond lui-même est plus court) pendant tout un épisode est normal ; y rester avec oxphp_queue_depth à zéro ne l'est pas. 0 en mode échec immédiat (QUEUE_WAIT_TIMEOUT_MS=0). Absente pour un exécuteur sans file. Voir Le budget d'attente adaptatif |
oxphp_admission_wait_budget_ceiling_us |
gauge | QUEUE_WAIT_TIMEOUT_MS en microsecondes : la valeur la plus longue que la jauge ci-dessus peut jamais prendre, constante pendant toute la vie du processus. Elle permet à une alerte de comparer le budget à son plafond à partir de ce seul endpoint plutôt qu'à un nombre codé en dur, sur le même principe que oxphp_queue_depth avec oxphp_queue_capacity. 0 en mode échec immédiat. Absente pour un exécuteur sans file |
oxphp_abandoned_work_total |
counter | Requêtes qu'un worker a retirées de la file alors que leur client était déjà parti. Le budget d'attente ci-dessus est ajusté en fonction de ce compte. Une hausse pendant que le budget baisse, c'est le contrôleur qui fait son travail ; une hausse avec le budget toujours à son plafond signifie que les clients partent pendant l'exécution de leur gestionnaire plutôt que pendant leur attente, et ce n'est donc pas l'attente qu'il faut raccourcir. C'est un compte de requêtes, pas de CPU gaspillé. Les requêtes refusées à leur échéance ne sont pas comptées (elles ne se sont jamais exécutées), pas plus que celles dont la réponse est partie avant la fin du gestionnaire (un flux dont les en-têtes ont été envoyés, ou une réponse anticipée via oxphp_finish_request()), si bien que la fin de chaque session SSE n'est pas comptée. Seul un client qui ferme sa connexion est visible ici |
oxphp_pool_stalled |
gauge | 1 tant que des requêtes attendent des workers qui sont inactifs et n'accomplissent rien, c'est-à-dire un pool qui a cessé de retirer du travail de sa file. Positionnée une fois que l'état a duré une minute (assez longtemps pour qu'un worker qui recharge l'application après un recyclage ne la déclenche pas) ; remise à zéro par un worker qui termine une requête, ou par une minute pendant laquelle le pool n'a pas du tout semblé bloqué. Alertez directement sur oxphp_pool_stalled == 1 ; la sonde de disponibilité répond 503 sur ce même état. Exportée en mode worker uniquement ; absente plutôt qu'à 0 là où l'état n'est pas surveillé. Voir Le signal de pool bloqué |
Lire la profondeur de file face aux places d'admission
Les deux jauges répondent à deux moitiés d'une même question : non pas quelle est la profondeur de la file, mais est-ce que quelque chose avance. oxphp_queue_depth indique ce qui attend ; oxphp_admission_slots_available indique si l'on peut encore laisser entrer quelque chose. Lisez les deux face à oxphp_busy_workers.
| Places disponibles | Profondeur de file | Lecture |
|---|---|---|
> 0 |
0 |
Rien n'attend ; normal |
> 0 |
> 0 et en baisse entre les scrapings |
Arriéré ordinaire : le pool est en retard mais le résorbe |
> 0 |
> 0 et pas en baisse, avec oxphp_busy_workers à 0 |
Le pool a cessé de retirer du travail de la file alors que l'admission a encore de la place. C'est un blocage dans sa première phase. Les arrivées sont admises et chacune reçoit un 529 quand son propre budget s'épuise (ou un 499 si son client a abandonné avant) ; oxphp_admission_wait_wasted_total augmente à chaque 529 tant que le budget est à son plafond. Rien d'autre ne le trahit : aucun worker n'est occupé, et les refus, pris isolément, ressemblent à une surcharge |
0 |
à oxphp_queue_capacity |
Le même blocage une fois la file remplie, ou une surcharge ordinaire sur laquelle le pool a pris beaucoup de retard. oxphp_busy_workers à 0 de façon soutenue d'un scraping à l'autre les distingue ; un échantillon isolé peut aussi se lire ainsi sous surcharge |
0 |
à 0 ou presque |
Des permis détenus hors de la file. Sur un seul scraping, il peut s'agir de requêtes entre l'admission et la distribution ; soutenu d'un scraping à l'autre, ce sont des permis pris et jamais rendus : plus rien ne peut être admis, et le serveur refuse toute requête PHP jusqu'à son redémarrage |
Les trois dernières lignes sont les formes que prend un pool bloqué. Dans chacune d'elles, le reste du tableau semble sain alors qu'aucune requête PHP ne se termine : aucun worker occupé, 200 de la sonde de vivacité tout du long et de la sonde de disponibilité pendant la première minute, fichiers statiques servis normalement. Les compteurs de refus bougent, mais ils bougent aussi sous une surcharge ordinaire. oxphp_pending_requests monte avec la file bloquée puis redescend à mesure que chaque arrivée reçoit sa réponse sur son propre budget ; pris isolément, il ne veut donc rien dire ici. Lu face à oxphp_queue_depth, si : des requêtes en attente proches de zéro alors que la profondeur reste à la capacité, c'est le blocage qui retient tout ce qu'on lui a jamais confié.
Un blocage est précisément la situation qui pousse les clients à partir. Derrière un répartiteur de charge dont le délai de lecture est plus court que QUEUE_WAIT_TIMEOUT_MS, les arrivées deviennent des 499 qui ne font bouger aucune des deux séries de refus, si bien que oxphp_admission_wait_wasted_total peut rester muet pendant tout l'incident. Ces arrivées atterrissent dans oxphp_request_cancelled_total{reason="client_abort"}, et oxphp_queue_depth tient dans tous les cas.
Le serveur remarque aussi un blocage. Chaque fois que deux analyses consécutives d'une seconde voient du travail en attente (des requêtes dans la file, ou des refus en hausse) avec au moins un worker inactif et un pool qui n'a rien terminé, il journalise :
{"timestamp":"2026-08-28T10:56:59.015691Z","level":"WARN","fields":{"message":"PHP requests are waiting while the pool has idle workers and got nothing done since the last scan","queue_depth":22,"queue_capacity":512,"admission_slots_available":490,"workers_idle":4}}Le message se répète environ une fois par minute tant que l'état dure, et INFO PHP pool is reaching workers again suit une fois que le travail reprend. Les champs se trouvent sous fields, donc une règle d'alerte doit viser .fields.queue_depth, et non .queue_depth. Trois garde-fous évitent les faux signalements : deux analyses plutôt qu'une ; une progression mesurée à ce que les workers ont terminé plutôt qu'à ce que les clients ont reçu, si bien qu'une avalanche d'abandons clients se lit comme le pool occupé qu'elle est ; et aucun signalement tant que le pool n'a pas terminé sa première requête, pour que l'amorçage au démarrage ne soit pas pris pour un blocage.
Ce dernier garde-fou ne couvre que le premier amorçage du processus. Un worker recyclé plus tard (sur le plafond mémoire ou via Worker::scheduleExit()) amorce à nouveau son application et apparaît inactif jusqu'à ce qu'il prenne sa première requête ; un amorçage lent sur un pool d'un seul worker (ou sur un pool plus grand dont tous les autres workers sont occupés) produit donc un avertissement intempestif suivi de son avis de rétablissement. Un oxphp_workers_spawned_total qui bouge au même moment permet de l'identifier.
L'avertissement n'est émis qu'en mode worker, puisque seul ce pool tient le compte de ce que les workers ont terminé ; l'état qu'il décrit ne peut pas se produire là où un worker se bloque lui-même sur la file. Les trois jauges sont exportées dans tous les modes.
Attentes gaspillées
oxphp_admission_wait_wasted_total compte le travail commencé plutôt que les entrées qui quittent la file, car un worker qui évacue des requêtes auxquelles leur échéance a déjà répondu fait bouger la file sans servir personne. Il est conditionné au fait que le budget soit à son plafond parce qu'une fois que le serveur a raccourci le budget, « le pool n'a rien commencé » en dit plus sur la fenêtre que sur le pool : un pool sain de quatre workers avec des gestionnaires d'un quart de seconde démarre une requête toutes les 62 ms, si bien qu'au plancher la plupart des attentes se terminent entre deux démarrages.
Ce que coûte cette condition dépend de si le budget bouge :
- Un pool qui ne démarre rien n'atteint jamais le nombre de requêtes démarrées sur lequel le contrôleur fonde sa décision ; il garde donc son plafond et ce compteur reste armé.
- Un pool qui s'appelle lui-même démarre une requête chaque fois qu'une requête externe renonce à son appel interne. Dès que ses propres appelants sont moins patients que ses gestionnaires, le budget baisse (mesuré : deux divisions par deux en quelques secondes), et ce compteur se tait après les arrivées antérieures à la baisse. Rien sur cette page ne prend le relais à ce stade : cherchez le motif dans les appels sortants de l'application elle-même. Là où les appelants sont patients (un cron interne, un sidecar avec un délai généreux), rien n'est abandonné, le plafond tient, et c'est ce compteur qui le révèle.
La cause classique est une application qui rappelle ce même serveur en HTTP, mais le compteur n'est pas un détecteur d'auto-appel : un pool dont tous les gestionnaires durent plus longtemps que le budget ne libère rien pendant une attente non plus. En mode worker, oxphp_worker_request_duration_us (temps du gestionnaire, attente en file exclue) les distingue : des gestionnaires en dessous du budget pendant que ce compteur augmente signifient que les attentes ne sont pas simplement en file derrière du travail lent. N'utilisez pas oxphp_request_duration_us pour cela : elle inclut ces refus, chacun contribuant presque un budget entier. Hors mode worker, utilisez les mesures de temps propres à l'application.
QUEUE_WAIT_TIMEOUT_MS=0 n'est pas la version la plus courte d'un budget raccourci : il supprime l'attente d'une place dans la file et laisse l'attente dans la file sans limite, si bien que sur un pool qui ne prend rien en charge, une requête qui trouve une place libre ne reçoit jamais de réponse. Associez 0 à un QUEUE_CAPACITY assez petit pour que la file soit pleine au moment où cela compte.
Ce que dit le journal pendant le délestage des requêtes
Le superviseur surveille les quatre raisons de surcharge de oxphp_admission_refused_total lors de la même analyse d'une seconde et signale l'épisode plutôt que les requêtes individuelles, car une ligne par refus serait elle-même la panne. La première analyse qui voit l'une d'elles bouger journalise :
{"timestamp":"2026-09-16T11:02:41.337104Z","level":"WARN","fields":{"message":"PHP admission has started shedding requests under overload","refused":4812,"refused_queue_full":0,"refused_wait_timeout":4812,"refused_waiting_full":0,"refused_waiting_bytes":0,"queue_depth":896,"queue_capacity":896,"admission_slots_available":0,"wait_budget_us":15625,"wait_budget_ceiling_us":1000000}}refused correspond à ce que cette analyse-là a refoulé, sauf lors de la première analyse qui a une file à examiner, qui signale tout ce qui a été délesté avant elle (un pool encore en train de charger son application pendant que les arrivées s'accumulent constitue un véritable épisode). Une profondeur à la capacité sans aucune place libre, c'est un pool en retard sur son travail, et un wait_budget_us bien inférieur à wait_budget_ceiling_us indique que le serveur a déjà raccourci l'attente ; les deux nombres sont présents parce qu'un budget poussé à son plancher et un budget configuré aussi bas se ressemblent pris isolément. Les chiffres de la file sont un instantané pris lors de l'analyse, tandis que refused couvre la seconde qui vient de s'écouler ; un refused élevé à côté d'une file vide est donc une rafale brève, pas une contradiction.
Les deux comptes sont ventilés par raison, selon les quatre labels de surcharge de oxphp_admission_refused_total, avec des noms de champs construits à partir du compte qu'ils décomposent : refused_queue_full, refused_wait_timeout, refused_waiting_full et refused_waiting_bytes ont pour somme refused, et la répétition comme la ligne de clôture ci-dessous portent episode_refused_queue_full et ses trois pendants, dont la somme est episode_refused. C'est la raison qui détermine le réglage à ajuster (la ligne du compteur ci-dessus indique ce que chacune désigne), et ce compteur n'est servi que sur INTERNAL_ADDR. Sur une instance sans écouteur interne, ces champs et l'attribut de span oxphp.shed_reason, là où le traçage est actif, sont les endroits où la raison reste lisible. Les quatre sont écrits même à zéro : un 0 explicite est ce qui permet d'écarter une raison.
Tant que l'épisode dure, l'avertissement se répète sous la forme PHP admission is still shedding requests under overload, pas plus d'une fois par minute et uniquement lors d'une analyse qui refuse elle-même des requêtes (l'intervalle peut donc atteindre presque deux minutes), avec episode_refused (le total cumulé) à côté du refused par analyse. Ce que la répétition ventile par raison, c'est le total de l'épisode, pas celui de l'analyse. C'est important parce que la ligne de clôture est écrite au niveau INFO, en dessous du warn auquel journalisent les configurations de production : là, la répétition est la seule ligne qui dit de quoi l'épisode a été fait jusqu'ici, jusqu'à l'analyse lors de laquelle elle a été écrite. Un épisode qui se termine dans sa première minute n'a aucune répétition, et ce qui est refusé après la dernière répétition n'apparaît que dans la ligne de clôture. Une minute sans aucun refus met fin à l'épisode, au niveau INFO :
{"timestamp":"2026-09-16T11:05:52.812440Z","level":"INFO","fields":{"message":"PHP admission has stopped shedding requests","episode_refused":18374,"episode_refused_queue_full":0,"episode_refused_wait_timeout":17950,"episode_refused_waiting_full":424,"episode_refused_waiting_bytes":0,"duration_ms":131000,"queue_depth":0,"admission_slots_available":896}}duration_ms va jusqu'au dernier refus, pas jusqu'à la ligne, si bien que la minute calme de clôture n'est pas comptée. Cette minute calme signifie aussi que les interruptions de moins d'une minute sont regroupées en un seul épisode, ce qui limite la sortie à deux lignes par minute au pire ; l'entrée dans l'épisode n'a pas ce délai, puisqu'un refus est un fait et non un échantillon.
Le signalement est émis dans tous les modes de routage et ne réagit qu'aux quatre raisons de surcharge, si bien qu'un redémarrage (shutting_down) ou un pool disparu (pool_unavailable) ne s'annonce pas comme de la charge. Il n'existe aucune jauge pour cet état, et aucune sonde n'en est informée : le délestage ne change jamais ce que répond /health/readiness, car une instance qui déleste des 529 répond rapidement sur un pool qui fonctionne, et retirer tour à tour de la rotation chaque réplique surchargée laisserait le Service sans aucun endpoint. Répondez à la surcharge en ajoutant de la capacité. Un pool bloqué déleste lui aussi, et en mode worker cet état-là répond bien 503 une fois qu'il a duré une minute. La file distingue les deux : un pool simplement en retard la vide, un pool bloqué non.
Métriques du pool de workers
| Métrique | Type | Description |
|---|---|---|
oxphp_workers_current |
gauge | Nombre actuel de threads worker PHP |
oxphp_workers_min |
gauge | Nombre minimal de workers (égal au nombre actuel en mode statique) |
oxphp_workers_max |
gauge | Nombre maximal de workers (égal au nombre actuel en mode statique) |
oxphp_workers_idle |
gauge | Threads worker sans aucune requête en cours, calculé comme workers_current - busy_workers |
oxphp_busy_workers |
gauge | Threads worker exécutant actuellement au moins une requête ; ne dépasse jamais oxphp_workers_current. Compte des threads, pas des requêtes — en mode worker, un thread multiplexe de nombreuses Fibers de requête et ne compte qu'une fois. Les requêtes en attente d'admission ou assises dans la file ne sont pas comptées ; celles-là apparaissent dans oxphp_pending_requests |
oxphp_workers_spawned_total |
counter | Total des workers créés depuis le démarrage (inclut les workers initiaux) |
oxphp_workers_retired_total |
counter | Total des workers retirés en raison d'un délai d'inactivité (mode dynamique uniquement) |
Métriques du superviseur de workers
Observabilité par worker émise par le superviseur de workers. Chaque série porte un label worker_id (index de slot). Ces métriques apparaissent une fois que le superviseur suit l'état de chaque worker.
| Métrique | Type | Description |
|---|---|---|
oxphp_worker_request_age_seconds |
gauge | Âge de la requête en cours de traitement sur chaque worker, en secondes. Label : worker_id |
oxphp_worker_long_running_total |
counter | Analyses du superviseur ayant observé une requête plus ancienne que le seuil de blocage (60 secondes, fixe). Label : worker_id |
oxphp_worker_stuck_total |
counter | Compteur de classification de blocage par worker. Labels : worker_id, kind (io, c_call, cpu). Signalement uniquement : une requête classée comme bloquée n'est pas annulée et son worker n'est pas recyclé. Rien dans le serveur n'y réagit, alertez donc sur cette série |
Histogramme d'attente en file
| Métrique | Type | Description |
|---|---|---|
oxphp_queue_wait_us |
histogram | Temps qu'une requête attend dans la file avant qu'un worker ne la prenne en charge, en microsecondes |
Limites des buckets (microsecondes) : 50, 100, 250, 500, 1000, 2500, 5000, 10000, 50000, 100000, 250000, 500000, 1000000, +Inf.
Ceci mesure le temps passé à attendre, pour l'admission puis dans la file, jusqu'au moment où un worker retire la requête de la file, si bien qu'il répond à « combien de temps avant qu'un worker ne prenne cette requête » plutôt qu'à « combien de temps la requête a-t-elle pris ». Rien de ce qui suit ce moment n'y figure : la préparation de la requête par le worker, le script (jusqu'à l'en-tête de la réponse, pour une réponse en flux) et la remise de la réponse aux threads d'E/S du serveur comptent tous dans oxphp_request_duration_us. Cette remise n'est pas gratuite : la réponse attend qu'un thread d'E/S soit réveillé ou vienne à bout du travail en file devant elle, si bien que, sur un script peu coûteux, le temps écoulé après cette attente peut représenter plusieurs fois celui du script lui-même.
Seule une requête qu'un worker a retirée de la file dans les limites de son budget est enregistrée ici. Un 529 refusé pour surcharge, ou un 503/500 lorsque le serveur s'arrête ou que le pool a disparu, est compté dans oxphp_admission_refused_total ; un 499 pour un client parti avant qu'un worker ne l'atteigne est compté dans oxphp_request_cancelled_total{reason="client_abort"} et dans aucune série de refus.
Une attente en file élevée signifie que les requêtes ont mis longtemps à atteindre un worker, ce qui n'est pas en soi un manque de workers : chaque mesure inclut le temps qu'il faut à un thread worker pour arriver à une requête qui lui a été remise, et ce temps existe aussi sur un serveur sans charge. Avant d'augmenter PHP_WORKERS, vérifiez que les requêtes trouvent tous les workers occupés et que la machine a des cœurs disponibles : les threads d'E/S du serveur lui-même, d'autres processus et un quota CPU de conteneur sont en concurrence avec les workers. oxphp_busy_workers face à oxphp_workers_current ne répond qu'en partie à la première question : cette valeur est lue au moment du scraping, donc une rafale qui remplit le pool entre deux scrapings apparaît ici et pas là, et en mode worker un thread compte comme occupé tant qu'il porte une requête, même pendant que le gestionnaire de cette requête est suspendu et que le thread en prend d'autres. La plage atteint une seconde, en accord avec le QUEUE_WAIT_TIMEOUT_MS par défaut, si bien qu'une requête qui a dépensé l'essentiel de son budget d'attente avant de s'exécuter est quantifiée plutôt que reléguée dans +Inf. Rien de ce qui est servi n'attend plus longtemps que le budget (au-delà, la requête est refusée), donc relever QUEUE_WAIT_TIMEOUT_MS est le seul réglage qui renvoie des attentes dans +Inf.
Métriques de limitation de débit
| Métrique | Type | Description |
|---|---|---|
oxphp_rate_limited_total |
counter | Requêtes rejetées par le limiteur de débit (429 renvoyé) |
oxphp_php_deny_total |
counter | Requêtes bloquées par PHP_DENY_PATHS (exécution .php refusée). Voir Liste de refus d'exécution PHP |
oxphp_path_deny_total |
counter | Requêtes refusées par une règle .oxphpdeny. Une requête que PHP_DENY_PATHS refuse également n'est comptée qu'ici. Voir Règles de refus de chemins |
Métriques du cache de fichiers statiques
| Métrique | Type | Description |
|---|---|---|
oxphp_static_cache_hits_total |
counter | Requêtes de fichiers statiques servies depuis le cache en mémoire |
oxphp_static_cache_misses_total |
counter | Requêtes de fichiers statiques ayant nécessité une lecture disque |
Métriques de compression
| Métrique | Type | Description |
|---|---|---|
oxphp_compressed_responses_total |
counter | Réponses envoyées sous un codage de contenu (Brotli, zstd ou gzip) |
oxphp_compression_bytes_saved_total |
counter | Total des octets économisés par la compression (taille originale moins taille compressée) |
Métriques du mode worker
Ces métriques ne sont émises que lorsque le mode worker est actif (WORKER_MODE_ENABLED=true).
Compteurs globaux
| Métrique | Type | Description |
|---|---|---|
oxphp_worker_mode_enabled |
gauge | Toujours 1 lorsque le mode worker est actif |
oxphp_worker_requests_handled_total |
counter | Total des requêtes traitées par les workers persistants |
oxphp_worker_recycles_total |
counter | Total des recyclages de workers : la boucle d'un worker s'est terminée pour l'une des raisons ci-dessous. Un pool dynamique qui retire un worker inactif réduit sa taille plutôt que de recycler : c'est compté dans oxphp_workers_retired_total, ni ici ni dans la série ci-dessous |
oxphp_worker_recycles_by_reason_total |
counter | Recyclages par raison. Label : reason (scheduled, max_memory, error) |
oxphp_worker_soft_resets_total |
counter | Total des réinitialisations souples effectuées entre les requêtes |
Jauges par worker
| Métrique | Type | Description |
|---|---|---|
oxphp_worker_memory_bytes |
gauge | Tas PHP détenu par chaque worker à la fin de sa dernière requête. C'est ce que renvoie memory_get_usage() dans un gestionnaire, et la quantité à laquelle se mesure WORKER_MAX_MEMORY_MIB. La valeur est lue juste avant cette vérification du plafond, pendant que les superglobales de la requête terminée sont encore sur le tas ; elle se situe donc quelques Ko au-dessus du chiffre qui déclenche un recyclage : alertez en dessous du plafond plutôt qu'à son niveau. Un worker qui n'a encore servi aucune requête affiche zéro. Label : worker (index de slot, p. ex. "0", "1" ; un worker recyclé réutilise le slot de son prédécesseur et sa dernière valeur jusqu'à ce qu'il termine une requête à lui) |
oxphp_worker_uptime_seconds |
gauge | Secondes écoulées depuis la création de chaque worker. Label : worker |
oxphp_worker_requests_count |
gauge | Requêtes traitées par chaque instance de worker. Label : worker |
oxphp_worker_request_fibers_active |
gauge | Fibers de requête que le worker porte en ce moment : 0 inactif, 1 pour une requête servie du début à la fin, davantage lorsque des requêtes se suspendent et sont multiplexées sur le thread. Rafraîchie à chaque tour de la boucle du worker plutôt qu'à la fin d'une requête, elle ne se fige donc pas sur sa dernière valeur saine sur un worker qui a cessé de terminer des requêtes. Un worker admet au plus 256 Fibers de requête, et un worker bloqué à 256 a cessé de retirer du travail de la file. Lisez-la avec oxphp_busy_workers : 256 Fibers sur un worker que le pool compte comme inactif sont des Fibers qui ont survécu à leurs requêtes (déjà répondues) et n'ont jamais été récupérées, alors que 256 sur un worker occupé correspondent à autant de requêtes en cours. Label : worker |
Histogramme de durée de requête des workers
| Métrique | Type | Description |
|---|---|---|
oxphp_worker_request_duration_us |
histogram | Temps d'exécution du gestionnaire PHP par requête en microsecondes (mode worker uniquement) |
Limites des buckets (microsecondes) : 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, +Inf.
Cet histogramme mesure le temps passé à l'intérieur du callback du gestionnaire PHP, hors temps d'attente en file. Utilisez-le pour identifier les gestionnaires lents et suivre la latence de queue en mode worker.
Métriques du pool asynchrone
Ces métriques nécessitent que ASYNC_WORKERS soit défini à une valeur non nulle, et chacune possède sa propre condition d'émission : les compteurs n'apparaissent qu'après qu'au moins une tâche a été distribuée ou rejetée, les jauges _in_flight / _in_flight_limit apparaissent une fois que le pool a câblé son compteur de tâches en cours, et oxphp_async_output_discarded_bytes_total n'apparaît qu'après que de la sortie a été rejetée.
| Métrique | Type | Description |
|---|---|---|
oxphp_async_tasks_dispatched_total |
counter | Total des tâches asynchrones distribuées au pool d'arrière-plan |
oxphp_async_tasks_completed_total |
counter | Tâches asynchrones terminées avec succès |
oxphp_async_tasks_failed_total |
counter | Tâches asynchrones ayant levé une exception |
oxphp_async_tasks_cancelled_total |
counter | Tâches asynchrones annulées |
oxphp_async_tasks_rejected_total |
counter | Tâches asynchrones rejetées à la distribution — parce que la file du pool était pleine ou que le plafond de tâches en cours (ASYNC_MAX_FIBERS × ASYNC_WORKERS) était atteint |
oxphp_async_tasks_stranded_total |
counter | Workers laissés en exécution au-delà d'un délai d'expiration await_race / await_any. Chaque tâche abandonnée peut prolonger RSHUTDOWN jusqu'à 5 secondes. |
oxphp_async_tasks_in_flight |
gauge | Tâches asynchrones actuellement en file ou en cours d'exécution (émise une fois que le pool câble son compteur de tâches en cours) |
oxphp_async_tasks_in_flight_limit |
gauge | Nombre maximal de tâches asynchrones concurrentes (ASYNC_MAX_FIBERS × ASYNC_WORKERS) |
oxphp_async_output_discarded_bytes_total |
counter | Octets de sortie de tâche asynchrone rejetés à l'inactivité du worker (un echo dans une tâche asynchrone n'a aucun client pour le recevoir) |
Métriques d'export OpenTelemetry
Ces métriques nécessitent OTEL_ENABLED=true. Elles comptent les lots de spans qu'OxPHP envoie au collecteur OTLP : les spans de requête, plus les spans APM lorsque le plugin APM est actif.
| Métrique | Type | Description |
|---|---|---|
oxphp_otel_export_failures_total |
counter | Exports de spans en échec, après la nouvelle tentative lorsqu'il y en a eu une. Leurs spans ne sont pas renvoyés |
oxphp_otel_export_failed_spans_total |
counter | Spans portés par ces exports en échec |
oxphp_otel_export_retries_total |
counter | Exports retentés une fois parce que la première tentative a échoué rapidement, sans réponse du collecteur |
Un export est retenté une fois lorsque sa première tentative échoue sans réponse du collecteur dans la moitié de OTEL_EXPORTER_OTLP_TIMEOUT ou dans une seconde, selon la durée la plus courte : la connexion a été réinitialisée ou fermée, ou n'a pas pu être établie. La cause habituelle est un répartiteur de charge ou un NAT qui a coupé la connexion pendant qu'elle était inactive entre deux exports. Un statut d'erreur renvoyé par le collecteur, comme une clé d'API rejetée ou une limitation de débit, n'est pas retenté : la spécification OTLP interdit de retenter la plupart d'entre eux, et l'exportateur ne transmet pas le délai que demande un collecteur qui limite le débit. Une nouvelle tentative qui livre le lot n'est comptée que dans oxphp_otel_export_retries_total. Une tentative qui atteint le délai d'expiration de l'export n'est pas retentée ; un collecteur lent ne reçoit donc jamais deux fois le même lot.
La nouvelle tentative peut toutefois dupliquer. Lorsque le collecteur a bien reçu la première tentative avant que la connexion ne se rompe, la nouvelle tentative livre ces spans une seconde fois, et pour la même raison un export compté comme en échec peut tout de même avoir atteint le collecteur.
Les spans que le traceur abandonne avant l'export parce que sa file est pleine (OTEL_BSP_MAX_QUEUE_SIZE, 2048 spans par défaut) ne sont pas comptés ici.
Conseils pour tableau de bord Grafana
Les requêtes PromQL suivantes sont utiles pour construire des tableaux de bord :
Taux de requêtes (requêtes par seconde) :
rate(oxphp_requests_total[5m])Temps de réponse moyen (millisecondes) :
rate(oxphp_request_duration_us_sum[5m])
/ rate(oxphp_requests_total[5m]) / 1000Durée de requête p99 (millisecondes) :
histogram_quantile(0.99, rate(oxphp_request_duration_us_bucket[5m])) / 1000Taux d'erreur (réponses 5xx en pourcentage) :
rate(oxphp_responses_by_status_total{status="5xx"}[5m])
/ rate(oxphp_requests_total[5m]) * 100Utilisation du pool de workers :
oxphp_busy_workers / oxphp_workers_currentC'est une vraie fraction entre 0 et 1. Des valeurs soutenues à 1 signifient que chaque worker est occupé et que les arrivées supplémentaires font la queue. Associez-la à rate(oxphp_admission_refused_total{reason=~"queue_full|wait_timeout|waiting_full|waiting_bytes"}[5m]) pour voir si cet arriéré se transforme en refus, et à oxphp_pending_requests pour en mesurer la profondeur.
Saturation de la file (taux d'abandon par seconde) :
rate(oxphp_dropped_requests_total[5m])Attente en file p99 (microsecondes) :
histogram_quantile(0.99, rate(oxphp_queue_wait_us_bucket[5m]))Taux de succès du cache de fichiers statiques :
rate(oxphp_static_cache_hits_total[5m])
/ (rate(oxphp_static_cache_hits_total[5m]) + rate(oxphp_static_cache_misses_total[5m]))Octets économisés par la compression par seconde :
rate(oxphp_compression_bytes_saved_total[5m])Latence p99 en mode worker (microsecondes) :
histogram_quantile(0.99, rate(oxphp_worker_request_duration_us_bucket[5m]))Budget d'attente poussé sous son plafond (fraction) :
oxphp_admission_wait_budget_us / oxphp_admission_wait_budget_ceiling_us1 est normal ; toute valeur inférieure signifie que le serveur raccourcit les attentes sous surcharge. Alertez sur le ratio plutôt que sur un nombre recopié de QUEUE_WAIT_TIMEOUT_MS.
Blocages de la boucle d'acceptation (alerter si non nul) :
rate(oxphp_accept_stalls_total[5m])Taux de recyclage des workers (par minute) :
rate(oxphp_worker_recycles_total[5m]) * 60Utilisation mémoire moyenne des workers :
avg(oxphp_worker_memory_bytes)Configuration de scraping Prometheus
Ajoutez un job de scraping à votre prometheus.yml :
scrape_configs:
- job_name: "oxphp"
scrape_interval: 15s
static_configs:
- targets: ["oxphp:9090"]Pour la découverte de services Kubernetes :
scrape_configs:
- job_name: "oxphp"
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
regex: oxphp
action: keep
- source_labels: [__meta_kubernetes_pod_ip]
target_label: __address__
replacement: "$1:9090"Prompts pour votre agent
Ces prompts supposent un agent connecté au serveur MCP de la documentation OxPHP — il lira lui-même les pages dont il a besoin.
Fais en sorte que Prometheus scrape mon service OxPHP. Lis la page `operations/metrics` via le serveur MCP de la documentation OxPHP.
Ajoute la configuration de scraping et ce qu'il faut à mes fichiers Compose ou Kubernetes pour que l'endpoint des métriques soit joignable, en utilisant les noms de services déjà présents dans ce dépôt.Construis-moi un tableau de bord Grafana de départ pour OxPHP. Lis la page `operations/metrics` via le serveur MCP de la documentation OxPHP.
Choisis les métriques qui me disent vraiment si mon application est en bonne santé, et explique-moi ce que chaque panneau m'indiquerait quand il bouge.Voir aussi
- Contrôles de santé — les endpoints
/healthet/configsur le serveur interne - Référence de configuration — toutes les variables d'environnement, y compris
INTERNAL_ADDR - Arrêt gracieux — comment le drainage des connexions affecte
oxphp_active_connections - Mode worker — les workers persistants et les métriques qu'ils émettent