Сбор ошибок сайта в одном месте - перехват, уведомление, секреты
Собираем ошибки боевого сайта в одно место: свой класс журнала, уведомление команде, защита от шторма сообщений и от утечки секретов.
Что нужно знать заранее
Обработку ошибок платформа настраивает отдельной секцией файла настроек. Там же задаётся класс, которому она отдаёт каждое перехваченное исключение и каждую фатальную ошибку.
Свой класс журнала - обычный наследник класса ядра с одним методом записи. Он получает исключение и признак типа, а дальше делает с ними что угодно: пишет файл, шлёт уведомление, складывает в очередь.
Прямой запрос наружу прямо из обработчика ошибок опасен для самого сайта. Сервис уведомлений отвечает медленно, ошибка приходит на каждом запросе, и сайт встаёт уже из-за самой системы оповещения.
Шаги
- Включить запись ошибок в журнал и убедиться, что вывод на экран выключен.
- Написать свой класс журнала и указать его в настройках обработки ошибок.
- Схлопывать одинаковые ошибки, чтобы не получить тысячу одинаковых сообщений подряд.
- Вырезать из сообщения пароли, куки и любые персональные данные посетителей сайта.
- Отправлять уведомление фоновым заданием, а не прямо из обработчика самой ошибки.
Решение
Включаем перехват своим классом:
'exception_handling' => ['value' => [ 'debug' => false, // вывод на экран на бою выключен 'handled_errors_types' => E_ALL & ~E_NOTICE & ~E_DEPRECATED, 'exception_errors_types' => E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED, 'log' => ['class_name' => 'Vendor\\Project\\ErrorLog', 'level' => E_ALL],]],Класс журнала подключается автозагрузкой решения, как и любой другой его класс. Если пространство имён не зарегистрировано, платформа тихо вернётся к записи в файл, и вы этого даже не заметите.
Пишем свой класс журнала:
namespace Vendor\Project;
class ErrorLog extends \Bitrix\Main\Diag\FileExceptionHandlerLog{ public function write($exception, $logType): void { parent::write($exception, $logType); // файл остаётся как был Notifier::queue($exception->getMessage(), $exception->getFile(), $exception->getLine()); // уведомление уходит фоном }}Запись в файл оставляют на месте всегда. Внешний сервис бывает недоступен, а файл на диске переживает и обрыв сети, и отказ стороннего сервиса уведомлений.
Гасим шторм одинаковых сообщений:
$key = md5($message . $file . $line);$storage = \Bitrix\Main\Application::getInstance()->getManagedCache();if ($storage->read(300, 'err_' . $key)) { return; } // уже слали пять минут назад$storage->setImmediate('err_' . $key, true);Одна ошибка в цикле по товарам даёт тысячи сообщений в минуту. Ключ из текста, файла и строки схлопывает их в одно уведомление и оставляет команде шанс его прочитать.
Убираем секреты из контекста:
$context = ['url' => $_SERVER['REQUEST_URI'] ?? '', 'user' => $USER->GetID()];$mask = ['password', 'token', 'card', 'cvv', 'phone', 'email'];foreach ($_REQUEST as $k => $v) { $context['request'][$k] = in_array(mb_strtolower($k), $mask, true) ? '***' : $v;}Наружу уходит адрес страницы и идентификатор пользователя, а не его данные. Сообщение об ошибке легко превращается в утечку, если положить в него весь запрос целиком.
Отправляем уведомление фоном:
\Bitrix\Main\Application::getInstance()->addBackgroundJob( static fn() => (new \Bitrix\Main\Web\HttpClient(['socketTimeout' => 3, 'streamTimeout' => 5]))->post($webhookUrl, ['text' => $text]));// отправка идёт после ответа страницы и не задерживает посетителяТипичные проблемы
После включения уведомлений сайт заметно замедлился.
Запрос к сервису уведомлений выполняется прямо в обработчике ошибки и ждёт ответа. Отправку выносят в фоновое задание и задают короткие таймауты соединения.
За минуту пришла тысяча одинаковых сообщений.
Ошибка возникает в цикле, а уведомление отправляется на каждом её появлении. Одинаковые ошибки схлопывают по ключу из текста сообщения, файла и номера строки.
В чат команды попал пароль или номер карты посетителя.
В сообщение об ошибке положили весь массив запроса без разбора его содержимого. Поля с секретами и персональными данными маскируют звёздочками до отправки наружу.
Свой класс журнала не вызывается вовсе.
Пространство имён решения не зарегистрировано, и платформа вернулась к записи в файл. Регистрацию пространства имён решения проверяют отдельным скриптом ещё до правки настроек.
Ошибка внутри обработчика ошибок роняет сайт целиком.
Код уведомления сам бросает исключение, и обработка ошибок уходит в рекурсию. Тело обработчика оборачивают перехватом и никогда не дают ему бросать наружу.
Частые вопросы
Где настраивается обработка ошибок?
В секции обработки ошибок файла настроек ядра, рядом с настройками подключений. Там же задают класс журнала и уровень перехватываемых ошибок.
Нужен ли внешний сервис сбора ошибок?
Для крупного проекта он окупается: группировка, история и поиск по ошибкам. Небольшому сайту хватает файла журнала и уведомления в чат команды.
Что делать с ошибками, которые не чинятся?
Гасить их в источнике, а не в фильтре уведомлений. Постоянный шум приучает команду не читать сообщения, и настоящая авария теряется среди них.
Можно ли включить вывод ошибок на боевом сайте?
Нет: он показывает пути, куски запросов и структуру проекта посторонним. Ту же информацию берут из журнала, который читают только свои.
Как отличить всплеск ошибок от постоянного фона?
По счётчику сообщений за час рядом с текстом ошибки. Резкий рост после выкладки означает свежую поломку, ровный фон - старую и известную.
Смежное
-
Ошибки сервера и базы - оглавление подтемы
-
Отладка на боевом сайте: журналы, режим ошибок, поиск виновника - как читают журналы вручную
-
Логирование решения: логгер по стандарту, настройка, контекст - журнал своего кода
-
Свой журнал решения: файл, ротация, что писать - файл журнала и его ротация
-
Уведомления команде в мессенджер: вебхук, шаблон, ошибки - куда отправлять сообщение
-
Ошибки в своём коде: результат вместо исключения, журнал, показ - что считать ошибкой в своём коде
-
Белая страница вместо сайта: разбор причин - когда ошибка не видна вовсе
-
Сервер и поиск - устройство площадки целиком
-
Мониторинг интеграций: что проверять, пороги, оповещение - вторая половина наблюдения за проектом