In the Context tutorial I said that my next log4net post would focus on real-world examples.  However, I received this request from a college in the U.K.:

I would like to have 2 logs, ... I would like to have levels Debug, Info & Warn logged to the one log and all Error & Fatal logged to the second log. ...  Do you know if this is possible with log4net?

This is very easy to do using a feature of log4net I haven't discussed yet: filters.  Filters are applied to individual appenders via the log4net configuration, and they help the appender determine whether a log event should be processed by the appender.  They may sound like the appender threshold property, and indeed some filters behave similarly; however, filters offer far more control over logging behavior.

Filter Example

Open Visual Studio, create a new console project, and reference log4net.  Add the following code to the Program class:

namespace Tutorial7_Filters
{
    class Program
    {
        static void Main( string[] args )
        {
            log4net.Config.XmlConfigurator.Configure();
            log4net.ILog Log = log4net.LogManager.GetLogger(
                System.Reflection.MethodBase.GetCurrentMethod().DeclaringType
               );
            Log.Fatal( "this is a fatal msg" );
            Log.Error( "this is an error msg" );
            Log.Warn( "this is a warn msg" );
            Log.Info( "this is an info msg" );
            Log.Debug( "this is a debug msg" );
        }
    }
}

Add an app.config to the project and update it like so:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
  </configSections>
  <log4net>
    <appender name="LogFileAppender" type="log4net.Appender.FileAppender">
      <file value="log.txt" />
      <filter type="log4net.Filter.LevelRangeFilter">
        <levelMin value="DEBUG" />
        <levelMax value="WARN" />
      </filter>
      <layout type="log4net.Layout.SimpleLayout" />
    </appender>
    <appender name="ErrorFileAppender" type="log4net.Appender.FileAppender">
      <file value="errors.txt" />
      <filter type="log4net.Filter.LevelRangeFilter">
        <levelMin value="ERROR" />
        <levelMax value="FATAL" />
      </filter>
      <layout type="log4net.Layout.SimpleLayout" />
    </appender>
    <root>
      <level value="DEBUG" />
      <appender-ref ref="LogFileAppender" />
      <appender-ref ref="ErrorFileAppender" />
    </root>
  </log4net>
</configuration>

This configuration defines two file appenders, each with a unique filter applied (lines 12 and 23).  The filter on the first appender will log events with levels DEBUG, INFO, and WARN; the filter on the second appender will log event with levels ERROR and FATAL.

Build and run; the command console will flash for a moment with no output.  Check the working directory and open log.txt:

WARN - this is a warn msg
INFO - this is an info msg
DEBUG - this is a debug msg

Note that only the DEBUG, INFO, and WARN messages appear in this file.  The errors.txt log file contains only ERROR and FATAL log messages:

FATAL - this is a fatal msg
ERROR - this is an error msg

Filter Survey

Here is a list of the filters available in the log4net distribution:

Type NameDescription
log4net.Filter.LevelMatchFilter Filters log events that match a specific logging level; alternatively this can be configured to filter events that DO NOT match a specific logging level.
log4net.Filter.LevelRangeFilter Similar to the LevelMatchFilter, except that instead of filtering a single log level, this filters on an inclusive range of contiguous levels.
log4net.Filter.LoggerMatchFilter Filters log events based on the name of the logger object from which they are emitted.
log4net.Filter.StringMatchFilter Filters log events based on a string or regular expression match against the log message.
log4net.Filter.PropertyFilter Filters log events based on a value or regular expression match against a specific context property.
log4net.Filter.DenyAllFilter Effectively drops all logging events for the appender.

You can also define your own filter by implementing the log4net.Filter.IFilter interface or deriving from the log4net.Filter.FilterSkeleton class.

You can apply multiple filters to an appender; they will be evaluated as a chain in configuration order.  Each filter in the chain can either accept the message, deny the message, or punt to the next filter in the chain.   The first filter to explicitly allow or deny a log message stops the filter chain and determines the fate of log message.

Logger and Level Filters

The LoggerMatchFilter filters against the name of the logger emitting the message.  This filter can be configured using the following properties:

  • loggerToMatch: a string value to match against the message's logger name.  The match is made using the String.StartsWith method;
  • acceptOnMatch: a boolean value indicating whether a matching logger name results in accepting the message (true) or rejecting it (false).  This defaults to true, meaning that only matching logger names will be allowed into the appender.

The LevelMatchFilter filters against a specific log message level.  This filter can be configured using the following properties:

  • levelToMatch: the log level to match - either DEBUG, INFO, WARN, ERROR, or FATAL;
  • acceptOnMatch: a boolean value indicating whether to accept log levels matching the levelToMatch property (true), or reject log levels matching the levelToMatch property (false).  Defaults to true.

