Files
XC_VM/docs/ru/guides/error-handling.md
T
2026-09-16 22:04:48 +03:00

380 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Модель обработки ошибок
XC_VM обработка ошибок состоит из трех уровней:
- **Коды ошибок** -- что привело к сбою (централизованный реестр именованных строк ошибок)
- **Обработчики ошибок** -- как генерируется HTTP-ответ клиента (`generateError()`, `generate404()`)
- **Подсистема регистратора** -- фиксация во время выполнения PHP ошибок, неперехваченных исключений и фатальных сбоев
---
## Обзор потока
```text
Application code
|
+-- generateError('CODE') // deliberate error response
| -> debug mode: styled HTML page with code + description
| -> production: generate404() or explicit HTTP code
|
+-- PHP warning / notice / error // runtime errors
| -> Logger::handleError()
| -> maps errno to level (ERROR, WARNING, NOTICE, INFO)
| -> writes base64-encoded JSON to error_log.log
| -> optionally displays on screen
|
+-- Uncaught exception // unhandled Throwable
| -> Logger::handleException()
| -> logs as EXCEPTION with full chained trace
|
+-- Fatal error at shutdown // E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR
-> Logger::handleFatal()
-> logs as FATAL (no stack trace available)
```
---
## Реестр кодов ошибок
Все коды объявляются методом `ErrorResponder::codes()` в `src/Core/Error/ErrorResponder.php`, который возвращает массив code => English-описание.
Формат кода:
- Ключ: строка в верхнем регистре (пример: `INVALID_CREDENTIALS`)
- Значение: понятное для человека описание на английском языке
Используйте только централизованные определения кода. Не следует жестко кодировать текст ошибки в обработчиках конечных точек.
### Полный список кодов
|Код|Описание|
| --- | --- |
| `API_IP_NOT_ALLOWED` |IP-адрес не разрешен для доступа к API.|
| `ARCHIVE_DOESNT_EXIST` |Для этого идентификатора потока отсутствуют архивные файлы.|
| `ASN_BLOCKED` |ASN был заблокирован.|
| `BANNED` |Линия была заблокирована.|
| `BLOCKED_USER_AGENT` |Пользовательский агент был заблокирован.|
| `CACHE_INCOMPLETE` |Генерируется кэш...|
| `DEVICE_NOT_ALLOWED` |Устройствам MAG и Enigma не разрешен доступ к этому файлу.|
| `DISABLED` |Линия была отключена.|
| `DOWNLOAD_LIMIT_REACHED` |Достигнут лимит одновременной загрузки.|
| `E2_DEVICE_LOCK_FAILED` |Проверка блокировки устройства не удалась.|
| `E2_DISABLED` |Устройство было отключено.|
| `E2_NO_TOKEN` |Токен не был указан.|
| `E2_TOKEN_DOESNT_MATCH` |Токен не соответствует записям.|
| `E2_WATCHDOG_TIMEOUT` |Истек лимит времени.|
| `EMPTY_USER_AGENT` |Пустые пользовательские агенты запрещены.|
| `EPG_DISABLED` |EPG был отключен.|
| `EPG_FILE_MISSING` |Кэшированные файлы EPG отсутствуют.|
| `EXPIRED` |Срок действия строки истек.|
| `FORCED_COUNTRY_INVALID` |Страна не совпадает с принудительной страной.|
| `GENERATE_PLAYLIST_FAILED` |Не удалось создать список воспроизведения.|
| `HLS_DISABLED` |HLS был отключен.|
| `HOSTING_DETECT` |Обнаружен хостинг-сервер.|
| `INVALID_API_PASSWORD` |Неверный пароль API.|
| `INVALID_CREDENTIALS` |Имя пользователя или пароль неверны.|
| `INVALID_HOST` |Доменное имя не распознано.|
| `INVALID_STREAM_ID` |Идентификатор потока не существует.|
| `INVALID_TYPE_TOKEN` |Токены не могут быть использованы для этого типа потока.|
| `IP_BLOCKED` |IP-адрес был заблокирован.|
| `IP_MISMATCH` |Текущий IP-адрес не соответствует исходному IP-адресу подключения.|
| `ISP_BLOCKED` |Провайдер был заблокирован.|
| `LB_TOKEN_INVALID` |Токен AES не может быть расшифрован.|
| `LEGACY_EPG_DISABLED` |Устаревший epg.php доступ был отключен.|
| `LEGACY_GET_DISABLED` |Устаревший get.php доступ был отключен.|
| `LEGACY_PANEL_API_DISABLED` |Устаревший panel_api.php доступ был отключен.|
| `LINE_CREATE_FAIL` |Не удалось вставить строку в базу данных.|
| `NO_CREDENTIALS` |Учетные данные не были указаны.|
| `NO_SERVERS_AVAILABLE` |В настоящее время серверы для этого потока не доступны.|
| `NO_TIMESTAMP` |Временная метка архива не указана.|
| `NO_TOKEN_SPECIFIED` |Зашифрованный токен AES не был указан.|
| `NOT_ENIGMA_DEVICE` |Линия - это не загадочное устройство.|
| `NOT_IN_ALLOWED_COUNTRY` |Нет в списке разрешенных стран.|
| `NOT_IN_ALLOWED_IPS` |Его нет в списке разрешенных IP-адресов.|
| `NOT_IN_ALLOWED_UAS` |Отсутствует в списке разрешенных пользовательских агентов.|
| `NOT_IN_BOUQUET` |У Line нет доступа к этому идентификатору потока.|
| `PLAYER_API_DISABLED` |API плеера был отключен.|
| `PROXY_ACCESS_DENIED` |Вы не можете получить прямой доступ к этому потоку, пока включен прокси-сервер.|
| `PROXY_DETECT` |Обнаружен прокси-сервер.|
| `PROXY_NO_API_ACCESS` |Не удается получить доступ к API через прокси.|
| `RESTREAM_DETECT` |Обнаружен повторный поток.|
| `STALKER_CHANNEL_MISMATCH` |Идентификатор потока не совпадает с токеном stalker.|
| `STALKER_DECRYPT_FAILED` |Не удалось расшифровать токен сталкера.|
| `STALKER_INVALID_KEY` |Недействительный ключ сталкера.|
| `STALKER_IP_MISMATCH` |IP-адрес не соответствует токену stalker.|
| `STALKER_KEY_EXPIRED` |Срок действия жетона сталкера истек.|
| `STREAM_OFFLINE` |Трансляция в данный момент отключена.|
| `SUBTITLE_DOESNT_EXIST` |Файл субтитров не существует.|
| `THUMBNAIL_DOESNT_EXIST` |Файл миниатюр не существует.|
| `THUMBNAILS_NOT_ENABLED` |Миниатюра не включена для этого потока.|
| `TOKEN_ERROR` |Токен AES содержит неполные данные.|
| `TOKEN_EXPIRED` |Срок действия токена AES истек.|
| `TS_DISABLED` |MPEG-TS был отключен.|
| `USER_ALREADY_CONNECTED` |Линия уже подключена с другого IP-адреса.|
| `USER_DISALLOW_EXT` |Добавочный номер отсутствует в списке разрешенных.|
| `VOD_DOESNT_EXIST` |VOD файл не существует.|
| `WAIT_TIME_EXPIRED` |Время начала трансляции истекло, запустить не удалось.|
Коды, относящиеся к потоковой передаче данных (`CACHE_INCOMPLETE`, `SUBTITLE_DOESNT_EXIST`, `NO_SERVERS_AVAILABLE`, `PROXY_ACCESS_DENIED`), были перенесены из `stream/init.php` в централизованный реестр.
---
## Обработчики ошибок
Определено в `src/Core/Error/ErrorHandler.php`. Это простые функции (не методы класса), загружаемые в начале начальной загрузки.
### `generateError(string $rError, bool $rKill = true, ?int $rCode = null)`
Выдает ответ об ошибке HTTP. Поведение зависит от параметра `debug_show_errors`:
```text
if debug_show_errors === true
render styled HTML page showing error key + description
if $rKill -> exit()
else (production)
if $rKill
if $rCode is set -> http_response_code($rCode) + exit()
else -> generate404()
// if !$rKill, does nothing in production mode
```
Параметры:
|Параметр|Тип|По умолчанию|Значение|
| --- | --- | --- | --- |
| `$rError` | `string` |--|Ключ от `ErrorResponder::codes()`|
| `$rKill` | `bool` | `true` |Завершить работу скрипта после вывода|
| `$rCode` |`инт\|нулевой`| `null` |Явный код ответа HTTP (обходит 404 в рабочей среде)|
Примеры:
```php
generateError('INVALID_CREDENTIALS'); // production: 404 + exit
generateError('API_IP_NOT_ALLOWED', true, 403); // production: 403 + exit
generateError('STREAM_OFFLINE', false); // production: no output, no exit
```
### `generate404(bool $rKill = true)`
Возвращает страницу в стиле nginx `404 Not Found` и устанавливает HTTP 404. HTML-код содержит комментарии с дополнениями, чтобы скрыть страницы ошибок, отображаемые в браузере MSIE и Chrome.
```php
generate404(); // 404 + exit
generate404(false); // 404, continue execution
```
---
## Подсистема регистратора
Определен в `src/Core/Logging/Logger.php`. Класс `final`, который регистрирует три глобальных обработчика PHP для отслеживания всех ошибок во время выполнения и записи их в файл.
### Инициализация
```php
Logger::init(bool $showErrors, string $logFile): void
```
Регистры:
1. `set_error_handler([Logger::class, 'handleError'])` -- PHP предупреждения, извещения, ошибки
2. `set_exception_handler([Logger::class, 'handleException'])` -- не перехвачено `Throwable`
3. `register_shutdown_function([Logger::class, 'handleFatal'])` -- неустранимые ошибки при завершении работы
Также настраивает `error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED)` и устанавливает `display_errors` / `display_startup_errors` на основе `$showErrors`.
### Где вызывается функция Logger::init()
Регистратор инициализируется в двух местах, в зависимости от пути запроса:
|Путь входа|Файл|Как|
| --- | --- | --- |
|Bootstrap (все контексты)| `src/bootstrap.php` |`XC_Bootstrap::loadConstants()` вызовы `Logger::init(PHP_ERRORS, LOGS_TMP_PATH . 'error_log.log')`|
|Конечные точки потоковой передачи| `src/Core/Http/RequestGuard.php` |Загружает настройки из файлового кэша, определяет `PHP_ERRORS`, затем вызывает `Logger::init(PHP_ERRORS, LOGS_TMP_PATH . 'error_log.log')`|
В обоих случаях параметр `PHP_ERRORS` соответствует параметру `debug_show_errors` (по умолчанию используется значение `false`, когда настройки недоступны).
### Отображение уровня ошибок
`Logger::handleError()` сопоставляет PHP константы ошибок со строками уровня журнала с помощью `mapErrorLevel()`:
|PHP константа(ы)|Уровень регистрации|
| --- | --- |
|`E_ERROR`, `E_CORE_ERROR`, `E_COMPILE_ERROR`| `ERROR` |
|`E_WARNING`, `E_USER_WARNING`| `WARNING` |
|`E_NOTICE`, `E_USER_NOTICE`| `NOTICE` |
|Все остальные значения `errno`| `INFO` |
Обработчик завершения работы (`handleFatal()`) проверяет `error_get_last()` на наличие этих фатальных типов и регистрирует их как `FATAL`:
|PHP постоянные значения при выключении|Уровень регистрации|
| --- | --- |
|`E_ERROR`, `E_PARSE`, `E_CORE_ERROR`, `E_COMPILE_ERROR`| `FATAL` |
Неперехваченные исключения, зарегистрированные с помощью `handleException()`, всегда используют уровень `EXCEPTION`.
Ошибки, подавленные с помощью оператора `@`, игнорируются (обработчик проверяет `error_reporting() & $errno`).
### Формат журнала
Each log entry is written as a single line: `base64_encode(json_encode($data))` followed by a newline. This prevents line corruption from multi-line messages.
Декодированная структура JSON:
```json
{
"type": "WARNING",
"log_message": "Undefined variable $foo",
"file": "/home/xc_vm/Domain/Stream/StreamService.php",
"line": 142,
"log_extra": "#0 /home/xc_vm/...(line): function()\n#1 ...",
"time": 1716220800,
"env": "fpm-fcgi"
}
```
|Поле|Содержание|
| --- | --- |
| `type` |Уровень регистрации: `ERROR`, `WARNING`, `NOTICE`, `INFO`, `EXCEPTION`, или `FATAL`|
| `log_message` |Текст сообщения об ошибке/исключении|
| `file` |Абсолютный путь к исходному файлу|
| `line` |Номер строки, в которой произошла ошибка|
| `log_extra` |Трассировка стека (форматированная строка). Пусто для неустранимых ошибок.|
| `time` |Временная метка Unix|
| `env` |PHP Имя SAPI (`cli`, `fpm-fcgi` и т.д.)|
### Расположение файла журнала
Путь по умолчанию: `LOGS_TMP_PATH . 'error_log.log'`
Если каталог журнала не существует, Logger создает его с правами доступа `0775`. При запуске от имени root (распространенного в контейнерах) файлу присваивается значение `xc_vm:xc_vm` с режимом `0664`.
### Вывод на экран
Когда `$showErrors` равно `true`, регистратор также отображает ошибки напрямую:
- **КЛИ:** выходной сигнал терминала с цветовой кодировкой (красный - НЕИСПРАВИМОСТЬ/ОШИБКА, желтый - ПРЕДУПРЕЖДЕНИЕ, синий - УВЕДОМЛЕНИЕ)
- **Сеть:** встроенный `<div>` с моноширинным шрифтом, красной рамкой и трассировкой стека в блоке `<pre>`
---
## Конвейер ведения журнала: Передача файла в базу данных
Программа ведения журнала записывает данные в файл `error_log.log` на диске. Отдельная подсистема считывает этот файл и сохраняет записи в таблице базы данных `panel_logs`:
1. **Лесоруб** записывает строки JSON в кодировке base64 в `error_log.log`
2. **Файловый регистратор** (`src/Core/Logging/FileLogger.php`) предоставляет дополнительный интерфейс ведения журнала, используемый кодом приложения (ошибки PDO, ошибки EPG и т.д.), который записывает данные в тот же файл в том же формате
3. Записи заносятся в таблицу `panel_logs`
4. **Диагностическая служба** (`src/Core/Diagnostics/DiagnosticsService.php`) считывается из `panel_logs` для:
- `downloadPanelLogs()` -- извлекает до 1000 последних ошибок, не связанных с EPG, затем обрезает таблицу
- `submitPanelLogs()` -- отправляет логи на центральный сервер API для анализа
5. Панель администратора отображает эти журналы в разделе **Управление > Журналы > Ошибки панели**
### Фильтрация шума файлового регистратора
`FileLogger::log()` пропускает записи, которые соответствуют:
- Сообщения, содержащие `panel_logs` в дополнительном поле (предотвращает рекурсивное ведение журнала)
- Сообщения, соответствующие `timeout exceeded`, `lock wait timeout` или `duplicate entry` (зашумленные ошибки MySQL)
---
## Другие лесорубы
Каталог `src/Core/Logging/` содержит дополнительные специализированные регистраторы:
|Класс|Файл|Цель|
| --- | --- | --- |
| `Logger` | `Logger.php` |Обработчик глобальной PHP ошибки/исключения/фатального исхода (описанный выше)|
| `FileLogger` | `FileLogger.php` |Ведение журнала на уровне приложения (ошибки PDO, EPG и т.д.) до `error_log.log`|
| `DatabaseLogger` | `DatabaseLogger.php` |Клиент передает события потокового запроса в `client_request.log` (вводимые в таблицу `client_logs`)|
| `UpdateLogger` | `UpdateLogger.php` |Операции обновления системы до `MAIN_HOME/update.log` (обычный текст, не base64)|
Все регистраторы, кроме `UpdateLogger`, реализуют `LoggerInterface` и записывают JSON в кодировке base64.
---
## Типы исключений в кодовой базе
В кодовой базе определено небольшое количество пользовательских классов исключений. Все неперехваченные исключения перехватываются командой `Logger::handleException()`, которая регистрирует всю цепочку исключений (включая `getPrevious()`).
|Класс исключений|Базовый класс|Местоположение|
| --- | --- | --- |
| `DropboxException` | `\Exception` | `src/Core/Storage/DropboxException.php` |
| `M3uParser\Exception` | `\Exception` | `src/vendor/gemorroj/m3u-parser/src/Exception.php` |
| `DataBuildingException` | `\RuntimeException` | `src/vendor/chrisyue/php-m3u8/src/Parser/DataBuildingException.php` |
| `DefinitionException` | `\RuntimeException` | `src/vendor/chrisyue/php-m3u8/src/Definition/DefinitionException.php` |
| `DumpingException` | `\RuntimeException` | `src/vendor/chrisyue/php-m3u8/src/Dumper/DumpingException.php` |
В большинстве случаев в коде приложения используются общие ошибки `Exception` или используется встроенная система ошибок PHP. Обработчик исключений регистратора принимает любое `Throwable`.
---
## Отладка против производства
### Производство (по умолчанию: `debug_show_errors = false`)
- `generateError()` возвращает общую страницу 404 (или явный HTTP-код), скрывая внутреннюю причину сбоя
- Регистратор по-прежнему записывает все ошибки в `error_log.log` на диск
- для `display_errors` и `display_startup_errors` заданы значения `'0'`
- Ошибки видны только через панель администратора (страница ошибок панели) или файлы журналов
### Отладка (`debug_show_errors = true`)
- `generateError()` показывает стилизованную страницу с ключом ошибки и сопоставленным описанием
- Регистратор дополнительно отображает ошибки на экране (вывод CLI с цветовой кодировкой или встроенный HTML).
- для `display_errors` и `display_startup_errors` заданы значения `'1'`
Не включайте отображение отладки на рабочих узлах.
---
## Регистрация обработчика ошибок Bootstrap
Инфраструктура обработки ошибок загружается на ранней стадии загрузки:
1. `bootstrap.php` определяет `MAIN_HOME` и регистрирует автозагрузчик Composer
2. `XC_Bootstrap::loadConstants()` загружает (по порядку):
- `Core/Error/ErrorHandler.php` -- определяет `generateError()` и `generate404()` (загружается глобально через Composer `autoload.files`); сам каталог кодов равен `ErrorResponder::codes()` в `Core/Error/ErrorResponder.php`
- Путь и конфигурационные файлы
- `Core/Logging/Logger.php` -- определение класса
3. вызывается `Logger::init(PHP_ERRORS, LOGS_TMP_PATH . 'error_log.log')`, регистрирующий три глобальных обработчика
4. Начиная с этого момента, регистрируются все ошибки PHP, неперехваченные исключения и фатальные сбои
Для конечных точек потоковой передачи, которые обходят полную загрузку, `RequestGuard.php` выполняет шаги 2-3 независимо: загружает настройки из файлового кэша, определяет `PHP_ERRORS` и вызывает `Logger::init()`.
---
## Добавление нового кода ошибки
1. Добавьте новую запись в массив, возвращаемый `ErrorResponder::codes()` в `src/Core/Error/ErrorResponder.php`:
```php
'MY_NEW_ERROR' => 'Human-readable description.',
```
2. Используйте это в коде:
```php
generateError('MY_NEW_ERROR');
```
Описания должны быть на английском языке для приведения в соответствие с существующим реестром.
---
## Связанные файлы
|Файл|Цель|
| --- | --- |
| `src/Core/Error/ErrorResponder.php` |Централизованная карта кодов ошибок (`ErrorResponder::codes()`)|
| `src/Core/Error/ErrorHandler.php` |функции `generateError()` и `generate404()`|
| `src/Core/Logging/Logger.php` |Глобальные PHP обработчики ошибок, исключений и фатальных исходов|
| `src/Core/Logging/LoggerInterface.php` |Интерфейс контракта ведения журнала|
| `src/Core/Logging/FileLogger.php` |Ведение журнала файлов на уровне приложения (PDO, EPG и т.д.)|
| `src/Core/Logging/DatabaseLogger.php` |Ведение журнала событий потокового запроса клиента|
| `src/Core/Logging/UpdateLogger.php` |Ведение журнала операций обновления системы|
| `src/Core/Http/RequestGuard.php` |Путь потоковой передачи: защита от наводнений, проверка хоста, запуск регистратора|
| `src/Core/Diagnostics/DiagnosticsService.php` |Считывает таблицу `panel_logs` для отображения администратором и отправки по API|
| `src/bootstrap.php` |Включает уровень ошибок и регистратор во всех контекстах начальной загрузки|