Внутрішній сервер

OxPHP запускає окремий HTTP-сервер на виділеному порту для перевірок стану, метрик Prometheus і перегляду поточної конфігурації. Він повністю ізольований від основного слухача застосунку: жодного TLS, обмеження частоти запитів, ідентифікаторів запитів чи обробки подій.

Як це працює

Задайте в INTERNAL_ADDR адресу для прослуховування (наприклад, 127.0.0.1:9090), і OxPHP запустить на ній другий HTTP-слухач. Внутрішній сервер надає такі вбудовані ендпоінти:

  • /health — зведений статус у JSON
  • /health/liveness, /healthz (псевдонім) — проба liveness
  • /health/readiness, /readyz (псевдонім) — проба readiness
  • /health/startup, /startupz (псевдонім) — проба startup
  • /metrics — метрики у форматі Prometheus
  • /config — JSON з активною конфігурацією сервера

Плагіни можуть реєструвати додаткові ендпоінти з префіксом /__<plugin>/. Під час плавного завершення роботи внутрішній сервер лишається доступним, доки основний сервер не закінчить завершувати зʼєднання.

Note

Внутрішній сервер запускається лише тоді, коли INTERNAL_ADDR задано явно. Без неї ендпоінти стану, метрик і конфігурації через HTTP недоступні.

Налаштування

Змінна За замовчуванням Опис
INTERNAL_ADDR (не задано) Адреса внутрішнього сервера. Якщо не задано, сервер не запускається. Значення лише з портом (:9090) привʼязується до 127.0.0.1; щоб відкрити доступ ззовні хоста, вкажіть явно 0.0.0.0:9090. Приклад: 127.0.0.1:9090
INTERNAL_ALLOW_IPS (не задано) Список дозволених CIDR/IP через кому. Клієнти поза списком отримують 403 на шляхах /metrics, /config і /__<plugin>/; проби стану завжди лишаються доступними. Не задано або порожньо — дозволено всім. Loopback не додається неявно: щоб зберегти доступ з localhost, додайте 127.0.0.1/32. Некоректний список перериває запуск

Ендпоінти

GET /health

Повертає статус стану в JSON для дашбордів і перевірок стану контейнерів. Для Kubernetes натомість використовуйте окремі ендпоінти проб (див. Інтеграція з Kubernetes): /health звітує про всі підсистеми одразу, тож, підключений до liveness, він перезапускав би под через збій будь-якої з них.

200 OK — усі системи справні:

json
{ "active_connections": 7, "executor_healthy": true, "plugins": { "otel": "ok" }, "pool_stalled": false, "status": "ok", "total_requests": 48203, "uptime_secs": 3612 }

503 Service Unavailable — плагін повідомив про збій:

json
{ "active_connections": 7, "executor_healthy": true, "plugins": { "otel": "failed" }, "pool_stalled": false, "status": "degraded", "total_requests": 48203, "uptime_secs": 3612 }

Обʼєкт plugins містить кожен завантажений плагін із його станом: "ok", "degraded" або "failed". Ендпоінт повертає 503, коли будь-який плагін звітує "failed", коли не лишилося жодного робочого потоку PHP (executor_healthy: false) або коли пул заклинило (pool_stalled: true, лише в режимі воркерів; див. Перевірки стану). Плагіни зі станом "degraded" зʼявляються в тілі JSON, але HTTP-статус лишається 200. Навантаження теж не вважається несправністю: сервер, який під перевантаженням відкидає запити з 529, усе одно звітує "ok".

GET /metrics

Повертає метрики, сумісні з Prometheus, у текстовому форматі (text/plain; version=0.0.4; charset=utf-8). Завжди повертає 200.

bash
curl http://localhost:9090/metrics
text
# HELP oxphp_requests_total Total HTTP requests # TYPE oxphp_requests_total counter oxphp_requests_total 48203 # HELP oxphp_active_connections Current open connections # TYPE oxphp_active_connections gauge oxphp_active_connections 7 ...

Повний перелік доступних метрик див. у розділі Метрики.

