Работа с 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 транзакции или версию сборки, не переписывая каждый вызов логгера вручную.