Метрики Prometheus

OxPHP надає Prometheus-сумісні метрики в текстовому форматі експозиції за адресою GET /metrics на внутрішньому сервері. Вони охоплюють пропускну здатність запитів, час відповіді, стан з’єднань, стан пулу воркерів, кешування статичних файлів, ефективність стиснення та продуктивність режиму воркерів.

Увімкнення метрик

Задайте INTERNAL_ADDR, щоб запустити внутрішній сервер:

bash
INTERNAL_ADDR=127.0.0.1:9090

Після цього збирайте метрики з Prometheus або будь-якого сумісного колектора:

bash
curl http://localhost:9090/metrics

Метрики сервера

Метрика Тип Опис
oxphp_uptime_seconds gauge Кількість секунд від запуску процесу сервера
oxphp_requests_total counter Загальна кількість HTTP-запитів, отриманих на основному порту

Метрики запитів

Метрика Тип Опис
oxphp_requests_by_method_total counter Запити за HTTP-методом. Мітка: method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT, QUERY, OTHER)
oxphp_responses_by_status_total counter Відповіді за класом статусу. Мітка: status (1xx, 2xx, 3xx, 4xx, 5xx)
oxphp_request_bytes_total counter Загальна кількість отриманих байтів тіла запитів
oxphp_response_bytes_total counter Загальна кількість надісланих байтів тіла відповідей
oxphp_request_cancelled_total counter Скасовані запити за причиною. Мітка: reason (client_abort, timeout, shutdown). Враховується один раз для кожного запиту, який сервер диспетчеризує (а не лише для тих, що доходять до PHP), у момент, коли на цей запит надається відповідь. Тож client_abort охоплює і клієнта, що від’єднався, поки його запит ще стояв у черзі (типовий випадок), і клієнта, що від’єднався під час роботи обробника; клієнт, який від’єднується вже після того, як відповідь почала надсилатися, цей момент минув і не враховується. Надається завжди
Note

Надаються лише методи й класи статусів, для яких зафіксовано хоча б одну подію. Мітки з нульовим лічильником пропускаються.

Гістограма тривалості запитів

Метрика Тип Опис
oxphp_request_duration_us histogram Наскрізна тривалість запиту в мікросекундах для всіх запитів (статичних файлів і PHP)

Межі бакетів (мікросекунди): 100, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000, 250000, 500000, 1000000, +Inf.

Використовуйте цю гістограму, щоб відстежувати загальну затримку, знаходити повільні ендпоінти й вимірювати перцентилі хвостової затримки.

Метрики з’єднань