GET /config

Повертає активну конфігурацію сервера у форматі JSON. Завжди повертає 200.

bash
curl -s http://localhost:9090/config | jq .
json
{ "access_log": "all", "async_in_flight_cap": 0, "async_max_fibers": 256, "async_queue_capacity": 0, "async_workers": 0, "brotli_level": 5, "deny_file": { "allow": 1, "deny": 3, "entry": 1, "fallback": "404", "loaded": true, "rules": 5 }, "document_root": "/var/www/html/public", "drain_timeout_seconds": 25, "entry_file": "/var/www/html/public/index.php", "executor_type": "sapi", "gzip_level": 6, "header_timeout_seconds": 5, "listen_addr": "0.0.0.0:443", "log_level": "info", "max_connections": 10000, "max_query_body": 524288, "php_workers": "8", "plugins": {}, "queue_capacity": 1024, "queue_max_waiting": 1024, "queue_max_waiting_bytes": 67108864, "queue_wait_timeout_ms": 1000, "rate_limit": 100, "rate_window_seconds": 60, "runtime_hooks": ["sleep", "streams", "net"], "static_max_age": 2592000, "static_revalidate": false, "superglobals_enabled": true, "tls_enabled": true, "tls_min_version": "1.2", "tokio_workers": 4, "trace_context": false, "trusted_proxies": false, "worker_max_memory_mib": 0, "worker_mode_enabled": false, "zstd_level": 6 }

runtime_hooks перелічує категорії хуків середовища виконання, які процес встановив під час запуску. Це розібрана множина, а не сире значення RUNTIME_HOOKS, тож all, 1 і sleep, streams, net однаково читаються як ["sleep", "streams", "net"], і збирачу метрик не треба заново реалізовувати цю граматику. Порожній список означає, що жоден хук не встановлено; те саме дасть і помилка в назві змінної. Поле показує, що було встановлено, а не що діє: там, де файберів немає взагалі, хуки передають виклик вбудованим функціям, які вони замінили, тому сервер у традиційному режимі перелічує налаштовані категорії, але поводиться як зазвичай. Див. Хуки середовища виконання.

deny_file підсумовує файл .oxphpdeny, прочитаний з DOCUMENT_ROOT під час запуску: кількість правил з розбивкою за дією (deny, entry для правил >, allow для правил !) і те, чим відповідає правило заборони, — або кодом статусу з PHP_DENY_FALLBACK, або "script" (без шляху до скрипта). Самі правила не виводяться: на внутрішньому сервері, доступному ззовні, вони були б картою саме тих шляхів, які файл захищає. Щоб побачити, як було прочитано кожне правило, запустіть сервер з LOG_LEVEL=debug і шукайте рядки oxphpdeny rule. Якщо файлу немає, запис має вигляд {"loaded": false}.

Шляхи до файлів TLS-сертифіката й ключа ніколи не виводяться (надається лише булеве значення tls_enabled), а internal_addr і error_pages_dir вилучаються з відповіді — це топологія розгортання та шляхи файлової системи, які допомагають зловмисникові й не потрібні збирачам метрик.

Ендпоінти плагінів

Плагіни можуть реєструвати власні ендпоінти з префіксом /__<plugin_name>/. Наприклад, плагін з назвою otel міг би надавати /__otel/status. Ці ендпоінти доступні, лише якщо відповідний плагін завантажено і він зареєстрував обробник.

Будь-який шлях, що не відповідає вбудованому ендпоінту чи ендпоінту плагіна, повертає 404 Not Found.

Інтеграція з Kubernetes

Підключайте кожну пробу до призначеного для неї ендпоінту, а не спрямовуйте всі три на /health. Цей ендпоінт зводить стан усіх підсистем в один статус, тож у ролі liveness він відповідав би на збій плагіна чи заклинений пул перезапуском пода.

Проба readiness

/health/readiness визначає, чи спрямовує Kubernetes трафік на под:

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

