Как использовать свойство Exception.Data, чтобы логировать дополнительные сведения об исключениях

от автора

Хорошее, подробное исключение — мощный инструмент, который помогает найти и исправить проблему. Поэтому в исключения стоит вносить больше деталей. 

Один из способов — добавить в текст сообщения исключения дополнительные сведения. Как в коде ниже.

try {     return await _ordersRepository.Get(id, cancellationToken); } catch (Exception exception) {     throw new Exception($"Unable to get order info, user {userName}, order id {id}", exception); }

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

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

Класс .NET Framework Exception имеет свойство Data, которое уже обеспечивает хранение дополнительных сведений в виде коллекции заданных пар «ключ — значение». Так как это свойство базового класса, я уверен, вы сможете настроить логгер. В наших примерах ниже мы будем использовать NLog, который настраивается достаточно легко. Чтобы избежать конфликта ключей и обработать ошибку наиболее эффективным способом, советую также создать свое исключение.

Половина классов исключений .NET Framework имеет свойства, которые не логируются, — вы можете добавить эти данные в свойство Exception.Data вашего нового исключения. Свойство InnerException будет содержать ссылку на оригинальное исключение. 

Код ниже демонстрирует, как использовать свойство Exception.Data.

try {     return await _ordersRepository.Get(id, cancellationToken); } catch (Exception exception) {     const string message = "Unable to get order info";     var yourException = new YourAppException(message, exception);     yourException.Data[nameof(userName)] = userName;     yourException.Data[nameof(id)] = id;     throw yourException; }

Упс. Похоже, мы добавили больше кода — но можем исправить это, создав расширения, которые позволят задействовать паттерн Fluent interface. Пример ниже демонстрирует, как сделать код более читабельным и простым в использовании.

try {     return await _ordersRepository.Get(id, cancellationToken); } catch (Exception exception) {     throw exception.With("Unable to get order info")         .DetailData(nameof(userName), userName)         .DetailData(nameof(id), id); }

Давайте рассмотрим пример настройки NLog layout для логирования свойства Exception.Data.

${shortdate} ${time} [${level:uppercase=true}]: ${message:withException=true}${when:when=length('${exception:format=Data}')>0:Inner=${newline}--- Exception Data ---${newline}${exception:format=Data:exceptionDataSeparator=,\r\n}}

Как это выглядит в консоли.

Этот пример работает замечательно, если добавлять простые структуры: NLog вызывает ToString(), чтобы записать значения в targets. Поэтому мы можем правильно залогировать объекты, только если они корректно переопределяют метод ToString().

Но переопределить ToString для всех классов практически невозможно. Наиболее простой способ представить объект в виде строки — сериализовать его в JSON. Код ниже добавляет C#-класс, который это делает.

/// <summary> /// Defines a value/json pair to represent an exception data value as JSON /// </summary> public record ExceptionDataEntry {     private static readonly JsonSerializerOptions SerializerOptions = new()     {         PropertyNamingPolicy = JsonNamingPolicy.CamelCase,         Converters = {new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)},         WriteIndented = true     };      private ExceptionDataEntry(in object value, in string json)     {         Value = value;         Json = json;     }      public object Value { get; }     public string Json { get; }      public static ExceptionDataEntry FromValue(in object value)     {         if (value == null)         {             throw new ArgumentNullException(nameof(value));         }          var json = JsonSerializer.Serialize(value, SerializerOptions);         return new ExceptionDataEntry(value, json);     }      /// <summary>     ///  Represents an exception data value as JSON     /// </summary>     /// <returns></returns>     public override string ToString()     {         return Json;     } }

А этот код добавляет расширение.

public static YourAppException DetailData(this YourAppException exception, in string key, in object value) {     try     {         exception.Data[key] = ExceptionDataEntry.FromValue(value);     }     catch     {         // ignored, because we use it inside another exception catch block         // so, we should avoid throwing a new exception to keep the original exception     }      return exception; }

Как это будет выглядеть в консоли в нашем примере.

Я надеюсь, этот подход к использованию Exception.Data для логирования дополнительных сведений поможет вам в поддержке приложений. Если у вас есть идеи, как улучшить предложенный подход, пожалуйста, пишите в комментариях. Спасибо =)


ссылка на оригинал статьи https://habr.com/ru/company/plarium/blog/597421/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *