Освоение Serilog в C#: Эффективное структурное логирование

Практический курс по внедрению и использованию библиотеки Serilog в приложениях .NET для создания качественных и структурированных логов. Вы научитесь настраивать запись в различные источники, обогащать события контекстом и интегрировать логирование в веб-сервисы.

1. Введение в Serilog: установка, концепция структурного логирования и первая конфигурация

Введение в Serilog: установка, концепция структурного логирования и первая конфигурация

Добро пожаловать в курс «Освоение Serilog в C#». Логирование — это одна из тех тем, которую часто откладывают «на потом», но именно она спасает проекты, когда что-то идет не так в продакшене. Без качественных логов разработчик слеп: он не знает, почему упал сервис, какие данные привели к ошибке и как часто это происходит.

В этой первой статье мы разберем фундамент: что такое структурное логирование, почему стандартный текстовый вывод устарел, и как настроить библиотеку Serilog в вашем .NET приложении за несколько минут.

Проблема классического логирования

Прежде чем писать код, давайте поймем, какую проблему мы решаем. Представьте, что вы пишете лог о заказе пользователя. В классическом подходе (например, используя Console.WriteLine или старые библиотеки) вы бы написали что-то вроде:

csharp Log.CloseAndFlush(); csharp Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() // Разрешаем логи уровня Debug и выше .WriteTo.Console() .CreateLogger(); ``

Анатомия события Serilog

Когда вы вызываете метод логирования, Serilog создает событие (LogEvent), которое состоит из:

* Timestamp (Временная метка): Когда это произошло. * Level (Уровень): Насколько это важно. * MessageTemplate (Шаблон): Исходная строка с плейсхолдерами. * Properties (Свойства): Словарь значений, подставленных в шаблон. * Exception (Исключение): Объект ошибки (если есть).

!Схема компонентов, из которых состоит одно событие логирования в Serilog.

Заключение

Мы сделали первый шаг в мир структурного логирования. Теперь вы знаете, что:

* Текстовые логи сложно анализировать машинным способом. * Структурные логи сохраняют данные как объекты (ключ-значение). * Serilog использует «Шаблоны сообщений» (Message Templates) для захвата данных. * Настройка начинается с LoggerConfiguration` и выбора «стоков» (Sinks).

В следующей статье мы углубимся в тему Sinks: научимся писать логи в файлы, настраивать ротацию файлов (чтобы они не занимали все место на диске) и разберем, как писать в несколько мест одновременно.

2. Работа с Sinks: запись логов в файлы, базы данных и системы агрегации

Работа с Sinks: запись логов в файлы, базы данных и системы агрегации

В предыдущей статье мы успешно подключили Serilog и вывели первое сообщение в консоль. Это отличный старт для локальной разработки, но в реальном мире консоль — вещь эфемерная. Как только вы закроете приложение, логи исчезнут навсегда. Если сервер перезагрузится ночью из-за критической ошибки, утром вы увидите чистую консоль и не узнаете причину сбоя.

Сегодня мы превратим нашу систему логирования в надежный инструмент. Мы разберем концепцию Sinks (стоков), научимся сохранять историю событий в файлы, записывать их в базы данных и отправлять в специализированные системы аналитики.

Что такое Sinks?

В архитектуре Serilog Sink (сток, приемник) — это компонент, который знает, как и куда записать сформированное событие лога. Ядро Serilog занимается формированием структуры данных (сообщение, свойства, временная метка), а Sink отвечает за доставку этих данных в конкретное хранилище.

Представьте, что Serilog — это почтовое отделение. Вы приносите посылку (лог). Почтовому отделению не важно, полетит она самолетом, поедет поездом или будет доставлена курьером. За каждый из этих способов доставки отвечает свой Sink.

!Архитектура Serilog: одно ядро распределяет сообщения по разным каналам записи.

Сила Serilog в том, что вы можете подключить сколько угодно стоков одновременно. Одно и то же сообщение может быть показано в консоли разработчика, записано в архивный файл на диске и отправлено в базу данных для аналитиков.

Логирование в файл

Самый распространенный способ хранения логов — это файлы. Это просто, надежно и не требует сложной инфраструктуры.

Базовая настройка

Для работы с файлами нам понадобится пакет Serilog.Sinks.File. Установите его:

Теперь настроим запись:

После запуска приложения в папке проекта появится директория logs с файлом myapp.txt. Внутри вы увидите текстовые записи.

Ротация логов (Rolling Files)

Запись в один файл myapp.txt имеет критический недостаток: файл будет расти бесконечно. Через месяц он может весить гигабайты, и открыть его в текстовом редакторе станет невозможно.

Решение — ротация логов. Мы можем настроить Serilog так, чтобы он создавал новый файл каждый час, день или месяц.

Обратите внимание на синтаксис имени файла: log-.txt. Serilog автоматически подставит дату на место тире перед расширением. В результате вы получите файлы вида: * log-20231027.txt * log-20231028.txt

Ограничение размера и очистка

Даже с ротацией логи могут забить все свободное место на диске сервера. Чтобы этого избежать, используйте политики удержания (retention policy):

Такая конфигурация гарантирует, что ваша система логирования не станет причиной падения сервера из-за нехватки места на диске.

Логирование в базу данных (SQL Server)

Иногда логи нужно хранить в реляционной базе данных, чтобы связывать их с бизнес-данными или строить отчеты средствами SQL.

Установим пакет:

Конфигурация выглядит чуть сложнее, так как требует строки подключения:

Теперь каждый лог — это строка в таблице Logs. Вы можете делать выборки: SELECT * FROM Logs WHERE Level = 'Error' AND TimeStamp > DATEADD(day, -1, GETDATE())

> Важно: Запись в базу данных — операция дорогая и может быть медленной. Если база данных «тормозит», ваше приложение тоже может замедлиться, ожидая завершения записи лога. О том, как этого избежать, мы поговорим в разделе про асинхронность.

Системы агрегации логов (на примере Seq)

Запись в файлы и базы данных — это «старая школа». Современный стандарт — использование специализированных систем агрегации логов. Одной из лучших систем для .NET является Seq. Она создана теми же разработчиками, что и Serilog, и идеально с ним интегрируется.

Seq позволяет:

  • Хранить логи в структурированном виде (JSON).
  • Мгновенно искать по свойствам (например, OrderId == 123).
  • Строить дашборды и графики ошибок.
  • Подключение Seq

    Для начала вам нужно запустить сам сервер Seq (обычно через Docker или установщик Windows). Предположим, он доступен по адресу http://localhost:5341.

    Установим пакет:

    Настройка элементарна:

    Теперь, отправляя лог: Log.Information("Пользователь {User} купил товар {Item}", "Alice", "Laptop");

    В Seq вы увидите не просто текст, а событие, где User и Item — это отдельные поля, по которым можно кликнуть и отфильтровать все события.

    !Интерфейс Seq позволяет фильтровать логи по свойствам и визуализировать частоту событий.

    Проблема производительности и Serilog.Sinks.Async

    Запись на диск или отправка по сети (в БД или Seq) занимает время. По умолчанию Serilog работает синхронно. Это значит, что вызов Log.Information(...) блокирует выполнение вашего кода до тех пор, пока данные не будут записаны.

    Если диск перегружен или сеть отвалилась, ваше приложение может «зависнуть» на попытке записать лог. Это недопустимо для высоконагруженных систем.

    Решение — асинхронная обертка.

    Установим пакет:

    Теперь мы оборачиваем наши медленные Sinks в метод .WriteTo.Async:

    Теперь логирование происходит в фоновом потоке. Ваше приложение просто кладет сообщение в очередь в памяти и мгновенно продолжает работу, а Serilog сам разгребает эту очередь и пишет в файлы/сеть.

    Разделение уровней логирования по стокам

    Часто бывает нужно писать в консоль всё подряд (для отладки), а в файл — только ошибки, чтобы не засорять диск.

    Для этого у каждого метода .WriteTo есть параметр restrictedToMinimumLevel.

    Конфигурация через appsettings.json

    Хардкодить пути к файлам и строки подключения в C# коде — плохая практика. В .NET принято выносить настройки в appsettings.json.

    Установим пакет для чтения конфигурации:

    В файле appsettings.json добавим секцию Serilog:

    А в коде инициализации (Program.cs) считаем эти настройки:

    Теперь, чтобы изменить путь к логам или уровень детализации, вам не нужно перекомпилировать приложение — достаточно поправить JSON файл.

    Заключение

    Мы научились направлять потоки данных Serilog в разные русла. Теперь ваши логи надежно сохраняются в файлах с ротацией, структурированно лежат в базах данных или анализируются в Seq. Более того, благодаря асинхронности, логирование не замедляет работу основного приложения.

    В следующей статье мы рассмотрим Enrichers (Обогатители) и LogContext. Мы узнаем, как автоматически добавлять к каждому логу имя пользователя, ID транзакции или версию сборки, не переписывая каждый вызов логгера вручную.

    3. Сила структурных данных: шаблоны сообщений, свойства и использование Enrichers

    Сила структурных данных: шаблоны сообщений, свойства и использование Enrichers

    Мы уже научились устанавливать Serilog и направлять логи в различные хранилища (Sinks), такие как файлы, базы данных и системы агрегации (Seq). Однако, если вы продолжите писать логи в стиле «старой школы», просто склеивая строки, вы используете мощный инструмент лишь на 10%.

    В этой статье мы раскроем истинную силу структурного логирования. Мы разберем, почему интерполяция строк — это враг логгера, как правильно использовать шаблоны сообщений, как сохранять сложные объекты и как автоматически обогащать (Enrich) каждый лог контекстной информацией без дублирования кода.

    Шаблоны сообщений: сердце Serilog

    В классическом .NET программировании мы привыкли использовать интерполяцию строк (знак "Пользователь {userId} совершил платеж на сумму {amount}"); csharp // Правильный подход Log.Information("Пользователь {UserId} совершил платеж на сумму {Amount}", userId, amount); csharp Log.Information("Запрос обработан за {Elapsed} мс", 45); // Свойство Elapsed = 45 (число) csharp var order = new Order { Id = 123, Customer = "Alice", Items = new[] { "Laptop", "Mouse" } }; csharp Log.Information("Обработка заказа {@Order}", order); json { "MessageTemplate": "Обработка заказа {@Order}", "Properties": { "Order": { "Id": 123, "Customer": "Alice", "Items": ["Laptop", "Mouse"] } } }

    Enrichers: Автоматическое обогащение контекстом

    Часто нам нужно добавлять одни и те же данные к каждому логу: имя машины, ID потока, версию приложения или имя текущего пользователя. Писать это вручную в каждом вызове Log.Information — нарушение принципа DRY (Don't Repeat Yourself).

    Для этого в Serilog существуют Enrichers (Обогатители). Это компоненты, которые вклиниваются в пайплайн логирования и добавляют свойства к событию автоматически.

    Подключение стандартных обогатителей

    Для начала установим пакет с полезными обогатителями:

    Теперь настроим логгер:

    Теперь любой вызов лога:

    Фактически создаст событие с дополнительными свойствами: * ThreadId: 1 * MachineName: "SERVER-01" * EnvironmentUserName: "SYSTEM"

    Это критически важно для отладки многопоточных приложений, когда нужно понять, выполнялись ли операции параллельно или последовательно.

    Глобальные свойства

    Вы можете добавить любое статическое свойство ко всем логам:

    Теперь все логи этого экземпляра будут помечены тегом Application: PaymentService. Это очень удобно, если в одно хранилище (например, Elasticsearch) пишут логи десятки разных микросервисов.

    LogContext: Динамический контекст

    Что делать, если данные меняются динамически? Например, в веб-приложении нам нужно добавить RequestId или TransactionId ко всем логам, которые произойдут в рамках обработки одного конкретного HTTP-запроса.

    Для этого используется LogContext. Это механизм, позволяющий добавлять свойства в стек выполнения.

    Шаг 1. Включение поддержки

    Сначала нужно обязательно разрешить использование контекста в конфигурации:

    Шаг 2. Использование PushProperty

    Метод LogContext.PushProperty возвращает IDisposable объект. Пока этот объект не уничтожен, свойство будет добавляться ко всем логам внутри этого блока кода.

    !Схематичное изображение области видимости LogContext, показывающее временное добавление свойств.

    Вы можете вкладывать контексты друг в друга:

    Это невероятно мощный инструмент для трассировки. В ASP.NET Core middleware часто используют этот подход, чтобы автоматически добавлять RequestId ко всем логам запроса, даже если вы сами об этом не просили.

    Соглашения об именовании

    Чтобы ваши структурные логи были чистыми и удобными для поиска, следуйте простым правилам:

  • PascalCase для свойств: Используйте UserId, OrderId, ElapsedMs. Избегайте userid или user_id. Это стандарт в экосистеме .NET и Serilog.
  • Уникальность имен: Старайтесь, чтобы свойство Id всегда означало одно и то же. Если в одном месте Id — это число, а в другом — строка (GUID), база данных логов может отказать в записи из-за конфликта типов.
  • Не используйте точки в именах: Свойство User.Id может быть интерпретировано некоторыми системами как объект User с полем Id, а другими — как просто строка с точкой. Лучше используйте UserId`.
  • Заключение

    Сегодня мы перешли от простого вывода текста к настоящему структурному логированию. Мы узнали:

    * Шаблоны сообщений позволяют сохранять данные отдельно от текста, делая логи доступными для машинного анализа. * Оператор @ позволяет сохранять целые объекты, а $ — принудительно превращать их в строку. * Enrichers (Обогатители) автоматически добавляют метаданные (имя машины, поток) ко всем событиям. * LogContext позволяет временно добавлять свойства для определенного участка кода (scope).

    В следующей статье мы рассмотрим интеграцию Serilog с ASP.NET Core. Мы узнаем, как заменить встроенный логгер Microsoft, как настроить логирование HTTP-запросов (Request Logging) и как избавиться от лишнего шума в логах веб-приложения.

    4. Интеграция Serilog с ASP.NET Core: Request Logging и чтение настроек из appsettings.json

    Интеграция Serilog с ASP.NET Core: Request Logging и чтение настроек из appsettings.json

    Добро пожаловать обратно! В предыдущих статьях мы освоили фундамент Serilog: научились создавать структурные логи, писать их в файлы и базы данных, а также использовать обогатители (Enrichers) для добавления контекста. Теперь пришло время применить эти знания в самой популярной среде разработки на C# — ASP.NET Core.

    Веб-приложения — это особый мир. Здесь каждую секунду происходят сотни событий: приходят HTTP-запросы, выполняются контроллеры, происходят обращения к базе данных. Если использовать стандартный подход к логированию, ваши логи превратятся в бесконечный поток шума, в котором невозможно найти причину ошибки.

    В этой статье мы научимся:

  • Правильно интегрировать Serilog в Program.cs (используя двухэтапную инициализацию).
  • Настраивать логирование полностью через appsettings.json.
  • Использовать Request Logging — киллер-фичу Serilog для веб-приложений.
  • Очищать логи от системного шума Microsoft.
  • Проблема стандартного логирования в ASP.NET Core

    Когда вы создаете новый проект ASP.NET Core (dotnet new webapi), он уже имеет встроенную систему логирования. Она неплохая, но имеет два существенных недостатка при работе под нагрузкой:

  • Избыточность (Шум): На один HTTP-запрос фреймворк генерирует 5-10 сообщений лога (начало запроса, роутинг, выполнение endpoint, завершение запроса). Если у вас 100 запросов в секунду, это 1000 записей в логе.
  • Разрозненность: Информация о времени выполнения, статус-коде и пути запроса размазана по разным сообщениям.
  • Serilog предлагает элегантное решение: одно событие на один запрос, содержащее всю необходимую информацию.

    !Визуальное сравнение стандартного многословного логирования и сводного логирования запросов в Serilog.

    Шаг 1. Установка пакетов

    Для работы с ASP.NET Core нам понадобится специальный пакет-адаптер. Откройте терминал в папке вашего проекта и выполните:

    Этот пакет включает в себя всё необходимое: ядро Serilog, поддержку конфигурации из JSON и Sinks для консоли и файлов.

    Шаг 2. Двухэтапная инициализация (Two-Stage Initialization)

    Многие разработчики совершают ошибку, настраивая Serilog только внутри построителя приложения. Проблема в том, что ошибки могут возникнуть до того, как приложение полностью загрузится (например, ошибка в appsettings.json или проблема с Dependency Injection).

    Чтобы поймать ошибки старта приложения, мы используем паттерн двухэтапной инициализации.

  • Создаем простой «стартовый» логгер сразу при запуске.
  • Запускаем приложение в блоке try-catch.
  • Внутри построителя переключаемся на полноценную конфигурацию.
  • Вот как должен выглядеть ваш Program.cs:

    Обратите внимание на метод builder.Host.UseSerilog(...). Он говорит приложению: «Забудь про встроенный логгер, используй Serilog и возьми настройки из конфигурации».

    Шаг 3. Настройка через appsettings.json

    Теперь нам не нужно писать C# код для добавления Sinks или смены уровней логирования. Всё управляется через файл appsettings.json.

    Вот пример идеальной конфигурации для веб-приложения:

    Разбор секции Override

    Это самая важная часть для чистоты логов:

    По умолчанию ASP.NET Core пишет очень много логов уровня Information (например, "Executing endpoint...", "Route matched..."). Поскольку мы будем использовать UseSerilogRequestLogging, эти системные логи нам больше не нужны — они только дублируют информацию. Устанавливая уровень Warning для пространства имен Microsoft, мы говорим: «Показывай мне системные сообщения, только если что-то пошло не так».

    Шаг 4. Магия UseSerilogRequestLogging

    В коде Program.cs мы добавили строку:

    Это middleware (промежуточное ПО), которое замеряет время выполнения запроса и пишет одно итоговое сообщение при завершении обработки.

    По умолчанию сообщение выглядит так: HTTP GET /api/users/5 responded 200 in 15.4200 ms

    Но внутри это мощная структура данных:

    Где разместить этот middleware?

    Порядок имеет значение. UseSerilogRequestLogging логирует только то, что происходит после него в конвейере (pipeline).

    * Рекомендуемое место: Сразу после UseStaticFiles (если вы не хотите логировать каждый запрос к картинкам и CSS) и перед UseRouting или UseAuthorization.

    Шаг 5. Обогащение логов запросов (EnrichDiagnosticContext)

    Стандартный лог запроса хорош, но иногда нам нужно больше. Например, мы хотим знать, кто сделал запрос (ID пользователя) или какие query-параметры были переданы.

    Мы не можем просто добавить Log.Information в контроллере, так как это создаст дополнительное сообщение, а мы хотим расширить итоговое сообщение Serilog.

    Для этого используется опция EnrichDiagnosticContext.

    Изменим вызов в Program.cs:

    Теперь каждый лог запроса будет содержать поля UserName, QueryString и UserAgent. Вы сможете легко найти все запросы конкретного пользователя или увидеть, с каких браузеров к вам заходят чаще всего.

    Заключение

    Интеграция Serilog в ASP.NET Core — это не просто замена одного логгера на другой. Это смена парадигмы от «текстового шума» к «структурированной аналитике».

    Мы сделали:

  • Настроили двухэтапную инициализацию, чтобы ловить ошибки старта.
  • Вынесли конфигурацию в appsettings.json.
  • Включили Request Logging, получив чистые и информативные сводки по каждому запросу.
  • Научились добавлять кастомные данные в эти сводки через EnrichDiagnosticContext.
  • Теперь ваше приложение готово к продакшену. В следующей статье мы разберем продвинутые темы: как логировать исключения так, чтобы не терять StackTrace, и как использовать Destructuring для безопасного логирования сложных объектов без утечки чувствительных данных.

    5. Продвинутые сценарии: фильтрация событий, динамическое изменение уровней и лучшие практики

    Продвинутые сценарии: фильтрация событий, динамическое изменение уровней и лучшие практики

    Поздравляю! Вы прошли долгий путь от Console.WriteLine до настройки полноценного структурного логирования в ASP.NET Core. Вы уже умеете писать логи в файлы и базы данных, обогащать их контекстом и анализировать HTTP-запросы.

    Однако в реальных высоконагруженных проектах просто «включить логирование» недостаточно. Если вы будете писать всё подряд, вы столкнетесь с двумя проблемами:

  • Информационный шум: В гигабайтах логов невозможно найти нужную ошибку.
  • Стоимость: Хранение логов (особенно в облаках типа AWS CloudWatch или Azure Monitor) стоит денег. Логирование каждого «чиха» может разорить проект.
  • В этой финальной статье курса мы превратимся из пользователей Serilog в его архитекторов. Мы научимся фильтровать лишнее, менять уровень логирования «на лету» без перезагрузки сервера и защищать чувствительные данные.

    Фильтрация событий: отделяем зерна от плевел

    Мы уже касались темы MinimumLevel и Override в предыдущих статьях. Это грубая настройка: мы либо включаем всё, либо выключаем всё для определенного пространства имен. Но что, если нам нужна хирургическая точность?

    Сценарий: Игнорирование Health Checks

    Представьте, что у вас есть Kubernetes, который каждые 10 секунд дергает эндпоинт /health, чтобы проверить, жив ли сервис. За сутки это 8640 записей в логе, которые не несут никакой полезной информации, кроме «мы живы».

    Чтобы исключить конкретные события, мы можем использовать метод .Filter.

    Метод Filter.ByExcluding принимает предикат (условие). Если условие истинно, лог не будет записан ни в один Sink.

    !Визуализация процесса фильтрации событий в конвейере Serilog.

    Сложная логика фильтрации

    Вы можете писать любую логику на C#. Например, не логировать ошибки TaskCanceledException, если они происходят во время завершения работы приложения:

    Также существует метод .Filter.ByIncludingOnly(...), который работает наоборот: пропускает только те логи, которые соответствуют условию. Это полезно, если вы настраиваете отдельный файл, куда должны попадать только события, связанные с платежами.

    Динамическое изменение уровня логирования

    Это одна из самых мощных возможностей Serilog, о которой знают немногие.

    Проблема: В продакшене у вас стоит уровень Information или Warning. Вдруг пользователи начинают жаловаться на ошибку. Чтобы понять причину, вам нужен уровень Debug. Но чтобы его включить, нужно поменять appsettings.json и перезагрузить приложение. Перезагрузка сбрасывает текущие подключения и может скрыть плавающую ошибку.

    Решение: Использовать LoggingLevelSwitch.

    Как это работает

    LoggingLevelSwitch — это класс-обертка над уровнем логирования, который можно менять в рантайме.

    Практическое применение в API

    Вы можете создать секретный административный эндпоинт в вашем API, который меняет этот переключатель:

    ``csharp [ApiController] [Route("api/admin/logs")] public class LogController : ControllerBase { private readonly LoggingLevelSwitch _levelSwitch;

    // Внедряем тот же инстанс switch, который использовали при старте public LogController(LoggingLevelSwitch levelSwitch) { _levelSwitch = levelSwitch; }

    [HttpPost("level")] public IActionResult SetLevel([FromQuery] string level) { if (Enum.TryParse<LogEventLevel>(level, true, out var newLevel)) { _levelSwitch.MinimumLevel = newLevel; return Ok("User {id}")~~ — Плохо. * Log.Info("User {UserId}", id) — Хорошо.

  • Логируйте исключения правильно:
  • * Передавайте объект Exception первым аргументом. * Log.Error(ex, "Ошибка при оплате") — сохранит StackTrace и тип ошибки.
  • Используйте асинхронность:
  • * Всегда оборачивайте медленные Sinks (File, Database, Network) в WriteTo.Async(...), чтобы не блокировать потоки приложения.
  • Соблюдайте чистоту типов:
  • * Не пишите в свойство OrderId то число, то строку. Elasticsearch или SQL перестанут индексировать это поле.
  • Добавляйте CorrelationId:
  • * Используйте LogContext или middleware, чтобы добавить уникальный ID запроса ко всем логам. Это позволит собрать всю цепочку событий по одному клику.

    Заключение курса

    Мы прошли путь от основ до продвинутых техник. Теперь вы владеете инструментом, который является стандартом де-факто в мире .NET.

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

    Спасибо за прохождение курса «Освоение Serilog в C#». Пишите чистый код и информативные логи!