SLF Hands-on Tutorial, Part 1
This is an introductory tutorial on SLF, the Simple Logging Façade. This tutorial covers the basics, which will be all you’ll ever need in most projects. I’m planning on writing a second tutorial that will show custom factories and resolvers soon.
Downloading SLF
To get started, visit http://slf.codeplex.com and download the latest release. Source code is available as a Visual Studio 2008 solution, but if you’re working with VS2005, there’s also precompiled binaries for .NET 2.0 available (.NET 1.0 or 1.1 is not supported).
Sample Projects
SLF comes with a lot of samples, all organized as independent projects that discuss a specific use case. As you will see, most scenarios only require a few lines of code. We are planning to extend this section over time, based on your feedback.
ILogger
The central interface in SLF is ILogger. ILogger provides a versatile API to log information to a specific logging framework. You could roughly say there’s three “categories” of loggers:
- A few simple implementations directly in the SLF core library (e.g. ConsoleLogger or DelegateLogger) that cover common scenarios.
- Façades for logging frameworks that are currently supported (e.g. log4net). A façade is usually an independent DLL.
- Custom loggers that you implement yourself. You can do that very easily, usually by extending a simple base class. Creating fancy custom loggers will be covered in the second part of this tutorial.
Here’s a first snippet, that creates a ConsoleLogger, which is part of the core library:
ILogger logger = new ConsoleLogger(); logger.Info("hello world");
This produces the following output
However, rather than creating your loggers all over the place, you will usually use LoggerService to obtain your logger(s):
ILogger logger = LoggerService.GetLogger();
logger.Info("hello world");
Setting up logging with LoggerService is described in a bit further below.
Logging in Categories
As a general guideline on how and when to use logging categories, I recommend Colin Eberhardt’s “The Art of Logging”, which is an excellent introduction to logging in general.
SLF knows 5 logging categories (or log levels), which are covered through a set of methods that allow you to easily submit logging information.
Logging Level | ILogger Methods |
Info | Info() |
Warn | Warn() |
Error | Error() |
Fatal | Fatal() |
Debug | Debug() |
Every one of these methods provides several overloads, which allow you to submit simple text, exceptions, or even format text on the fly. A few exemplary logging instructions:
ILogger logger = new ConsoleLogger(); logger.Info("Simple Message."); logger.Info("User {0} logged in at {1}", GetUserName(), DateTime.Now); logger.Warn(e, "Message for uncritical exception."); logger.Error(someException); logger.Fatal(e, "Login exception for user {0}", GetUserName());
Logging a LogItem
Apart from the the above mentioned Info(), Warn(), Error(), Fatal() and Debug() methods, there’s also the Log method, which expects an parameter of type LogItem. LogItem is a simple class that encapsulates information that belongs to a given log entry:
Working with LogItem is not as straightforward as using the other methods, but it may be useful if you want to submit more detailed logging information. Here’s a sample that logs a warning along with an event ID:
public void Login(User user) { //do the login LoginResult result = ... //log an Info message with a title and an event ID LogItem item = new LogItem(); item.LogLevel = LogLevel.Warn; item.EventId = 201; item.Title = "Invalid login attempted by: " + user.Name; item.Message = result.ToDetailedString(); ILogger logger = ... logger.Log(item); }
Setting up Logging in Code
Do it Yourself: Storing a Reference to ILogger
Basically, the simplest possible implementation to use SLF is to just store an ILogger instance somewhere and be on your way. The sample below uses the BitFactory logger façade, which internally forwards log messages to the BitFactory logging framework:
public static class MyApplication { //there’s a static convenience method that creates a logger for a given file private ILogger logger = BitFactoryLogger.CreateFileLogger("log.txt"); public static ILogger Logger { get { return logger; } } }
However, we recommend to use LoggerService instead:
LoggerService
LoggerService is a static repository you can use in different ways. It provides support for declarative configurations (taken from app.config), lets you just plug in a single logger, or completely customize and plug in advanced logger resolution strategies.
Lets have a look at a simplistic example: Create the above ConsoleLogger and make it available through LoggerService. Again, this is as simple as it gets:
ILogger logger = new ConsoleLogger();
LoggerService.SetLogger(logger);
With a logger plugged in like this, we can use LoggerService from anywhere in our application to get a hold of the ConsoleLogger we just plugged in:
ILogger logger = LoggerService.GetLogger();
logger.Info("hello world");
Good to Know: LoggerService Never Returns Null
LoggerService guarantees you to always return a valid ILogger instance. If no logger is configured, it will just return you a NullLogger instance. NullLogger is a special implementation of the ILogger interface, which just discards everything you throw at it. You could say that NullLogger switches off logging without requiring any changes in your code.
Thanks to NullLogger, you never have to check for null references when retrieving a logger. Look at the snippet below: This code will not result in an exception, because GetLogger will not return null, but a NullLogger instance:
//you can explicitly assign a null reference… LoggerService.SetLogger(null); //…and the service will return you a NullLogger ILogger logger = LoggerService.GetLogger(); //NullLogger will just discard this message, so no real logging happens logger.Info("This message will be discarded");
Setting up Logging Declaratively (App.config)
Per default, LoggerService analyzes the application’s configuration file (app.config) in order to detect configured loggers. So if you want to go the declarative route, you don’t have to write a single line of code – it just works.
Here’s a sample app.config file that makes SLF write anything to the console through a ConsoleLogger:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="slf" type="Slf.Config.SlfConfigurationSection, slf"/> </configSections> <slf> <factories> <!-- log everything through the built-in ConsoleLogger --> <factory type="Slf.Factories.ConsoleLoggerFactory, SLF" /> </factories> </slf> </configuration>
Declarative configurations often make sense in more complex scenarios, especially if you use more powerful logging frameworks such as log4net. Here’s a complete configuration file that makes SLF use log4net. This app.config file contains two sections:
- The slf section tells SLF to forward all logging data to log4net.
- The log4net section is a standard log4net configuration (it has nothing to do with SLF). In this example, log4net is configured to write everything into a single log file.
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net"/> <section name="slf" type="Slf.Config.SlfConfigurationSection, slf"/> </configSections> <slf> <factories> <!-- configure single log4net factory, which will get all logging output --> <factory type="SLF.Log4netFacade.Log4netLoggerFactory, SLF.Log4netFacade"/> </factories> </slf> <!-- configures log4net to write into a local file called "log.txt" --> <log4net> <appender name="MainAppender" type="log4net.Appender.FileAppender"> <param name="File" value="log.txt" /> <param name="AppendToFile" value="true" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%date - %message %newline" /> </layout> </appender> <root> <level value="ALL" /> <appender-ref ref="MainAppender" /> </root> </log4net> </configuration>
If you configure SLF through app.config, you don’t have to write any code, because SLF will detect the configuration on its own. Accordingly, just get the logger through LoggerService and log away:
ILogger logger = LoggerService.GetLogger();
logger.Info("Application started at {0}", DateTime.Now);
Do note that if you plug in a custom logger through LoggerService.SetLogger, this will override any declaratively configured loggers. You can, however, always reset LoggerService through the LoggerService.Reset method.
Working With Loggers and Façades
Using the Built-in Loggers
SLF comes with a number of built-in loggers that implement ILogger, which you can use to quickly add logging functionality to your application. There’s the usual suspects such as ConsoleLogger, DebugLogger, or TraceLogger, as well as a few more fancy implementations (e.g. DelegateLogger or DecoratorLogger) and base classes you can use to quickly create your own logger implementations.
There’s a lot of samples that demonstrate the loggers and possibilities, all wrapped into individual projects. However, here’s a few samples:
Delegating Logging through DelegateLogger
DelegateLogger is a simple logger that allows you to quickly plug in some custom logic through a delegate or lambda expression. Here’s dummy sample for a WinForms application, which just shows a message box for every item that is logged to SLF.
DelegateLogger logger = new DelegateLogger(ShowMessage); //this message is being invoked by the logger private void ShowMessage(LogItem item) { MessageBox.Show("Logged Message: " + item.Message); }
…or even shorter, with a Lambda Expression:
var logger = new DelegateLogger(item => MessageBox.Show(item.Message));
Combining Logged Output via CompositeLogger
CompositeLogger is a logger that does not log on its own, but forwards all logging information to an arbitrary number of other loggers:
CompositeLogger compositeLogger = new CompositeLogger(); compositeLogger.Loggers.Add(new ConsoleLogger); compositeLogger.Loggers.Add(new TraceLogger); compositeLogger.Loggers.Add(new DebugLogger); compositeLogger.Info("Logging to all three loggers at once!");
You can also submit the underlying loggers directly as constructor parameters:
ILogger consoleLogger = new ConsoleLogger(); ILogger traceLogger = new TraceLogger(); ILogger debugLogger = new DebugLogger(); ILogger composite = new CompositeLogger(consoleLogger, traceLogger, debugLogger);
Formatting Logging Output
The first snippet in this tutorial wrote “hello world” to the console, which looked like this:
Now, a lot of loggers derive from FormattableLoggerBase. This base class provides a Formatter property, which takes an instance of ILogItemFormatter. ILogItemFormatter is a very simple interface, which has only one task: Converting a given LogItem into a string.
Accordingly, if you wanted to change the format of the above message, you could just write a custom formatter and assign it to your logger. Here’s an alternative for a simple XmlFormatter:
public class XmlFormatter : ILogItemFormatter { private XmlSerializer serializer = new XmlSerializer(typeof(LogItem)); public string FormatItem(LogItem item) { StringWriter writer = new StringWriter(); serializer.Serialize(writer, item); return writer.ToString(); } }
Accordingly, we can plug in this formatter into our ConsoleLogger like this:
IFormattableLogger logger = new ConsoleLogger(); logger.Formatter = new XmlFormatter(); logger.Info("hello world");
The above snippet produces the following output, that renders a LogItem as an XML element:
Directing Trace Output To SLF
If you have an existing codebase that logs through the Trace class, and you would like to make use of SLF, you don’t necessarily have to change the existing code. The SLF core library contains the SlfTraceListener class which just redirects trace messages to SLF.
SLF comes with a sample that shows how to declare SlfTraceListener declaratively via app.config. But you can do this also programmatically:
static void Main(string[] args) { //add SLF listener to Trace Trace.Listeners.Add(new SlfTraceListener()); //write to Trace – this message will be redirected to SLF Trace.WriteLine("We are writing to Trace."); }
Your own Logger: Extending the Logger Base Classes
If you want to create your own ILogger implementation, you can either create a new class that implements ILogger, or extend either LoggerBase or FormattableLoggerBase.
ILogger is not a complex interface, but there’s a lot of boilerplate code to implement. So in most cases, it’s probably more convenient to extend one of the base classes. Both require you to override just one method:
public class MyCustomLogger : LoggerBase { public override void Log(LogItem item) { //your implementation goes here } }
Sample: Implementing a ColoredConsoleLogger
The class below is a custom console logger which writes colored output to the console, depending on the LogLevel of the logged item. The class derives from FormattableLoggerBase in order to allow you to plug in a custom message formatter.
public class ColoredConsoleLogger : FormattableLoggerBase { public override void Log(LogItem item) { //set console color depending on log level switch (item.LogLevel) { case LogLevel.Info: Console.ForegroundColor = ConsoleColor.Green; break; case LogLevel.Warn: Console.ForegroundColor = ConsoleColor.Cyan; break; case LogLevel.Error: Console.ForegroundColor = ConsoleColor.Yellow; break; case LogLevel.Fatal: Console.ForegroundColor = ConsoleColor.Red; break; default: Console.ResetColor(); break; } //delegate string formatting to base class string text = FormatItem(item); //write the string to the console Console.Out.WriteLine(text); Console.ResetColor(); } }
If you log through this ColoredConsoleLogger, a logged Info is displayed in green now:
Working with Available SLF Façade Libraries
SLF comes with a few façade libraries you can use to forward to a given logging frameworks. Currently, there is support for the following commonly known frameworks:
- log4net
- NLog
- Enterprise Library
- BitFactory Framework
…and if you would like to use SLF along with a framework that is not listed yet, writing your own façade is pretty easy!
We’ve provided samples and sample configurations for all these façades. Accordingly, you can just find the sample that suits you, and copy/paste the configuration code into your application.
Named Loggers
The “Named Loggers” feature allows you to define several loggers (programmatically or in code), and then access them through their names. You can even direct logging instructions to different logging frameworks based on logger names!
In order to get a named logger, just use the overload of LoggerService.GetLogger that takes a string parameter:
ILogger defaultLogger = LoggerService.GetLogger();
ILogger namedLogger = LoggerService.GetLogger("LAB-DATA");
Note that the default logger is basically a named logger with a special name (an empty string). The snippets below all return the default logger:
ILogger defaultLogger = LoggerService.GetLogger();
ILogger defaultLogger = LoggerService.GetLogger("");
ILogger defaultLogger = LoggerService.GetLogger(LoggerService.DefaultLoggerName);
The downloadable SLF solution contains sample applications that show how to configure loggers with different names both programmatically or via configuration files.
The Hierarchical Lookup Process
When requesting a named logger, you don’t have to submit a name that exactly matches the name of a configured logger. Instead, named loggers are organized in a hierarchical manner. As soon as a named logger is being requested, SLF will just resolve the closest match and return you a matching logger instance:
In the above sample, SLF was configured with a total of eight loggers. You can see, that the logger names form a hierarchy. With this setup in place, the closest match will be returned on every request.
Accordingly, LoggerService.GetLogger("Foo.AAA") would return a logger of type Foo (closest match). And LoggerService.GetLogger("Foo.XXX.AAA") would return a logger of type Foo.XXX (closest match again).
Here’s some more samples, matching the above hierarchy:
Requested Logger Name | Returned Logger Type | Name of Returned Logger Instance |
"Foo" | Type declared for "Foo" | "Foo" |
"Foo.XXX" | Type declared for "Foo.XXX" | "Foo.XXX" |
"Foo.XXXXXX" | Type declared for "Foo.XXX" | "Foo.XXXXXX" |
"Foo.X" | Type declared for "Foo" | "Foo.X" |
"FooXXX" | Type declared for "Foo" | "FooXXX" |
"Bar.XXX" | Type declared for "Bar.XXX" | "Bar.XXX" |
"Bar.ZZZ" | Type declared for "Bar" | "Bar.ZZZ" |
"XXX" | Type declared for Default Logger | "XXX" |
"XXX.Foo" | Type declared for Default Logger | "XXX.Foo" |
"TEST" | Type declared for Default Logger | "TEST" |
"" | Type declared for Default Logger | "" |
null | Type declared for Default Logger | "" |
Fallback to Default Logger
The lookup for a logger will always fall back to the default logger type if no matching named logger was found. This should not come as a surprise. After all, the default logger is just a logger with the name [String.Empty].
And don’t forget – LoggerService always returns a valid logger instance and never a null reference. If no unnamed logger was configured at all, you would just receive an instance of NullLogger.
Exceptions to Named Logger Resolving
Hierarchical lookups work out of the box, but they are not guaranteed if you customize LoggerService:
- If you inject a single ILogger instance into LoggerService via LoggerService.SetLogger, all requests will of course return this specific logger instance, regardless of the requested logger name.
- If you inject a custom resolver into LoggerService by setting the LoggerService.FactoryResolver property, you gain full control over the lookup and logger creation process. Accordingly, you can completely change this behavior.
Configuring Named Loggers in Code or Declaratively
The sample projects show how to set up named loggers in code, or via the application configuration file (app.config).
Filtering
Currently, SLF provides not built-in filtering mechanism, but leaves that to the underlying logging frameworks. The reason for this is that we just wanted to provide a very lean interface to get started with.
However, we’ve already opened a topic on CodePlex’ discussion board, so please express your opinion. If we feel there is a demand for built-in filtering, we will provide the functionality in a matter of days:
http://slf.codeplex.com/Thread/View.aspx?ThreadId=76857
Tutorial Part 2
The next part of this tutorial will discuss Resolvers, Factories, and creating your own logging façades. It will cover simple scenarios (you can plug-in factories with a single line of code) and more complex resolution scenarios for those among you who happen to deal with challenging logging situations. Stay tuned 🙂
Thank you for the article, it is very easy to follow and undestand.
Any estimated date for part two of the tutorial?
Alexandrul,
Thanks for the feedback! I’ve already written most of it, I hope I can publish it next week.
Happy coding 🙂
Hi,
wonderful crafted piece of code thanks. We will use it in our next project. I had a little testrun today and found 2 things:
a) It seems that its not possible to configure a composite logger in app.conf, right?
b) I personally would prefer that the constructor of a logger has a overloaded variant where I can set the formatter directly (the abstract base class has this)
any news about the second part? even a draft?
+1 for a second part!
How does one configure SLF via a separate config file? E.g. If one has a web site web.config and one would like the log4net stuff to be kept in log4net.config instead.
Hello, Philipp!
I enjoyed your tutorial. I’m thinking of integrating SQL with NHibernate. Do you have any hint or suggestion to pass me?
Thanks in advance
Hello, Philipp!
correction:
I enjoyed your tutorial. I’m thinking of integrating the SLF and NHibernate. Do you have any hint or suggestion to pass me?
Thanks in advance