[`\Shef\Problems\Integration\Monolog`] Monolog

Почитать

Откуда берётся Monolog

Из Composer проекта, если он там есть, иначе — из своей копии в vendor/monolog/monolog модуля. Решает .settings.php модуля через ShProjectContext из shef.options: если в vendor проекта Monolog лежит, своя копия не регистрируется. Без shef.options или при ошибке разбора composer.json проекта — своя копия.

Версия своей копии обязана подходить под ограничение из composer.json модуля и не давать deprecation на поддерживаемых версиях PHP — сторожит tests/vendor_test.php.

Предустановленные логгеры

Сервисы \Bitrix\Main\DI\ServiceLocator, ключ services в .settings.php. Каждому соответствует случай enum \Shef\Problems\Logger. Файлы — в каталоге логов \Shef\Problems\Main\Constants::getLogDir(), вне корня сайта: при корне /home/bitrix/www это /home/bitrix/sh_log, см. security.md.

сервисenumуровенькуда
shef.problems.pr.debugPrDebugна экран, без оформления; только администратору
shef.problems.prHtml.debugPrHtmlDebugна экран, с цветом по уровню; только администратору
shef.problems.log.debugLogDebuglog.log, потолок 20 МБ — дальше прежний файл уходит в log.log.1
shef.problems.log1.debugLog1Debuglog1.log, первая запись за запрос стирает файл
shef.problems.deprecations.alertDeprecationsAlertdeprecations.log, тоже стирается первой записью
shef.problems.factory.system.loggerProblemsзадаёт вызывающийфабрика: файл по типу проблемы + журнал событий

Через enum:

\Bitrix\Main\Loader::includeModule('shef.problems');

\Shef\Problems\Logger::PrHtml->getLogger()->debug('что пришло', ['fields' => $fields]);

Через сервис напрямую:

$logger = \Bitrix\Main\DI\ServiceLocator::getInstance()->get('shef.problems.log.debug');
$logger->info('Импорт начат');

Logger::Problems->getLogger() бросает LogicException: это фабрика, а не логгер, её строят через трейт — ниже.

Проблемы: фабрика и трейт

Проблема — запись, которую надо не только сохранить, но и найти потом в журнале событий по типу и понять, кому она адресована. Фабрика \Shef\Problems\Factory\SystemLoggerFactory::build() строит логгер, который пишет сразу:

  • в файл <тип>.log в каталоге логов;
  • в журнал событий Битрикса с этим типом.

В каждую запись добавляются модуль, класс и ответственный.

Обычно фабрику напрямую не зовут, а подключают трейт \Shef\Problems\Factory\Trait\LoggerProblems:

\Bitrix\Main\Loader::includeModule('shef.problems');

final class OrdersExchange
{
    use \Shef\Problems\Factory\Trait\LoggerProblems;

    public function __construct()
    {
        $this->initLogger();
    }

    public static function getClassName(): string
    {
        return static::class;
    }

    public static function getModuleId(): string
    {
        return 'acme.exchange';
    }

    // Необязательное — умолчания: Info, SH_PROBLEMS_PROBLEM, «сотрудник по умолчанию».
    protected static function getLogLevel(): \Monolog\Level
    {
        return \Monolog\Level::Error;
    }

    protected static function getAuditType(): string
    {
        return \Shef\Problems\Main\Constants::AuditTypeSync;
    }

    public static function getAssignedId(): int
    {
        return \Shef\Problems\Main\Constants::getSyncUserId();
    }

    public function run(): void
    {
        $this->logger->critical('1С не ответила за 30 секунд', ['itemId' => 1024]);
    }
}

Трейт \Shef\Problems\Factory\Trait\DebuggerProblems — то же для отладки: даёт $this->debugger на сервисе shef.problems.prHtml.debug.

чтогде
\Shef\Problems\Factory\SystemLoggerFactory::buildфабрика проблем
\Shef\Problems\Factory\Trait\LoggerProblems::initLogger$this->logger на фабрике
\Shef\Problems\Factory\Trait\LoggerProblems::configureLoggerподменить логгер снаружи — например, в тесте
\Shef\Problems\Factory\Trait\DebuggerProblems::initDebugger$this->debugger для отладки
\Shef\Problems\Main\Constants::getDefUserIdответственный по умолчанию, из настроек

Запускаемый пример — examples/problems.php.

Свои логгеры через \Bitrix\Main\Diag\Logger::create

Ядро умеет создавать логгеры по имени из ключа loggers в /bitrix/.settings.php или /bitrix/.settings_extra.php. Логгеры модуля туда встают так:

