First off, allow me to apologise. Several months ago some people e-mailed asking for some help with my logging component and I sorta kinda promised that I'd come up with and post an example, for the benefit of everyone. That was 14th August and, to this day, I've managed to avoid keeping up my promise. I'm sorry.
There, got it off my chest, I feel much better now.
Once again, you might wanna check the new source.
A show stopper
So, I had to come up with a practical example of how to use the component. "How hard can it be?" I thought. And it wasn't, had it up and running in half an hour. Woo hoo! Only, it didn't work. The log file looked nothing like I expected it to look like after running the test app. Far too few entries. It turns out that I got the whole LogLevel enum wrong. The one little bit I decided not to go into in the first Logging component post turned out to be the bit of code that prevented the component to run as expected. *sigh*.
Before
1: public enum LogLevel
2: {
3: Info,
4: Debug,
5: Warning,
6: Error,
7: Fatal
8: }
After
1: public enum LogLevel
2: {
3: Fatal,
4: Error,
5: Warning,
6: Debug,
7: Info
8: }
Subtle, innit? Tell me about it. The thing is, the order of the enum values in itself isn't the problem. The problem is the following line of code in FileLogger.cs --where it doesn't belong at all, but I'll come to that in a minute:
1: public void Log(string text, LogLevel entryLevel)
2: {
3: // Only log entries with same or lower level than LogManager.LogLevel
4:
5: if ((int)entryLevel > (int)_level) return; // Offending line
6:
7: // Some more code...
8: }
What am I doing here? Messing up, that's what. Yeah, yeah, but more concretely? Well, literally: if the given level of the current entry is greater than the LogLevel of LoggerManager do not log the entry. The idea being that the main LogLevel sets the minimum level of importance an entry has to have in order to be logged. That is, if LogLevel is set to LogLevel.Error, refrain yourself from passing Info or Warning entries, for they shall not be logged.
So if you started doubting whether you were using the logging component correctly because you saw too few entries in your log file, doubt no further. It was me.
Tidying up
Since we're at it, and before we get into the test app, lemme get picky. Remember Line 4 of the preceding code sample and how I said it lives in FileLogger.cs? Well, since that’s probably a check we’ll want to do pretty much every time we log something I can’t really expect everyone who’s willing to implement a new Logger to add this one little line of code. Not very OO, really.
So what I’ve done is this:
Basically, I now provide, for a limited time only, an base implementation of the ILogger interface in the form of an abstract class, BaseLogger. Groovy, innit? Come, take a look:
1: public abstract class BaseLogger : ILogger
2: {
3: private string _initialisationParams;
4:
5: public abstract void Dispose();
6: public LogLevel LogLevel { get; set; }
7:
8: public string InitialisationParams
9: {
10: get { return _initialisationParams; }
11: set
12: {
13: _initialisationParams = value;
14:
15: // We have some initialization parameters so we can do
16: // whatever it is that we need to do to
17: // initialize this baby.
18: Initialize();
19: }
20: }
21:
22: public virtual void Log(string text, LogLevel entryLevel)
23: {
24: // Only log entries with same or lower level than LogManager.LogLevel
25: if ((int)entryLevel > (int)LogLevel) return;
26: }
27:
28: public virtual void Log(Exception e)
29: {
30: // No entryLevel check here. Exceptions should be logged no matter what...
31: throw new System.NotImplementedException();
32: }
33:
34: protected abstract void Initialize();
35: }
As you see, it’s the virtual implementation of the first Log method that performs the check. Of course, not all is sweetness. I haven’t really taken care of the problem, I’ve just made it less ugly:
1: public override void Log(string text, LogLevel entryLevel)
2: {
3: base.Log(text, entryLevel);
4:
5: // Some more code here
6: }
See what I mean? I still force every implementation of the Log method to make a call to the base class (BaseLogger in this case) in other to perform that one check. Not that error prone, but still easy to forget.
BaseLogger also provides an extra abstract method called Initialize that gets called whenever the InitializationParams property is set:
1: public string InitialisationParams
2: {
3: get { return _initialisationParams; }
4: set
5: {
6: _initialisationParams = value;
7:
8: // We have some initialization parameters so we can do
9: // whatever it is that we need to do to
10: // initialize this baby.
11: Initialize();
12: }
13: }
Extreme Makeover
While at it, I decided to bring the LogManager to the C# 3.0 realm, making it quite a lot more readable. LogManager exposes now only a generic GetLogger method. Cleaner, meaner and more readable:
1: public static ILogger GetLogger<TLogger>(string initialization) where TLogger : ILogger, new()
2: {
3: Type log = typeof(TLogger);
4:
5: // Avoid creating a new instance of an already active ILogger
6: ILogger requestedLog = _activeLogs
7: .Find(match => (match.GetType() == log && match.InitialisationParams == initialization));
8:
9: // Got it? return it.
10: if (requestedLog != null)
11: return requestedLog;
12:
13: // Otherwise instantiate it
14: requestedLog = new TLogger { InitialisationParams = initialization, LogLevel = _level };
15:
16: _activeLogs.Add(requestedLog);
17:
18: return requestedLog;
19: }
Functionality remains the same: every time an ILogger instance is summoned into existence LogManager checks whether that concrete type of ILogger has been instantiated before. If so, it returns the existing instance. Otherwise it creates a new instance and returns it.
And that’s pretty much it. I’ve had the decency to include a test app with it so that y’all can see how to use the damned thing. Enjoy!