Коли вона повертає 503, Kubernetes вилучає под зі списку ендпоінтів Service; трафік відновлюється, щойно вона знову повертає 200. Проба звітує про несправності, а не про навантаження, тому перевантажений под лишається в ротації. Див. Перевірки стану.

Проба liveness

/health/liveness відповідає 200 щоразу, коли процес узагалі здатен обслужити HTTP-запит. Саме це й має означати liveness, адже перезапуск не виправить перевантажений пул, зате забере з собою всю роботу, що виконується:

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

Проба startup

Для застосунків із повільним початковим завантаженням (великі фреймворки, важкі автозавантажувачі):

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

Безпека

Внутрішній сервер не має автентифікації за bearer-токеном; доступ визначається тим, до якої адреси він привʼязаний, і змінною INTERNAL_ALLOW_IPS. Щоб захистити його:

  • Привʼязуйте до localhost — INTERNAL_ADDR лише з портом (:9090) вже привʼязується до 127.0.0.1; явне 127.0.0.1:9090 залишає порт доступним лише зсередини контейнера чи хоста
  • Задайте INTERNAL_ALLOW_IPS, коли слухач має бути доступним ззовні хоста — це список дозволених CIDR/IP, який повертає 403 на /metrics, /config і шляхах плагінів для клієнтів поза ним, тоді як проби стану лишаються доступними. Loopback не додається неявно, тож додайте 127.0.0.1/32, якщо вам і далі потрібен доступ з localhost. Під час запуску сервер попереджає, якщо слухач відкритий ззовні хоста без заданого списку дозволених адрес
  • Не відкривайте його як Service у Kubernetes — оголосіть порт як containerPort, але не створюйте для нього Service. Проби Kubernetes звертаються до портів контейнера напряму
  • Використовуйте мережеві політики — обмежте доступ на мережевому рівні, якщо порт усе ж потрібно відкрити
Warning

Ендпоінт /config розкриває операційні подробиці (корінь документів, обмеження частоти запитів, кількість воркерів, значення тайм-аутів). Шляхи TLS, internal_addr і error_pages_dir вилучаються, але зважте, чи має решта бути доступною ззовні пода.

Приклад для Docker

Щоб швидко подивитися, можна опублікувати внутрішній порт; у продакшені привʼяжіть внутрішній сервер до localhost, а в Kubernetes, чиї проби httpGet не дістаються до слухача на loopback, привʼяжіть його до 0.0.0.0:9090 і задайте INTERNAL_ALLOW_IPS.

compose.yaml
services: app: image: ghcr.io/oxphp/oxphp:0.13.0 ports: - "80:80" - "9090:9090" environment: - DOCUMENT_ROOT=/var/www/html/public - INTERNAL_ADDR=0.0.0.0:9090

Усунення несправностей

Ендпоінти стану, метрик і конфігурації недоступні

INTERNAL_ADDR не задано.

Рішення: додайте змінну середовища:

bash
INTERNAL_ADDR=0.0.0.0:9090
/health повертає 503, хоча застосунок працює

Один із завантажених плагінів звітує PluginHealth::Failed або, рідше, executor_healthy дорівнює false чи pool_stalled дорівнює true. Перегляньте обʼєкт plugins у відповіді перевірки стану, щоб зʼясувати, який плагін збоїть:

bash
curl -s http://localhost:9090/health | jq '.plugins'
Не вдається дістатися до внутрішнього сервера ззовні контейнера

Сервер привʼязаний до 127.0.0.1, який доступний лише зсередини контейнера.

Рішення: змініть адресу на 0.0.0.0 для зовнішнього доступу й задайте INTERNAL_ALLOW_IPS. Пробам Kubernetes httpGet потрібна та сама зміна, бо вони не дістаються до слухача на loopback.

Метрики не показують режим воркерів або асинхронні метрики

Метрики режиму воркерів зʼявляються, лише коли цей режим увімкнено (WORKER_MODE_ENABLED=true). Асинхронні метрики зʼявляються, лише коли ASYNC_WORKERS > 0 і хоча б одне завдання було передано на виконання або відхилено.

Див. також