Как использовать свойство 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}}

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

d18f227cf70f48c2f542195973a44486.jpg

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

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

/// 
/// Defines a value/json pair to represent an exception data value as JSON
/// 
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);
    }

    /// 
    ///  Represents an exception data value as JSON
    /// 
    /// 
    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;
}

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

824da7cf1135a973b74cb01953bfed5c.jpg

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

© Habrahabr.ru