Теперь рассмотрим область действия этого LoggingScope.
Метод BeginScope возвращает IDisposable который по-умолчанию мы оборачиваем в using.
После того как LoggingScope будет удален, эти данные из словаря перестанут прикрепляться в логи.
Это очень удобно, но есть нюансы из-за особенности обработки исключений, так как при выбросе исключения CLR выполняет размотку стека вызовов (unwind stack), чтобы найти метод с блоком catch который может поймать этот тип исключения и в большинстве случаев к моменту захода в блок catch, dispose скоупа уже будет выполнен.
Об этом поведении можно подробно почитать в статье от Stephen Cleary - A New Pattern for Exception Logging
Чтобы учесть все эти нюансы, я обычно скоупы объявляю в начале UseCase, сразу же после проверки аргументов и непосредственно перед блоком try catch, который оборачивает все тело метода UseCase.
А чтобы при любой ситуации был присвоен код ошибки, в блоке catch ловлю любые не доменные исключения и
превращаю их в доменное исключение со специальным для данного юзкейса кодом ошибки.
Выглядит это примерно так:
public async Task ExecuteAsync(Command command, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(command);
// здесь объявляем скоупы
try
{
// тело сценария
}
catch (Exception e) when (e is not DomainException)
{
throw new DomainException(ErrorCodes.MyUseCaseFailed, innerException: e);
}
}
При этом явно логировать доменные исключения нет необходимости, так как в их конструкторе вызывается функция OnExceptionCreated, который используется в Composition Root для логирования исключений, еще до выброса этого исключения, а значит в том же контексте.
Теперь представьте что абонент пытается вернуть книгу, которой нет в базе данных вообще. Вернуть несуществующую книгу нельзя, поэтому будет возвращен соответствующий код ошибки и добавлен лог об этом:
{
"Timestamp": "2024-04-29T14:03:44.7722095+05:00",
"Level": "Information",
"MessageTemplate": "{ErrorCode}: {Message}",
"TraceId": "054b1bd4b647d40442ef57bf898e913d",
"SpanId": "0399f7806cbd8721",
"Exception": "BL00032: This book not found or not borrowed by abonent\r\nBookLibrary.Domain.Exceptions.BookLibraryException: This book not found or not borrowed by abonent",
"Properties": {
"ErrorCode": "BL00032",
"Message": "This book not found or not borrowed by abonent",
"EventId": { "Id": 1, "Name": "LogDomainException" },
"SourceContext": "Sstv.DomainExceptions.DomainException",
"AbonentId": "018e8ed6-27a3-75bd-b28e-1afd9f5c3bd6",
"BookId": "018e90a5-88e5-7566-be05-a8a6fc60c932",
"ActionId": "26f62c17-4e2a-4b47-a1af-0623c4d36052",
"ActionName": "BookLibrary.Api.Features.ReturnBookController.ReturnBookAsync (BookLibrary.Api)",
"RequestId": "0HN385D0E35F5:00000001",
"RequestPath": "/api/v1/books/return",
"ConnectionId": "0HN385D0E35F5",
"Host": "My-PC",
"ErrorId": "c508483a-aac7-4bb9-97bb-c398346437dd"
}
}
Как мы видим, в Properties есть и BookId и AbonentId, которые я добавляю в скоупы в начале UseCase и есть много других контекстных данных проставленных самим AspNetCore.
Если выбросить не доменное исключение внутри try catch, то размотка стека будет выполнена частично (не успеем выйти за пределы текущего метода т.к. catch это исключение поймает), поэтому объявленные скоупы все ещё будут доступны при логировании.
Получается что при любом раскладе, скоупы в UseCase работают так как от них ожидается.
Если бы UseCase не был обёрнут в такой try catch или если перенести BeginScope внутрь try catch, то LoggingScope будут удалены к моменту входа в catch блок.
Еще настоятельно рекомендую наименования переменных в LoggingScope писать единообразно, в идеале зафиксировав их имена где-нибудь в константах.
Например идентификатор абонента следует везде указывать строго в поле "AbonentId". Создание новых UseCase почти всегда будет с оглядкой на существующие UseCase, поэтому подход будет распространяться сам по себе через копирование вне зависимости делает это джун или сеньор :)
Метод BeginScope возвращает IDisposable который по-умолчанию мы оборачиваем в using.
После того как LoggingScope будет удален, эти данные из словаря перестанут прикрепляться в логи.
Это очень удобно, но есть нюансы из-за особенности обработки исключений, так как при выбросе исключения CLR выполняет размотку стека вызовов (unwind stack), чтобы найти метод с блоком catch который может поймать этот тип исключения и в большинстве случаев к моменту захода в блок catch, dispose скоупа уже будет выполнен.
Об этом поведении можно подробно почитать в статье от Stephen Cleary - A New Pattern for Exception Logging
Чтобы учесть все эти нюансы, я обычно скоупы объявляю в начале UseCase, сразу же после проверки аргументов и непосредственно перед блоком try catch, который оборачивает все тело метода UseCase.
А чтобы при любой ситуации был присвоен код ошибки, в блоке catch ловлю любые не доменные исключения и
превращаю их в доменное исключение со специальным для данного юзкейса кодом ошибки.
Выглядит это примерно так:
public async Task ExecuteAsync(Command command, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(command);
// здесь объявляем скоупы
try
{
// тело сценария
}
catch (Exception e) when (e is not DomainException)
{
throw new DomainException(ErrorCodes.MyUseCaseFailed, innerException: e);
}
}
При этом явно логировать доменные исключения нет необходимости, так как в их конструкторе вызывается функция OnExceptionCreated, который используется в Composition Root для логирования исключений, еще до выброса этого исключения, а значит в том же контексте.
Теперь представьте что абонент пытается вернуть книгу, которой нет в базе данных вообще. Вернуть несуществующую книгу нельзя, поэтому будет возвращен соответствующий код ошибки и добавлен лог об этом:
{
"Timestamp": "2024-04-29T14:03:44.7722095+05:00",
"Level": "Information",
"MessageTemplate": "{ErrorCode}: {Message}",
"TraceId": "054b1bd4b647d40442ef57bf898e913d",
"SpanId": "0399f7806cbd8721",
"Exception": "BL00032: This book not found or not borrowed by abonent\r\nBookLibrary.Domain.Exceptions.BookLibraryException: This book not found or not borrowed by abonent",
"Properties": {
"ErrorCode": "BL00032",
"Message": "This book not found or not borrowed by abonent",
"EventId": { "Id": 1, "Name": "LogDomainException" },
"SourceContext": "Sstv.DomainExceptions.DomainException",
"AbonentId": "018e8ed6-27a3-75bd-b28e-1afd9f5c3bd6",
"BookId": "018e90a5-88e5-7566-be05-a8a6fc60c932",
"ActionId": "26f62c17-4e2a-4b47-a1af-0623c4d36052",
"ActionName": "BookLibrary.Api.Features.ReturnBookController.ReturnBookAsync (BookLibrary.Api)",
"RequestId": "0HN385D0E35F5:00000001",
"RequestPath": "/api/v1/books/return",
"ConnectionId": "0HN385D0E35F5",
"Host": "My-PC",
"ErrorId": "c508483a-aac7-4bb9-97bb-c398346437dd"
}
}
Как мы видим, в Properties есть и BookId и AbonentId, которые я добавляю в скоупы в начале UseCase и есть много других контекстных данных проставленных самим AspNetCore.
Если выбросить не доменное исключение внутри try catch, то размотка стека будет выполнена частично (не успеем выйти за пределы текущего метода т.к. catch это исключение поймает), поэтому объявленные скоупы все ещё будут доступны при логировании.
Получается что при любом раскладе, скоупы в UseCase работают так как от них ожидается.
Если бы UseCase не был обёрнут в такой try catch или если перенести BeginScope внутрь try catch, то LoggingScope будут удалены к моменту входа в catch блок.
Еще настоятельно рекомендую наименования переменных в LoggingScope писать единообразно, в идеале зафиксировав их имена где-нибудь в константах.
Например идентификатор абонента следует везде указывать строго в поле "AbonentId". Создание новых UseCase почти всегда будет с оглядкой на существующие UseCase, поэтому подход будет распространяться сам по себе через копирование вне зависимости делает это джун или сеньор :)