Bitrix D7 Diag\Logger и FileLogger: логи модулей, уровни и типичные ошибки

В модулях Битрикс до сих пор часто пишут AddMessage2Log, file_put_contents в /upload/ или просто echo в агенте. Это работает до первого продакшена: логи размазаны, нет уровней, нет ротации, в лог попадают пароли и PII. Нормальный контракт в D7 — Bitrix\Main\Diag\Logger (и наследники вроде FileLogger): один интерфейс, уровни PSR-3, контекст массивом, подключение через .settings.php или свой сервис.

Ниже — практический разбор: как завести файловый логгер, писать из модуля/агента/контроллера, чем он отличается от AddMessage2Log и от блока exception_handling, как не засветить секреты. Связка с остальным D7: ошибки операции отдаёте через Result, а детали пишете в Logger; подписки на сбои — через EventManager; пороги и feature-флаги логирования — в Option / Configuration.

Что такое Logger в D7

Bitrix\Main\Diag\Logger — абстрактный логгер с уровнями в духе PSR-3: emergency, alert, critical, error, warning, notice, info, debug. Сообщение — строка с плейсхолдерами {key}, второй аргумент — массив контекста. Конкретная реализация решает, куда писать: файл, syslog, внешний коллектор.

Из коробки чаще всего используют Bitrix\Main\Diag\FileLogger: пишет в файл, умеет ограничивать размер (ротация по maxSize). Для отладки ядра и необработанных исключений отдельно настраивается блок exception_handling в .settings.php — это не «логгер модуля», а обработчик фаталов/исключений ядра.

Быстрый старт: FileLogger в коде модуля

Минимальный рабочий пример без ServiceLocator — создать логгер рядом с точкой вызова (агент, CLI, одноразовый скрипт):

use Bitrix\Main\Diag\FileLogger;
use Bitrix\Main\IO\Directory;

$logDir = $_SERVER['DOCUMENT_ROOT'] . '/local/logs';
if (!Directory::isDirectoryExists($logDir)) {
    Directory::createDirectory($logDir);
}

$logger = new FileLogger($logDir . '/mymodule.log', 5_000_000); // ~5 MB
$logger->setLevel(\Psr\Log\LogLevel::INFO);

$logger->info('Import started', [
    'iblockId' => 12,
    'userId' => (int)($GLOBALS['USER']->GetID() ?? 0),
]);

try {
    // ... бизнес-логика
    $logger->info('Import finished', ['rows' => 1540]);
} catch (\Throwable $e) {
    $logger->error('Import failed: {message}', [
        'message' => $e->getMessage(),
        'file' => $e->getFile(),
        'line' => $e->getLine(),
    ]);
    throw $e;
}

Плейсхолдеры {message} подставляются из контекста. Не склеивайте строку через конкатенацию, если значение может содержать переводы строк или секреты — контекст проще фильтровать и маскировать в одном месте.

Регистрация через ServiceLocator

В нормальном модуле логгер лучше один раз зарегистрировать и доставать через ServiceLocator. Тогда агент, контроллер и событие пишут в один файл с одним уровнем.

// local/php_interface/init.php или include.php модуля
use Bitrix\Main\DI\ServiceLocator;
use Bitrix\Main\Diag\FileLogger;
use Bitrix\Main\IO\Directory;
use Psr\Log\LogLevel;

$locator = ServiceLocator::getInstance();
$locator->addInstanceLazy('mymodule.logger', static function () {
    $dir = $_SERVER['DOCUMENT_ROOT'] . '/local/logs';
    if (!Directory::isDirectoryExists($dir)) {
        Directory::createDirectory($dir);
    }
    $logger = new FileLogger($dir . '/mymodule.log', 10_000_000);
    $logger->setLevel(LogLevel::WARNING); // на проде не INFO/DEBUG
    return $logger;
});

// использование
/** @var \Psr\Log\LoggerInterface $logger */
$logger = ServiceLocator::getInstance()->get('mymodule.logger');
$logger->warning('Retry queue overflow', ['queue' => 'orders', 'size' => 1200]);

Уровень на проде держите не ниже warning/error, иначе диск и I/O съедят импорты. Для стенда уровень можно читать из Option::get('mymodule', 'log_level', 'warning') и менять без деплоя.

exception_handling в .settings.php

Это другой контур: необработанные исключения и ошибки PHP, которые доходят до ядра. Типичная настройка на BitrixVM / своём сервере:

// /bitrix/.settings.php (фрагмент)
'exception_handling' => [
    'value' => [
        'debug' => false,
        'handled_errors_types' => E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED,
        'exception_errors_types' => E_ALL & ~E_NOTICE & ~E_STRICT & ~E_USER_NOTICE,
        'ignore_silence' => false,
        'assertion_throws_exception' => true,
        'assertion_error_type' => E_USER_ERROR,
        'log' => [
            'settings' => [
                'file' => '/var/log/bitrix/exception.log',
                'log_size' => 10_000_000,
            ],
        ],
    ],
    'readonly' => false,
],