Метрика Тип Опис
oxphp_active_connections gauge TCP-з’єднання, відкриті зараз на основному порту
oxphp_accept_stalled gauge 1, поки цикл прийняття з’єднань стоїть в очікуванні вільного дозволу MAX_CONNECTIONS. Ядро приймає з’єднання, але нічого нового не обслуговується, тож для клієнта сервер перестав відповідати. Налаштовуйте сповіщення безпосередньо на oxphp_accept_stalled == 1; порівнювати з налаштованим бюджетом не потрібно
oxphp_accept_stalls_total counter З’єднання, яким після прийняття довелося чекати на дозвіл MAX_CONNECTIONS. Змінюється навіть тоді, коли зупинка починається й закінчується між двома зборами метрик, чого сам gauge не помітив би: rate(oxphp_accept_stalls_total[5m]) > 0 означає, що бюджет вичерпується, хай навіть ненадовго. Збільште MAX_CONNECTIONS або зменште чергу PHP (QUEUE_CAPACITY + QUEUE_MAX_WAITING); див. Як зберегти запас для циклу прийняття
oxphp_pending_requests gauge PHP-запити, прийняті, але ще без відповіді: ті, що чекають на місце в черзі, стоять у черзі або виконуються. Лише запити, спрямовані до PHP: статичний файл, 404 чи заборонений шлях отримують відповідь в обхід черги й тут не з’являються
oxphp_dropped_requests_total counter Запити, на яких PHP-воркер зазнав збою вже після прийняття запиту
oxphp_admission_refused_total counter Запити, що отримали відповідь, так і не дійшовши до воркера. Мітка: reason зі значеннями wait_timeout (запит вичерпав увесь свій бюджет очікування, тобто QUEUE_WAIT_TIMEOUT_MS або менше, якщо oxphp_admission_wait_budget_us показує, що сервер його скоротив. Дайте пулу більше запасу, але спершу перегляньте oxphp_admission_wait_wasted_total: якщо за час цих очікувань пул не почав жодного запиту, потрібне протилежне рішення. Запит, клієнт якого вже пішов на момент вичерпання бюджету, тут не враховується: він отримує відповідь 499 і враховується в oxphp_request_cancelled_total{reason="client_abort"}, тож ця серія залишається твердженням саме про пул), waiting_full (в очікуванні вже QUEUE_MAX_WAITING запитів; збільште це значення або MAX_CONNECTIONS), waiting_bytes (тіла вже відкладених запитів заповнюють QUEUE_MAX_WAITING_BYTES, тож тілу цього запиту ніде розміститися; збільште це значення або зменште обсяг, який клієнт може завантажити), queue_full (QUEUE_WAIT_TIMEOUT_MS=0, очікування вимкнено), shutting_down (дедлайн дренажу минув, поки запит ще чекав на допуск), pool_unavailable (не залишилося жодного потоку воркера, якому можна передати запит: пулу немає, а не він зайнятий). Перевантаженням є лише перші чотири, і вони відповідають 529; shutting_down відповідає 503, як і решта плавного дренажу, а pool_unavailable відповідає 500. Сповіщення про перевантаження налаштовуйте саме на ці чотири: метрика загалом змінюється й під час перезапуску. Не входить до oxphp_queue_wait_us
oxphp_admission_wait_wasted_total counter Запити, що вичерпали весь свій бюджет очікування, поки пул не почав жодного запиту. Підмножина oxphp_admission_refused_total{reason="wait_timeout"}, яка відокремлює очікування, програне іншим запитам у конкуренції за воркер, від очікування, за яке пул нічого не почав. Запити, клієнт яких уже пішов, виключено, як описано вище. Враховується, лише поки oxphp_admission_wait_budget_us на своїй стелі. Стабільно ненульове значення означає, що очікування нічого не дає: скоротіть QUEUE_WAIT_TIMEOUT_MS. Див. Марні очікування
oxphp_queue_depth gauge Запити, що зараз перебувають у черзі воркерів: допущені, але ще не взяті воркером. Запис залишає чергу лише тоді, коли його бере воркер, тож тут враховуються і запити, на які вже відповіли за дедлайном (529 або 499, якщо клієнт пішов), і ті, що досі чекають; глибина понад oxphp_pending_requests і є цією різницею. Присутня лише для SAPI-виконавця, єдиного з чергою
oxphp_queue_capacity gauge Загальна кількість місць у черзі: QUEUE_CAPACITY, межа, відносно якої читається oxphp_queue_depth
oxphp_admission_slots_available gauge Дозволи на допуск, які ніхто не тримає. Запит бере один перед входом у чергу й повертає, коли його бере воркер, тож це вільна місткість, яку новий запит може отримати без очікування. Під час плавного дренажу шлюз закритий і не допускає нічого, хоч би що показувала ця метрика
oxphp_admission_wait_budget_us gauge Скільки запит, що надходить зараз, може чекати на воркер, у мікросекундах (одиниця oxphp_queue_wait_us, яку ця метрика обмежує). QUEUE_WAIT_TIMEOUT_MS — стеля цього значення: сервер зменшує його вдвічі, поки робота, яку допускає очікування, завершується для клієнтів, що вже пішли, і поступово повертає вгору, щойно черга спорожніє й менше чверті того, що починає пул, припадає на клієнтів, які пішли. Значення нижче стелі означає, що пулу пропонують більше, ніж він здатен обслужити, від клієнтів, менш терплячих за стелю. Перебування на мінімумі (одна шістдесят четверта стелі, піднята до 10 мс, якщо сама стеля не коротша) упродовж епізоду — нормальне явище; перебування там з oxphp_queue_depth, що дорівнює нулю, — ні. 0 у режимі швидкої відмови (QUEUE_WAIT_TIMEOUT_MS=0). Відсутня для виконавця без черги. Див. Адаптивний бюджет очікування
oxphp_admission_wait_budget_ceiling_us gauge QUEUE_WAIT_TIMEOUT_MS у мікросекундах: найбільше значення, якого може досягти gauge вище, стале впродовж усього життя процесу. Дає змогу сповіщенню порівнювати бюджет зі стелею лише за даними цього ендпоінта, а не з жорстко закодованим числом, — та сама пара, що й oxphp_queue_depth з oxphp_queue_capacity. 0 у режимі швидкої відмови. Відсутня для виконавця без черги
oxphp_abandoned_work_total counter Запити, які воркер узяв із черги, коли їхній клієнт уже пішов. Бюджет очікування вище коригується саме за цим лічильником. Зростання, поки бюджет падає, означає, що регулятор виконує свою роботу; зростання, коли бюджет досі на стелі, означає, що клієнти йдуть під час роботи обробника, а не під час очікування, тож скорочувати треба не очікування. Це кількість запитів, а не змарнованого процесорного часу. Запити, відхилені за дедлайном, не враховуються (вони ніколи не виконувалися), як і ті, відповідь на які пішла до завершення обробника (потік, заголовки якого вже надіслано, або рання відповідь через oxphp_finish_request()), тож завершення кожної SSE-сесії не враховується. Тут видно лише клієнта, який закриває своє з’єднання
oxphp_pool_stalled gauge 1, поки запити чекають на воркери, що простоюють і нічого не виконують, тобто пул перестав забирати роботу з черги. Встановлюється, коли такий стан триває хвилину (достатньо довго, щоб воркер, який після перезапуску повторно завантажує застосунок, його не спричинив); скидається, коли воркер завершує запит, або після хвилини, упродовж якої пул жодного разу не виглядав заклиненим. Налаштовуйте сповіщення безпосередньо на oxphp_pool_stalled == 1; проба готовності відповідає 503 за того самого стану. Надається лише в режимі воркерів; там, де цей стан не відстежується, метрика відсутня, а не дорівнює 0. Див. Сигнал заклиненого пулу

Як читати глибину черги разом із місцями допуску

Ці два gauge відповідають на дві половини одного питання: чи щось рухається, а не наскільки глибока черга. oxphp_queue_depth показує, що чекає; oxphp_admission_slots_available — чи можна допустити ще щось. Читайте обидва разом з oxphp_busy_workers.

Доступні місця Глибина черги Тлумачення
> 0 0 Нічого не чекає; норма
> 0 > 0 і зменшується між зборами метрик Звичайне відставання: пул не встигає, але розбирає чергу
> 0 > 0 і не зменшується, а oxphp_busy_workers дорівнює 0 Пул перестав забирати роботу з черги, хоча допуск ще має місце. Це заклинювання в першій фазі. Нові запити допускаються, і кожен отримує 529, коли спливає його власний бюджет (або 499, якщо його клієнт здався раніше); oxphp_admission_wait_wasted_total зростає з кожною 529, поки бюджет на стелі. Більше ніщо цього не видає: жоден воркер не зайнятий, а самі відмови виглядають як перевантаження
0 дорівнює oxphp_queue_capacity Те саме заклинювання після заповнення черги або звичайне перевантаження, за якого пул сильно відстає. Їх розрізняє oxphp_busy_workers, що стабільно дорівнює 0 упродовж кількох зборів; одна вибірка може так виглядати й під час перевантаження
0 0 або близько до 0 Дозволи утримуються поза чергою. В одному зборі це можуть бути запити між допуском і диспетчеризацією; якщо це триває кілька зборів, дозволи взято й ніколи не повернено: нічого не може бути допущено, і сервер відхилятиме кожен PHP-запит, доки його не перезапустять

Останні три рядки — це форми, яких набуває заклинений пул. У кожній з них решта картини виглядає здоровою, хоча жоден PHP-запит не завершується: жодного зайнятого воркера, 200 від проби життєздатності весь час і від проби готовності впродовж першої хвилини, статичні файли обслуговуються як завжди. Лічильники відмов змінюються, але вони змінюються й за звичайного перевантаження. oxphp_pending_requests зростає разом із застряглою чергою, а потім знову падає, бо кожен новий запит отримує відповідь за своїм бюджетом, тож сама по собі ця метрика тут нічого не означає; у зіставленні з oxphp_queue_depth — означає: pending близько нуля, коли глибина тримається на рівні місткості, — це заклинювання, що утримує все, що йому колись дали.

За нетерплячим балансувальником навантаження

Заклинювання — саме той стан, через який клієнти йдуть. За балансувальником навантаження, тайм-аут читання якого коротший за QUEUE_WAIT_TIMEOUT_MS, нові запити перетворюються на 499, що не змінюють жодної серії відмов, тож oxphp_admission_wait_wasted_total може мовчати впродовж усього інциденту. Такі запити потрапляють до oxphp_request_cancelled_total{reason="client_abort"}, а oxphp_queue_depth тримається в будь-якому разі.

Сервер теж помічає заклинювання. Щоразу, коли два поспіль односекундні скани бачать роботу, що чекає (запити в черзі або зростання відмов), при цьому принаймні один воркер простоює, а пул нічого не завершив, він пише в журнал:

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}}

Повідомлення повторюється приблизно раз на хвилину, поки триває стан, а коли робота зрушить, з’являється INFO PHP pool is reaching workers again. Поля розміщено під fields, тож правилу сповіщення потрібне .fields.queue_depth, а не .queue_depth. Хибним повідомленням запобігають три запобіжники: два скани замість одного; прогрес вимірюється тим, що воркери завершили, а не тим, що отримали клієнти, тож шквал обривів з боку клієнтів читається як зайнятий пул, яким він і є; і нічого не повідомляється, доки пул не завершить свій перший запит, тож початкове завантаження не приймається за заклинювання.

