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

Реализация собственных библиотек не занимает большого количества времени. Но данные решения будут малофункциональными и могут быть недотестированными. Поэтому разработчики используют готовые решения по управлению и записи логов во время работы программы.

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

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

В данной статье представлен обзор трех наиболее популярных библиотек логирования: Log4Net, SeriLog, NLog.

Log4Net

Открытый исходный код: https://github.com/apache/logging-log4net

Лицензия: Apache-2.0 license

Официальный сайт: https://logging.apache.org/

Документация: https://logging.apache.org/log4net/

В данной библиотеке можно реализовывать поименные логгеры, т.е. можно получить собственный экземпляр логгера по типу класса или строковому ключу, использую статический класс LogManager.

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

Пример:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <configSections>
   <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
 </configSections>
 <log4net>
   <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
     <file value="log.txt" />
     <appendToFile value="true" />
     <rollingStyle value="Size" />
     <maxSizeRollBackups value="10" />
     <maximumFileSize value="250KB" />
     <staticLogFileName value="true" />
     <layout type="log4net.Layout.PatternLayout">
       <conversionPattern value="%date [%thread] %-5level %logger [%property{NDC}] - %message%newline" />
     </layout>
   </appender>
   <root>
     <level value="ALL" />
     <appender-ref ref="RollingFileAppender" />
   </root>
 </log4net>
</configuration>

Паттерны отображения сообщения логов можно удобно собирать, используя следующую таблицу.

Также в файл конфигурации можно добавлять различные фильтры. Для этого нужно в конфиг лога добавить подобный блок:

Пример:

<filter type="log4net.Filter.LevelRangeFilter">
   <levelMin value="INFO" />
   <levelMax value="FATAL" />
</filter>

Другие фильтры представлены по ссылке.

Также в логгер можно помещать дополнительные модули (плагины), которые добавляют дополнительную возможность, например, удаленную запись логов на сервер. Подробнее о плагинах можно прочитать по ссылке.

Данная библиотека (как и все далее перечисленные) может устанавливать уровни сообщений от Debug до Fatal.

SeriLog

Открытый исходный код: https://github.com/serilog/serilog

Лицензия: Apache-2.0 license

Официальный сайт: https://logging.apache.org/

Документация: https://github.com/serilog/serilog/wiki

Данная библиотека изначально не имеет настройки путем изменения файлов конфигурации. Это влечет следующее неудобство – нельзя оперативно поменять конфигурацию для получения более подробных сведений у программы.

Пример создания логгера:

var log = new LoggerConfiguration()
   .MinimumLevel.Debug()
   .WriteTo.File("log.txt")
   .WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Information)
   .CreateLogger();

Для добавления возможности управления настройками логгера путем изменения конфигурации можно при установке пакета Serilog.Settings.AppSettings (подробный гайд по ссылке).

Также у данной библиотеки есть статическое свойство Logger класса Log, в которое помещается объект логгера.

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

Пример ввода сообщения:

var input = new { Latitude = 25, Longitude = 134 };
var time = 34;
log.Information("Processed {@SensorInput} in {TimeMS:000} ms.", input, time);

Оператор @ перед SensorInput инструктирует Serilog сохранять структуру переданного объекта. Если данный оператор опущен, Serilog распознает простые типы, такие как строки, числа, даты и время, словари и перечисления; все остальные объекты преобразуются в строки с помощью ToString(). «Структурирование» (вывод типа объекта) может быть принудительно выполнено с помощью оператора $ вместо @:

var unknown = new[] { 1, 2, 3 }
Log.Information("Received {$Data}", unknown);

В результате выведет:

Received "System.Int32[]"

Сегмент :000, следующий за TimeMS, представляет собой строку стандартного формата .NET, которая влияет на то, как свойство отображается (а не на то, как оно захватывается). Стандартный приемник консоли, включенный в Serilog, отобразит приведенное выше сообщение как:

09:14:22 [Information] Processed { Latitude: 25, Longitude: 134 } in 034 ms.

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

Log.Logger = new LoggerConfiguration()
   .WriteTo.Console()
   .WriteTo.File("log-.txt", rollingInterval: RollingInterval.Day)
   .CreateLogger();

Или устанавливать формат вывода:

 .WriteTo.File("log.txt",        outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")

Также есть возможность устанавливать как общий минимальный уровень вывода логов, так и для каждого отдельного типа вывода сообщений:

Log.Logger = new LoggerConfiguration()
   .MinimumLevel.Debug()
   .WriteTo.File("log.txt")
   .WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Information)
   .CreateLogger();

