Алгоритм ранжирования ошибок

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

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

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

Проблема большинства статей на эту тему в том, что они фактически предлагают записывать субъективные ощущения разработчика. Вроде, уровень “Notice” - это уровень для “существенных событий”, но не ошибок. Эээ… а инструкцию, как определять “существенность” где взять? Описание уровней самим RFC syslog просто бессмысленное - "critical - critical conditions". Спасибо, кэп. Есть варианты получше, например “остановка приложения - fatal, надо исправлять что-то - error, аномалия - warning, рядовое событие - info. Такой вариант существенно лучше, но у меня к нему тоже есть претензии, которые я покажу позже, сравнивая с тем, к чему пришел сам.

Alert/Fatal

Первое и очень простое правило следующее: факт неожиданного завершения процесса я записываю как Alert (Fatal) уровень. Важно отметить, что запись делается о факте закрытия приложения, а не о причине - обычно исключении - его вызвавшей. Некоторая путаница может возникать из-за того, что к нему можно приложить убившее его исключение. Также слово “неожиданное” подразумевает неожиданность с точки зрения внутренней логики процесса, а не внешней вызывающей стороны. Т.е. если Вы целенаправленно в силу какой-то технической задумки тормозите процесс с помощью ThreadAbortException или другим специально заведенным для этой цели исключением - это не считается неожиданным, это - часть плана.

Normal -> Limited

Следующий вопрос, который нужно задать: “Нормально ли это событие”? Нормальность определяется функциональной спецификацией приложения. Легко убедиться в том, что событие нормально, если Вы никак не будете на него реагировать, в каких бы количествах оно не происходило. Такие события я отношу к классу Information, это все те скучные записи, что “такой-то залогинился”, “такая-та операция началась”, “такая-то операция завершилась”.

Упоминание про количество в предыдущем абзаце не с проста. Представьте себе, что Ваше приложение связывается по сети с сервером. В целом, все хорошо, но иногда бывает так, что сервер не отвечает. Чтобы решить проблему - достаточно просто повторить запрос еще раз, максимум два. Таким образом, мы фактически считаем нормальным сбои сервера. Для подобных ситуаций я использую уровень Notice - ошибки, с которыми мы смирились. Никто не будет разбираться, почему раз в месяц она происходит, тем не менее следить за тем, что их количество не начинает нарастать - хотелось бы. Таким образом Notice - это события, которые нормальны лишь в ограниченных количествах. Вы можете спросить "Пфф, ну и как же определять количество, до которого события нормальны?" Ответ - никак, потому что это и не нужно. Здесь работает просто сам факт, что мы согласны о том, что существует некий предел, после которого мы как минимум удивимся, почему этих событий так много. Обычно у каждой операции есть основной, "идеальный" сценарий и множество причин, почему он не дойдет до конца. Эти причины нормальны с определенной частотой. Такого предела точно нет, скажем, для количества успешного входа пользователей в систему. Какое-то количество неуспешных попыток входа тоже нормально. Но думаю никто не станет спорить, что есть какая-то цифра - в принципе - для счетчика неуспешных попыток, после которой мы захотим посмотреть, хотя бы просто из любопытства, откуда их столько?

Not normal -> Error -> Input

Давайте рассмотрим ненормальные события. Из них легко можно выделить два подмножества: те, которые делают невозможным продолжение операции - “ошибки” и те, которые не влияют на выполнение алгоритма - “аномалии”.

Под “не влияют” имеется ввиду то, что внешняя вызывающая сторона не имеет ни единого шанса по полученному результату понять, произошла внутри аномалия или нет. Например операция нестрогого Delete. Может оказаться, что записи, которую Вы пытаетесь удалить - уже нет. Это может быть подозрительным, но точно никак не влияет на вызывающую сторону, потому что конечный результат тот, что она и ожидает.

Давайте еще рассмотрим другой пример - метод DocumentUpdate(id). Для нашего случая - “обновить документ по id” - ясно, что счетчик обновленных строк в базе данных должен быть равен строго 1. А теперь представьте себе, как похолодеет Ваша спина, когда по каким-то - неважно каким - причинам вдруг Вы увидите “10547 rows were updated” в операции из примера? И это к тому же никак не видит вызывающая сторона! Которая продолжит работать и дальше, с каждой секундой уничтожая данные. Именно для таких сценариев, я предлагаю использовать уровень Emergency, когда Вам нужно что-то вроде гибрида стоп-крана в поезде и звука сирены воздушной тревоги.

Теперь рассмотрим ошибки. Для них я предлагаю следующее: обозначать как Critical сбои в операциях input направления и как Error - в  операциях output направления. Под input понимаются такие операции, которые влияют на внешние модули системы относительно места, из которого вызываются: они меняют или данные или поведение. Например, Вы запрашиваете состояние своего банковского счета. Сколько бы раз Вы его не обновили - для Вас поведение системы и сам счет не изменится - output. И та же самая операция - но сделанная через другой банк, с комиссией - совершенно другая история - каждый просмотр списывает какой-то количество денег с Вашего счета - input. Грубо говоря, это все кроме "R" из CRUD операций либо POST, PATCH, PUT из http протокола.