Останній запобіжник стосується лише першого завантаження процесу. Воркер, перезапущений пізніше (через стелю пам’яті чи Worker::scheduleExit()), знову завантажує свій застосунок і вважається таким, що простоює, доки не візьме перший запит, тож повільне завантаження в пулі з одного воркера (або у більшому пулі, всі інші воркери якого зайняті) дає одне хибне попередження з подальшим повідомленням про відновлення. Одночасна зміна oxphp_workers_spawned_total вказує саме на цей випадок.

Попередження надається лише в режимі воркерів, бо лише цей пул веде облік того, що завершили воркери; описаний стан не може виникнути там, де воркер блокується безпосередньо на черзі. Три gauge надаються в кожному режимі.

Марні очікування

oxphp_admission_wait_wasted_total рахує розпочату роботу, а не записи, що залишають чергу, бо воркер, який прибирає запити, на які вже відповіли їхні дедлайни, рухає чергу, нікого не обслуговуючи. Лічильник працює лише тоді, коли бюджет на стелі, бо після того, як сервер скоротив бюджет, «пул нічого не почав» говорить більше про вікно, ніж про пул: здоровий пул із чотирьох воркерів з обробниками на чверть секунди починає один запит кожні 62 мс, тож на мінімумі більшість очікувань закінчуються між двома стартами.

Чого коштує ця умова, залежить від того, чи змінюється бюджет:

  • Пул, який нічого не починає, ніколи не досягає кількості розпочатих запитів, за якою ухвалює рішення регулятор, тож тримає свою стелю, і цей лічильник залишається активним.
  • Пул, що викликає сам себе, починає запит щоразу, коли зовнішній запит відмовляється від свого внутрішнього виклику. Щойно його власні клієнти стають менш терплячими за його обробники, бюджет знижується (виміряно два зменшення вдвічі за лічені секунди), і цей лічильник замовкає після запитів, що надійшли до зниження. Жодна метрика на цій сторінці там не бере естафету: шукайте цей шаблон у вихідних викликах самого застосунку. Там, де клієнти терплячі (внутрішній cron, sidecar з великим тайм-аутом), ніхто не йде, стеля тримається, і саме цей лічильник вказує на проблему.

Класична причина — застосунок, що звертається по HTTP до цього ж сервера, але лічильник не є детектором самовикликів: пул, усі обробники якого працюють довше за бюджет, так само нічого не звільняє під час очікування. У режимі воркерів їх розрізняє oxphp_worker_request_duration_us (час обробника без очікування в черзі): обробники коротші за бюджет, поки цей лічильник зростає, означають, що очікування не просто стоять у черзі за повільною роботою. Не використовуйте для цього oxphp_request_duration_us: вона включає ці відмови, кожна з яких додає майже цілий бюджет. Поза режимом воркерів користуйтеся власними вимірюваннями застосунку.

QUEUE_WAIT_TIMEOUT_MS=0 — не найкоротший варіант скороченого бюджету: воно прибирає очікування на місце в черзі й залишає необмеженим очікування всередині черги, тож у пулі, який нічого не бере, запит, що знайшов вільне місце, так і не отримає відповіді. Поєднуйте 0 з QUEUE_CAPACITY, достатньо малим, щоб черга була заповнена до того, як це стане важливим.

Що пише журнал, поки запити відкидаються

Супервізор стежить за чотирма причинами перевантаження в oxphp_admission_refused_total під час того самого односекундного сканування й повідомляє про епізод, а не про окремі запити, бо рядок на кожну відмову сам по собі став би збоєм. Перше сканування, яке бачить зміну будь-якої з них, пише:

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 — це те, що відхилило саме це сканування, за винятком першого сканування, якому є на яку чергу дивитися: воно повідомляє про все відкинуте до нього (пул, що ще завантажує застосунок, поки накопичуються запити, — це справжній епізод). Глибина на рівні місткості без жодного вільного місця означає пул, що відстає від роботи, а wait_budget_us, значно менший за wait_budget_ceiling_us, означає, що сервер уже скоротив очікування; обидва числа наведено, бо бюджет, доведений до мінімуму, і бюджет, налаштований так низько, окремо виглядають однаково. Показники черги — це знімок на момент сканування, тоді як refused охоплює щойно минулу секунду, тож великий refused поруч із порожньою чергою — це короткий сплеск, а не суперечність.