На проде debug => false, иначе посетитель увидит стек. Файл лога должен быть вне web-root или закрыт nginx/apache. Не путайте этот лог с бизнес-логами модуля: exception.log — про падения, mymodule.log — про доменные события (импорт оборвался, webhook отклонён, квота исчерпана).

AddMessage2Log vs Logger

  • AddMessage2Log($message, $moduleId) — старый хелпер, пишет в файл из настроек (часто /bitrix/modules/error.log или путь из настроек главного модуля). Нет уровней, слабый контроль формата, легко получить кашу из разных модулей в одном файле.
  • Logger / FileLogger — явный файл (или свой sink), уровни, контекст, удобно тестировать и подменять в DI.
  • Для одноразовой отладки на стенде AddMessage2Log ещё терпим. Для кода, который уходит в прод и в Marketplace — заводите свой Logger.
// плохо на проде
AddMessage2Log(print_r($_REQUEST, true), 'mymodule');

// лучше
$logger->debug('Incoming request', [
    'method' => $_SERVER['REQUEST_METHOD'] ?? '',
    'path' => parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH),
    // не логируйте целиком $_REQUEST / заголовки Authorization
]);

Связка с Result и агентами

Типичный паттерн сервиса: наружу — Result, внутрь — лог. Вызывающий код смотрит isSuccess(), а детали остаются в файле для разбора инцидента.

use Bitrix\Main\Result;
use Bitrix\Main\Error;

function runSync(\Psr\Log\LoggerInterface $logger): Result
{
    $result = new Result();
    $logger->info('Sync start');

    try {
        $count = doHeavySync();
        $logger->info('Sync ok', ['count' => $count]);
        $result->setData(['count' => $count]);
    } catch (\Throwable $e) {
        $logger->error('Sync failed: {message}', ['message' => $e->getMessage()]);
        $result->addError(new Error('Синхронизация не удалась'));
    }

    return $result;
}

В агентах обязательно думайте про конкурентный запуск: лог «started» дважды подряд — симптом отсутствия flock/флага. Пишите в лог pid и исход блокировки, иначе потом не отличите нормальный ретрай от гонки.

Что нельзя писать в лог

  • Пароли, токены OAuth, refresh-токены, webhook-секреты, ключи платежек.
  • Полные тела карточек CRM с телефонами и паспортами — маскируйте или логируйте только ID.
  • Сырой SQL с подставленными значениями, если там ПДн.
  • Бинарные куски и огромные print_r коллекций — режьте длину строки.

Простой хелпер маскирования перед записью:

function maskSecret(?string $value): string
{
    if ($value === null || $value === '') {
        return '';
    }
    $len = strlen($value);
    if ($len <= 8) {
        return str_repeat('*', $len);
    }
    return substr($value, 0, 4) . str_repeat('*', $len - 8) . substr($value, -4);
}

$logger->info('Token refreshed', [
    'access' => maskSecret($token->getAccessToken()),
]);

Ротация, права и nginx

Второй аргумент FileLogger — максимальный размер файла. При превышении Битрикс переименовывает/обрезает по своей логике реализации — не рассчитывайте на logrotate-семантику один в один. На сервере дополнительно:

  • Каталог /local/logs — права пользователя PHP-FPM, не world-readable.
  • В nginx запретите выдачу /local/logs/ (и вообще /local/ кроме того, что реально нужно с веба).
  • Для долгоживущих проектов лучше слать логи во внешний коллектор (journald, Vector, ELK) — FileLogger остаётся локальным буфером/фолбэком.

Типичные ошибки

  • Лог в /upload/ — файл могут скачать или зацепить бэкапом публички. Держите рядом с /local/ или в /var/log/....
  • DEBUG на проде — диск, нагрузка, утечка данных. Уровень через Option/Configuration.
  • Один общий error.log на все модули — невозможно фильтровать. Имя файла = модуль или bounded context.
  • Логировать только «успех» — при инциденте нужны warning/error с контекстом ID сущности.
  • Глотать исключение после лога — либо пробросьте дальше, либо верните Result с Error, иначе вызывающий решит, что всё ок.
  • Путать exception_handling и бизнес-лог — падения ядра и «заказ не выгрузился» живут в разных файлах и с разным retention.

Чеклист перед выкладкой

  1. Есть один сервис *.logger в ServiceLocator (или явная фабрика).
  2. На проде уровень ≥ warning, путь вне web-root или закрыт веб-сервером.
  3. В сообщениях нет секретов и сырых PII.
  4. Ошибки операций идут в Result; детали — в лог.
  5. Каталог логов создаётся кодом или деплоем, права проверены под пользователем FPM.
  6. Понятно, кто смотрит лог при алерте (см. также мониторинг и Telegram-алерты на сайте).

Вывод

Diag\Logger / FileLogger — базовый инструмент модульной разработки на Битрикс: уровни, контекст, предсказуемый файл. Не заменяйте им обработку ошибок для UI (для этого Result), не смешивайте с exception_handling ядра и не тащите отладочный AddMessage2Log($_REQUEST) на прод. Заведите сервис логгера, закройте каталог от веба, маскируйте секреты — и разбор инцидентов перестанет быть археологией по пяти разным путям.