Перейти к содержимому

Сбор ошибок сайта в одном месте - перехват, уведомление, секреты

Собираем ошибки боевого сайта в одно место: свой класс журнала, уведомление команде, защита от шторма сообщений и от утечки секретов.

Что нужно знать заранее

Обработку ошибок платформа настраивает отдельной секцией файла настроек. Там же задаётся класс, которому она отдаёт каждое перехваченное исключение и каждую фатальную ошибку.

Свой класс журнала - обычный наследник класса ядра с одним методом записи. Он получает исключение и признак типа, а дальше делает с ними что угодно: пишет файл, шлёт уведомление, складывает в очередь.

Прямой запрос наружу прямо из обработчика ошибок опасен для самого сайта. Сервис уведомлений отвечает медленно, ошибка приходит на каждом запросе, и сайт встаёт уже из-за самой системы оповещения.

Шаги

  1. Включить запись ошибок в журнал и убедиться, что вывод на экран выключен.
  2. Написать свой класс журнала и указать его в настройках обработки ошибок.
  3. Схлопывать одинаковые ошибки, чтобы не получить тысячу одинаковых сообщений подряд.
  4. Вырезать из сообщения пароли, куки и любые персональные данные посетителей сайта.
  5. Отправлять уведомление фоновым заданием, а не прямо из обработчика самой ошибки.

Решение

Включаем перехват своим классом:

/bitrix/.settings.php
'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]));
// отправка идёт после ответа страницы и не задерживает посетителя

Типичные проблемы

После включения уведомлений сайт заметно замедлился.

Запрос к сервису уведомлений выполняется прямо в обработчике ошибки и ждёт ответа. Отправку выносят в фоновое задание и задают короткие таймауты соединения.

За минуту пришла тысяча одинаковых сообщений.

Ошибка возникает в цикле, а уведомление отправляется на каждом её появлении. Одинаковые ошибки схлопывают по ключу из текста сообщения, файла и номера строки.

В чат команды попал пароль или номер карты посетителя.

В сообщение об ошибке положили весь массив запроса без разбора его содержимого. Поля с секретами и персональными данными маскируют звёздочками до отправки наружу.

Свой класс журнала не вызывается вовсе.

Пространство имён решения не зарегистрировано, и платформа вернулась к записи в файл. Регистрацию пространства имён решения проверяют отдельным скриптом ещё до правки настроек.

Ошибка внутри обработчика ошибок роняет сайт целиком.

Код уведомления сам бросает исключение, и обработка ошибок уходит в рекурсию. Тело обработчика оборачивают перехватом и никогда не дают ему бросать наружу.

Частые вопросы

Где настраивается обработка ошибок?

В секции обработки ошибок файла настроек ядра, рядом с настройками подключений. Там же задают класс журнала и уровень перехватываемых ошибок.

Нужен ли внешний сервис сбора ошибок?

Для крупного проекта он окупается: группировка, история и поиск по ошибкам. Небольшому сайту хватает файла журнала и уведомления в чат команды.

Что делать с ошибками, которые не чинятся?

Гасить их в источнике, а не в фильтре уведомлений. Постоянный шум приучает команду не читать сообщения, и настоящая авария теряется среди них.

Можно ли включить вывод ошибок на боевом сайте?

Нет: он показывает пути, куски запросов и структуру проекта посторонним. Ту же информацию берут из журнала, который читают только свои.

Как отличить всплеск ошибок от постоянного фона?

По счётчику сообщений за час рядом с текстом ошибки. Резкий рост после выкладки означает свежую поломку, ровный фон - старую и известную.

Смежное

Первоисточник