Обидва лічильники розбито за причинами — на чотири мітки перевантаження oxphp_admission_refused_total, а назви полів утворено від лічильника, який вони розбивають: refused_queue_full, refused_wait_timeout, refused_waiting_full і refused_waiting_bytes у сумі дають refused, а повторне попередження й завершальний рядок нижче містять episode_refused_queue_full і три подібні поля, які в сумі дають episode_refused. Саме причина визначає, яке налаштування змінювати (рядок цього лічильника вище пояснює, на що вказує кожна), а сам лічильник надається лише на INTERNAL_ADDR. На інстансі без внутрішнього слухача причину ще можна прочитати в цих полях і, якщо ввімкнено трасування, в атрибуті спану oxphp.shed_reason. Усі чотири поля записуються навіть із нульовим значенням: саме явний 0 виключає причину.

Поки триває епізод, попередження повторюється як PHP admission is still shedding requests under overload, не частіше ніж раз на хвилину й лише під час сканування, яке саме щось відхиляє (тож проміжок може сягати майже двох хвилин), і містить episode_refused (накопичений підсумок) поруч із refused за сканування. Повторне попередження розбиває за причинами підсумок епізоду, а не сканування. Це важливо, бо завершальний рядок пишеться на рівні INFO, нижчому за warn, з яким пишуть журнал продакшн-конфігурації, тож там лише повторне попередження показує, з чого досі складався епізод — аж до сканування, під час якого його записано. Епізод, що завершується протягом першої хвилини, взагалі не має повторного попередження, а те, що відхилено після останнього повтору, потрапляє лише в завершальний рядок. Хвилина без жодної відмови завершує епізод на рівні 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 рахується до останньої відмови, а не до цього рядка, тож завершальна тиха хвилина не враховується. Тиха хвилина також означає, що проміжки, коротші за хвилину, об’єднуються в один епізод, що обмежує вивід щонайбільше двома рядками на хвилину; вхід в епізод такої затримки не має, бо відмова — це факт, а не вибірка.

Звіт надається в кожному режимі маршрутизації й реагує лише на чотири причини перевантаження, тож перезапуск (shutting_down) чи зниклий пул (pool_unavailable) не видають себе за навантаження. Gauge для цього стану немає, і жодна проба про нього не дізнається: відкидання запитів ніколи не змінює відповіді /health/readiness, бо інстанс, що відкидає запити з 529, швидко відповідає з робочим пулом, а виведення з ротації кожної перевантаженої репліки по черзі залишило б Service без жодного ендпоінта. На перевантаження відповідайте додаванням потужностей. Заклинений пул теж відкидає запити, і в режимі воркерів цей стан таки дає 503, щойно протриває хвилину. Розрізняє їх черга: пул, що просто відстає, її розбирає, а заклинений — ні.

Метрики пулу воркерів

Метрика Тип Опис
oxphp_workers_current gauge Поточна кількість потоків PHP-воркерів
oxphp_workers_min gauge Мінімальна кількість воркерів (у статичному режимі дорівнює поточній)
oxphp_workers_max gauge Максимальна кількість воркерів (у статичному режимі дорівнює поточній)
oxphp_workers_idle gauge Потоки воркерів без запиту в обробці, обчислюється як workers_current - busy_workers
oxphp_busy_workers gauge Потоки воркерів, що зараз виконують принаймні один запит; ніколи не перевищує oxphp_workers_current. Рахує потоки, а не запити: у режимі воркерів один потік мультиплексує багато файберів запитів і все одно враховується один раз. Запити, що чекають на допуск або стоять у черзі, не враховуються; вони відображаються в oxphp_pending_requests
oxphp_workers_spawned_total counter Загальна кількість воркерів, створених від запуску (включно з початковими)
oxphp_workers_retired_total counter Загальна кількість воркерів, виведених через тайм-аут простою (лише в динамічному режимі)

Метрики супервізора воркерів

Спостережуваність кожного воркера, яку надає супервізор воркерів. Кожна серія має мітку worker_id (індекс слота). Вони з’являються, щойно супервізор починає відстежувати стан кожного воркера.

Метрика Тип Опис
oxphp_worker_request_age_seconds gauge Вік запиту в обробці на кожному воркері, у секундах. Мітка: worker_id
oxphp_worker_long_running_total counter Сканування супервізора, які виявили запит, старший за поріг застрягання (60 секунд, фіксовано). Мітка: worker_id
oxphp_worker_stuck_total counter Лічильник класифікації застрягань для кожного воркера. Мітки: worker_id, kind (io, c_call, cpu). Лише для звітності: запит, класифікований як застряглий, не скасовується, а його воркер не перезапускається. Сервер ніяк на це не реагує, тож налаштуйте сповіщення на цю серію

