При разработке и поддержке крупных конфигураций на платформе 1С:Предприятие одной из самых частых задач является качественное логирование ошибок. Долгое время разработчики сталкивались со сложностью: при возникновении исключения стандартный объект ИнформацияОбОшибке содержал подробные данные только о крайней точке падения — конкретном модуле и номере строки. Из-за этого было крайне сложно понять цепочку вызовов (стек вызовов), которая привела к ошибке, особенно если вызов происходил через несколько общих модулей.
Начиная с версии платформы 8.3.15, поведение системы изменилось в лучшую сторону — появился полноценный стек ошибок. Давайте подробно разберем, как устроен этот механизм, почему стандартное свойство Причина может возвращать пустые значения, и как программно "выскрести" и структурировать полный стек вызовов для дальнейшего анализа или отправки в систему мониторинга.
При анализе свойств объекта ИнформацияОбОшибке разработчики часто обращают внимание на свойство Причина (в англоязычной версии — Cause). Логично предположить, что именно там хранится вложенный стек или цепочка исключений. Однако на практике при возникновении обычной ошибки (например, "Поле объекта не обнаружено") свойство Причина возвращает Неопределено.
Выясним причину такого поведения. Свойство Причина заполняется платформой только тогда, когда в коде было реализовано вложенное исключение. То есть когда в блоке Исключение разработчик явным образом перехватил одну ошибку и вызвал новое исключение, передав предыдущее в качестве причины. Если же произошел обычный линейный вызов цепочки процедур (например, МодульДокумента -> ОбщийМодуль1 -> ОбщийМодуль2), то для платформы это одно "плоское" исключение, и свойство Причина останется незаполненным на всем пути прохождения ошибки.
Как обнаружили разработчики, начиная с версии 8.3.15, полный стек вызовов стал неявным образом записываться в текстовое представление ошибки. Чтобы получить всю цепочку вызовов в виде текста, достаточно использовать глобальную функцию ПодробноеПредставлениеОшибки().
Рассмотрим пример того, как это выглядит на практике. Допустим, у нас есть следующая цепочка вызовов:
Расчет.РассчитатьЗаписи()Расчет2.вложение1()Если мы перехватим ошибку в самом верху (в модуле объекта документа) и вызовем функцию ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()), мы получим следующий структурированный текст:
Поле объекта не обнаружено (ошибка)
{ОбщийМодуль.Расчет2.Модуль(57)}: тест = Запись.ошибка;
{ОбщийМодуль.Расчет.Модуль(49)}: Расчет2.вложение1(Запись);
{Документ.ВводПроизвольныхНачисленийИУдержаний.МодульОбъекта(29)}: Расчет.РассчитатьЗаписи(Ссылка);
Важное примечание по совместимости: Данный механизм работает и выводит полный стек даже в том случае, если у вашей конфигурации установлен режим совместимости с более старыми версиями (вплоть до 8.3.10), главное условие — чтобы само приложение запускалось на физической платформе версии 8.3.15 или выше.
Начиная с версии 8.3.17, платформа претерпела глобальные изменения в области обработки исключений. Классические функции ПодробноеПредставлениеОшибки(), КраткоеПредставлениеОшибки() и процедура ПоказатьИнформациюОбОшибке() были объявлены устаревшими (deprecated). Вместо них разработчикам предписано использовать методы нового глобального менеджера ОбработкаОшибок.
Для получения подробного стека ошибок в современных конфигурациях следует писать следующий код:
Попытка
// Ваша бизнес-логика, в которой может возникнуть ошибка
Расчет.РассчитатьЗаписи(Ссылка);
Исключение
ИнформацияОБ_Ошибке = ИнформацияОбОшибке();
ТекстСтека = ОбработкаОшибок.ПодробноеПредставлениеОшибки(ИнформацияОБ_Ошибке);
// Запись в журнал регистрации по стандартам 1С
ЗаписьЖурналаРегистрации("ВыполнениеРасчетов",
УровеньЖурналаРегистрации.Ошибка,
,
,
ТекстСтека);
КонецПопытки;
Текстовое представление стека отлично подходит для записи в Журнал регистрации, но если перед нами стоит задача отправить ошибку во внешнюю систему (например, в Telegram, Slack, или специализированный трекер ошибок Sentry), нам необходим структурированный вид (массив структур с именами модулей и номерами строк).
Поскольку прямого метода во встроенном языке для этого до сих пор нет, мы можем написать собственную функцию-парсер, которая преобразует текстовый стек в удобную таблицу или массив структур. Разберем этот алгоритм по шагам.
Напишем функцию, которая принимает объект ИнформацияОбОшибке, извлекает его подробное представление и парсит строки по регулярному шаблону платформы:
// Функция разбирает текстовое представление стека ошибок в структурированный массив.
//
// Параметры:
// ИнформацияОбОшибке - ИнформацияОбОшибке - объект исключения.
//
// Возвращаемое значение:
// Массив - массив структур, содержащих свойства: Модуль, Строка, ТекстВызова.
//
Функция РазобратьСтекОшибок(ИнформацияОбОшибке) Экспорт
Результат = Новый Массив;
// Получаем строковое представление стека в зависимости от версии платформы
#Если Клиент Или Сервер Тогда
ТекстОшибки = ОбработкаОшибок.ПодробноеПредставлениеОшибки(ИнформацияОбОшибке);
#Иначе
ТекстОшибки = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке);
#КонецЕсли
// Разбиваем текст на отдельные строки по символу перевода строки
МассивСтрок = СтрРазделить(ТекстОшибки, Символы.ПС, Ложь);
Для Каждого СтрокаТекста Из МассивСтрок Цикл
СтрокаТекста = СокрЛП(СтрокаТекста);
// Каждая строка стека вызовов начинается с фигурной скобки "{"
Если Лев(СтрокаТекста, 1) = "{" Тогда
ПозицияЗакрывающейСкобки = Найти(СтрокаТекста, "}");
Если ПозицияЗакрывающейСкобки > 0 Тогда
// Извлекаем блок метаданных модуля и строки: "ОбщийМодуль.Расчет.Модуль(49)"
БлокМетаданных = Сред(СтрокаТекста, 2, ПозицияЗакрывающейСкобки - 2);
// Извлекаем текст вызова, который идет после двоеточия
ТекстВызова = СокрЛП(Сред(СтрокаТекста, ПозицияЗакрывающейСкобки + 2));
// Находим номер строки в скобках
ПозицияСкобкиСтроки = Найти(БлокМетаданных, "(");
Если ПозицияСкобкиСтроки > 0 Тогда
ИмяМодуля = Лев(БлокМетаданных, ПозицияСкобкиСтроки - 1);
// Извлекаем номер строки кода
СтрокаКодаТекст = Сред(БлокМетаданных, ПозицияСкобкиСтроки + 1, СтрДлина(БлокМетаданных) - ПозицияСкобкиСтроки - 1);
НомерСтроки = 0;
Попытка
НомерСтроки = Число(СтрокаКодаТекст);
Исключение
// Оставляем 0, если не удалось преобразовать
КонецПопытки;
// Формируем элемент стека
ЭлементСтека = Новый Структура;
ЭлементСтека.Вставить("Модуль", ИмяМодуля);
ЭлементСтека.Вставить("Строка", НомерСтроки);
ЭлементСтека.Вставить("ТекстВызова", ТекстВызова);
Результат.Добавить(ЭлементСтека);
КонецЕсли;
КонецЕсли;
КонецЕсли;
КонецЦикла;
Возврат Результат;
КонецФункции
Используя эту функцию, мы получим чистый, структурированный массив, с которым удобно работать программно — например, группировать схожие ошибки или определять наиболее проблемные модули в нашей системе.
Для того чтобы механизм отслеживания ошибок работал корректно и стек вызовов не терялся при передаче по цепочке вверх, необходимо соблюдать важные стандарты разработки:
Исключение, обработали ее (например, записали в лог) и хотите пробросить её дальше наверх, используйте конструкцию ВызватьИсключение; без параметров.
Если написать ВызватьИсключение "Мое описание ошибки";, то оригинальный стек вызовов будет полностью уничтожен платформой и заменен на текущую строку. Пустой вызов ВызватьИсключение; сохраняет исходный стек неизменным.
ОписаниеОшибки() или КраткоеПредставлениеОшибки(). По стандартам 1С, в комментарий события ЖР должно попадать только ПодробноеПредставлениеОшибки(), так как только оно содержит критически важный для расследования инцидентов стек вызовов.
ОбщегоНазначенияКлиентСервер.ЭтоИсключениеСКодомОшибки() позволяет осуществлять поиск определенных кодов ошибок или ключевых слов по всему стеку, даже если исходная ошибка была обернута внешними обработчиками.
Таким образом, появление полноценного стека ошибок значительно упростило жизнь разработчикам 1С. Используя современные методы менеджера ОбработкаОшибок совместно с приведенным выше парсером, мы можем легко автоматизировать сбор и детальный анализ любых внештатных ситуаций в наших информационных базах.