Как стать автором
Обновить

Паттерн написания универсальной системы ошибок приложения

Уровень сложностиПростой
Время на прочтение5 мин
Количество просмотров7K

Всем доброго времени суток.

Мотивация к написанию данной статьи

За свою карьеру написал больше 100 микросервисов и около 30 брал на сопровождение, рефакторинг и доработку. Среди них были сервисы аутентификации, криптографии, адаптеры, прокси, эмитенты токенов, DataStore/DataMart, калькулирующие измерения к срезам статистики на холодных данных и на потоке, оркестраторы с широким спектром смежных систем (пример на хабре) etc. Писал на таких языках, как С#, Java, Kotlin, Scala, Node.js. И некоторое время проходил "день сурка" в момент проектирования или рефакторинга полученного в наследство кода, когда руки доходят до аспекта логирования, мониторинга, обработки ошибок etc. В этой статье опишу с какими реализациями слоя обработки ошибок я сталкивался или находил в качестве best practice, как обычно ее интегрируют в SLA, метрики и логи, почему стал изобретать велосипед и к чему пришел, а также сравню собирательный образ классических подходов с выбраным по итогу проб и ошибок.

2. Собирательный образ классической реализации

2.1 В объектно-ориентированных языках создается целая система кастомных ошибок, в фундамент которой выбирае[ю]тся наиболее подходящий[е] из предложенных языков в качестве суперкласса.

Минусы:

  • если система наследования ошибок не прямая как гвоздь, то усложнится и система проверок и рефлексии в логике обработки ошибок;

  • чем длиннее жизнь сервиса и/или обширней его бизнес логика (особенно актуально для оркестраторов), тем больше кастомных классов ошибок, что замусоревает код и структуру проекта, а полную карту ошибок можно увидеть только в аналитике к сервису или бегая по пакетам проекта с помощью возможностей IDE (этот минус особенно стреляет в ноги новым разработчикам или тебе, когда забудешь о написанном и решишь себе экскурсию устроить);

  • аналитика сервиса требует актуализации при добавлении нового класса-ошибки;

  • трата времени на поиск наиболее подходящего причине возникновения ошибки суперкласса, если выбор самого базового в языке не устраивает (неоднократно фиксировал такую дотошность в pr-ах и грумминге).

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

Минусы:

  • хоть наследования и нет, а первые три минуса выше имеются и у такого решения.

2.3 Система ошибок подразумевает объединение нескольких причин возникновения не целевого поведения в рамках одной сущности или класса-ошибки.

Минусы:

  • усложняется мониторинг через метрики, на потоке непонятно сколько, каких причин возникает;

  • troubleshooting будет отъедать больше времени, а отдел сопровождения - частенько задавать тебе вопросы приходя с боевыми кейсами;

  • отслеживать - какие причины объединены случайно, а какие специально, порой неочевидно, особенно спустя n-месяцев;

2.4 Система ошибок является не сквозной и ограничена контуром твоего сервиса. Например сервису-клиенту отдаются другие представления ошибок, в худшем варианте реализации перекрывающие целые пулы причин не целевого поведения под одним кодом (например если сервис имеет REST API, то ограничиваются HTTP кодом и строковым описанием).

Минусы:

  • непрозрачность, добавляет лишний уровень абстракции без сохранения изначальной информации о причине (если отдел сопровождения твоего сервиса и сервиса-клиента не один и тот же, то в случае проблем интеграции между сервисами, жди вопросы от обоих отделов и команды разработки сервиса-клиента, ну а тебе вспоминать связи между двумя системами ошибок);

  • доработки бизнес-логики в сервисе-клиенте на нецелевые поведения твоего сервиса с течением времени может потребовать изменений системы ошибок на твоей стороне или контракта между вами для уточнения причины ошибки.

2.5 Ошибку можно идентифицировать только по текстовой информации (названию, stack-trace, description, message etc.).

Минусы:

  • обработка таких ошибок, особенно если их вариаций будет много, приведет к "зубной боли" при взгляде на код их парсинга и условий рефлексии на них на клиентской части, что скажется на вероятности багов с клиентской стороны и вовлечении тебя в их разборы на живых кейсах из прома;

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

2.6 Ошибки пишутся в систему мониторинга в несколько метрик или не пишутся вообще.

Минусы:

  • чем больше видов метрик ошибок, тем сложнее в этой системе ориентироваться, сложно разобраться сколько негативных исходов с неизвестной причиной на боевом стенде за целевой интервал.

Есть и другие черты таких систем. Думаю на этом этапе посыл уже ясен, что есть масса нюансов, которые стоит учесть в процессе создания описанного слоя.

3. Пилим идеальную систему ошибок

+ у каждой ошибки должен быть свой code типа int;
+ если есть необходимость использовать в архитектуре сквозную систему кодов ошибок и управляющих статусов, то -int будет ошибкой, а +int управляющим статусом;
+ если есть необходимость в сквозной системе ошибок между сервисами, то обычно шаг < 10000 достаточный на сервис, т.е. у каждого сервиса будет свой интервал в рамках которого он будет разбивать коды на меньшие интервалы по какому нибудь принципу, например на каждую смежную систему, с которой интегрирован сервис будет приходиться свой интервал кодов:
1 - 99 // зарезервированы под внутренние ошибки кода твоего сервиса;
100 - 199 // зарезервированы под первую интеграцию, например ошибки клиентских сторон;
200-299 // под вторую интеграцию, например базы данных;
300-399 // под третью интеграцию, например кафки;
400-499 // под четвертую интеграцию, например смежный сервис 1;
500-599 // под третью интеграцию, например смежный сервис 2;
+ в объектно-ориентированных языках можно ограничиться созданием Enum с кастомным полем int code, тогда Enum.name будет давать суть причины, ёмко и в текстовом виде, a code - станет якорем в справочнике ошибок твоего приложения, далее создается один кастомный класс ошибки от самого базового класса (например Throwable для Java), добавь ему поле Enum и готово;
+ система ошибок должна прошивать насквозь код твоего сервиса->метрики->логи->ответы клиентам:
* код твоего сервиса - все ошибки в месте возникновения должны оборачиваться в твою кастомную ошибку;
* метрики - например в Prometheus можно создать единственную метрику errors с лэйблом code, что объединит все исходы в одной метрике, круглые (с 2-мя и более нулями) границы интервалов позволят коллегам из сопровождения и смежникам ориентироваться в интервалах, определяя сторону к которой нужно идти с вопросами или в целом в каком направлении расследовать причины ошибки (если code равен первому значению из интервала);
* в логах также желательно выделить code, например если ELK, то в отдельное поле индекса;
* клиентам в ответ также необходимо отдавать code (если требования к безопасности воспрещают такое поведение, то хотя бы первое значение из интервала в который данный код входит).


Первый code ошибки из любого интервала является дефолтным и процент его возникновения от общего количества ошибок является процентом ТВОЕЙ лени или незнания. Например, если в метрики приложения вынести частотность возникновения той или иной ошибки по code, то стремиться нужно будет к светлому будущему, в котором нет исходов с 1, 100, 200, 300, 400, 500 ошибками, а значит все возможные негативные исходы тебе известны и обработаны тобой в частном порядке.

И да, это все. Такая система лишена всех выше описанных недостатков. Прошла проверку временем на разных языках, разных бизнес задачах, при разработке в духе стартапа и интерпрайз.

Теги:
Хабы:
Всего голосов 11: ↑6 и ↓5+2
Комментарии19

Публикации

Ближайшие события