Гістограма очікування в черзі

Метрика Тип Опис
oxphp_queue_wait_us histogram Час, який запит чекає в черзі, доки його не візьме воркер, у мікросекундах

Межі бакетів (мікросекунди): 50, 100, 250, 500, 1000, 2500, 5000, 10000, 50000, 100000, 250000, 500000, 1000000, +Inf.

Метрика вимірює час очікування — спершу на допуск, потім у черзі — аж до моменту, коли воркер забирає запит із черги, тож вона відповідає на питання «скільки минуло, перш ніж воркер це взяв», а не «скільки тривав запит». Усе, що відбувається після цього моменту, до неї не входить: підготовка запиту воркером, скрипт (до заголовка відповіді, якщо відповідь потокова) і передача відповіді назад до потоків введення-виведення сервера — усе це враховується в oxphp_request_duration_us. Ця передача не безкоштовна: відповідь чекає, доки потік введення-виведення прокинеться або впорається з роботою, що стоїть у черзі перед нею, тож для легкого скрипту час після цього очікування може в кілька разів перевищувати час роботи самого скрипту.

Тут фіксується лише запит, який воркер узяв із черги в межах свого бюджету. 529, відхилена через перевантаження, або 503/500, коли сервер завершує роботу чи пулу немає, враховуються в oxphp_admission_refused_total; 499 для клієнта, що пішов раніше, ніж до нього дійшов воркер, враховується в oxphp_request_cancelled_total{reason="client_abort"} і в жодній серії відмов.

Високе очікування в черзі означає, що запити довго не доходили до воркера, і це саме по собі не свідчить про нестачу воркерів: кожне значення включає час, за який потік воркера дістається до переданого йому запиту, а він є й на ненавантаженому сервері. Перш ніж збільшувати PHP_WORKERS, переконайтеся, що запити застають усіх воркерів зайнятими і що на машині є вільні ядра: з воркерами конкурують власні потоки введення-виведення сервера, інші процеси й квота CPU контейнера. oxphp_busy_workers у зіставленні з oxphp_workers_current відповідає на перше лише частково: значення зчитується в момент збору, тож сплеск, що заповнює пул між зборами, видно тут, а там — ні, і в режимі воркерів потік вважається зайнятим, поки несе будь-який запит, навіть коли обробник цього запиту призупинено, а потік бере нові. Діапазон сягає однієї секунди, що відповідає типовому QUEUE_WAIT_TIMEOUT_MS, тож запит, який витратив більшу частину свого бюджету очікування до виконання, отримує кількісну оцінку, а не потрапляє скопом у +Inf. Жоден обслужений запит не чекає довше за бюджет (після нього запит відхиляється), тож збільшення QUEUE_WAIT_TIMEOUT_MS — єдине налаштування, яке повертає очікування в +Inf.

Метрики обмеження частоти запитів

Метрика Тип Опис
oxphp_rate_limited_total counter Запити, відхилені обмежувачем частоти (повернуто 429)
oxphp_php_deny_total counter Запити, заблоковані PHP_DENY_PATHS (виконання .php заборонено). Див. Список заборони виконання PHP
oxphp_path_deny_total counter Запити, заборонені правилом .oxphpdeny. Запит, який забороняє також PHP_DENY_PATHS, враховується лише тут. Див. Правила заборони шляхів

Метрики кешу статичних файлів

Метрика Тип Опис
oxphp_static_cache_hits_total counter Запити статичних файлів, обслужені з кешу в пам’яті
oxphp_static_cache_misses_total counter Запити статичних файлів, для яких знадобилося читання з диска

Метрики стиснення

Метрика Тип Опис
oxphp_compressed_responses_total counter Відповіді, надіслані з кодуванням вмісту (Brotli, zstd або gzip)
oxphp_compression_bytes_saved_total counter Загальна кількість байтів, заощаджених стисненням (початковий розмір мінус стиснений)

Метрики режиму воркерів

Ці метрики надаються лише тоді, коли активний режим воркерів (WORKER_MODE_ENABLED=true).

Глобальні лічильники

Метрика Тип Опис
oxphp_worker_mode_enabled gauge Завжди 1, коли активний режим воркерів
oxphp_worker_requests_handled_total counter Загальна кількість запитів, оброблених постійними воркерами
oxphp_worker_recycles_total counter Загальна кількість перезапусків воркерів: цикл воркера завершився з однієї з наведених нижче причин. Коли динамічний пул виводить воркер, що простоює, це масштабування вниз, а не перезапуск: воно враховується в oxphp_workers_retired_total, а не тут і не в серії нижче
oxphp_worker_recycles_by_reason_total counter Перезапуски за причиною. Мітка: reason (scheduled, max_memory, error)
oxphp_worker_soft_resets_total counter Загальна кількість м’яких скидань між запитами

