Metryki Prometheus

OxPHP udostępnia metryki zgodne z Prometheus w tekstowym formacie ekspozycji pod GET /metrics na serwerze wewnętrznym. Obejmują one przepustowość żądań, czasy odpowiedzi, stan połączeń, kondycję puli workerów, buforowanie plików statycznych, wydajność kompresji oraz wydajność trybu worker.

Włączanie metryk

Ustaw INTERNAL_ADDR, aby uruchomić serwer wewnętrzny:

bash
INTERNAL_ADDR=127.0.0.1:9090

Następnie zbieraj dane z Prometheusa lub dowolnego kompatybilnego kolektora:

bash
curl http://localhost:9090/metrics

Metryki serwera

Metric Type Description
oxphp_uptime_seconds gauge Liczba sekund od uruchomienia procesu serwera
oxphp_requests_total counter Łączna liczba żądań HTTP odebranych na porcie głównym

Metryki żądań

Metric Type Description
oxphp_requests_by_method_total counter Żądania według metody HTTP. Etykieta: method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT, QUERY, OTHER)
oxphp_responses_by_status_total counter Odpowiedzi według klasy statusu. Etykieta: status (1xx, 2xx, 3xx, 4xx, 5xx)
oxphp_request_bytes_total counter Łączna liczba odebranych bajtów treści żądań
oxphp_response_bytes_total counter Łączna liczba wysłanych bajtów treści odpowiedzi
oxphp_request_cancelled_total counter Anulowane żądania według przyczyny. Etykieta: reason (client_abort, timeout, shutdown). Liczona raz dla każdego żądania, które serwer rozdziela (nie tylko tych, które docierają do PHP), w chwili udzielenia na nie odpowiedzi. Dlatego client_abort obejmuje klienta, który się rozłączył, gdy jego żądanie wciąż czekało w kolejce (typowy przypadek), jak i takiego, który rozłączył się w trakcie działania handlera; klient, który rozłącza się, gdy odpowiedź zaczęła już wychodzić, jest poza tym punktem i nie jest liczony. Zawsze emitowana
Note

Emitowane są tylko metody i klasy statusów, dla których zarejestrowano co najmniej jedno zdarzenie. Etykiety o zerowej liczbie są pomijane.

Histogram czasu trwania żądania

Metric Type Description
oxphp_request_duration_us histogram Czas trwania żądania end-to-end w mikrosekundach dla wszystkich żądań (pliki statyczne i PHP)

Granice przedziałów (mikrosekundy): 100, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000, 250000, 500000, 1000000, +Inf.

Użyj tego histogramu, aby śledzić ogólne opóźnienia, identyfikować wolne endpointy i mierzyć percentyle opóźnień ogona.

Metryki połączeń