The LevelRangeFilter allows you to configure a range of log levels to filter on:

  • levelMin: the minimum log level to match - either DEBUG, INFO, WARN, ERROR, or FATAL;
  • levelMax: the minimum log level to match - either DEBUG, INFO, WARN, ERROR, or FATAL;
  • acceptOnMatch: a boolean value indicating whether to accept log levels matching the levelToMatch property (true), or to punt filtering to the next filter in the chain (false).  Defaults to true.  Note that any log level outside of the [levelMin, levelMax] range is denied by this filter.

Message and Property Filters

If you need to filter messages based on their message content, you need StringMatchFilter.  This filter can be configured as follows:

  • regexToMatch: a regular expression to match against the log message.  Note this regex is created with the Compiled option enabled for performance;
  • stringToMatch: a static string to match against the log message.  The match is made using the String.IndexOf method to see if the static string exists in the log message, which is a case-sensitive search.
  • acceptOnMatch: a boolean value indicating whether to accept log messages matching the string or regex (true), or to deny log messages matching the string or regex (false).  Defaults to true.  

Note that any log messages not matching the filter criteria are punted to the next filter in the chain.  If you specify both the regexToMatch and stringToMatch filters, the regexToMatch wins out and the string comparison is never executed, even if the regex fails to match.

The PropertyFilter performs the same behavior as the StringMatchFilter, except that the match is made against the value of a context property.  It is configured like so:

  • key: the name of the property value to match;
  • regexToMatch: a regular expression to match against the specified property value.  Note this regex is created with the Compiled option enabled for performance;
  • stringToMatch: a static string to match against the specified property value.  The match is made using the String.IndexOf method to see if the static string exists in the property value, which is a case-sensitive search.
  • acceptOnMatch: a boolean value indicating whether to accept messages with a property value matching the string or regex (true), or to deny messages with a property value matching the string or regex (false).  Defaults to true.  

Just like the StringMatchFilter, the PropertyFilter will punt filtering when the property value does not match the filter criteria, and regexToMatch trumps and excludes stringToMatch.  In addition, if the property identified does not exist in the current context, filtering is punted.

DenyAllFilter

This filter simply denies all filtering.  When this is used, it's always at the end of a filter chain to block unwanted log messages from the appender.

An Extended Example

Update Program.cs to the following:

namespace Tutorial7_Filters
{
    class Program
    {
        static void Main( string[] args )
        {
            log4net.Config.XmlConfigurator.Configure();
            log4net.ILog Log = log4net.LogManager.GetLogger( System.Reflection.MethodBase.GetCurrentMethod().DeclaringType );
            log4net.ILog OMGLog = log4net.LogManager.GetLogger( "OMGLogger" );
            
            Log.Fatal( "this is a fatal msg" );
            Log.Error( "this is an error msg" );
            Log.Warn( "this is a warn msg" );
            Log.Info( "this is an info msg" );
            Log.Debug( "this is a debug msg" );
            OMGLog.Fatal( "OMG!!  this is a fatal msg" );
            OMGLog.Error( "OMG!!  this is an error msg" );
            OMGLog.Warn( "OMG!!  this is a warn msg" );
            OMGLog.Info( "OMG!!  this is an info msg" );
            OMGLog.Debug( "OMG!!  this is a debug msg" );
        }
    }
}

On lines 8 and 9 two logger objects are created, one based off of the name of the containing type and another named simply "OMGLogger".  Now update the app.config to the following:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
  </configSections>
  <log4net>
    <appender name="OMGLogAppender" type="log4net.Appender.FileAppender">
      <file value="omglog.txt" />
      
      <filter type="log4net.Filter.LevelRangeFilter">
        <levelMin value="ERROR" />
        <levelMax value="FATAL" />
        <acceptOnMatch value="false" />
      </filter>
      <filter type="log4net.Filter.LoggerMatchFilter">
        <loggerToMatch value="OMGLogger" />
      </filter>
      <filter type="log4net.Filter.DenyAllFilter" />
      <layout type="log4net.Layout.SimpleLayout" />
    </appender>
    <root>
      <level value="DEBUG" />
      <appender-ref ref="OMGLogAppender" />
    </root>
  </log4net>
</configuration>


Compile and run, the command console will blip on your screen.  Open the omglog.txt file:

FATAL - OMG!!  this is a fatal msg
ERROR - OMG!!  this is an error msg

Note that because of our chained filter, only log messages that meet the following criteria were allowed into the omglog.txt file:

  1. the log message level is ERROR or FATAL;
  2. the log message originated from the OMGLogger.

Coming Up

For realsies next time I'll discuss some personal experiences with log4net saving me hours of work and making me a hero.  I'll also cover some logging best practices in that or a later post.