Логирование решения - логгер по стандарту, настройка, контекст
Заводим журнал решения так, чтобы его можно было настроить без правки кода: логгер по общему стандарту, настройка приёмника и уровня, безопасный контекст.
Механика
Ядро платформы даёт логгер по общепринятому стандарту логирования. Прикладной код зависит от интерфейса логгера, а конкретную реализацию подставляет фабрика по строковому идентификатору.
Идентификатор логгера собирают из имени модуля и области решения. Такое имя позволяет настраивать журналы по частям решения, не трогая при этом код самих классов.
Уровень записи, приёмник и форматтер задают в файле настроек продукта. Секция настроек описывает журналы по именам, и смена приёмника не требует ни одной правки в бизнес-логике.
Замыкания в настройках журналов держат в отдельном файле. Основной файл настроек перезаписывается при сохранении из административной части, а дополнительный остаётся нетронутым.
Устойчивая зависимость от журнала передаётся классу через конструктор. Сервис, команда или обработчик получают логгер снаружи, а не создают его заново в каждом методе.
Сообщение и его контекст разделяют вполне осознанно. В сообщении остаётся понятный текст с подстановками, а значения уезжают в контекст отдельными полями.
Пустой логгер - это штатный способ отключить запись журнала. Класс продолжает вызывать журнал, а реализация просто ничего не пишет, и проверок на наличие логгера в коде не появляется.
Сообщение журнала формируется даже тогда, когда сама запись не идёт. Тяжёлые дампы в аргументах стоят ресурсов независимо от уровня, поэтому их собирают только по условию.
Файловый приёмник умеет ротацию журнала по размеру. Значение по умолчанию невелико, и для нагруженного журнала его задают осознанно, а сам файл держат вне каталога сайта.
Журнал решения не заменяет собой контракт ошибки. Пользователю по-прежнему возвращают результат с ошибкой, а запись в журнал только помогает разобраться потом.
Журнал полезен ровно настолько, насколько его читают. Записи без имени операции и без идентификатора объекта складываются в поток текста, по которому нельзя ответить ни на один вопрос через неделю после сбоя.
Поэтому у каждой записи есть три обязательные части: что происходило, с каким объектом и чем закончилось. Всё остальное - подробности, которые добавляют по мере необходимости.
Шаги
- Выбрать имена журналов по крупным частям решения: обмен, оплата, фоновые задания и импорт.
- Пробросить логгер в классы решения через конструктор, а не создавать его внутри методов.
- Описать уровень записи, приёмник и форматтер в секции настроек по именам журналов.
- Замыкания настроек вынести в дополнительный файл настроек продукта.
- Разделить сообщение и его контекст, убрав из записи чувствительные данные.
- Задать размер ротации файла и положить сам журнал вне каталога сайта проекта.
Код
Получаем логгер по имени:
use Bitrix\Main\Diag\LoggerFactory;
$logger = (new LoggerFactory())->createById('vendor.exchange.orders');$logger->info('Выгрузка заказов начата');// имя собирают из модуля и области: так журналы настраиваются по частям// уровни стандартные: debug, info, warning, error и другиеФабрика по имени - современный способ получить журнал. Создание конкретной реализации прямо в прикладном коде лишает возможности перенастроить приёмник без правки этого кода.
Принимаем логгер снаружи:
use Psr\Log\LoggerAwareInterface;use Psr\Log\LoggerAwareTrait;use Psr\Log\NullLogger;
class OrderExporter implements LoggerAwareInterface{ use LoggerAwareTrait; // даёт метод установки логгера
public function __construct() { $this->logger = new NullLogger(); } // пустой логгер по умолчанию: класс работает и без настройки журнала}Класс с пустым логгером по умолчанию работает и без настройки журнала. Проверок на наличие логгера в методах при этом не появляется, а поведение остаётся предсказуемым.
Пишем сообщение с контекстом:
$this->logger->error('Заказ {orderId} не выгружен: {reason}', [ 'orderId' => $order->getId(), 'reason' => $result->getErrorMessages()[0] ?? 'нет описания', 'attempt' => $attempt,]);// подстановки берутся из контекста форматтером, склеивать строку не нужно// уровень выбирают по смыслу: error - сбой, info - обычный ход работыСтруктурированный контекст читается и человеком, и разбором журнала. Склеенная строка с дампом массива выглядит информативной ровно до первой попытки найти в журнале все ошибки одного заказа.
Настраиваем журнал в файле настроек:
'loggers' => ['value' => [ 'vendor.exchange.orders' => [ 'className' => \Bitrix\Main\Diag\FileLogger::class, 'constructorParams' => ['/var/log/bitrix/exchange.log', 10485760], 'level' => \Psr\Log\LogLevel::INFO, ],], 'readonly' => true],Настройка по имени журнала меняет уровень и приёмник без правки кода. Второй параметр файлового приёмника - размер ротации, и на нагруженном обмене его задают явно.
Убираем из журнала лишнее:
$this->logger->info('Ответ сервиса получен', [ 'status' => $response->getStatus(), 'hasToken' => $token !== '', // сам токен в журнал не попадает 'bodyLength' => strlen($body), // тело целиком тоже не нужно]);Пароли, токены и тела запросов в журнале - готовая утечка. Записывают признаки и размеры, а не значения, и это правило особенно важно для журналов, доступных подрядчикам.
Оставляем старый способ только там, где иначе нельзя:
AddMessage2Log('аварийная ситуация в прологе', 'vendor.module');// годится для процедурного кода и раннего этапа загрузки// в новом коде решения его место занимает логгер по стандартуСтарая функция записи остаётся рабочей и удобной в двух случаях: ранний этап загрузки и правка чужого процедурного кода. Во всём остальном она проигрывает настраиваемому журналу решения.
Проверяем, что журнал действительно пишется:
ls -lh /var/log/bitrix/exchange.log && tail -5 /var/log/bitrix/exchange.loggrep -c "orderId" /var/log/bitrix/exchange.log# пустой файл при работающем обмене означает уровень выше уровня сообщенийПроверка занимает секунду и сразу отделяет ненастроенный журнал от несработавшего кода. Отсутствие файла означает неверный путь приёмника, пустой файл - слишком высокий уровень записи.
Ограничения
Журнал решения не отменяет обработку самой ошибки. Запись в файл не сообщает пользователю ничего, и результат операции всё равно нужно вернуть вызывающему коду.
Слишком подробный журнал на боевом сайте вредит. Запись каждого шага на нагруженном обмене съедает диск и время, поэтому уровень выбирают под задачу.
Файл журнала - это тоже данные с ограничениями доступа. Он лежит вне каталога сайта, и доступ к нему выдают так же осознанно, как доступ к базе.
Разные части решения ведут свои отдельные журналы. Общий файл на всё решение неудобно читать и невозможно настроить по частям.
Типичные проблемы
Журнал не пишется, хотя вызовы есть.
Уровень записи журнала выше уровня сообщения или приёмник не настроен. Настройка идёт по имени журнала в файле настроек, а не в коде класса.
Настройки журналов исчезли после сохранения в админке.
Они лежат в основном файле настроек продукта, который перезаписывается целиком. Замыкания и тонкие настройки держат в дополнительном файле настроек.
Файл журнала вырос до гигабайтов.
Не задан размер ротации файла или уровень записи слишком подробный. Размер задают параметром приёмника, а уровень записи выбирают под задачу.
В журнале нашлись токены и пароли.
В контекст записи попали сами значения целиком. В журнал пишут признаки и размеры значений, а не сами значения целиком.
Класс требует логгер, а его негде взять.
Логгер сделан обязательной зависимостью класса без значения по умолчанию. Пустой логгер решает эту задачу без единой проверки в коде.
Частые вопросы
Чем логгер по стандарту лучше своей функции записи?
Его уровень и приёмник настраиваются снаружи, а код остаётся прежним. Своя функция записи в файл жёстко фиксирует и место, и формат.
Где хранить файлы журналов?
Вне каталога сайта, в системном каталоге журналов. Файл внутри сайта скачивается из браузера всеми, кто угадает имя.
Нужны ли отдельные журналы под каждую часть решения?
Да, по крупным областям: обмен, оплата, фоновые задания. Так уровень записи настраивается по частям, а читать журнал заметно проще.
Как быть с журналом в агентах и заданиях?
Так же, как и в остальном коде: тот же логгер по имени. У фоновых задач это единственный способ узнать, что происходило ночью.
Считать ли журнал заменой мониторингу?
Нет, журнал отвечает на вопрос «что случилось», а мониторинг - «что происходит сейчас». Вместе они полезны, по отдельности каждый оставляет слепые зоны.
Смежное
-
События на практике - оглавление подтемы
-
Свой журнал решения: файл, ротация, что писать - простой файловый журнал
-
Ошибки в своём коде: результат вместо исключения, журнал, показ - что именно попадает в журнал
-
Журнал событий: чтение, очистка, свои записи - штатный журнал платформы
-
Отладка на боевом сайте: журналы, режим ошибок, поиск виновника - где искать записи при разборе
-
Очередь сообщений: фоновая обработка задач в ядре - журнал фонового обработчика
-
Свой модуль: структура, установка, автозагрузка классов - куда встраивать журналы решения
-
Подсистемы ядра D7: логирование, валидация, GeoIP, Stepper - устройство подсистемы целиком
-
Сбор ошибок сайта в одном месте: перехват, уведомление, секреты - перехват ошибок всей площадки