Updated (Sept2007) The code of this sample can be found at codeplex: http://www.codeplex.com/RidoCode

 

One year ago we needed to define how to instrument our system, our first User Story was.

 

  • Simple Interface to Log messages from code
  • Configuration mechanism to indicate the destination sink
  • Decoupling the write mechanism from the destination sink
  • Decoupling from the component hoster (ASP.Net, Nunit, .EXE)
  • Define Switches to define different trace levels (INFO, WARNING, ERROR)

 

After some explorations (see http://blogs.msdn.com/rido/archive/2004/05/07/LogsAndTraces.aspx ) we decided to extend a little bit the Trace utilities provided by System.Diagnostics.

 

While developing the system we have been using the System.Diagnostics.TextWriterTraceListener   to write log content to a file.

 

Now, we are in production, and we have seen some limitation with this tracelistener

 

  • Log files can grow without limit, making difficult to find problems
  • Log files are locked by the process that are using it, so in ASP.Net you have to kill the process (or recycle the appdomain) to rotate the log files
  • Relative paths does not work, since the current path is the %windir%

 

So today I have spent some time to fix this limitations.

 

The first step is (as always) to realize some explorations, the first place to start is the new Enterprise Library, it comes with a powerful Logging Framework, however it requires a big change to our current system, and we have decided to postpone to the next release the integration with entlib.

 

Anyway, the entlib does not include a FileRolling mechanism as Log4Net does, however I found via @baz a free implementation of a Rolling File Sink

 

So I decided to implement a RollingFileTraceListener, this way we could keep all the logging infrastructure and the only thing to change are the configuration files.

 

I've never implement a trace listener, and the first thing to see is the implementation of the TextWriterTraceListener . Using reflector and a code generator addin, I got the sources for the class, and found the ctor that is called when initialized from the system.diagnostics configuration section handler

 

 

public TextWriterTraceListener(string fileName) :

this(new StreamWriter(fileName, true)){}

 

The filename parameter is the value of the initializeData configuration attribute

 

<listeners>

  <add name="FileTrace" type="TextWriterTraceListener"

initializeData="MyLogFile.log" />

</listeners>

 

 

Design of the RollingFileTraceListener

 

To implement the same Rolling capabilities of log4net, or @baz sink, it's necessary a configuration scheme more complex that the attribute initializeData used by the TraceListeners, however we don't need all these features, all what we need is to be sure that if the app domain has been restarted  we have a chance to create a new file so the old file is no more locked.

 

So, we have to tweak the initialization of the listener to parse the initializeData and creates a new file based on the current date.

 

The first idea that comes to my mind is something like

 

initializeData="MyLogFile_{yyMMMdd}.log"

 

So, every time the listener is initialized it will create a new file with the current date in the filename as specified by the pattern (MyLogFile_05May07.log)

 

The first thing to implement is guided by the next test

 

[Test]

public void ReplacePattern()

{

  string initData = "LogName_{yyMMdd}.log";   

  string expected = "LogName_" + DateTime.Now.ToString("yyMMdd") + ".log";

  Assert.AreEqual(expected, FileRoller.ReplacePattern(initData));

}

 

 

The next thing I want to add to the new listener is the ability to work with relative paths, very useful for web applications. Also if the destination folder does not exists the FileRoller will create the folder and the log file to write to.

 

The next test method illustrate the desired behavior of the FileRoller class

 

[Test]

public void FolderAnPattern()

{

string initData = @".\..\..\Log\LogName_{yyMMdd}.log";   

Assert.IsTrue(File.Exists(

FileRoller.ResolveOrCreateRollingFile(initData)));   

}

 

Once implemented this features, it's time to test the TraceListener running inside an ASP.Net process. When the initializeData parameter contains a relative path, the log folder is created, relative to %windir%. To resolve the current path, we are using the AppDomain.BaseDirectory property, that returns the current root folder of the ASP.Net app.

 

The full implementation of the file Roller class

 

internal sealed class FileRoller

{   

static public string ResolveOrCreateRollingFile(string pathToReplace)

{

string fileName = ReplacePattern(pathToReplace);       

 

FileInfo fi = new FileInfo(

Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName));

DirectoryInfo di = new DirectoryInfo(fi.DirectoryName);

 

if (!di.Exists)

{

di.Create();

}

 

if (!fi.Exists)

{

fi.CreateText().Close();

}

return fi.FullName;

}

 

static internal string ReplacePattern(string data)

{

string pattern = "";

 

int start = data.IndexOf("{");

int end = data.IndexOf("}");

 

if ((start>0) && (end>0))

{

pattern = data.Substring(start +1 , end-start-1);

}    

if (pattern.Length>0)

{

return data.Replace("{" + pattern + "}",

DateTime.Now.ToString(pattern));

}

else

{

return data;

}

}

}

 

The last thing to do is to define the TraceListener to use the FileRoller before open the stream for writing. The only thing we have to do is to update the overloaded constructor like this.

 

public sealed class FileRollerTraceListener : TraceListener

{

 

private TextWriter writer;

 

public FileRollerTraceListener() : base("TextWriter")

{

}

 

public FileRollerTraceListener(Stream stream) :

 this(stream, string.Empty)

{

}

 

public FileRollerTraceListener(TextWriter writer) :

this(writer, string.Empty)

{

}

 

public FileRollerTraceListener(string fileName) :

this(new StreamWriter(

FileRoller.ResolveOrCreateRollingFile(fileName), true))

{   

}

}

 

And that's it !!

 

If you want the full code please let me know !!