Если направление не ясно (иначе говоря, "в любой непонятной ситуации") - Error. Я исхожу из того, что система логгирования никогда не должна лгать: за счет этого, Вы всегда уверены, что critical-ошибка - это точно input-сбой, а error-сообщение - в принципе может быть чем угодно.

По моему опыту, сбой input-операции более неприятен, так как всегда привносит некоторую дополнительную неопределенность насчет состояния системы. Плюс есть некоторый шанс, что пользователь даже  не будет знать об этом: потому что кто-то решил избегать методов, которые бросают исключения - “исключения зло!” - но забыл проверить код ошибки DocumentUpdateOrError метода.

Конечно же, output-операция тоже может нести серьезные проблемы - работа блокируется, но это, по крайней мере, точно очевидно для вызывающей стороны мгновенно. Да и "упущенную выгоду" люди переживают не так эмоционально, как прямой урон. В примере с тем же банком - прочувствуйте разницу - увидеть ошибку, что "невозможно отобразить счет" либо потратить немного денег и(или) времени - и увидеть ту же саму ошибку.

Таким образом как-то так незаметно разошлись почти все уровни, остался лишь Debug. Его я предлагаю использовать исключительно в ненормальных ситуациях как уровень, куда писать данные для "расследования". Запись о самом факте ошибки - уровень Error или Critical, дополнительные данные о ее контексте - Debug.

Мой опыт так же показал, что нет какого-то особого смысла выделять какие-то особенные причины ошибок вроде "Нет связи с базой данных", что обычно принято записывать как проблема "особой важности". Важность этого события бросается в глаза и так, без всяких дополнительных уровней, когда Вы получаете резкий всплеск количества Error/Critical событий - не нужно быть Шерлоком Холмсом для того, чтобы понять, что проблема существенна и откладывать нельзя. Но если очень нужно - не вижу проблемы прикрутить счетчик таких ошибок и при достижении некоторого количества, скажем, 12 ошибок за последнюю минуту - высылать Emergency и поднимать тревогу.

Чем плох Fatal-Error-Warning?

Выше я упоминал, что у меня есть некоторые претензии к схеме логгирования крэш-ошибка-аномалия-инфо.

Первая претензия в том, что в простой системе мы не может отличить ситуацию сбоя процесса - что конечно плохо - от еще худшей ситуации, когда система начинает идти в разнос. Да, вероятность этого ничтожна, но эта ситуация идеально описывается японской мудростью “Даже если меч понадобится Вам всего один раз - этот раз будет стоить того, чтобы носить его всю жизнь”. Когда происходит крэш приложения, обычно я не прерываю задачи, над которыми команда работает в текущий момент, а просто ставлю таску "разобраться и починить" следующей в очереди. Ситуация "emergency" требует же именно "бросить все" и потому хочется различать их с первого взгляда.

Вторая моя претензия заключается в том, что в такой системе нет хорошего места для Notice’ов в моем определении. Вы не можете с чистой душой отправить их в Information, так как это не совсем “Окей”. Но и сделать их Warning’ами тоже будет так себе идея, так как Вы скорее всего никогда не почините их и они будут просто мозолить глаза, снижая основное предназначение Warning - привлекать внимание к потенциальным проблемам.

Пример

Давайте на парочке простых ситуаций я проиллюстрирую применение моей схемы. Пускай у нас есть приложение, где в текстовом  поле нужно указать путь к файлу и кнопка “Окей”. При нажатии приложение должно считать файл, загрузить его в хранилище, обратиться в внешнему сервису, который вычислит, насколько этот файл хорош (чтобы это ни значило) и показать отчет, где будет указан размер этого файла и удельное “качество файла” на байт.

Если такого файла не существует - это проблема пользователя, мы никак не можем влиять на то, что пользователь вводит. Мы ожидаем, что иногда такое будет проскакивать Notice.

Если к файлу нет доступа - это нормально с точки зрения нашей функциональной спецификации, мы не ожидаем, что работать будет исключительно там, где полные права доступа и иногда такое должно случатся. Можно перефразировать по-другому - нам нечего чинить в этой ситуации. Notice

if (!System.IO.File.Exists(path))
{
  Log.Notice("Wrong file path: {0}", path);

  MessageBox.Show("The file does not exist");

  return;
}

byte[] file = null;

try
{
  file = System.IO.File.ReadAllBytes(path);

  Log.Info("File size: {0} bytes", file.Length);
}
catch (UnauthorizedAccessException ex) //не планируем реагировать
{
  Log.Notice(ex);

  MessageBox.Show("There is no permission to read the file");
}
catch (Exception ex) //нам нужно реагировать на такой сценарий
{
  Log.Error(ex);

  MessageBox.Show("[Unified app error message]");
}

Если просто почему-то не смогли прочитать файл, но не из-за прав доступа - это Error - надо разбираться и либо устранять причину, либо уточнять спецификацию.

Прочитали файл - заурядное событие Info - мы всегда записываем это событие.