Metric Type Description
oxphp_active_connections gauge Aktualnie otwarte połączenia TCP na porcie głównym
oxphp_accept_stalled gauge 1, dopóki pętla akceptująca jest zaparkowana w oczekiwaniu na wolne zezwolenie MAX_CONNECTIONS. Jądro przyjmuje połączenia, ale nic nowego nie jest obsługiwane, więc z punktu widzenia klienta serwer przestał odpowiadać. Alertuj bezpośrednio na oxphp_accept_stalled == 1; nie wymaga to porównania ze skonfigurowanym budżetem
oxphp_accept_stalls_total counter Połączenia, które po przyjęciu musiały czekać na zezwolenie MAX_CONNECTIONS. Rośnie nawet wtedy, gdy wstrzymanie zaczyna się i kończy między dwoma scrape'ami, co sam wskaźnik by przeoczył: rate(oxphp_accept_stalls_total[5m]) > 0 oznacza, że budżet jest wyczerpywany, choćby na krótko. Podnieś MAX_CONNECTIONS albo zmniejsz zaległość PHP (QUEUE_CAPACITY + QUEUE_MAX_WAITING); zobacz Zachowanie zapasu dla pętli akceptującej
oxphp_pending_requests gauge Żądania PHP przyjęte, ale jeszcze bez odpowiedzi — czekające na miejsce w kolejce, w kolejce albo wykonywane. Tylko żądania skierowane do PHP: plik statyczny, 404 lub odrzucona ścieżka są obsługiwane bez kolejki i nigdy się tu nie pojawiają
oxphp_dropped_requests_total counter Żądania, w których worker PHP zawiódł po przyjęciu żądania
oxphp_admission_refused_total counter Żądania, na które odpowiedziano bez dotarcia do workera. Etykieta: reason, o wartościach wait_timeout (odczekało cały swój budżet oczekiwania, czyli QUEUE_WAIT_TIMEOUT_MS albo mniej tam, gdzie oxphp_admission_wait_budget_us pokazuje, że serwer go skrócił. Daj puli więcej zapasu, ale najpierw sprawdź oxphp_admission_wait_wasted_total, bo jeśli pula w trakcie tych oczekiwań nie rozpoczęła niczego, właściwe lekarstwo jest odwrotne. Żądanie, którego klient zniknął już przed wyczerpaniem budżetu, nie jest tu liczone: dostaje odpowiedź 499 i trafia do oxphp_request_cancelled_total{reason="client_abort"}, dzięki czemu ta seria pozostaje stwierdzeniem o puli), waiting_full (czekało już QUEUE_MAX_WAITING żądań, podnieś tę wartość lub MAX_CONNECTIONS), waiting_bytes (już zaparkowane ciała wypełniają QUEUE_MAX_WAITING_BYTES, więc ciało tego żądania nie miało gdzie się zmieścić; podnieś tę wartość albo obniż, ile klient może wysłać), queue_full (QUEUE_WAIT_TIMEOUT_MS=0, oczekiwanie jest wyłączone), shutting_down (termin wygaszania minął, gdy żądanie wciąż czekało na przyjęcie), pool_unavailable (nie został żaden wątek workera, któremu można przekazać żądanie: pula zniknęła, a nie jest zajęta). Tylko cztery pierwsze oznaczają przeciążenie i odpowiadają 529; shutting_down odpowiada 503 jak reszta łagodnego wygaszania, a pool_unavailable odpowiada 500. Alertuj o przeciążeniu konkretnie na tych czterech: metryka jako całość rośnie też przy restarcie. Wyłączone z oxphp_queue_wait_us
oxphp_admission_wait_wasted_total counter Żądania, które odczekały cały swój budżet oczekiwania, podczas gdy pula nie rozpoczęła żadnego żądania. Podzbiór oxphp_admission_refused_total{reason="wait_timeout"}, oddzielający oczekiwanie przegrane z innymi żądaniami rywalizującymi o workera od takiego, podczas którego pula niczego nie rozpoczęła. Żądania, których klient już zniknął, są wyłączone, jak wyżej. Liczy tylko wtedy, gdy oxphp_admission_wait_budget_us jest na swoim pułapie. Utrzymująca się wartość niezerowa oznacza, że czekanie nic nie daje: skróć QUEUE_WAIT_TIMEOUT_MS. Zobacz Zmarnowane oczekiwania
oxphp_queue_depth gauge Żądania siedzące w tej chwili w kolejce workerów: przyjęte, jeszcze niepodjęte przez workera. Wpis opuszcza kolejkę tylko przy podjęciu, więc liczone są tu żądania, na które już odpowiedziano po upływie ich terminu (529 albo 499, gdy klient zniknął), obok tych wciąż czekających; głębokość powyżej oxphp_pending_requests to właśnie ta różnica. Obecny tylko dla executora SAPI, jedynego z kolejką
oxphp_queue_capacity gauge Łączna liczba slotów kolejki: QUEUE_CAPACITY, granica, względem której odczytuje się oxphp_queue_depth
oxphp_admission_slots_available gauge Zezwolenia na przyjęcie, których nikt nie trzyma. Żądanie bierze jedno, zanim wejdzie do kolejki, i oddaje je, gdy worker je podejmie, więc jest to wolna pojemność, którą nowo przybyłe żądanie może zająć bez czekania. Podczas łagodnego wygaszania bramka jest zamknięta i nie przyjmuje niczego, niezależnie od tego, co pokazuje ten wskaźnik
oxphp_admission_wait_budget_us gauge Jak długo żądanie przybywające teraz może czekać na workera, w mikrosekundach (jednostka oxphp_queue_wait_us, które ten budżet ogranicza). QUEUE_WAIT_TIMEOUT_MS jest pułapem tej wartości: serwer zmniejsza ją o połowę, dopóki praca przyjmowana przez oczekiwanie kończy się dla klientów, którzy już odeszli, i stopniowo podnosi ją z powrotem, gdy kolejka się opróżni, a mniej niż jedna czwarta tego, co pula rozpoczyna, przypada odeszłym klientom. Wartość poniżej pułapu oznacza, że pula dostaje więcej, niż jest w stanie obsłużyć dla klientów mniej cierpliwych niż pułap. Trwanie na minimum (jedna sześćdziesiąta czwarta pułapu, podniesiona do 10 ms, chyba że sam pułap jest krótszy) przez cały epizod jest normalne; trwanie na nim przy oxphp_queue_depth równym zero — nie. 0 w trybie fail-fast (QUEUE_WAIT_TIMEOUT_MS=0). Nieobecny dla executora bez kolejki. Zobacz Adaptacyjny budżet oczekiwania
oxphp_admission_wait_budget_ceiling_us gauge QUEUE_WAIT_TIMEOUT_MS w mikrosekundach: najdłużej, ile wskaźnik powyżej może kiedykolwiek wynosić, stałe przez cały czas życia procesu. Pozwala alertowi porównać budżet z jego pułapem na podstawie samego tego endpointu, zamiast z wpisaną na sztywno liczbą — ta sama para co oxphp_queue_depth z oxphp_queue_capacity. 0 w trybie fail-fast. Nieobecny dla executora bez kolejki
oxphp_abandoned_work_total counter Żądania zdjęte przez workera z kolejki, których klient już odszedł. Budżet oczekiwania powyżej jest dostosowywany na podstawie tej liczby. Wzrost przy spadającym budżecie oznacza, że kontroler robi swoje; wzrost przy budżecie wciąż na pułapie oznacza, że klienci odchodzą w trakcie działania handlera, a nie oczekiwania, więc to nie oczekiwanie należy skracać. Liczba żądań, a nie zmarnowanego czasu CPU. Żądania odrzucone po upływie terminu nie są liczone (nigdy nie zostały uruchomione), podobnie jak te, których odpowiedź wyszła przed zakończeniem handlera (strumień, którego nagłówki zostały wysłane, albo wczesna odpowiedź z oxphp_finish_request()), więc koniec każdej sesji SSE nie jest liczony. Widoczny jest tu tylko klient, który zamyka połączenie
oxphp_pool_stalled gauge 1, dopóki żądania czekają na workery, które są bezczynne i nic nie wykonują, czyli na pulę, która przestała zdejmować pracę z kolejki. Ustawiany, gdy stan utrzymuje się przez minutę (wystarczająco długo, by worker ponownie ładujący aplikację po recyklingu go nie wywołał); czyszczony, gdy worker zakończy żądanie albo po minucie, w której pula wcale nie wyglądała na zaklinowaną. Alertuj bezpośrednio na oxphp_pool_stalled == 1; sonda gotowości odpowiada 503 przy tym samym stanie. Eksportowany tylko w trybie worker; nieobecny, a nie 0, tam, gdzie stan nie jest obserwowany. Zobacz Sygnał zaklinowanej puli