return [
    'loggers' => [
        'value' => [
            'shef.problems.prHtml' => [
                'constructor' => static function () {
                    if(!\Bitrix\Main\Loader::includeModule('shef.problems'))
                    {
                        return null;
                    }

                    return \Shef\Problems\Logger::PrHtml->getLogger();
                },
            ],
        ],
        'readonly' => true,
    ],
];
\Bitrix\Main\Diag\Logger::create('shef.problems.prHtml')?->debug('сообщение', ['контекст']);

Собственный логгер с любыми обработчиками Monolog — например, файл плюс Telegram для важного:

'test' => [
    'constructor' => static function () {
        if(!\Bitrix\Main\Loader::includeModule('shef.problems'))
        {
            return null;
        }

        return (new \Shef\Problems\Integration\Monolog\Logger('test'))
            ->pushHandler(new \Monolog\Handler\StreamHandler(
                stream: \Shef\Problems\Main\Constants::getLogFullPath('test'),
                level: \Monolog\Level::Debug
            ))
            ->pushHandler(new \Monolog\Handler\TelegramBotHandler(
                apiKey: '<ключ бота>',
                channel: '<id канала>',
                level: \Monolog\Level::Info
            ));
    },
],

Ключ бота — секрет: держите его в .settings_extra.php, который не уезжает в репозиторий проекта.

Обработчики модуля

Все обработчики Monolog плюс свои:

классчто делает
Handler\BitrixCEventLogHandlerзапись в журнал событий; уровень сопоставляется с важностью журнала, см. уровни
Handler\PrHandlerвывод на экран; по умолчанию только администратору; всё экранируется
Handler\PrHtmlHandlerто же с оформлением; стили — расширение shef-problems.monolog-pr-html
Handler\Log1Handlerфайл, который первая запись за жизнь обработчика стирает
Handler\CappedStreamHandlerфайл с потолком размера: перерос — откладывается в <имя>.1, пишется новый
Processor\TraceProcessorтрассировка в extra.trace: откуда позвали логгер или, для исключения, его трассировка
Formatter\BitrixCEventLogFormatterзапись → описание для журнала; itemId и moduleId из контекста — в поля журнала

Вывод на экран экранируется. В сообщение и контекст попадает что угодно, в том числе ввод посетителя, а смотрит на вывод администратор. До 2.0.0 это шло в страницу как есть.

Трассировка начинается с места вызова, где бы ни висел TraceProcessor — на обработчике или на логгере: кадры самого Monolog и слоя интеграции отрезаются по файлам, а не по счёту.

Логгер \Shef\Problems\Integration\Monolog\Logger

Наследник \Monolog\Logger. Первым аргументом принимает не только строку:

что передалисообщениеконтекст
\Throwableтекст исключениясамо исключение под throwable
\Bitrix\Main\Errorтекст и кодошибка под BitrixError
\Bitrix\Main\Result[Result::Error: N] первая ошибка либо [Result::Success]ошибки и данные под BitrixResult
\Bitrix\Main\Type\Contract\ArrayableArrayabletoArray() под _message
массивArrayмассив под _message
\Bitrix\Main\Type\Contract\JsonableJsonabletoJson() под _message
\JsonSerializableJsonSerializablejsonSerialize() под _message
строка, \Stringableкак естькак передали

Остальное — InvalidArgumentException: лучше увидеть ошибку сразу, чем потерять запись молча. Превращения делают стратегии \Shef\Problems\Integration\Monolog\Strategy\LoggerConverter\IStrategy, по одной на тип. Запускаемый пример — examples/logger.php.

Сбой записи не бросает. Обработчик не смог записать — каталог логов вне open_basedir, нет прав, упал CEventLog — и исключение не уходит в вызывающий код: логгер пишет в лог PHP строку shef.problems: запись логгера <канал> не прошла: <класс>: <первая строка сообщения>. Запись при этом прерывается: обработчики ниже по стеку её не получают — так устроен Monolog\Logger::addRecord(). У фабрики проблем журнал событий стоит выше файла, поэтому сбой файла журнал не отменяет, а сбой журнала отменяет файл. Нужно иначе — свой обработчик через setExceptionHandler(). Неверный тип сообщения (InvalidArgumentException выше) по-прежнему бросает: это ошибка вызова, а не записи.

← Уровни логирования | ↑ Содержание | Ротация логов →

Источник: problems/docs/4_monolog.md — правки туда, сайт пересобирается сам.
CtrlI