Errors are a particular type of event that can occur in an application, as are warnings. There’s nothing to say that there aren’t other well defined events that occur in your application that you want to be able to easily identify in the log. Perhaps messages to a user or audit events. Well, these levels can be defined if you like. The thing to be careful of here though is to remember about that importance scale. Any new levels have to fit into the importance scale and depending upon the level of your output events may or may not get logged. A word of warning though, don’t start defining log levels for everything that can happen in your application, that’s not what they’re for. If you need to be able to do that then I’ll be writing up a post about log entry properties.
Now for the sake of the example, I’m going to create an audit log level but this wouldn’t be a great use case for real life because log4net is not reliable. By this I mean that if you’re writing log entries to a file on disk and you run out of disk space log4net will fail silently as far as your application code is concerned and this is by design. The failure to log, which is a secondary activity, should not prevent the running of the application. In their own words:
log4net is not reliable. It is a best-effort and fail-stop logging system. log4net FAQ
Let’s Do It
So, you’ve decided that you need a new log level. It takes four steps; all of which are quite straight forward.
- Define the new level
- Register it with log4net
- Optionally use it in configuration
- Use it at runtime
Define the New Log Level
If you’ve read the previous post about the additional log levels defined by log4net then you’ll have seen the log4net.Core.Level class and how it has members for each of the different log levels. On first inspection it looks like an enum but its not. Each of those log level members holds a reference to an instance of a Level object.
Level objects are really just a numeric value and a name. The numeric value is used to determine whether the level is more or less important than the threshold and the name is so that you can use it in configuration and how it prints in the output (these two can be different if you want them to be by also specifying a display name).
The numeric value is the bit you need to think about. Should it be more or less important than an INFO message? Or a DEBUG message? Where does it fit in with the existing levels that you use in your application? For comparison, here are the levels defined by log4net and their values.
| Level Name | Level Value |
| log4net:DEBUG | 120000 |
| EMERGENCY | 120000 |
| FATAL | 110000 |
| ALERT | 100000 |
| CRITICAL | 90000 |
| SEVERE | 80000 |
| ERROR | 70000 |
| WARN | 60000 |
| NOTICE | 50000 |
| INFO | 40000 |
| DEBUG or FINE | 30000 |
| TRACE or FINER | 20000 |
| VERBOSE or FINEST | 10000 |
I want to make sure that my new audit events are always logged and I usually run production code at least at INFO level so I’ll set my new level between the NOTICE (50000) and WARN (60000) levels at 55000 which still gives plenty of room at either side to add more new log levels should I wish to in the future. Here’s the code to do that (notice that I haven’t hard coded the value but rather derived it from an existing level just in the unlikely event that they get changed in future versions).
public static class Log4NetCustomLevel
{
public static readonly log4net.Core.Level Audit = new log4net.Core.Level(log4net.Core.Level.Notice.Value + 5000, "AUDIT");
}In case you want to be able to refer to the new log level with one name but have it use another in the log there is an override to the Level constructor that allows you to specify this separately. This is the signature of the constructor.
new log4net.Core.Level(int level, string levelName, string displayName);In the following example you would be expected to use “AUDIT” in any configuration, however it would print “audit level” in the logs.
new log4net.Core.Level(log4net.Core.Level.Notice.Value + 5000, "AUDIT", "audit level");The following output, using the SimpleLayout, shows how the two differ. The first line of output used the original declaration of the level and the second used the override with the display name specified.
AUDIT - Audit Test
new level - Audit Test
Register the New Log Level
Now you need to tell log4net about your new log level before you use it. This means that if you intend to be able to specify your log level as a filter in your configuration then it needs to be registered before you configure log4net.
Here’s how you register your new level.
log4net.LogManager.GetRepository().LevelMap.Add(Log4NetCustomLevel.Audit);Use the New Log Level in Configuration
log4net’s real power comes from its flexible configuration and the best way to give yourself easy access to that is to define the configuration in an XML that your application loads at runtime. The following snippet would do that for you (remember to register your new log level before this). I tend to create the XML file in my start-up project giving it the same name as the output executable and an extension of ‘log4net.xml’ so that you know the dialect of XML used in the file.
log4net.Config.XmlConfigurator.Configure(new System.IO.FileInfo("ConsoleTest01.log4net.xml"));The following is a very simple XML configuration example for a console application that would print all INFO messages and above to the console’s standard output.
<log4net>
<appender name="A1" type="log4net.Appender.ConsoleAppender">
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date %-5level %message%newline" />
</layout>
</appender>
<root>
<level value="INFO" />
<appender-ref ref="A1" />
</root>
</log4net>Due to where we chose to place the value of our AUDIT level between the NOTICE and WARN levels it is more important than the INFO set in my configuration file so would be printed to the console. Likewise, if I chose to set my logging level to NOTICE. However if I set my logging level to WARN, which is more important than our AUDIT level, we wouldn’t see any of our AUDIT log entries.
Also, as long as you do remember to register your new level before the call to configure log4net you can always set the logging level to your new level as in the following example:
<log4net>
<appender name="A1" type="log4net.Appender.ConsoleAppender">
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date %-5level %message%newline" />
</layout>
</appender>
<root>
<level value="AUDIT" />
<appender-ref ref="A1" />
</root>
</log4net>Use the New Log Level at Runtime
Now you’re going to have a similar problem here to if you try to use the additional log levels, such as Verbose, that aren’t included in the ILog interface. I described in an earlier post how to fix that though and the same solution applies here.
Here’s the sample extension class for the two basic Audit and AuditFormat methods you expect to find:
public static class ILogExtentions
{
public static void AuditFormat(this log4net.ILog log, string format, params object[] args)
{
log.Audit(string.Format(format, args));
}
public static void Audit(this log4net.ILog log, string message)
{
Exception exception = null;
log.Logger.Log(
System.Reflection.MethodBase.GetCurrentMethod().DeclaringType,
Log4NetCustomLevel.Audit,
message,
exception);
}
}This means that our code can look exactly like normal log4net code:
class Program
{
private static log4net.ILog Logger = log4net.LogManager.GetLogger(typeof(Program));
public static void Main (string[] args)
{
log4net.LogManager.GetRepository().LevelMap.Add(Log4NetCustomLevel.Audit);
log4net.Config.XmlConfigurator.Configure(new System.IO.FileInfo("ConsoleTest01.log4net.xml"));
Logger.Info("Info Test");
Logger.Audit("Audit Test");
Logger.Warn("Warn Test");
Console.WriteLine ("Done");
}
}Gotchas
If by chance or design you define two levels with the same value then don’t worry; its supported. A logging level is defined by the Level object not by its ID. The ID is used just to provide verbosity based filtering. Even if you’re using the convenience methods of the ILog interface, you’re ultimately logging with a specific level instance.
So, the log output would still be filtered in the same way however, whichever of the duplicate levels were used to perform the log would end up getting written to the output file.
As an example, looking at the standard defined log4net levels in the table at the top of this article, if you chose to log an entry at verbose level then you’d see VERBOSE in the log and if you’d used finest then you’d see FINEST in the log even though they both have the same log value.