Odczytywanie głębokości kolejki względem slotów przyjęcia

Te dwa wskaźniki odpowiadają na różne połowy jednego pytania: czy cokolwiek się rusza, a nie jak głęboka jest kolejka. oxphp_queue_depth mówi, co czeka; oxphp_admission_slots_available mówi, czy można wpuścić coś więcej. Odczytuj oba względem oxphp_busy_workers.

Wolne sloty Głębokość kolejki Interpretacja
> 0 0 Nic nie czeka; stan normalny
> 0 > 0 i spada między scrape'ami Zwykła zaległość: pula nie nadąża, ale ją odrabia
> 0 > 0 i nie spada, przy oxphp_busy_workers równym 0 Pula przestała zdejmować pracę z kolejki, choć przyjmowanie wciąż ma miejsce. To zaklinowanie w pierwszej fazie. Przybywające żądania są przyjmowane i każde dostaje 529, gdy wyczerpie się jego własny budżet (albo 499, jeśli klient zrezygnował wcześniej); oxphp_admission_wait_wasted_total rośnie z każdym 529, dopóki budżet jest na pułapie. Nic innego tego nie zdradza: żaden worker nie jest zajęty, a same odmowy wyglądają jak przeciążenie
0 równa oxphp_queue_capacity To samo zaklinowanie po zapełnieniu kolejki albo zwykłe przeciążenie, z którym pula jest daleko w tyle. Rozróżnia je oxphp_busy_workers utrzymujące się na 0 przez kolejne scrape'y; pojedyncza próbka może tak wyglądać także przy przeciążeniu
0 równa 0 lub bliska 0 Zezwolenia trzymane poza kolejką. W pojedynczym scrape'ie mogą to być żądania między przyjęciem a rozdzieleniem; utrzymujące się przez kolejne scrape'y to zezwolenia pobrane i nigdy nieoddane: nic nie może zostać przyjęte, a serwer odrzuca każde żądanie PHP aż do restartu

Trzy ostatnie wiersze to postaci, jakie przybiera wstrzymana pula. W każdej z nich reszta obrazu wygląda zdrowo, choć żadne żądanie PHP się nie kończy: żaden worker nie jest zajęty, liveness przez cały czas odpowiada 200, a readiness przez pierwszą minutę, pliki statyczne są serwowane normalnie. Liczniki odmów rosną, ale rosną też przy zwykłym przeciążeniu. oxphp_pending_requests rośnie wraz z zablokowaną kolejką, a potem spada, gdy każde przybywające żądanie dostaje odpowiedź po upływie własnego budżetu, więc samo w sobie nic tu nie znaczy; odczytane względem oxphp_queue_depth — już tak: pending bliskie zera przy głębokości na poziomie pojemności to zaklinowanie trzymające wszystko, co kiedykolwiek dostało.

Za niecierpliwym load balancerem

Zaklinowanie to dokładnie ten stan, który sprawia, że klienci odchodzą. Za load balancerem, którego limit czasu odczytu jest krótszy niż QUEUE_WAIT_TIMEOUT_MS, przybywające żądania zamieniają się w 499, które nie ruszają żadnej z serii odmów, więc oxphp_admission_wait_wasted_total może milczeć przez cały incydent. Te żądania trafiają do oxphp_request_cancelled_total{reason="client_abort"}, a oxphp_queue_depth i tak się utrzymuje.

Serwer również zauważa zaklinowanie. Ilekroć dwa kolejne jednosekundowe skany widzą czekającą pracę (żądania w kolejce albo rosnące odmowy), przy co najmniej jednym bezczynnym workerze i puli, która niczego nie zakończyła, loguje:

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

Komunikat powtarza się mniej więcej raz na minutę, dopóki stan trwa, a gdy praca ruszy, pojawia się INFO PHP pool is reaching workers again. Pola leżą pod fields, więc reguła alertu potrzebuje .fields.queue_depth, a nie .queue_depth. Trzy zabezpieczenia zapobiegają fałszywym zgłoszeniom: dwa skany zamiast jednego; postęp mierzony tym, co workery zakończyły, a nie tym, co otrzymali klienci, więc burza przerwań po stronie klientów wygląda jak zajęta pula, którą w istocie jest; oraz brak zgłoszeń, dopóki pula nie zakończy pierwszego żądania, więc bootstrap przy starcie nie jest mylony z zaklinowaniem.

