mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-10-27 03:03:31 +01:00
Overhaul the Docsify documentation (English + Russian) so it matches the current codebase and follows one consistent pattern. Content accuracy (post-migration): - Rewrite development/autoloader.md to PSR-4 / Composer (the old XC_Autoloader scanner, igbinary tmp/cache/autoload_map and registerDirectories are gone). - PascalCase every source path (src/core -> src/Core, domain/Stream, cli/Commands, public/Controllers, Infrastructure/Redis, ...) across all docs. - Replace the removed autoload.php references with vendor/autoload.php (build_system, bootstrap-contexts, error-handling, modules). - ssl-generation: note that the installer now auto-generates a unique self-signed certificate before Nginx starts. Common pattern (Clean & uniform): - Strip emoji from headings; remove the in-page Navigation blocks (the Docsify sidebar already provides navigation). - One H1 + intro per doc; uniform "Related files" / "Связанные файлы" section, added to the code-centric docs that lacked it. Structure: - Remove the empty stray docs/api/; move updates_checklist.md into builds/; link the previously-orphaned ucs-integration.md. - Regroup the sidebars (split the oversized guides group into Developer Guides / Security & Access / Integrations; fold builds into Build & Release). Augment: - dev-workflow: Local Setup (make dev-tools) + Quality Checks (phpstan, cs, gates). - build_system: Composer Dependencies section (committed prod-only vendor, committed lock, dev tools via composer install, no build-time vendor step). en/ru parity: - Apply the same structure, fixes and pattern to docs/ru/ (translated), including a new Russian ucs-integration.md. The en and ru file sets are now identical.
381 lines
22 KiB
Markdown
381 lines
22 KiB
Markdown
# Модель обработки ошибок
|
||
|
||
Обработка ошибок в XC_VM имеет три уровня:
|
||
|
||
- **Коды ошибок** — что пошло не так (централизованный реестр именованных строк ошибок)
|
||
- **Обработчики ошибок** — как формируется HTTP-ответ клиенту (`generateError()`, `generate404()`)
|
||
- **Подсистема Logger** — runtime-перехват ошибок PHP, необработанных исключений и фатальных сбоев
|
||
|
||
---
|
||
|
||
## Обзор потока
|
||
|
||
```text
|
||
Код приложения
|
||
|
|
||
+-- generateError('CODE') // намеренный ответ с ошибкой
|
||
| -> debug-режим: стилизованная HTML-страница с кодом + описанием
|
||
| -> production: generate404() или явный HTTP-код
|
||
|
|
||
+-- PHP warning / notice / error // runtime-ошибки
|
||
| -> Logger::handleError()
|
||
| -> сопоставляет errno с уровнем (ERROR, WARNING, NOTICE, INFO)
|
||
| -> пишет base64-encoded JSON в error_log.log
|
||
| -> опционально отображает на экране
|
||
|
|
||
+-- Необработанное исключение // unhandled Throwable
|
||
| -> Logger::handleException()
|
||
| -> логирует как EXCEPTION с полной цепочкой trace
|
||
|
|
||
+-- Фатальная ошибка при shutdown // E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR
|
||
-> Logger::handleFatal()
|
||
-> логирует как FATAL (stack trace недоступен)
|
||
```
|
||
|
||
---
|
||
|
||
## Реестр кодов ошибок
|
||
|
||
Все коды объявляются в `src/Core/Error/ErrorCodes.php` как глобальный массив `$rErrorCodes`.
|
||
|
||
Формат кода:
|
||
|
||
- Ключ: строка в верхнем регистре (например: `INVALID_CREDENTIALS`)
|
||
- Значение: человекочитаемое описание на английском
|
||
|
||
Используйте только централизованные определения кодов. Не хардкодьте тексты ошибок в обработчиках endpoint'ов.
|
||
|
||
### Полный список кодов
|
||
|
||
| Код | Описание |
|
||
| --- | --- |
|
||
| `API_IP_NOT_ALLOWED` | IP не разрешён для доступа к API. |
|
||
| `ARCHIVE_DOESNT_EXIST` | Архивные файлы отсутствуют для этого stream ID. |
|
||
| `ASN_BLOCKED` | ASN заблокирован. |
|
||
| `BANNED` | Линия забанена. |
|
||
| `BLOCKED_USER_AGENT` | User-agent заблокирован. |
|
||
| `CACHE_INCOMPLETE` | Кэш генерируется... |
|
||
| `DEVICE_NOT_ALLOWED` | Устройствам MAG и Enigma запрещён доступ. |
|
||
| `DISABLED` | Линия отключена. |
|
||
| `DOWNLOAD_LIMIT_REACHED` | Достигнут лимит одновременных загрузок. |
|
||
| `E2_DEVICE_LOCK_FAILED` | Проверки device lock не пройдены. |
|
||
| `E2_DISABLED` | Устройство отключено. |
|
||
| `E2_NO_TOKEN` | Токен не указан. |
|
||
| `E2_TOKEN_DOESNT_MATCH` | Токен не совпадает с записями. |
|
||
| `E2_WATCHDOG_TIMEOUT` | Достигнут лимит времени. |
|
||
| `EMPTY_USER_AGENT` | Пустые user-agent запрещены. |
|
||
| `EPG_DISABLED` | EPG отключён. |
|
||
| `EPG_FILE_MISSING` | Кэшированные файлы EPG отсутствуют. |
|
||
| `EXPIRED` | Срок линии истёк. |
|
||
| `FORCED_COUNTRY_INVALID` | Страна не совпадает с forced country. |
|
||
| `GENERATE_PLAYLIST_FAILED` | Не удалось сгенерировать плейлист. |
|
||
| `HLS_DISABLED` | HLS отключён. |
|
||
| `HOSTING_DETECT` | Обнаружен hosting-сервер. |
|
||
| `INVALID_API_PASSWORD` | Неверный пароль API. |
|
||
| `INVALID_CREDENTIALS` | Неверное имя пользователя или пароль. |
|
||
| `INVALID_HOST` | Имя домена не распознано. |
|
||
| `INVALID_STREAM_ID` | Stream ID не существует. |
|
||
| `INVALID_TYPE_TOKEN` | Токены нельзя использовать для этого типа потока. |
|
||
| `IP_BLOCKED` | IP заблокирован. |
|
||
| `IP_MISMATCH` | Текущий IP не совпадает с IP начального подключения. |
|
||
| `ISP_BLOCKED` | ISP заблокирован. |
|
||
| `LB_TOKEN_INVALID` | AES-токен не удаётся расшифровать. |
|
||
| `LEGACY_EPG_DISABLED` | Доступ к legacy epg.php отключён. |
|
||
| `LEGACY_GET_DISABLED` | Доступ к legacy get.php отключён. |
|
||
| `LEGACY_PANEL_API_DISABLED` | Доступ к legacy panel_api.php отключён. |
|
||
| `LINE_CREATE_FAIL` | Не удалось вставить линию в базу данных. |
|
||
| `NO_CREDENTIALS` | Учётные данные не указаны. |
|
||
| `NO_SERVERS_AVAILABLE` | Сейчас нет доступных серверов для этого потока. |
|
||
| `NO_TIMESTAMP` | Не указана временная метка архива. |
|
||
| `NO_TOKEN_SPECIFIED` | Не указан AES-зашифрованный токен. |
|
||
| `NOT_ENIGMA_DEVICE` | Линия не является устройством enigma. |
|
||
| `NOT_IN_ALLOWED_COUNTRY` | Не в списке разрешённых стран. |
|
||
| `NOT_IN_ALLOWED_IPS` | Не в списке разрешённых IP. |
|
||
| `NOT_IN_ALLOWED_UAS` | Не в списке разрешённых user-agent. |
|
||
| `NOT_IN_BOUQUET` | У линии нет доступа к этому stream ID. |
|
||
| `PLAYER_API_DISABLED` | Player API отключён. |
|
||
| `PROXY_ACCESS_DENIED` | Невозможно обратиться к потоку напрямую, пока включён proxy. |
|
||
| `PROXY_DETECT` | Обнаружен proxy. |
|
||
| `PROXY_NO_API_ACCESS` | Нельзя обращаться к API через proxy. |
|
||
| `RESTREAM_DETECT` | Обнаружен рестрим. |
|
||
| `STALKER_CHANNEL_MISMATCH` | Stream ID не совпадает со stalker-токеном. |
|
||
| `STALKER_DECRYPT_FAILED` | Не удалось расшифровать stalker-токен. |
|
||
| `STALKER_INVALID_KEY` | Неверный stalker-ключ. |
|
||
| `STALKER_IP_MISMATCH` | IP не совпадает со stalker-токеном. |
|
||
| `STALKER_KEY_EXPIRED` | Срок stalker-токена истёк. |
|
||
| `STREAM_OFFLINE` | Поток сейчас 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`. Это обычные функции (а не методы класса), подключаемые рано в bootstrap.
|
||
|
||
### `generateError(string $rError, bool $rKill = true, ?int $rCode = null)`
|
||
|
||
Формирует HTTP-ответ с ошибкой. Поведение зависит от настройки `debug_show_errors`:
|
||
|
||
```text
|
||
если debug_show_errors === true
|
||
рендерит стилизованную HTML-страницу с ключом ошибки + описанием
|
||
если $rKill -> exit()
|
||
иначе (production)
|
||
если $rKill
|
||
если задан $rCode -> http_response_code($rCode) + exit()
|
||
иначе -> generate404()
|
||
// если !$rKill, в production-режиме ничего не делает
|
||
```
|
||
|
||
Параметры:
|
||
|
||
| Параметр | Тип | По умолчанию | Смысл |
|
||
| --- | --- | --- | --- |
|
||
| `$rError` | `string` | -- | Ключ из `$rErrorCodes` |
|
||
| `$rKill` | `bool` | `true` | Завершить скрипт после вывода |
|
||
| `$rCode` | `int\|null` | `null` | Явный HTTP-код ответа (обходит 404 в production) |
|
||
|
||
Примеры:
|
||
|
||
```php
|
||
generateError('INVALID_CREDENTIALS'); // production: 404 + exit
|
||
generateError('API_IP_NOT_ALLOWED', true, 403); // production: 403 + exit
|
||
generateError('STREAM_OFFLINE', false); // production: без вывода, без exit
|
||
```
|
||
|
||
### `generate404(bool $rKill = true)`
|
||
|
||
Возвращает страницу `404 Not Found` в стиле nginx и устанавливает HTTP 404. HTML включает padding-комментарии для подавления «дружелюбных» страниц ошибок в MSIE и Chrome.
|
||
|
||
```php
|
||
generate404(); // 404 + exit
|
||
generate404(false); // 404, выполнение продолжается
|
||
```
|
||
|
||
---
|
||
|
||
## Подсистема Logger
|
||
|
||
Определена в `src/Core/Logging/Logger.php`. Класс `final`, регистрирующий три глобальных PHP-обработчика для перехвата всех runtime-ошибок и записи их в файл.
|
||
|
||
### Инициализация
|
||
|
||
```php
|
||
Logger::init(bool $showErrors, string $logFile): void
|
||
```
|
||
|
||
Регистрирует:
|
||
|
||
1. `set_error_handler([Logger::class, 'handleError'])` — PHP warning, notice, error
|
||
2. `set_exception_handler([Logger::class, 'handleException'])` — необработанные `Throwable`
|
||
3. `register_shutdown_function([Logger::class, 'handleFatal'])` — фатальные ошибки при shutdown
|
||
|
||
Также настраивает `error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED)` и устанавливает `display_errors` / `display_startup_errors` на основе `$showErrors`.
|
||
|
||
### Где вызывается Logger::init()
|
||
|
||
Logger инициализируется в двух местах в зависимости от пути запроса:
|
||
|
||
| Путь входа | Файл | Как |
|
||
| --- | --- | --- |
|
||
| Bootstrap (все контексты) | `src/bootstrap.php` | `XC_Bootstrap::loadConstants()` вызывает `Logger::init(PHP_ERRORS, LOGS_TMP_PATH . 'error_log.log')` |
|
||
| Стриминговые endpoint'ы | `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` |
|
||
|
||
Обработчик shutdown (`handleFatal()`) проверяет `error_get_last()` на эти фатальные типы и логирует их как `FATAL`:
|
||
|
||
| Константа(ы) PHP при shutdown | Уровень лога |
|
||
| --- | --- |
|
||
| `E_ERROR`, `E_PARSE`, `E_CORE_ERROR`, `E_COMPILE_ERROR` | `FATAL` |
|
||
|
||
Необработанные исключения, логируемые `handleException()`, всегда используют уровень `EXCEPTION`.
|
||
|
||
Ошибки, подавленные оператором `@`, игнорируются (обработчик проверяет `error_reporting() & $errno`).
|
||
|
||
### Формат лога
|
||
|
||
Каждая запись лога пишется одной строкой: `base64_encode(json_encode($data))` с переводом строки. Это предотвращает повреждение строк из многострочных сообщений.
|
||
|
||
Декодированная 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` | Stack trace (форматированная строка). Пустой для фатальных ошибок. |
|
||
| `time` | Unix timestamp |
|
||
| `env` | Имя PHP SAPI (`cli`, `fpm-fcgi` и т.д.) |
|
||
|
||
### Расположение файла лога
|
||
|
||
Путь по умолчанию: `LOGS_TMP_PATH . 'error_log.log'`
|
||
|
||
Если директория лога не существует, Logger создаёт её с правами `0775`. При запуске под root (часто в контейнерах) файл chown'ится в `xc_vm:xc_vm` с режимом `0664`.
|
||
|
||
### Вывод на экран
|
||
|
||
Когда `$showErrors = true`, Logger также рендерит ошибки напрямую:
|
||
|
||
- **CLI:** цветной вывод в терминал (красный для FATAL/ERROR, жёлтый для WARNING, синий для NOTICE)
|
||
- **Web:** inline `<div>` с моноширинным шрифтом, красной рамкой и stack trace в блоке `<pre>`
|
||
|
||
---
|
||
|
||
## Конвейер логирования: файл → база данных
|
||
|
||
Logger пишет в `error_log.log` на диск. Отдельная подсистема читает этот файл и сохраняет записи в таблицу `panel_logs`:
|
||
|
||
1. **Logger** пишет base64-encoded JSON-строки в `error_log.log`
|
||
2. **FileLogger** (`src/Core/Logging/FileLogger.php`) предоставляет вторичный логирующий интерфейс для кода приложения (ошибки PDO, ошибки EPG и т.д.), который пишет в тот же файл в том же формате
|
||
3. Записи импортируются в таблицу `panel_logs`
|
||
4. **DiagnosticsService** (`src/Core/Diagnostics/DiagnosticsService.php`) читает из `panel_logs` для:
|
||
- `downloadPanelLogs()` — получает до 1000 свежих не-EPG ошибок, затем truncate'ит таблицу
|
||
- `submitPanelLogs()` — отправляет логи на центральный API-сервер для анализа
|
||
5. Админ-панель показывает эти логи в **Management > Logs > Panel Errors**
|
||
|
||
### Шумовая фильтрация FileLogger
|
||
|
||
`FileLogger::log()` пропускает записи, соответствующие:
|
||
|
||
- Сообщения, содержащие `panel_logs` в дополнительном поле (предотвращает рекурсивное логирование)
|
||
- Сообщения, совпадающие с `timeout exceeded`, `lock wait timeout` или `duplicate entry` (шумные MySQL-ошибки)
|
||
|
||
---
|
||
|
||
## Другие логгеры
|
||
|
||
Директория `src/Core/Logging/` содержит дополнительные специализированные логгеры:
|
||
|
||
| Класс | Файл | Назначение |
|
||
| --- | --- | --- |
|
||
| `Logger` | `Logger.php` | Глобальный обработчик ошибок/исключений/fatal 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` и пишут base64-encoded JSON.
|
||
|
||
---
|
||
|
||
## Типы исключений в кодовой базе
|
||
|
||
Кодовая база определяет небольшое число пользовательских классов исключений. Все необработанные исключения перехватываются `Logger::handleException()`, который логирует полную цепочку исключений (включая `getPrevious()`).
|
||
|
||
| Класс исключения | Базовый класс | Расположение |
|
||
| --- | --- | --- |
|
||
| `DropboxException` | `Exception` | `src/Core/Storage/DropboxClient.php` |
|
||
| `M3uParser\Exception` | `\Exception` | `src/Core/Parsing/M3uParser/src/Exception.php` |
|
||
| `DataBuildingException` | `\RuntimeException` | `src/Core/Parsing/PhpM3u8/src/Parser/DataBuildingException.php` |
|
||
| `DefinitionException` | `\RuntimeException` | `src/Core/Parsing/PhpM3u8/src/Definition/DefinitionException.php` |
|
||
| `DumpingException` | `\RuntimeException` | `src/Core/Parsing/PhpM3u8/src/Dumper/DumpingException.php` |
|
||
|
||
Большая часть кода приложения использует выбросы обобщённого `Exception` или полагается на встроенную систему ошибок PHP. Exception-обработчик Logger принимает любой `Throwable`.
|
||
|
||
---
|
||
|
||
## Debug vs Production
|
||
|
||
### Production (по умолчанию: `debug_show_errors = false`)
|
||
|
||
- `generateError()` возвращает универсальную страницу 404 (или явный HTTP-код), скрывая внутреннюю причину сбоя
|
||
- Logger по-прежнему пишет все ошибки в `error_log.log` на диск
|
||
- `display_errors` и `display_startup_errors` устанавливаются в `'0'`
|
||
- Ошибки видны только через админ-панель (страница Panel Errors) или файлы логов
|
||
|
||
### Debug (`debug_show_errors = true`)
|
||
|
||
- `generateError()` показывает стилизованную страницу с ключом ошибки и сопоставленным описанием
|
||
- Logger дополнительно рендерит ошибки на экран (цветной CLI-вывод или inline HTML)
|
||
- `display_errors` и `display_startup_errors` устанавливаются в `'1'`
|
||
|
||
Не включайте debug-отображение на production-нодах.
|
||
|
||
---
|
||
|
||
## Регистрация обработчика ошибок в bootstrap
|
||
|
||
Инфраструктура обработки ошибок загружается рано в последовательности загрузки:
|
||
|
||
1. `bootstrap.php` определяет `MAIN_HOME` и регистрирует Composer-автозагрузчик
|
||
2. `XC_Bootstrap::loadConstants()` загружает (в порядке):
|
||
- `Core/Error/ErrorCodes.php` — заполняет `$rErrorCodes`
|
||
- `Core/Error/ErrorHandler.php` — определяет `generateError()` и `generate404()`
|
||
- Файлы путей и конфигурации
|
||
- `Core/Logging/Logger.php` — определение класса
|
||
3. Вызывается `Logger::init(PHP_ERRORS, LOGS_TMP_PATH . 'error_log.log')`, регистрирующий три глобальных обработчика
|
||
4. С этого момента все PHP-ошибки, необработанные исключения и фатальные сбои перехватываются
|
||
|
||
Для стриминговых endpoint'ов, обходящих полный bootstrap, `RequestGuard.php` выполняет шаги 2-3 независимо: загружает настройки из файлового кэша, определяет `PHP_ERRORS` и вызывает `Logger::init()`.
|
||
|
||
---
|
||
|
||
## Добавление нового кода ошибки
|
||
|
||
1. Добавьте новый ключ в `src/Core/Error/ErrorCodes.php`:
|
||
|
||
```php
|
||
'MY_NEW_ERROR' => 'Human-readable description.',
|
||
```
|
||
|
||
2. Используйте его в коде:
|
||
|
||
```php
|
||
generateError('MY_NEW_ERROR');
|
||
```
|
||
|
||
Описания должны оставаться на английском для согласованности с существующим реестром.
|
||
|
||
---
|
||
|
||
## Связанные файлы
|
||
|
||
| Файл | Назначение |
|
||
| --- | --- |
|
||
| `src/Core/Error/ErrorCodes.php` | Централизованная карта кодов ошибок (`$rErrorCodes`) |
|
||
| `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` | Стриминговый путь: защита от флуда, проверка хоста, инициализация Logger |
|
||
| `src/Core/Diagnostics/DiagnosticsService.php` | Читает таблицу `panel_logs` для отображения в админке и отправки в API |
|
||
| `src/bootstrap.php` | Подключает слой ошибок и Logger во всех контекстах bootstrap |
|