[Перевод] NLog: правила и фильтры

В Confirmit мы используем библиотеку NLog для логирования в наших .NET-приложениях. Хотя для этот библиотеки существует документация, для меня было сложно понять, как все это работает. В данной статье я попытаюсь объяснить, как правила и фильтры применяются в NLog. Давайте начнем.


Как конфигурировать NLog

И начнем мы с небольшого напоминания, что мы можем делать с помощью конфигурации NLog. В простейшем случае, эта конфигурация представляет собой XML-файл (например, NLog.config):




  
    
      
    
    
      
    
    
      
    
  

  
    
  

Вы можете загрузить этот файл одной строкой кода:

LogManager.Configuration = new XmlLoggingConfiguration("NLog.config");

Что мы можем сделать с его помощью? Мы можем установить несколько приемников сообщений (target) на правило:


  

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


  
  

Мы можем задать фильтры для каждого правила:


  
    
      
    
  

И, наконец, мы можем определять вложенные правила:


  
    
  

Пришло время узнать, как все это работает.


Создание конфигурации логгера

Когда вы запрашиваете экземпляр логгера,

var commonLogger = LogManager.GetLogger("Common");

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

Кратко говоря, конфигурация логгера представляет собой отдельные цепочки приемников и соответствующих фильтров для каждого уровня логирования (Trace, Debug, Info, Warn, Error, Fatal) (см. здесь). Сейчас я покажу вам, как эти цепочки строятся.

Главный метод, ответственный за создание данных цепочек — GetTargetsByLevelForLogger класса LogFactory. Вот как он работает. Все правила, заданные в конфигурации NLog перебираются по очереди. Прежде всего проверяется, соответствует ли имя правила имени логгера. Имена правил могут содержать подстановочные символы, такие же как те, что мы используем для объектов файловой системы:


  • * — произвольная последовательность символов
  • ? — любой единичный символ

Таким образом имя правила '*' соответствует любому имени логгера, а 'Common*' соответствует всем логгерам, чьи имена начинаются с 'Common'.

Если имя правила не соответствует имени логгера, то данной правило отбрасывается со всеми вложенными в него правилами. В противном случае метод GetTargetsByLevelForLogger получает все уровни логирования, для которых это правило включено. Для каждого такого уровня NLog добавляет все приемники сообщений, указанные в правиле, в соответствующие цепочки приемников вместе с фильтрами этого правила.

Есть еще одна важная особенность конструирования цепочек приемников. Если текущее правило помечено как final и его имя соответствует имени логгера, то NLog завершает на нем построение цепочек для всех уровней логирования, включенных для данного правила. Это означает, что ни вложенные правила, ни последующие правила ничего не добавят в эти цепочки приемников. Их создание полностью завершено и они уже не будут меняться. Отсюда следует, что не имеет смысла писать что-то подобное:


  
    
  

Никакие сообщения не попадут в target2. Но возможно написать что-то такое:


  
    
  

Поскольку внешнее правило не включено для уровня Info, цепочка приемников для этого уровня не закончится на внешнем правиле. Поэтому все сообщения с уровнем Info попадут в target2.

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

В итого конфигурация для логгера готова. Она содержит цепочки приемников с фильтрами для каждого возможного уровня логирования:

Цепочка приемников

Пришло время посмотреть, как эта конфигурация используется.


Использование конфигурации логгера

Начнем с простых вещей. Класс Logger имеет метод IsEnabled и связанные свойства IsXXXEnabled (IsDebugEnabled, IsInfoEnabled, …). Как они работают? На самом деле они просто проверяют, содержат ли цепочки приемников для заданного уровня логирования хотя бы одно звено (см. здесь). Это означает, что фильтры никогда не влияют на значения этих свойств.

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

Вот как фильтры задаются в конфигурации:


  
    
      
    
  

Обычно фильтр содержит некоторое булевское условие. Здесь вы можете решить, что фильтр возвращает true или false для каждого сообщения. Но это не так. Результатом их работы является значение типа FilterResult. Если условие фильтра возвращает true, то результатом работы фильтра становится значение, указанное в атрибуте action (в нашем примере это Ignore). Если же условие возвращает false, то результатом работы фильтра будет Neutral. Это означает, что фильтр не хочет решать, что делать с сообщением.

Вы можете посмотреть, как обрабатывается цепочка приемников здесь. Для каждого приемника вычисляется результат работы соответствующих фильтров в методе GetFilterResult. Он равен результату работы первого фильтра, вернувшего не Neutral. Это означает, что если некоторый фильтр возвращает значение, отличное от Neutral, все последующие фильтры не выполняются.

Но что произойдет, если все фильтры вернут Neutral? В этом случае будет использоваться значение по умолчанию. Это значение устанавливается с помощью атрибута defaultAction у элемента filters для правила. Как вы думаете, каково значение по умолчанию для defaultAction? Вы правы, если считаете, что это Neutral. То есть вся цепочка фильтров может вернуть Neutral в качестве результата. В этом случае NLog ведет себя так же, как и получив Log. Сообщение будет записано в приемник (см. здесь).

Как вы могли догадаться, если фильтр возвращает Ignore или IgnoreFinal, сообщение не будет записано в приемник. Если результат работы фильтра — Log или LogFinal, сообщение будет записано. Но в чем разница между Ignore и IgnoreFinal и между Log или LogFinal? Это просто. В случае IgnoreFinal и LogFinal NLog прекращает обработку цепочки приемников и не пишет ничего в приемники, содержащиеся в последующих звеньях.


Заключение

Анализ кода NLog помог мне разобраться с тем, как работают правила и фильтры. Надеюсь, что эта статья будет полезна и вам. Удачи!

© Habrahabr.ru