В библиотеке Serilog можно добавлять фильтры для записей логов.

Пример:

Log.Logger = new LoggerConfiguration()
   .WriteTo.Console()
   .Filter.ByExcluding(Matching.WithProperty<int>("Count", p => p < 10))
   .CreateLogger();

Для каждой записи будет проверяться свойство Count, если оно удовлетворяет условию, то произведётся запись.

Также Serilog поддерживает подлоги, т.е. в основной объект класса лога можно поместить другой способ вывода лога, в котором можно определить свои правила, фильтры и обвертки (sink).

Пример:

Log.Logger = new LoggerConfiguration()
   .WriteTo.Console()
   .WriteTo.Logger(lc => lc
       .Filter.ByIncludingOnly(...)
       .WriteTo.File("log.txt"))
   .CreateLogger();

Serilog позволяет динамически изменять уровень логирования, путем использования класса LoggingLevelSwitch:

var levelSwitch = new LoggingLevelSwitch();
levelSwitch.MinimumLevel = LogEventLevel.Warning;
var log = new LoggerConfiguration()
 .MinimumLevel.ControlledBy(levelSwitch)
 .WriteTo.ColoredConsole()
 .CreateLogger();

В дальнейшем можно будет изменять объект levelSwitch для вывода нужного уровня.

NLog

Открытый исходный код: https://github.com/NLog/NLog

Лицензия: BSD-3-Clause license

Официальный сайт: https://nlog-project.org/

Документация: https://github.com/nlog/nlog/wiki

Данная библиотека позволяет создавать объекты логов двумя способами: через код и через файл конфиг

Пример:

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 <targets>
   <target name="logfile" xsi:type="File" fileName="file.txt" />
   <target name="logconsole" xsi:type="Console" />
 </targets>
 <rules>
   <logger name="*" minlevel="Info" writeTo="logconsole" />
   <logger name="*" minlevel="Debug" writeTo="logfile" />
 </rules>
</nlog>

Подробнее о файлах конфигурации логов по ссылке.

Пример создание объекта через код:

var config = new NLog.Config.LoggingConfiguration();
// Куда выводим: Файл и Консоль
var logfile = new NLog.Targets.FileTarget("logfile") { FileName = "file.txt" };
var logconsole = new NLog.Targets.ConsoleTarget("logconsole");
// Правила сопоставления регистраторов          
config.AddRule(LogLevel.Info, LogLevel.Fatal, logconsole);
config.AddRule(LogLevel.Debug, LogLevel.Fatal, logfile);
// Установка конфигурации         
NLog.LogManager.Configuration = config;

Подробнее о конфигурации в коде по ссылке

Формат вывода сообщений можно устанавливать путем добавления в target элемент layout:

<target xsi:type="File" name="jsonFile" fileName="c:\temp\nlog-json-${shortdate}.log">
     <layout>${longdate}|${level}|${logger}|${message}|${all-event-properties}{exception:format=tostring}</layout>
</target>

Данная библиотека поддерживает следующие структурные форматы:

  • CsvLayout

  • JsonLayout

  • XmlLayout

Формат в файле конфигурации будет выглядеть следующим образом:

<target xsi:type="File" name="jsonFile" fileName="c:\temp\nlog-json-${shortdate}.log">
     <layout xsi:type="JsonLayout" includeAllProperties="true">
       <attribute name="time" layout="${longdate}" />
       <attribute name="level" layout="${level:upperCase=true}"/>
       <attribute name="message" layout="${message}" />
     </layout>
</target>

NLog содержит в себе такие операторы для формирования строк, как и Serilog.

Библиотека содержит асинхронную обертку, что позволяет логировать несколько потоков в одном объекте логгера.

NLog позволяет создавать переменные внутри файла конфигурации, которые могут использоваться в последующих частях конфигурации:

<nlog>
 <variable name="logDirectory" value="logs/${shortdate}"/>
 <targets>
   <target name="file1" xsi:type="File" fileName="${logDirectory}/file1.txt"/>
   <target name="file2" xsi:type="File" fileName="${logDirectory}/file2.txt"/>
 </targets>
</nlog>

Заключение

Основные возможности ранее перечисленных библиотек малоотличимы. Чтобы выбрать решение, нужно углубиться в возможности данных библиотек. Автором было дано предпочтение библиотеке NLog. Данная библиотека уже содержит асинхронную обертку, что позволяет логировать многопоточные приложения. Следующим преимуществом стала изначальная возможность измения настройки логгера через файл конфигурации. Последним приемуществом, почему автор отдает предпочтение библиотеки NLog – возможность создавать переменные в файлах конфигурации логгера.

