Что такое хорошее сообщение об ошибке?
Как разработчики программного обеспечения, мы все сталкивались с этими раздражающими, не очень полезными сообщениями об ошибках при использовании некоторых библиотек или фреймворков: «Не удалось разобрать файл конфигурации», «Недостаточно прав для этой операции» и т. д. Хорошо, хорошо, значит что-то пошло не так видимо; но что именно? Какой файл конфигурации? Какие разрешения? И что с этим делать? Сообщения об ошибках, в которых отсутствует такая информация, быстро вызывают чувство разочарования и беспомощности.
Так что же тогда делает хорошее сообщение об ошибке? Для меня это сводится к трем частям информации, которые должны быть переданы сообщением об ошибке:
- Контекст: что привело к ошибке? Что пытался сделать код, когда потерпел неудачу?
- Ошибка: Что именно не удалось?
- Решение: Что нужно сделать, чтобы устранить ошибку?
Давайте немного углубимся в эти индивидуальные аспекты. Прежде чем мы начнем, позвольте мне уточнить, что речь идет о сообщениях об ошибках, созданных кодом библиотеки или фреймворка, например, в форме сообщения об исключении или в форме сообщения, записанного в какой-либо файл журнала. Это означает, что получателями этих сообщений об ошибках обычно являются либо другие разработчики программного обеспечения (обнаруживающие ошибки, возникающие из-за сторонних зависимостей во время разработки приложения), либо специалисты по эксплуатации (обнаруживающие ошибки при запуске приложения).
Это отличается от сообщений об ошибках, с которыми сталкивается пользователь, для которых должны применяться другие рекомендации и правила (в частности, в отношении проблем безопасности). Например, вам обычно не следует раскрывать какие-либо детали реализации в сообщении для пользователя, тогда как это не так уж важно — или, наоборот, может быть даже желательно — для обсуждаемых здесь сообщений об ошибках.
Контекст
В некотором смысле сообщение об ошибке рассказывает историю; и, как и в случае с любой хорошей историей, вам необходимо установить некоторый контекст относительно ее общих настроек. Для сообщения об ошибке это должно сообщить получателю, что данный код пытался сделать, когда это не удалось. В свете этого первый приведенный выше пример «Не удалось разобрать файл конфигурации» в какой-то степени касается этого аспекта (и только этого), но, вероятно, этого недостаточно. Например, было бы очень полезно знать точное имя файла:
Не удалось проанализировать файл конфигурации: /etc/sample-config.properties"
Используя пример из Debezium , платформы сбора данных об изменениях с открытым исходным кодом, над которой я работаю в своей повседневной работе, второе сообщение может выглядеть так с некоторым контекстом о том, что произошло:
Failed to create an initial snapshot of the data; lacking permission for this operation
Не удалось создать начальный снимок данных; нет разрешения на эту операцию
Возвращаясь к сообщениям об ошибках, связанных с обработкой некоторых входных или конфигурационных файлов, неплохо было бы напечатать абсолютный путь. В случае, если ресурсы файловой системы предоставляются как относительные пути, это может помочь выявить неверные предположения относительно текущего рабочего каталога или чего-либо еще, используемого в качестве корня для разрешения относительных путей. С другой стороны, в частности, в случае сценариев с несколькими арендаторами или SaaS, вы можете рассматривать макеты файловой системы как конфиденциальную деталь реализации, которую вы можете предпочесть не раскрывать неизвестному коду, который вы запускаете. Что здесь лучше, зависит от вашей конкретной ситуации.
Если какой-то фреймворк поддерживает разные типы файлов, конкретный тип рассматриваемого проблемного файла также должен быть частью сообщения: «Не удалось разобрать файл сопоставления сущностей…». Если ошибка связана с определенными частями содержимого файла, хорошей идеей будет отображение номера строки и/или самой строки.
С точки зрения того, как передать контекст ошибки, он может быть частью самих сообщений, как показано выше. Многие платформы ведения журналов также поддерживают понятие сопоставленного диагностического контекста (MDC), карты для распространения произвольных пар ключ/значение в сообщениях журнала. Поэтому, если ваши сообщения должны отображаться в журналах, установка контекстной информации в MDC может быть очень полезной. В Debezium это используется, например, для распространения имени затронутого соединителя, позволяя пользователям Kafka Connect различать сообщения журнала, исходящие из разных соединителей, развернутых в одном и том же кластере Connect.
Примечание: Что касается распространения контекстной информации через сообщения журнала (в отличие, скажем, от сообщений об ошибках, распечатываемых инструментом CLI), структурированное ведение журнала, обычно в форме JSON, упрощает любую последующую обработку. Помещая контекстную информацию в отдельные атрибуты структурированной записи журнала, потребители могут легко фильтровать сообщения, принимать только определенные подмножества сообщений на основе их содержимого и т. д.
В случае исключений, цепочка исключений ведущих к первопричине, также является важной контекстной информацией. Поэтому я бы рекомендовал всегда регистрировать всю цепочку исключений, а не перехватывать исключения и вместо того чтобы регистрировать только какое-то замещающее сообщение.
Ошибка
Перейдем к следующей части, описание самой фактической ошибки. Вот где вы должны кратко описать, что именно произошло. Придерживаясь приведенных выше примеров, первое сообщение, включая контекст и описание ошибки, может выглядеть так:
Couldn’t parse config file: /etc/sample-config.properties; given snapshot mode 'nevr' isn’t valid
Не удалось проанализировать файл конфигурации: /etc/sample-config.properties; данный режим снимка «nevr» недействителен
Failed to create an initial snapshot of the data; database user 'snapper' is lacking the required permissions
Не удалось создать начальный снимок данных; у пользователя базы данных 'snapper' нет необходимых разрешений
Кроме этого, здесь особо нечего сказать; старайтесь быть эффективным: делайте сообщения настолько длинными, насколько это необходимо, и настолько короткими, насколько это возможно. Одна из идей может состоять в том, чтобы работать с разными вариантами сообщений для одного и того же вида ошибки, более короткого и более длинного. Какой из них используется, можно контролировать с помощью уровней журнала или какого-либо «подробного» флага. Разработчики Java могут найти библиотеку jdoctor Седрика Шампо полезной для реализации этого. Лично я еще не использовал такой подход, но, возможно, это стоит усилий для конкретных ситуаций.
Решение
После установления контекста сбоя и того, что именно пошло не так, последней — и часто самой интересной — частью является описание того, как пользователь может устранить ошибку. Какие действия они должны предпринять, чтобы этого избежать? Это может быть так же просто, как сообщить пользователю об ограничениях и/или допустимых значениях в случае примера с файлом конфигурации (т. е. аналогично сообщениям об ошибках тестирования, которые показывают как ожидаемые, так и фактические значения):
Couldn’t parse config file: /etc/sample-config.properties; given snapshot mode 'nevr' isn’t valid (must be one of 'initial', 'always', 'never')
Не удалось проанализировать файл конфигурации: /etc/sample-config.properties; данный режим моментального снимка «nevr» недействителен (должен быть одним из «начальный», «всегда», «никогда»)
В случае проблем с разрешениями, вы можете уточнить, какие из них необходимы:
Couldn’t take database snapshot: database user 'snapper' is lacking the required permissions 'SELECT', 'REPLICATION'
Не удалось сделать снимок базы данных: у пользователя базы данных 'snapper' отсутствуют необходимые разрешения "SELECT", "REPLICATION"
В качестве альтернативы, если требуются более длительные стратегии смягчения последствий, вы можете указать (стабильный!) URL-адрес в своем справочном документе, который предоставляет необходимую информацию:
Couldn’t take database snapshot: database user 'snapper' is lacking the required permissions. Please see https://example.com/knowledge-base/snapshot-permissions/for the complete set of necessary permissions
Не удалось сделать снимок базы данных: у пользователя базы данных 'snapper' нет необходимых разрешений. Полный набор необходимых разрешений см. на странице https://example.com/knowledge-base/snapshot-permissions/
Если требуется какое-то изменение конфигурации (например, доступ к базе данных или IAM), ваши пользователи полюбят вас еще больше, если вы поделитесь этой информацией в «исполняемой» форме, например, в виде операторов GRANT, которые они могут просто скопировать, или вызовов CLI для конкретного поставщика. такие как aws iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/SomePolicy --role-name SomeRole
.
Говоря о внешних ресурсах, на которые ссылаются в сообщениях об ошибках, было бы неплохо иметь уникальные коды ошибок как часть ваших сообщений (например, коды ORA Oracle или сообщения об ошибках, создаваемые WildFly и его компонентами). Соответствующие ресурсы (предоставленные вами или извне, например, в ответах на StackOverflow) можно будет легко найти с помощью вашей любимой поисковой системы. Бонусные баллы за добавление ссылки на собственный канонический ресурс прямо в само сообщение об ошибке:
Couldn’t take database snapshot: database user 'snapper' is lacking the required permissions (DBZ-42). Please see https://dbz.codes/dbz-42/ for the complete set of necessary permissions
Не удалось сделать снимок базы данных: у пользователя базы данных 'snapper' отсутствуют необходимые разрешения (DBZ-42). Полный набор необходимых разрешений см. на странице https://dbz.codes/dbz-42/
(Это вымышленный пример, в настоящее время мы не используем этот подход в Debezium, но мне, вероятно, следует подумать о покупке домена dbz.codes 😉).
Ключевым выводом является то, что вы не должны оставлять своих пользователей в неведении относительно того, что им нужно сделать, чтобы устранить ошибку, с которой они столкнулись. Нет ничего более разочаровывающего, чем, по сути, сказать: «Ты сделал это неправильно!», не получив намека на то, что вместо этого следует сделать правильно.
Общие рекомендации
Наконец, некоторые приемы в отношении сообщений об ошибках, которых я стараюсь придерживаться и которые я обычно рекомендую:
- Единый залог и стиль: выбранный конкретный стиль не имеет большого значения, но вы должны выбрать либо активный, либо пассивный залог
(«couldn’t parse config file» или «config file couldn’t be parsed»)
, использовать последовательный регистр, либо заканчивайте, либо не заканчивайте сообщения точкой и т. д.; ничего критичного в этом нет, но с вашими сообщениями будет легче работать
- Одна концепция, один термин. Избегайте ссылок на одну и ту же концепцию из вашего домена, используя разные термины в разных сообщениях об ошибках; Точно так же избегайте использования одного и того же термина для нескольких вещей. Используйте те же термины, что и в других местах, например, документация по API, справочные руководства и т. д.; Чем последовательнее и недвусмысленнее, тем лучше
- Не локализуйте сообщения об ошибках: здесь не все так однозначно, но я обычно рекомендую не переводить сообщения об ошибках на другие языки, кроме английского; Опять же, речь идет не о сообщениях об ошибках, с которыми сталкивается пользователь, а о сообщениях, предназначенных для разработчиков программного обеспечения и специалистов по эксплуатации, которые, как правило, должны владеть разумными знаниями английского языка; в зависимости от вашей аудитории и целевого рынка переводы на определенные языки могут иметь смысл, и в этом случае общий, однозначный код ошибки обязательно должен быть частью сообщений, чтобы облегчить поиск ошибки в Интернете.
- Не делайте сообщения об ошибках контрактом API: если потребители вашего API должны иметь возможность реагировать на различные типы ошибок, от них не требуется анализировать какие-либо сообщения об ошибках для этого. Вместо этого создайте тип исключения, который предоставляет машинно-обрабатываемый код ошибки, или создайте определенные типы исключений, которые могут быть отдельно перехвачены вызывающей стороной.
- Будьте осторожны с раскрытием конфиденциальных данных: если ваша библиотека занимается обработкой конфиденциальных пользовательских данных, позаботьтесь о том, чтобы не создавать никаких проблем с конфиденциальностью; например, «показать фактическое и ожидаемое значение» может не создавать проблем для значений, предоставленных разработчиком или администратором приложения; но это может создать проблему, если фактическое значение представляет собой пользовательские данные, защищенные GDPR.
- Либо вызовите исключение, либо логируйте ошибку, но не то и другое одновременно: о данной ошибке следует либо сообщить, вызвав исключение, либо логировать её. В противном случае, при выполнении обоих действий, поскольку исключение обычно в конечном итоге регистрируется через какой-то универсальный обработчик, пользователь увидит информацию об одной и той же ошибке в своих журналах дважды, что только добавляет путаницы.
- Ошибиться раньше: речь идет не столько о том, как выражать сообщения об ошибках, сколько о том, когда их вызывать; вообще, чем раньше, тем лучше; сообщение при запуске приложения проигрывает сообщение позже во время выполнения; сообщение во время сборки лучше сообщения при запуске и т. д. Более быстрая обратная связь сокращает время обработки исправлений, а также помогает предоставить контекст любых сбоев.
Обновление от 13 января: этот пост обсуждается на Reddit
Обновление от 7 февраля: этот пост обсуждается в Hacker News.
Источник: https://www.morling.dev/blog/whats-in-a-good-error-message/