Logger, Exception, Giga
https://giga.chat/link/gcsFTHKZHE
----------------------------------------------------------------------
.NetCore protected override async Task ExecuteAsync(CancellationToken stoppingToken)
Очень часто В этой функции пишут такое
catch (Exception ex) {
_logger.LogError(ex, "MetricsService: Произошла ошибка {Message}", ex.Message);
}
Мне кажется - это какая-то ерунда, ошибка - я имею в виду Первый параметр в _logger.LogError(ex Что при этом высветиться в Консоль ?Ответ
Анализ проблемы
Давайте разберём сигнатуру метода LogError, который вы используете (из интерфейса ILogger):
public static void LogError(
this ILogger logger,
Exception? exception,
string message,
params object?[] args)Первый параметр (
exception): Это место для передачи объекта исключения (ex), которое было поймано. Логгеры в .NET спроектированы так, чтобы при получении объектаExceptionони автоматически извлекали из него всю доступную информацию: само сообщение об ошибке (Message), стек вызовов (StackTrace), а также все вложенные внутренние исключения (InnerException). Это самый важный и информативный параметр.Второй параметр (
message): Это шаблон сообщения, который вы хотите записать в лог. Он может содержать плейсхолдеры{0},{1}или именованные плейсхолдеры{PropertyName}.Третий параметр (
args): Это массив объектов, которые подставляются в шаблон сообщения вместо плейсхолдеров.
Теперь посмотрим на ваш код:
catch (Exception ex)
{
_logger.LogError(ex, "MetricsService: Произошла ошибка {Message}", ex.Message);
}Здесь происходит следующее:
- Вы передаёте объект исключения
exкак первый аргумент. Логгер получит его и запишет в лог полную трассировку стека и другие детали этого исключения. - Затем вы формируете строку
"MetricsService: Произошла ошибка {Message}"и передаёте значениеex.Messageв качестве аргумента для подстановки в плейсхолдер{Message}.
В результате в консоль (или другой целевой источник логов) будет выведено что-то вроде этого:
MetricsService: Произошла ошибка Не удалось установить соединение с базой данных.
System.Exception: Не удалось установить соединение с базой данных. в MyApp.Services.MetricsService.ExecuteAsync() ...
Проблема дублирования очевидна. Информация о том, что именно пошло не так ("Не удалось установить соединение"), записана дважды: один раз как часть вашего рукотворного сообщения, а второй — автоматически извлечена из объекта исключения.
Это делает логи более шумными и затрудняет их чтение и парсинг.
Правильный подход
Чтобы избежать этой ошибки, нужно использовать только один из двух способов записи:
Способ 1: Передача исключения без кастомного сообщения
Если вам достаточно стандартного вывода логгера, просто передайте исключение. Логгер сам создаст информативное сообщение.
try
{
// Ваш код...
}
catch (Exception ex)
{
// Правильно: передаем только исключение.
// Логгер выведет: "An error occurred executing task 'MyTask'"
// А затем полные данные исключения (сообщение и стек вызовов).
_logger.LogError(ex, "Произошла ошибка при выполнении задачи");
}Способ 2: Создание полностью кастомного сообщения БЕЗ передачи исключения
Этот способ используется, когда вы ловите общее исключение, анализируете его и хотите записать в лог свою, более понятную формулировку, не показывая оригинальный стек вызовов.
try
{
// Ваш код...
}
catch (Exception ex)
{
if (ex is TimeoutException timeoutEx)
{
// Здесь мы НЕ передаем объект исключения, только сообщение.
_logger.LogError("Сервис MetricsService не отвечает. Таймаут {Timeout} мс.", timeoutEx.Timeout);
}
else
{
_logger.LogError("Произошла непредвиденная ошибка.");
}
}Итог
Ваше ощущение, что это "какая-то ерунда", верно. Код _logger.LogError(ex, "... {Message}", ex.Message); избыточен и приводит к дублированию информации в логах.
Что высветится в консоли: Сообщение, которое вы сконструировали ("MetricsService: Произошла ошибка ..."), и сразу под ним — полный текст самого исключения, включая его сообщение и стек вызовов. Таким образом, ключевая информация об ошибке будет напечатана дважды.
Вопрос
При таком подходе получается логически, что лучше всего писать вот так.
_logger.LogError(ex) Но не так Я считаю лучше писать вот так
_logger.LogError(ex.Message, "Мое объясняющее сообщение")
?Ответ
Давайте разберем оба подхода более детально, чтобы окончательно прояснить ситуацию.
Почему _logger.LogError(ex) не является лучшим решением
Хотя этот вызов синтаксически корректен (благодаря перегрузке метода), он имеет существенный недостаток с точки зрения семантики и читаемости логов.
Когда вы вызываете LogError(ex), стандартный логгер (например, от Serilog или встроенный) генерирует стандартное сообщение вроде "An error occurred while executing task '...'". Это сообщение малоинформативно. Оно говорит о том, что ошибка произошла, но не дает контекста: где именно, при каких условиях и что пытались сделать.
В результате в логах будет:
- Стандартная, бесполезная фраза.
- Сообщение исключения (
ex.Message). - Стек вызовов.
Вы теряете возможность добавить свой контекст, а стандартные сообщения засоряют логи.
Почему ваш подход _logger.LogError(ex, "Мое объясняющее сообщение") — правильный
Этот подход решает все проблемы предыдущего. Он состоит из двух ключевых компонентов:
- Объект исключения (
ex): Передается как первый аргумент. Логгер понимает, что ему нужно извлечь всю техническую информацию: само сообщение ошибки, стек вызовов (StackTrace), источник и любые вложенные исключения (InnerException). Эта информация критически важна для разработчика при отладке. - Контекстное сообщение (
"Мое объясняющее сообщение"): Передается как строка-шаблон. Это позволяет вам описать бизнес-контекст или операцию, во время которой возникла проблема.
Пример правильного использования:
try
{
// Попытка обновить данные пользователя в базе данных
await _userRepository.UpdateUserAsync(user);
}
catch (DbUpdateConcurrencyException ex)
when (ex.Entries.Any(e => e.Entity is User)) // Фильтруем только нужные исключения
{
// Вот идеальный пример вашего подхода
_logger.LogError(
ex,
"Не удалось обновить данные пользователя {UserId} из-за конфликта версий.",
user.Id);
// Здесь можно выбросить кастомное исключение или вернуть ошибку пользователю
}Что попадет в лог:
Не удалось обновить данные пользователя 55 из-за конфликта версий.
System.Data.DbUpdateConcurrencyException: Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions. ...
Преимущества такого подхода очевидны:
- Читаемость: Человек, читающий логи, сразу видит суть проблемы ("конфликт версий у пользователя"), а затем может изучить технические детали, если они необходимы.
- Структурированность: Если вы используете современный логгер (Serilog, NLog), то
{UserId}станет отдельным полем (property) в вашей системе сбора логов (например, в Elasticsearch или Kibana). Это позволит вам легко фильтровать все ошибки по конкретному идентификатору пользователя. - Полнота информации: У вас есть и контекст, и полный технический стектрейс для отладки.
Заключение
Ваше предположение верно на 100%. Правильный паттерн — всегда передавать объект исключения первым параметром, а вторым — информативную строку-шаблон, описывающую контекст операции. Никогда не передавайте ex.Message в качестве строки-шаблона, так как это приводит к дублированию информации.
Таким образом, запись _logger.LogError(ex, "Мое объясняющее сообщение") является золотым стандартом логирования исключений в .NET.
Вопрос
Я считаю лучше писать вот так _logger.LogError(ex.Message, "Мое объясняющее сообщение")
Ответ
Ваш вариант _logger.LogError(ex.Message, "Мое объясняющее сообщение") не является правильным с точки зрения архитектуры .NET-логирования, хотя он и будет работать без ошибок компиляции. Это одна из самых распространенных ошибок при работе с ILogger.
Почему это неправильно?
Проблема заключается в том, как метод LogError интерпретирует свои параметры. Существует несколько перегрузок этого метода, но самая важная для нашего обсуждения выглядит так:
void LogError(Exception exception, string message, params object[] args);- Первый параметр (
exception): Ожидается объект исключения. Логгер использует его, чтобы извлечь не только текст ошибки, но и стек вызовов (stack trace), который критически важен для отладки. - Второй параметр (
message): Строка-шаблон. - Третий параметр (
args): Аргументы для подстановки в шаблон.
Когда вы пишете _logger.LogError(ex.Message, ...), происходит следующее: вы передаёте строку (ex.Message) вместо объекта исключения. Для логгера это означает, что никакого исключения нет. Он просто запишет два ваших сообщения в лог, но полностью потеряет стек вызовов.
В результате в логах вы увидите:
Мое объясняющее сообщение Не удалось подключиться к базе данных.
Но вы не увидите самого главного — информации о том, в какой строке кода произошла эта ошибка. Отлаживать такую проблему по логам практически невозможно.
Как правильно
Правильный подход, который сохраняет всю информацию об ошибке и добавляет контекст, выглядит именно так:
_logger.LogError(ex, "Мое объясняющее сообщение: {Message}", ex.Message);Давайте посмотрим, что здесь происходит:
_logger.LogError(ex, ...): Вы передаете сам объект исключения. Логгер понимает, что нужно сохранить весь стек вызовов."Мое объясняющее сообщение: {Message}": Это ваш шаблон сообщения, который добавляет бизнес-контекст.ex.Message: Это аргумент для подстановки в плейсхолдер{Message}.
Что попадёт в лог?
Мое объясняющее сообщение: Не удалось подключиться к базе данных.
System.Data.SqlClient.SqlException (0x80131904): Не удалось подключиться к базе данных. в MyApp.Services.UserService.GetUserById(Int32 id) at MyApp.Controllers.UsersController.GetUser(String id) ...
Как видите, мы получили и ваше поясняющее сообщение, и оригинальное сообщение об ошибке, и, самое важное, полный стек вызовов.
Итог
| Подход | Что попадает в лог | Правильно? |
|---|---|---|
_logger.LogError(ex, "... {Message}", ex.Message) | Контекстное сообщение, сообщение об ошибке, стек вызовов. | Да, это правильный и рекомендуемый способ. |
_logger.LogError("... {Message}", ex.Message) | Только контекстное сообщение и сообщение об ошибке. Стека вызовов нет! | Нет. Теряется ключевая информация для отладки. |
_logger.LogError(ex) | Стандартное сообщение логгера ("An error occurred"), сообщение об ошибке, стек вызовов. | Работает, но теряется возможность добавить свой контекст. Менее информативно. |
Таким образом, ваша интуиция верна в том, что нужно добавлять объяснение, но технически верный синтаксис отличается. Всегда передавайте объект исключения первым параметром, если хотите сохранить стек вызовов.Вопрос
Напишите еще раз правильный вариант
Ответ
Правильный вариант
Наиболее полный и полезный для отладки вариант выглядит так:
try
{
// Ваш код, который может выбросить исключение
await SomeService.DoWorkAsync();
}
catch (Exception ex)
{
// 1. Передаем объект исключения первым параметром.
// Это позволяет сохранить стек вызовов (stack trace).
// 2. Используем строку-шаблон с плейсхолдером {Message}.
// 3. Передаем значение для подстановки в шаблон.
_logger.LogError(ex, "Не удалось выполнить операцию 'DoWork'. Ошибка: {Message}", ex.Message);
}Что будет записано в лог?
В результате выполнения этого кода в ваш журнал попадёт следующая информация:
Не удалось выполнить операцию 'DoWork'. Ошибка: [текст сообщения из ex.Message]
System.Exception: [полный текст сообщения об ошибке] в [ИмяПространстваИмен].[ИмяКласса].ИмяМетода [строка №...] в ... (и так далее по всему стеку вызовов до места возникновения ошибки)
Почему это лучше?
- Сохраняется контекст: Вы добавляете своё объяснение — "Не удалось выполнить операцию 'DoWork'". Это помогает понять, что именно пошло не так на бизнес-уровне.
- Сохраняется техническая деталь: Плейсхолдер
{Message}подставит конкретное сообщение об ошибке (например, "Timeout expired"). - Сохраняется стек вызовов: Самое главное — благодаря тому, что вы передали
exкак первый аргумент, логгер запишет полную трассировку стека, без которой найти причину ошибки практически невозможно.
Альтернативный, более простой вариант
Если вам не нужно отдельно выводить сообщение из исключения, можно написать короче. Логгер сам включит его в общую запись вместе со стеком вызовов.
try
{
await SomeService.DoWorkAsync();
}
catch (Exception ex)
{
// Этот вариант также является правильным и часто используемым.
// Он добавляет ваше поясняющее сообщение к полной информации об исключении.
_logger.LogError(ex, "Произошла критическая ошибка при выполнении DoWork.");
}Этот подход проще и решает главную задачу — сохраняет и ваш контекст, и всю техническую информацию об ошибке.
Вопрос
_logger.LogInformation(message: "MyWorker: ", args: "Worker has been stopped.");
"MyWorker: " - подчеркнут зеленой волнистой линией Почему ?Зеленая волнистая линия подстрочного подчеркивания в Visual Studio (и Rider) — это не ошибка компиляции, а предупреждение от статического анализатора кода.
В данном случае анализатор указывает на неэффективное использование ресурсов. Это предупреждение называется "String is formatted but no arguments are passed" (Строка отформатирована, но аргументы не переданы).
Причина предупреждения
Давайте разберем ваш код:
_logger.LogInformation(message: "MyWorker: ", args: "Worker has been stopped.");message: "MyWorker: ": Это строка-шаблон. Анализатор видит, что в ней нет плейсхолдеров для подстановки (таких как{0},{1}или{UserId}).args: "Worker has been stopped.": Это аргумент, который предназначен для подстановки в плейсхолдеры.
Анализатор сопоставляет эти два факта и делает логичный вывод: "Вы передаете строку-шаблон без плейсхолдеров, но при этом тратите ресурсы на создание и передачу отдельного аргумента. Это бессмысленно. Если вы хотите просто соединить две строки, не нужно использовать механизм форматирования".
Как правильно и почему это важно
Внутри метод LogInformation работает примерно так:
- Он проверяет, есть ли в строке-шаблоне плейсхолдеры.
- Если они есть, он вызывает
string.Format()(или аналог), чтобы создать итоговую строку. Это операция, требующая выделения памяти. - Если их нет, он просто использует строку-шаблон как есть.
В вашем случае, когда плейсхолдеров нет, передача аргумента через параметр args — это лишняя операция. Метод просто проигнорирует аргумент args, но на его создание и передачу все равно будут потрачены ресурсы (хоть и незначительные).
Правильный вариант (без плейсхолдеров): Просто передайте уже сформированную строку. Это самый эффективный способ.
// Самый простой и эффективный способ
_logger.LogInformation("MyWorker: Worker has been stopped.");Правильный вариант (с использованием плейсхолдеров): Если вы хотите использовать все возможности логгера (например, структурированное логирование), используйте плейсхолдер в строке-шаблоне.
// Использование именованного плейсхолдера
// Это позволяет логгеру (например, Serilog) сохранить "Worker has been stopped." как отдельное поле,
// что очень удобно для поиска и фильтрации логов.
_logger.LogInformation("MyWorker: {Status}", "Worker has been stopped.");Итог: Зеленая волнистая линия — это дружеский совет от IDE. Она говорит вам: "Ты делаешь лишнюю работу. Можно проще и эффективнее". Вы можете навести на нее курсор, чтобы увидеть точный текст предупреждения, или просто исправить код одним из предложенных выше способов.
Вопрос
Но у меня же есть разделители
- message:
- args:
_logger.LogInformation(message: "MyWorker: ", args: "Worker has been stopped.");Вы абсолютно правы, указывая на именованные аргументы (message:, args:). Синтаксически ваш код абсолютно корректен, и компилятор не выдаст на него ошибку. Программа скомпилируется и выполнится.
Однако зеленая волнистая линия — это не ошибка компиляции, а предупреждение от статического анализатора кода (например, Roslynator или встроенного анализатора Visual Studio).
Анализатор смотрит не на синтаксис, а на смысловую нагрузку вашего кода. Он анализирует, как метод LogInformation будет обрабатывать переданные ему данные.
Вот как метод LogInformation (и другие методы Log*) работает внутри:
- Он получает строку-шаблон (
message). - Он проверяет эту строку на наличие специальных маркеров (плейсхолдеров), таких как
{0},{1}или{Status}. - Если маркеры найдены, он берет значения из
argsи подставляет их на место маркеров. - Если маркеров в строке-шаблоне нет, то значения из
argsпросто игнорируются.
Анализ вашего кода анализатором
- Анализатор видит строку-шаблон:
"MyWorker: ". В ней нет ни одного плейсхолдера. - Анализатор видит, что вы передаете аргумент в
args:"Worker has been stopped.". - Анализатор делает логический вывод: "Аргумент, переданный в
args, никогда не будет использован, потому что в строке-шаблоне нет места, куда его можно было бы подставить. Вызовstring.Format("MyWorker: ", "Worker has been stopped.")вернет просто"MyWorker: ", а второй аргумент будет отброшен. Это неэффективно".
Именно поэтому вы видите предупреждение. Анализатор подсказывает, что вы, вероятно, хотели сделать одно из двух:
- Просто соединить строки. В этом случае не нужно использовать механизм форматирования.csharp
// Правильно: просто одна строка _logger.LogInformation("MyWorker: Worker has been stopped."); - Использовать форматирование. В этом случае в строке-шаблоне должен быть плейсхолдер.csharp
// Правильно: используется плейсхолдер {0} _logger.LogInformation(message: "MyWorker: {0}", args: "Worker has been stopped."); // Или еще лучше, с именованным плейсхолдером (рекомендуется) _logger.LogInformation("MyWorker: {Status}", "Worker has been stopped.");
Заключение
Ваша запись синтаксически верна благодаря именованным аргументам. Но она семантически неверна (неэффективна), потому что вы используете механизм форматирования там, где он не нужен. Предупреждение от анализатора помогает писать более чистый и производительный код.
Важное замечание: В новых версиях .NET и при использовании некоторых логгеров (например, Serilog) передача пустой строки в
argsможет даже приводить к исключениям или нежелательному поведению, поэтому лучше следовать рекомендациям анализатора.
Комментариев нет:
Отправить комментарий