Комментарии (35)


  1. lair
    27.07.2022 20:27
    +4

    Почему нет Microsoft.Extensions.Logging, который сейчас стандарт де-факто для Core?
    Вы пишете про Serilog, но при этом ни словом не упоминаете про структурное логирование?


    Автором было дано предпочтение библиотеке NLog. Данная библиотека уже содержит асинхронную обертку, что позволяет логировать многопоточные приложения.

    А зачем для логирования многопоточных приложений какая-то "асинхронная обертка"?


    1. AgentFire
      27.07.2022 21:24

      Как вариант - писать логи в сеть, а сеть медленная.


      1. lair
        27.07.2022 21:28

        Как вариант — писать логи в сеть, а сеть медленная.

        При чем тут многопоточное приложение?


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


        Ну а если кому-то вдруг надо сделать синхронный приемник асинхронным насильно, то у серилога такая обертка тоже есть: https://github.com/serilog/serilog-sinks-async


        1. AgentFire
          28.07.2022 01:20

          Нет, не решается. Частично да, но не полностью.

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


          1. lair
            28.07.2022 01:22

            А чем отличается упомянутая автором "асинхронная обертка"?


            1. AgentFire
              28.07.2022 01:34
              -1

              Очень надеюсь, что своей синхронностью, если вы понимаете о чем я.


              1. lair
                28.07.2022 01:38

                Нет, не понимаю.


                Речь вообще о какой обертке идет? Вот об этой?


                1. AgentFire
                  29.07.2022 16:29

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


                  1. lair
                    29.07.2022 16:31

                    Так что же, по-вашему, делает упомянутая автором "асинхронная обертка"?


                    И что же надо делать?


                    1. AgentFire
                      29.07.2022 17:03
                      -1

                      Решает/решать вышеуказанные проблемы.


                      1. lair
                        29.07.2022 18:04

                        Ну, обертка по ссылке, которую я привел, делает именно что буфер в фоновом потоке. Вы знаете про какую-то другую "асинхронную обертку" для NLog?


                        Решает/решать вышеуказанные проблемы.

                        Как конкретно?


        1. funca
          28.07.2022 11:01
          +1

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

          Есть классический прием переложить проблему обратно на генератор: "диктуете помедленнее, я.. ик.. записываю" :)


          1. lair
            28.07.2022 14:49
            +1

            Для логирования это не всегда подходит.


            1. funca
              28.07.2022 19:28

              Ну the 12 factor app уже давно сказали, глядя все эти наши попытки родить серебряную пулю: вы все равно не сделаете как надо, поэтому универсальный совет - пишите все в stdout.

              Но да и это тоже не всегда работает - иженерные решения в любом случае чем-то ограничены.


              1. lair
                28.07.2022 20:16
                +1

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


                1. funca
                  28.07.2022 20:22

                  Stdout это не всегда консоль. Даже чаще это не консоль. Но не суть.


                  1. lair
                    28.07.2022 20:23
                    +1

                    И именно поэтому он еще более неудобен, чем консоль. Так что не вижу смысла (повторюсь, в моих применениях).


                    1. funca
                      28.07.2022 20:28

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


                      1. lair
                        28.07.2022 20:53
                        +1

                        Мне кажется, вы меня с кем-то путаете. Все намного тривиальнее: хосты, где либо stdout нет (типа IIS), либо stdout обрабатывается хуже, чем нативный логгер (типа AWS Lambda).


                      1. funca
                        28.07.2022 21:33

                        Простите, это моя ошибка.

                        Если вы имели ввиду логер из Amazon.Lambda.Core, то он как раз использует Console.WriteLine(), иными словами все тот же stdout.


                      1. lair
                        28.07.2022 23:32

                        А вы там видите, да, что этот делегат заменяем? Комментарий не зря написан:


                        When used outside of a Lambda environment, logs are written to Console.Out.

                        При этом скажем, их же имплементация для Microsoft.Extensions.Logging то ли использовала, то ли до сих пор использует именно Console.Out, и из-за этого вывод, если он многострочный, в CloudWatch попадает в разбивке. А через нативный логгер, который передается в контексте — нормально всё.


    1. Vanada Автор
      28.07.2022 07:03
      +1

      Нам нужно было найти подходящую библиотеку для разных версий framework, начиная с 3.5 и выше. Как я понял, Microsoft.Extensions.Logging поддерживает .Net framework начиная с версии 4.5.

      С Serilog вышел косяк насчет структурного логирования. Не добавил блок с ним.

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


      1. lair
        28.07.2022 14:48
        +1

        Нам нужно было найти подходящую библиотеку для разных версий framework, начиная с 3.5 и выше.

        Ну так с этого и надо начинать: "описание библиотек логирования, совместимых с .net 3.5". Далеко не всем в 2022 это интересно.


        И да, какая версия Serilog поддерживает 3.5?


        Асинхронная обертка была нужна для правильной записи результатов измерений принятых с устройств, получаемые в разных потоках одновременно, в один файл.

        А без нее что происходило?


        1. funca
          28.07.2022 19:48
          -1

          Я знаю, что тут бывает две проблемы. Условно М и Ж (евпочя). Могу предложить, что потоки либо пытались писать в файл одновременно всей бандой, и там все так и получалось - вперемешку, что не поддается ни какому анализу. Либо делали общий лок на запись, - чтобы одномоментно мог писать только один, а остальные ждали - и потом в равной степени все об него запинались. "Асинхронная обёртка", прекратила эту демократию, втихоря назначив файлу отдельного хозяина, а генераторов заставила приносить к нему свои логи дискретно, в баночках.


          1. lair
            28.07.2022 20:20
            +1

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

            Это кривая имплементация файлового приемника, если он такое позволяет. Серилоговский, скажем, просто не позволяет, там во сколько потоков не пиши в один приемник, записи будут целостными.


            Либо делали общий лок на запись, — чтобы одномоментно мог писать только один, а остальные ждали — и потом в равной степени все об него запинались.

            А это снова вырождается в проблему "мы производим логи быстрее, чем пишем" (которая прекрасно существует в однопоточных приложениях тоже), и для ее решения у серилога-таки есть два разных враппера.


            Я это к тому, что выглядит так, что автор поста выбрал NLog вместо Serilog не потому, что второй что-то не умеет, а потому, что у второго философия "батарейки отдельно" (и эту философию автор Serilog, кстати, явно озвучивает). Нужно вам чтение из конфига? Вот пакет. Нужен вам асинхронный враппер? Вот пакет. Не нужно ни то, ни другое? Вы не тащите эти зависимости с приложениям.


            1. funca
              28.07.2022 20:25
              -1

              Серилоговский

              У меня была другая метафора. Но так даже ещё смешнее :)


              1. lair
                28.07.2022 20:26
                +1

                Не очень понимаю, при чем тут какая-то метафора. Serilog — это название пакета.


                1. funca
                  28.07.2022 20:30

                  "there's two hard problems in computer science: we only have one joke and it's not funny."


    1. petuhov_k
      28.07.2022 14:01
      +1

      И нет старого доброго System.Diagnostics


  1. funca
    27.07.2022 21:54
    +3

    Вообще тема интересная. Тут все становится гораздо проще, когда у вас 100500 сервисов, и каждый изначально использует какой-нибудь свой, только одному ему понятный, формат логов (у логгеров же столько прикольных настроек - хрех не воспользоваться).

    А вы такой хотите видеть в этом хаосе ну хоть какое-нибудь однообразие. Плюс метаданные про сам сервис и ещё что-нибудь про межсервисные транзакции (correlation) покажите. И да, хочется чтобы вся эта толпа не устраивала вам локальный DDOS, работая под большой нагрузкой. И чтобы в логи не попадали PII.

    В таком случае, мне кажется, муки выбора библиотеки сводятся к выбору вендора, с помощью которого вы будете строить observability, а все остальное он уже выбрал за вас. Мы работаем с NewRelic. Но интересно какие есть ещё аналоги?


    1. lair
      28.07.2022 00:40
      +2

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

      Вообще, есть (или потихоньку выползает, по крайней мере) OpenTelemetry.


      А еще у логгеров есть свои синки для ваших вендоров (вот, например, Serilog -> NewRelic: https://github.com/thiagobarradas/serilog-sinks-newrelic-logs).


      1. funca
        28.07.2022 19:33

        Да вот об этом я и говорю. В итоге проект обрастает чем-то вроде вот такого и всем становится без разницы чем один логгер лучше другого когда на выходе результат одинаков.


        1. lair
          28.07.2022 20:21
          +1

          Ну, вам без разницы, а я до сих пор эту разницу вижу, так что каждому своё.


  1. MagMagals
    28.07.2022 07:03
    +3

    пересел с nlog на стандартное решение Microsoft.Extensions.Logging и забыл\перестал понимать зачем другие библиотеки для этого существуют


  1. urvanov
    28.07.2022 15:18
    +4

    Такой же бардак с этими библиотеками логирования, как и в Java, если честно.