To ostatnie zabezpieczenie obejmuje tylko pierwszy bootstrap procesu. Worker poddany recyklingowi później (z powodu pułapu pamięci albo Worker::scheduleExit()) ponownie uruchamia swoją aplikację i do podjęcia pierwszego żądania wygląda na bezczynny, więc powolny bootstrap w puli z jednym workerem (albo w większej puli, której pozostałe workery są wszystkie zajęte) daje jedno fałszywe ostrzeżenie, a po nim komunikat o powrocie do normy. Rozpoznasz go po oxphp_workers_spawned_total rosnącym w tym samym momencie.

Ostrzeżenie jest emitowane tylko w trybie worker, bo tylko ta pula liczy, co workery zakończyły; opisywany stan nie może wystąpić tam, gdzie worker blokuje się na samej kolejce. Trzy wskaźniki są eksportowane w każdym trybie.

Zmarnowane oczekiwania

oxphp_admission_wait_wasted_total liczy pracę rozpoczętą, a nie wpisy opuszczające kolejkę, bo worker usuwający żądania, na które odpowiedziały już ich terminy, porusza kolejkę, nikogo nie obsługując. Licznik działa tylko przy budżecie na pułapie, bo gdy serwer już skrócił budżet, „pula niczego nie rozpoczęła” mówi więcej o oknie niż o puli: zdrowa pula czterech workerów z handlerami trwającymi ćwierć sekundy rozpoczyna jedno żądanie co 62 ms, więc na minimum większość oczekiwań kończy się między dwoma startami.

Koszt tego warunku zależy od tego, czy budżet się zmienia:

  • Pula, która niczego nie rozpoczyna, nigdy nie osiąga liczby rozpoczętych żądań, na podstawie której decyduje kontroler, więc trzyma swój pułap, a ten licznik pozostaje aktywny.
  • Pula wywołująca samą siebie rozpoczyna żądanie za każdym razem, gdy zewnętrzne żądanie rezygnuje ze swojego wewnętrznego wywołania. Gdy jej własni wywołujący są mniej cierpliwi niż jej handlery, budżet spada (zmierzono dwa zmniejszenia o połowę w ciągu kilku sekund), a ten licznik milknie po żądaniach, które przybyły przed spadkiem. Nic na tej stronie nie przejmuje wtedy jego roli: szukaj wzorca w wywołaniach wychodzących samej aplikacji. Tam, gdzie wywołujący są cierpliwi (wewnętrzny cron, sidecar z hojnym limitem czasu), nic nie zostaje porzucone, pułap się utrzymuje, a to właśnie ten licznik nazywa problem.

Klasyczną przyczyną jest aplikacja wywołująca przez HTTP ten sam serwer, ale licznik nie jest detektorem samowywołań: pula, której handlery trwają dłużej niż budżet, również niczego nie zwalnia w trakcie oczekiwania. W trybie worker rozróżnia je oxphp_worker_request_duration_us (czas handlera, bez oczekiwania w kolejce): handlery poniżej budżetu przy rosnącym liczniku oznaczają, że oczekiwania nie są jedynie kolejką za powolną pracą. Nie używaj do tego oxphp_request_duration_us: obejmuje on te odmowy, z których każda wnosi niemal cały budżet. Poza trybem worker użyj własnych pomiarów czasu aplikacji.

QUEUE_WAIT_TIMEOUT_MS=0 nie jest najkrótszą wersją krótszego budżetu: usuwa oczekiwanie na slot w kolejce, a oczekiwanie wewnątrz kolejki pozostawia nieograniczone, więc w puli, która niczego nie podejmuje, żądanie, które znajdzie wolny slot, nigdy nie dostaje odpowiedzi. Łącz 0 z QUEUE_CAPACITY na tyle małym, by kolejka była pełna, zanim zacznie to mieć znaczenie.

Co mówi log, gdy żądania są odrzucane

Nadzorca obserwuje cztery przyczyny przeciążenia z oxphp_admission_refused_total w tym samym jednosekundowym skanie i zgłasza epizod, a nie pojedyncze żądania, bo linia na każdą odmowę sama byłaby awarią. Pierwszy skan, który zobaczy ruch któregokolwiek z nich, loguje:

json
{"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 to liczba odrzucona przez ten jeden skan, z wyjątkiem pierwszego skanu, który ma kolejkę do obejrzenia — ten zgłasza wszystko odrzucone przed nim (pula wciąż ładująca aplikację, podczas gdy przybywające żądania się piętrzą, to prawdziwy epizod). Głębokość równa pojemności bez wolnego slotu to pula, która nie nadąża z pracą, a wait_budget_us wyraźnie poniżej wait_budget_ceiling_us mówi, że serwer już skrócił oczekiwanie; obie liczby są podane, bo budżet zepchnięty do minimum i budżet tak nisko skonfigurowany wyglądają osobno tak samo. Dane kolejki to migawka z chwili skanu, a refused obejmuje ostatnią sekundę, więc duże refused obok pustej kolejki to krótki wybuch, a nie sprzeczność.

Obie liczby są rozbite według przyczyny na cztery etykiety przeciążenia z oxphp_admission_refused_total, a nazwy pól są tworzone od liczby, którą rozbijają: refused_queue_full, refused_wait_timeout, refused_waiting_full i refused_waiting_bytes sumują się do refused, a powtórzenie i linia zamykająca poniżej niosą episode_refused_queue_full i trzy pokrewne pola, które sumują się do episode_refused. To przyczyna decyduje, po którą gałkę sięgnąć (wiersz tego licznika powyżej mówi, na co wskazuje każda z nich), a ten licznik jest serwowany tylko na INTERNAL_ADDR. Na instancji bez wewnętrznego listenera przyczynę wciąż można odczytać z tych pól oraz, tam gdzie śledzenie jest włączone, z atrybutu spana oxphp.shed_reason. Wszystkie cztery są zapisywane nawet przy zerze: to jawne 0 wyklucza daną przyczynę.

Dopóki epizod trwa, ostrzeżenie powtarza się jako PHP admission is still shedding requests under overload, nie częściej niż raz na minutę i tylko przy skanie, który sam odrzuca (więc przerwa może sięgnąć prawie dwóch minut), niosąc episode_refused (bieżącą sumę) obok refused z pojedynczego skanu. Powtórzenie rozbija według przyczyny sumę epizodu, a nie skanu. Ma to znaczenie, bo linia zamykająca jest zapisywana na poziomie INFO, poniżej warn, na którym logują konfiguracje produkcyjne, więc tam powtórzenie jest jedyną linią, która mówi, z czego epizod dotąd się składał, aż do skanu, przy którym zostało zapisane. Epizod, który kończy się w swojej pierwszej minucie, nie ma żadnego powtórzenia, a to, co zostanie odrzucone po ostatnim powtórzeniu, trafia tylko do linii zamykającej. Minuta bez żadnej odmowy kończy epizod, na poziomie INFO:

json
{"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 liczy się do ostatniej odmowy, a nie do linii logu, więc zamykająca cicha minuta nie jest wliczana. Cicha minuta oznacza też, że przerwy krótsze niż minuta są łączone w jeden epizod, co ogranicza wyjście do najwyżej dwóch linii na minutę; wejście w epizod nie ma takiego opóźnienia, bo odmowa jest faktem, a nie próbką.

Raport jest emitowany w każdym trybie routingu i reaguje wyłącznie na cztery przyczyny przeciążenia, więc restart (shutting_down) ani zniknięta pula (pool_unavailable) nie ogłaszają się jako obciążenie. Nie ma wskaźnika dla tego stanu i żadna sonda nie jest o nim informowana: odrzucanie nigdy nie zmienia odpowiedzi /health/readiness, bo instancja odrzucająca 529 odpowiada szybko na działającej puli, a wycofywanie po kolei każdej przeciążonej repliki z rotacji zostawiłoby Service bez endpointów. Na przeciążenie odpowiadaj dodaniem pojemności. Zaklinowana pula również odrzuca żądania, a w trybie worker ten stan odpowiada 503, gdy utrzyma się przez minutę. Rozróżnia je kolejka: pula, która tylko nie nadąża, ją opróżnia, a zaklinowana — nie.

Metryki puli workerów

Metric Type Description
oxphp_workers_current gauge Bieżąca liczba wątków workerów PHP
oxphp_workers_min gauge Minimalna liczba workerów (równa liczbie bieżącej w trybie statycznym)
oxphp_workers_max gauge Maksymalna liczba workerów (równa liczbie bieżącej w trybie statycznym)
oxphp_workers_idle gauge Wątki workerów bez żadnego żądania w toku, obliczane jako workers_current - busy_workers
oxphp_busy_workers gauge Wątki workerów wykonujące aktualnie co najmniej jedno żądanie; nigdy nie przekracza oxphp_workers_current. Liczy wątki, nie żądania — w trybie worker jeden wątek multipleksuje wiele fiberów żądań i wciąż liczy się raz. Żądania czekające na przyjęcie lub siedzące w kolejce nie są liczone; te pojawiają się w oxphp_pending_requests
oxphp_workers_spawned_total counter Łączna liczba workerów utworzonych od uruchomienia (wliczając workery początkowe)
oxphp_workers_retired_total counter Łączna liczba workerów wycofanych z powodu limitu czasu bezczynności (tylko tryb dynamiczny)

Metryki nadzorcy workerów

Obserwowalność na poziomie pojedynczego workera emitowana przez nadzorcę workerów. Każda seria niesie etykietę worker_id (indeks slotu). Pojawiają się one, gdy nadzorca śledzi stan poszczególnych workerów.

Metric Type Description
oxphp_worker_request_age_seconds gauge Wiek żądania w trakcie realizacji na każdym workerze, w sekundach. Etykieta: worker_id
oxphp_worker_long_running_total counter Skany nadzorcy, które zaobserwowały żądanie starsze niż próg zablokowania (60 sekund, stały). Etykieta: worker_id
oxphp_worker_stuck_total counter Licznik klasyfikacji zablokowania na workera. Etykiety: worker_id, kind (io, c_call, cpu). Wyłącznie raportowanie: żądanie sklasyfikowane jako zablokowane nie jest anulowane, a jego worker nie jest poddawany recyklingowi. Nic w serwerze na to nie reaguje, więc ustaw alert na tę serię

Histogram czasu oczekiwania w kolejce

Metric Type Description
oxphp_queue_wait_us histogram Czas, przez jaki żądanie czeka w kolejce, zanim podejmie je worker, w mikrosekundach

Granice przedziałów (mikrosekundy): 50, 100, 250, 500, 1000, 2500, 5000, 10000, 50000, 100000, 250000, 500000, 1000000, +Inf.

Metryka mierzy czas spędzony na oczekiwaniu, na przyjęcie, a potem w kolejce, aż do chwili, gdy worker zdejmie żądanie z kolejki, więc odpowiada na pytanie „ile czasu minęło, zanim worker to podjął", a nie „ile trwało żądanie". Nic po tej chwili nie jest w niej ujęte: przygotowanie żądania przez workera, skrypt (do nagłówka odpowiedzi, w przypadku odpowiedzi strumieniowanej) i przekazanie odpowiedzi z powrotem do wątków I/O serwera wliczają się do oxphp_request_duration_us. To przekazanie nie jest darmowe: odpowiedź czeka, aż wątek I/O zostanie wybudzony albo upora się z pracą zakolejkowaną przed nią, więc przy tanim skrypcie czas po tym oczekiwaniu może kilkukrotnie przekraczać czas samego skryptu.

Rejestrowane jest tu tylko żądanie, które worker zdjął z kolejki w ramach jego budżetu. 529 odrzucone z powodu przeciążenia albo 503/500, gdy serwer się wyłącza lub pula zniknęła, są liczone w oxphp_admission_refused_total; 499 dla klienta, który odszedł, zanim dotarł do niego worker, jest liczone w oxphp_request_cancelled_total{reason="client_abort"} i w żadnej serii odmów.

Wysoki czas oczekiwania w kolejce oznacza, że żądania długo docierały do workera, co samo w sobie nie jest brakiem workerów: każdy odczyt obejmuje czas, jaki wątek workera potrzebuje, by dotrzeć do przekazanego mu żądania, a ten występuje także na nieobciążonym serwerze. Zanim podniesiesz PHP_WORKERS, sprawdź, czy żądania zastają wszystkie workery zajęte i czy maszyna ma wolne rdzenie: z workerami konkurują własne wątki I/O serwera, inne procesy i limit CPU kontenera. oxphp_busy_workers względem oxphp_workers_current odpowiada na pierwsze pytanie tylko częściowo: jest odczytywane w chwili scrape'a, więc wybuch, który wypełnia pulę między scrape'ami, widać tutaj, a tam nie, a w trybie worker wątek liczy się jako zajęty, dopóki niesie jakiekolwiek żądanie, nawet gdy handler tego żądania jest zawieszony, a wątek przyjmuje kolejne. Zakres sięga jednej sekundy, odpowiadając domyślnemu QUEUE_WAIT_TIMEOUT_MS, więc żądanie, które przed uruchomieniem zużyło większość swojego budżetu oczekiwania, zostaje policzone, a nie wrzucone do +Inf. Nic, co zostaje obsłużone, nie czeka dłużej niż budżet (po jego przekroczeniu żądanie jest odrzucane), więc podniesienie QUEUE_WAIT_TIMEOUT_MS to jedyne ustawienie, które z powrotem wprowadza oczekiwania do +Inf.

Metryki ograniczania liczby żądań

Metric Type Description
oxphp_rate_limited_total counter Żądania odrzucone przez ogranicznik liczby żądań (zwrócono 429)
oxphp_php_deny_total counter Żądania zablokowane przez PHP_DENY_PATHS (odmowa wykonania .php). Zobacz Lista blokowania wykonywania PHP
oxphp_path_deny_total counter Żądania zablokowane przez regułę .oxphpdeny. Żądanie, które blokuje również PHP_DENY_PATHS, jest liczone tylko tutaj. Zobacz Reguły blokowania ścieżek

Metryki pamięci podręcznej plików statycznych

Metric Type Description
oxphp_static_cache_hits_total counter Żądania plików statycznych obsłużone z pamięci podręcznej w pamięci operacyjnej
oxphp_static_cache_misses_total counter Żądania plików statycznych, które wymagały odczytu z dysku

Metryki kompresji

Metric Type Description
oxphp_compressed_responses_total counter Odpowiedzi wysłane z kodowaniem treści (Brotli, zstd lub gzip)
oxphp_compression_bytes_saved_total counter Łączna liczba bajtów zaoszczędzonych dzięki kompresji (rozmiar oryginalny minus rozmiar skompresowany)

Metryki trybu worker

Te metryki są emitowane tylko wtedy, gdy tryb worker jest aktywny (WORKER_MODE_ENABLED=true).

Liczniki globalne

Metric Type Description
oxphp_worker_mode_enabled gauge Zawsze 1, gdy tryb worker jest aktywny
oxphp_worker_requests_handled_total counter Łączna liczba żądań przetworzonych przez trwałe workery
oxphp_worker_recycles_total counter Łączna liczba recyklingów workerów: pętla workera zakończyła się z jednej z poniższych przyczyn. Pula dynamiczna wycofująca bezczynnego workera skaluje się w dół, a nie wykonuje recyklingu: jest to liczone w oxphp_workers_retired_total, a nie tutaj ani w serii poniżej
oxphp_worker_recycles_by_reason_total counter Recyklingi według przyczyny. Etykieta: reason (scheduled, max_memory, error)
oxphp_worker_soft_resets_total counter Łączna liczba miękkich resetów wykonanych pomiędzy żądaniami

Wskaźniki na poziomie workera

Metric Type Description
oxphp_worker_memory_bytes gauge Sterta PHP zajmowana przez każdego workera na koniec jego ostatniego żądania. To właśnie zwraca memory_get_usage() wewnątrz handlera i ta sama wielkość, względem której mierzony jest WORKER_MAX_MEMORY_MIB. Jest odczytywana tuż przed tym sprawdzeniem pułapu, gdy superglobalne zakończonego żądania są jeszcze na stercie, więc wypada o kilka KB powyżej wartości, która wyzwala recykling: ustaw alert poniżej pułapu, a nie na nim. Worker, który nie obsłużył jeszcze żadnego żądania, pokazuje zero. Etykieta: worker (indeks slotu, np. "0", "1"; worker po recyklingu używa ponownie slotu poprzednika i jego ostatniej wartości, dopóki nie zakończy własnego żądania)
oxphp_worker_uptime_seconds gauge Liczba sekund od utworzenia każdego workera. Etykieta: worker
oxphp_worker_requests_count gauge Liczba żądań obsłużonych przez każdą instancję workera. Etykieta: worker
oxphp_worker_request_fibers_active gauge Fibery żądań, które worker niesie w tej chwili: 0 bezczynny, 1 obsługuje jedno żądanie od początku do końca, więcej tam, gdzie żądania się zawieszają i są multipleksowane na wątku. Odświeżany przy każdym obrocie pętli workera, a nie przy zakończeniu, więc nie zamarza na ostatniej zdrowej wartości na workerze, który przestał kończyć żądania. Worker przyjmuje najwyżej 256 fiberów żądań, a taki, który trwa na 256, przestał zdejmować pracę z kolejki. Odczytuj go razem z oxphp_busy_workers: 256 fiberów na workerze, którego pula liczy jako bezczynnego, to fibery, które przeżyły swoje (już obsłużone) żądania i nigdy nie zostały odzyskane, a 256 na zajętym workerze to tyle żądań w toku. Etykieta: worker

Histogram czasu trwania żądania workera

Metric Type Description
oxphp_worker_request_duration_us histogram Czas wykonania handlera PHP na żądanie w mikrosekundach (tylko tryb worker)

Granice przedziałów (mikrosekundy): 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, +Inf.

Ten histogram mierzy czas spędzony wewnątrz callbacka handlera PHP, z wyłączeniem czasu oczekiwania w kolejce. Użyj go, aby identyfikować wolne handlery i śledzić opóźnienia ogona w trybie worker.

Metryki puli asynchronicznej

Te metryki wymagają ustawienia ASYNC_WORKERS na wartość niezerową, a każda ma własną bramkę emisji: liczniki pojawiają się dopiero po tym, jak co najmniej jedno zadanie zostało wysłane lub odrzucone, wskaźniki _in_flight / _in_flight_limit pojawiają się, gdy pula podłączy swój licznik zadań w trakcie realizacji, a oxphp_async_output_discarded_bytes_total pojawia się dopiero po odrzuceniu jakiegoś wyjścia.

Metric Type Description
oxphp_async_tasks_dispatched_total counter Łączna liczba zadań asynchronicznych wysłanych do puli w tle
oxphp_async_tasks_completed_total counter Zadania asynchroniczne, które zakończyły się pomyślnie
oxphp_async_tasks_failed_total counter Zadania asynchroniczne, które rzuciły wyjątek
oxphp_async_tasks_cancelled_total counter Zadania asynchroniczne, które zostały anulowane
oxphp_async_tasks_rejected_total counter Zadania asynchroniczne odrzucone przy wysyłaniu — ponieważ kolejka puli była pełna lub osiągnięto limit zadań w trakcie realizacji (ASYNC_MAX_FIBERS × ASYNC_WORKERS)
oxphp_async_tasks_stranded_total counter Workery pozostawione działające po upływie limitu czasu await_race / await_any. Każde osierocone zadanie może wydłużyć RSHUTDOWN nawet o 5 sekund.
oxphp_async_tasks_in_flight gauge Zadania asynchroniczne aktualnie w kolejce lub uruchomione (emitowane, gdy pula podłączy swój licznik zadań w trakcie realizacji)
oxphp_async_tasks_in_flight_limit gauge Maksymalna liczba równoczesnych zadań asynchronicznych (ASYNC_MAX_FIBERS × ASYNC_WORKERS)
oxphp_async_output_discarded_bytes_total counter Bajty wyjścia zadań asynchronicznych odrzucone przy bezczynności workera (echo w zadaniu asynchronicznym nie ma klienta, który mógłby je odebrać)

Metryki eksportu OpenTelemetry

Te metryki wymagają OTEL_ENABLED=true. Liczą partie spanów, które OxPHP wysyła do kolektora OTLP: spany żądań oraz spany APM, gdy włączona jest wtyczka APM.

Metric Type Description
oxphp_otel_export_failures_total counter Eksporty spanów, które się nie powiodły, po ponowieniu, jeśli do niego doszło. Ich spany nie są wysyłane ponownie
oxphp_otel_export_failed_spans_total counter Spany przenoszone przez te nieudane eksporty
oxphp_otel_export_retries_total counter Eksporty ponowione raz, ponieważ pierwsza próba szybko się nie powiodła, bez odpowiedzi od kolektora

Eksport jest ponawiany raz, gdy jego pierwsza próba nie powiedzie się bez odpowiedzi od kolektora w ciągu połowy OTEL_EXPORTER_OTLP_TIMEOUT lub jednej sekundy, zależnie od tego, co krótsze: połączenie zostało zresetowane lub zamknięte albo nie dało się go nawiązać. Zwykłą przyczyną jest load balancer lub NAT, który zerwał połączenie, gdy było bezczynne między eksportami. Status błędu, którym odpowiada sam kolektor, taki jak odrzucony klucz API lub dławienie (throttling), nie jest ponawiany: specyfikacja OTLP zabrania ponawiania większości z nich, a eksporter nie przekazuje dalej opóźnienia, o które prosi dławiący kolektor. Ponowienie, które dostarczy partię, jest liczone tylko w oxphp_otel_export_retries_total. Próba, która trafi na limit czasu eksportu, nie jest ponawiana, więc wolny kolektor nigdy nie dostaje tej samej partii dwa razy.

Ponowienie może jednak powodować duplikaty. Jeśli kolektor odebrał pierwszą próbę, zanim połączenie zostało zerwane, ponowienie dostarcza te spany po raz drugi, i z tego samego powodu eksport policzony jako nieudany mógł mimo wszystko dotrzeć do kolektora.

Spany, które tracer odrzuca przed eksportem, bo jego kolejka jest pełna (OTEL_BSP_MAX_QUEUE_SIZE, domyślnie 2048 spanów), nie są tutaj liczone.

Wskazówki do dashboardu Grafany

Poniższe zapytania PromQL są przydatne przy budowaniu dashboardów:

Częstotliwość żądań (żądania na sekundę):

text
rate(oxphp_requests_total[5m])

Średni czas odpowiedzi (milisekundy):

text
rate(oxphp_request_duration_us_sum[5m]) / rate(oxphp_requests_total[5m]) / 1000

Czas trwania żądania p99 (milisekundy):

text
histogram_quantile(0.99, rate(oxphp_request_duration_us_bucket[5m])) / 1000

Częstotliwość błędów (odpowiedzi 5xx jako procent):

text
rate(oxphp_responses_by_status_total{status="5xx"}[5m]) / rate(oxphp_requests_total[5m]) * 100

Wykorzystanie puli workerów:

text
oxphp_busy_workers / oxphp_workers_current

To prawdziwy ułamek między 0 a 1. Utrzymujące się wartości równe 1 oznaczają, że każdy worker jest zajęty, a kolejne nadchodzące żądania ustawiają się w kolejce. Zestaw go z rate(oxphp_admission_refused_total{reason=~"queue_full|wait_timeout|waiting_full|waiting_bytes"}[5m]), aby zobaczyć, czy ta zaległość zamienia się w odmowy, oraz z oxphp_pending_requests, aby zobaczyć, jak jest głęboka.

Nasycenie kolejki (częstotliwość odrzuceń na sekundę):

text
rate(oxphp_dropped_requests_total[5m])

Czas oczekiwania w kolejce p99 (mikrosekundy):

text
histogram_quantile(0.99, rate(oxphp_queue_wait_us_bucket[5m]))

Współczynnik trafień pamięci podręcznej plików statycznych:

text
rate(oxphp_static_cache_hits_total[5m]) / (rate(oxphp_static_cache_hits_total[5m]) + rate(oxphp_static_cache_misses_total[5m]))

Bajty zaoszczędzone dzięki kompresji na sekundę:

text
rate(oxphp_compression_bytes_saved_total[5m])

Opóźnienie p99 trybu worker (mikrosekundy):

text
histogram_quantile(0.99, rate(oxphp_worker_request_duration_us_bucket[5m]))

Budżet oczekiwania zepchnięty poniżej pułapu (ułamek):

text
oxphp_admission_wait_budget_us / oxphp_admission_wait_budget_ceiling_us

1 to stan normalny; każda niższa wartość oznacza, że serwer skraca oczekiwania pod przeciążeniem. Alertuj na ten stosunek, a nie na liczbę skopiowaną z QUEUE_WAIT_TIMEOUT_MS.

Wstrzymania pętli akceptującej (alert przy wartości niezerowej):

text
rate(oxphp_accept_stalls_total[5m])

Częstotliwość recyklingu workerów (na minutę):

text
rate(oxphp_worker_recycles_total[5m]) * 60

Średnie zużycie pamięci przez workera:

text
avg(oxphp_worker_memory_bytes)

Konfiguracja scrape'owania Prometheus

Dodaj zadanie scrape'owania do swojego pliku prometheus.yml:

prometheus.yml
scrape_configs: - job_name: "oxphp" scrape_interval: 15s static_configs: - targets: ["oxphp:9090"]

Dla wykrywania usług w Kubernetes:

prometheus.yml
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"

Prompty dla Twojego agenta

Te prompty zakładają agenta z podłączonym serwerem MCP dokumentacji OxPHP — sam przeczyta strony, których potrzebuje.

prompt
Spraw, by Prometheus scrape'ował moją usługę OxPHP. Przeczytaj stronę `operations/metrics` przez serwer MCP dokumentacji OxPHP. Dodaj konfigurację scrape'owania oraz wszystko, czego moje pliki Compose lub Kubernetes potrzebują, aby endpoint metryk był osiągalny, korzystając z nazw usług już obecnych w tym repozytorium.
prompt
Zbuduj mi startowy dashboard Grafany dla OxPHP. Przeczytaj stronę `operations/metrics` przez serwer MCP dokumentacji OxPHP. Wybierz metryki, które faktycznie mówią mi, czy moja aplikacja jest w dobrej kondycji, i wyjaśnij, co każdy panel będzie mi mówił, gdy się poruszy.

Zobacz również

Znalazłeś błąd? Zgłoś go →