Gauge для кожного воркера

Метрика Тип Опис
oxphp_worker_memory_bytes gauge Купа PHP, яку утримує кожен воркер наприкінці свого останнього запиту. Це те, що повертає memory_get_usage() усередині обробника, і величина, з якою порівнюється WORKER_MAX_MEMORY_MIB. Зчитується трохи раніше за перевірку цієї стелі, поки суперглобальні змінні завершеного запиту ще в купі, тож вона на кілька КБ вища за значення, що спричиняє перезапуск: ставте поріг сповіщення нижче стелі, а не на ній. Воркер, що ще не обслужив жодного запиту, показує нуль. Мітка: worker (індекс слота, наприклад "0", "1"; перезапущений воркер повторно використовує слот попередника та його останнє значення, доки не завершить власний запит)
oxphp_worker_uptime_seconds gauge Секунди від створення кожного воркера. Мітка: worker
oxphp_worker_requests_count gauge Запити, оброблені кожним екземпляром воркера. Мітка: worker
oxphp_worker_request_fibers_active gauge Файбери запитів, які воркер несе зараз: 0 — простій, 1 — обслуговує один запит від початку до кінця, більше — там, де запити призупиняються й мультиплексуються в потоці. Оновлюється на кожному оберті циклу воркера, а не після завершення, тож не застигає на останньому здоровому значенні у воркера, що перестав завершувати запити. Воркер допускає щонайбільше 256 файберів запитів, і той, що тримається на 256, перестав забирати роботу з черги. Читайте разом з oxphp_busy_workers: 256 файберів на воркері, який пул вважає таким, що простоює, — це файбери, що пережили свої (вже оброблені) запити й так і не були звільнені, тоді як 256 на зайнятому воркері — це стільки ж запитів в обробці. Мітка: worker

Гістограма тривалості запитів воркера

Метрика Тип Опис
oxphp_worker_request_duration_us histogram Час виконання PHP-обробника на запит у мікросекундах (лише в режимі воркерів)

Межі бакетів (мікросекунди): 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, +Inf.

Ця гістограма вимірює час усередині колбека PHP-обробника без урахування очікування в черзі. Використовуйте її, щоб знаходити повільні обробники й відстежувати хвостову затримку в режимі воркерів.

Метрики асинхронного пулу

Ці метрики потребують ненульового значення ASYNC_WORKERS, і кожна має власну умову появи: лічильники з’являються лише після того, як принаймні одне завдання було диспетчеризовано або відхилено, gauge _in_flight / _in_flight_limit з’являються, щойно пул підключить свій лічильник завдань в обробці, а oxphp_async_output_discarded_bytes_total з’являється лише після того, як якийсь вивід було відкинуто.

Метрика Тип Опис
oxphp_async_tasks_dispatched_total counter Загальна кількість асинхронних завдань, переданих у фоновий пул
oxphp_async_tasks_completed_total counter Асинхронні завдання, що успішно завершилися
oxphp_async_tasks_failed_total counter Асинхронні завдання, що викинули виняток
oxphp_async_tasks_cancelled_total counter Асинхронні завдання, які було скасовано
oxphp_async_tasks_rejected_total counter Асинхронні завдання, відхилені під час диспетчеризації, бо черга пулу заповнена або досягнуто ліміту завдань в обробці (ASYNC_MAX_FIBERS × ASYNC_WORKERS)
oxphp_async_tasks_stranded_total counter Воркери, що продовжують працювати після тайм-ауту await_race / await_any. Кожне таке завдання може подовжити RSHUTDOWN до 5 секунд.
oxphp_async_tasks_in_flight gauge Асинхронні завдання, що зараз стоять у черзі або виконуються (надається, щойно пул підключить свій лічильник завдань в обробці)
oxphp_async_tasks_in_flight_limit gauge Максимальна кількість одночасних асинхронних завдань (ASYNC_MAX_FIBERS × ASYNC_WORKERS)
oxphp_async_output_discarded_bytes_total counter Байти виводу асинхронних завдань, відкинуті, коли воркер простоює (echo в асинхронному завданні не має клієнта, який би його отримав)

Метрики експорту OpenTelemetry

Ці метрики потребують OTEL_ENABLED=true. Вони рахують пакети спанів, які OxPHP надсилає колектору OTLP: спани запитів, а також спани APM, коли плагін APM увімкнено.