Если не смогли записать файл в базу - мы знаем, что база хандрит -  потому закладываем всегда еще две попытки с небольшой задержкой - Notice

Не повезло и в базу так и не записали после всех попыток - Critical - есть надежда на то, что внутри самого метода, событие исключения перехватывалось и в Debug записывались дополнительные сведения. Затем исключение пробрасывалось дальше, к нам, где мы и остановили его путь.

Записали файл и от базы ушло предупреждение, что там место заканчивается или какая другая беда намечается - Warning - самому приложению от этого факта пока ни холодно ни жарко.

const int Attempts = 3;

for (int i = 0; i < Attempts; i++)
{
  try
  {
    string identifier = storage.FileCreate(file);

    Log.Info("File was stored: {0}", identifier);

    break;
  }
  catch (Exception ex)
  {
    if (i < Attempts - 1)
    {
      Log.Notice(ex);
    }
    else
    {
      Log.Critical(ex);

      MessageBox.Show("Can not save file");

      return;
    }
  }
}

Если не смогли получить ответ от Сервиса - это Error. Тем не менее, у нас есть некоторый выбор как поступить. Мы может полностью прервать операцию и не показывать никакой отчет - Info: "no report" - простой вариант, либо делать более сложную версию отчета - которая в случае неудачи показывает ну хотя бы размер файла Info: "partial report " и место для “удельного качества” скрыто либо явно написано “Не известно”.  Это все равно плохо - согласно нашей спецификации, в отчете обязательно “удельное качество”. Отчет без нее - это своего рода навороченное сообщение об ошибке, тем не менее потенциально более полезное, чем просто не показывать ничего. И в логах мы может проследить, что происходило - Error: "file quality service connection", Info: "partial report".

try
{
  var quality = service.Analyze(file);

  Log.Info("File quality: {0} points", quality);

  var qpb /*quality per byte*/ = quality / file.Length;

  MessageBox.Show($"L:{file.Length} Q:{quality} QPB:{qpb}");

  Log.Info("Report");
}
catch (Exception ex)
{
  Log.Error(ex);

  MessageBox.Show($"L:{file.Length}");

  Log.Info("Report - Partial");
}

Я специально уделяю это внимание потому, что может возникать некоторая путаница. Мол, если у нас что-то обернуто в try..catch - значит мы “ожидаем” это событие. Это не совсем так. Обертываем мы как раз для того, чтобы соответствовать другому требованию спецификации - а именно “Приложение не должно аварийно закрываться с экраном смерти”. Если Ваше приложения после неудачного запроса к внешней системе закрывается - то фактически у Вас две проблемы: сама по себе нештатная ситуация с внешним сервисом плюс некачественное приложение-клиент, которое не способно с этим справиться настолько, что погибает. Соответственно об этом Вы и увидите записи Error: "file quality service connection", Fatal: "App screen of death".

В хорошем приложение логи покажут Вам, что произошла ошибка Error:"file quality service connection", а затем - как приложение из этого выкрутилось: Info: "partial report". Если возникает некая неуверенность, считать ли то или иное событие ожидаемым - всегда можно дополнительно проверить вопросом “Является ли это событие следствием другого?”. Если ответ “да” - то это планируемое поведение, т.е. нормальное, в ответ на нештатную ситуацию. Система ведет себя штатно во внештатной ситуации.

На уровне всего приложения нам надо позаботиться, чтобы записывать факты крэша - которые наверняка иногда будут проскакивать.

Заключение

Для того, чтобы любой в команде мог легко вспомнить что куда писать - я составил вот такую таблицу, которую, наверное, стоит приложить к статье как pdf файл - если это будет кому-то интересно?

Я не стал включать в статью мысли о том, как избегать “двойной тревоги” (и “двойного стресса”), если на Вас начинают сыпаться уведомления и с клиента и сервиса, за которые Вы в ответе. Либо наоборот,  когда проблема уровня Emergency, но Вы сами никак не можете повлиять на причину, так как причина в подсистеме, за которую отвечают другие люди.

p.s. С меня плюс в карму каждому, кто укажет на логические нестыковки или противоречия в тексте, используя при этом минимально возможное количество сарказма.

Источник: https://habr.com/ru/post/560592/


Интересные статьи

Интересные статьи

Опять изучаем алгоритм. И уже который раз в заголовке слово "Эволюция". Эволюция программного проекта, эволюция памяти и теперь эволюция поведения. Это простое совпадение? Или ...
Перед записью на новый курс Machine Learning Advanced мы тестируем будущих студентов, чтобы определить уровень их готовности и понять, что именно им необходимо предложить для подгот...
Споры о том, нужно ли разработчикам писать алгоритмический код на собеседованиях, бесконечны. В поддержку положительного ответа я уже публиковал рассказ об алгоритмических секциях с написанием ко...
После появления DeBroglie и Tessera меня много раз просили объяснить, как они работают. Генерирование может выглядеть как волшебство, но лежащие в его основе правила на самом деле просты. ...
«Битрикс» — кошмар на костылях. Эта популярная характеристика системы среди разработчиков и продвиженцев ныне утратила свою актуальность.