Логирование в C# приложениях: лучшие практики и инструменты (.NET)
Логирование — это не просто запись сообщений в файл. Это важнейший компонент любого production-приложения на C#. Без качественной системы логирования вы работаете вслепую: ошибки остаются незамеченными, производительность сложно анализировать, а отладка превращается в угадывание. В этой статье мы разберём лучшие практики логирования в экосистеме .NET, рассмотрим популярные библиотеки (Serilog, NLog), правильные уровни логирования и структурированный подход к сбору данных.
1. Выбор библиотеки для логирования: Serilog vs NLog
В мире .NET есть несколько зрелых решений. Два самых популярных — Serilog и NLog. Оба поддерживают асинхронное логирование, множество sinks (целей вывода) и интеграцию с .NET Core/5+ через ILogger.
Serilog
Serilog известен своей философией структурированного логирования. Вместо простых строк вы передаёте объекты, а Serilog сериализует их в JSON. Это идеально для систем вроде Elasticsearch, Seq или Datadog.
// Пример Serilog
using Serilog;
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.Console()
.WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day)
.CreateLogger();
Log.Information("Пользователь {UserId} выполнил вход в {Time}", userId, DateTime.UtcNow);
NLog
NLog — более традиционный, но не менее мощный инструмент. Он отлично подходит для проектов, где нужна гибкая конфигурация через XML или код. Поддерживает ротацию файлов, отправку по email и интеграцию с базами данных.
// Пример NLog
using NLog;
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
Logger.Info("Пользователь {UserId} выполнил действие", userId);
Когда что выбрать? Если вы строите микросервисы или используете ELK-стек — берите Serilog. Если вам нужна простая настройка в legacy-проектах — NLog. В любом случае, оба поддерживают стандартный ILogger из Microsoft.Extensions.Logging.
2. Уровни логирования: когда и что писать
Одна из самых частых ошибок — писать всё подряд на уровне Information или, наоборот, ставить везде Debug. Используйте правильные уровни:
- Trace (0) — максимально детальная информация: вход/выход из методов, значения переменных. Используется только при отладке.
- Debug (1) — отладочные сообщения, полезные разработчику, но не оператору.
- Information (2) — бизнес-события: "заказ создан", "пользователь зарегистрирован". Это основной рабочий уровень.
- Warning (3) — нештатная ситуация, которая не ломает приложение: таймаут запроса, повторная попытка подключения.
- Error (4) — обработанные исключения, ошибки бизнес-логики, сбои в работе модуля.
- Critical (5) — фатальные ошибки: потеря соединения с БД, нехватка памяти, падение всего приложения.
// Пример правильного использования уровней
public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
{
Log.Debug("Начало создания заказа для пользователя {UserId}", request.UserId);
try
{
var order = new Order(request);
await _repository.SaveAsync(order);
Log.Information("Заказ {OrderId} создан пользователем {UserId}", order.Id, request.UserId);
return order;
}
catch (DbException ex)
{
Log.Error(ex, "Ошибка БД при создании заказа для пользователя {UserId}", request.UserId);
throw; // Пробрасываем выше
}
}
3. Структурированное логирование: почему это важно?
Традиционное логирование через строковую интерполяцию — зло. Посмотрите на два примера:
// Плохо: строка теряет контекст
Log.Information($"User {userId} logged in at {DateTime.Now}");
// Хорошо: параметры сохраняются отдельно
Log.Information("User {UserId} logged in at {Time}", userId, DateTime.UtcNow);
Во втором случае ваша система мониторинга (Seq, Kibana, Grafana) сможет фильтровать и группировать логи по UserId и Time. Вы сможете ответить на вопрос: "Покажите все логи для пользователя 123 за последний час" — одной строкой запроса, без парсинга текста.
Рекомендации:
- Всегда используйте именованные плейсхолдеры (
{UserId}), а не$"...". - Не передавайте конфиденциальные данные (пароли, токены) в логи — маскируйте их через
destructuring. - Для сложных объектов используйте
@(Serilog):Log.Information("Order: {@Order}", order).
4. Настройка и производительность логирования
Плохо настроенное логирование может убить производительность приложения. Вот ключевые правила:
4.1 Асинхронное логирование
По умолчанию многие sinks (например, файловый) работают синхронно. Включите асинхронный режим, чтобы не блокировать поток выполнения:
// Serilog
Log.Logger = new LoggerConfiguration()
.WriteTo.Async(a => a.File("logs/app.log"))
.CreateLogger();
// NLog (в конфигурации)
<target name="asyncFile" xsi:type="Async" queueLimit="5000" overflowAction="Discard">
<target xsi:type="File" fileName="logs/app.log" />
</target>
4.2 Управление уровнем в production
В production никогда не ставьте MinimumLevel.Debug — это создаст гигабайты логов. Оптимальный минимум — Information, а для критичных модулей можно поднять до Warning.
4.3 Ротация логов
Файлы логов должны ротироваться (разделяться по дням, размеру). Иначе через неделю вы получите один файл на 50 ГБ, который невозможно открыть.
// Serilog: ротация по