Метрика Тип Опис
oxphp_otel_export_failures_total counter Експорти спанів, що завершилися невдачею, — після повторної спроби, якщо її було зроблено. Їхні спани більше не надсилаються
oxphp_otel_export_failed_spans_total counter Спани, які несли ці невдалі експорти
oxphp_otel_export_retries_total counter Експорти, повторені один раз, бо перша спроба швидко завершилася невдачею, не отримавши відповіді від колектора

Експорт повторюється один раз, коли його перша спроба завершується невдачею без відповіді від колектора протягом половини OTEL_EXPORTER_OTLP_TIMEOUT або однієї секунди, залежно від того, що коротше: з’єднання було скинуто чи закрито або його не вдалося встановити. Звична причина — балансувальник навантаження чи NAT, що розірвав з’єднання, поки воно простоювало між експортами. Статус помилки, яким відповідає сам колектор, як-от відхилений API-ключ чи обмеження частоти, не повторюється: специфікація OTLP забороняє повторювати більшість із них, а експортер не передає затримку, про яку просить колектор, що обмежує частоту. Повторна спроба, що доставила пакет, враховується лише в oxphp_otel_export_retries_total. Спроба, що впирається в тайм-аут експорту, не повторюється, тож повільному колектору ніколи не надсилається той самий пакет двічі.

Утім, повторна спроба може дублювати дані. Коли колектор таки отримав першу спробу до того, як з’єднання обірвалося, повторна спроба доставляє ці спани вдруге, і з тієї ж причини експорт, урахований як невдалий, міг усе ж дійти до колектора.

Спани, які трасувальник відкидає ще до експорту, бо його черга заповнена (OTEL_BSP_MAX_QUEUE_SIZE, типово 2048 спанів), тут не враховуються.

Поради щодо дашбордів Grafana

Для побудови дашбордів корисні такі запити PromQL:

Частота запитів (запитів на секунду):

text
rate(oxphp_requests_total[5m])

Середній час відповіді (мілісекунди):

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

p99 тривалості запиту (мілісекунди):

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

Частка помилок (відповіді 5xx у відсотках):

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

Завантаженість пулу воркерів:

text
oxphp_busy_workers / oxphp_workers_current

Це справжня частка між 0 і 1. Стабільне значення 1 означає, що кожен воркер зайнятий, а нові запити стають у чергу. Поєднуйте її з rate(oxphp_admission_refused_total{reason=~"queue_full|wait_timeout|waiting_full|waiting_bytes"}[5m]), щоб побачити, чи перетворюється це відставання на відмови, і з oxphp_pending_requests, щоб побачити його глибину.

Насиченість черги (частота втрат на секунду):

text
rate(oxphp_dropped_requests_total[5m])

p99 очікування в черзі (мікросекунди):

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

Частка влучань у кеш статичних файлів:

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

Байти, заощаджені стисненням, на секунду:

text
rate(oxphp_compression_bytes_saved_total[5m])

p99 затримки в режимі воркерів (мікросекунди):

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

Бюджет очікування, знижений нижче стелі (частка):

text
oxphp_admission_wait_budget_us / oxphp_admission_wait_budget_ceiling_us

1 — норма; будь-яке менше значення означає, що сервер скорочує очікування через перевантаження. Налаштовуйте сповіщення на це співвідношення, а не на число, скопійоване з QUEUE_WAIT_TIMEOUT_MS.

Зупинки циклу прийняття (сповіщення за ненульового значення):

text
rate(oxphp_accept_stalls_total[5m])

Частота перезапусків воркерів (на хвилину):

text
rate(oxphp_worker_recycles_total[5m]) * 60

Середнє використання пам’яті воркерами:

text
avg(oxphp_worker_memory_bytes)

Конфігурація збору Prometheus

Додайте завдання збору до свого prometheus.yml:

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

Для виявлення сервісів у 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"

Промпти для вашого агента

Ці промпти розраховано на агента з підключеним MCP-сервером документації OxPHP: потрібні сторінки він прочитає сам.

prompt
Налаштуй збір метрик Prometheus з мого сервісу OxPHP. Прочитай сторінку `operations/metrics` через MCP-сервер документації OxPHP. Додай конфігурацію збору й усе, що потрібно в моїх файлах Compose або Kubernetes, щоб ендпоінт метрик був досяжний, використовуючи назви сервісів, які вже є в цьому репозиторії.
prompt
Збудуй мені стартовий дашборд Grafana для OxPHP. Прочитай сторінку `operations/metrics` через MCP-сервер документації OxPHP. Вибери метрики, які справді показують, чи здоровий мій застосунок, і поясни, про що говоритиме кожна панель, коли її значення зміниться.

Див. також