Extending Log4NetAlthough Log4Net has a lot of functionality already built in, down the road you may want to extend Log4Net by creating a Custom Filter or Appender. You will find the framework easy and intuitive to work with. Error HandlingIn a logging framework, great care must be taken so that an error or exception is never passed to the calling assembly. Any errors must be handled internally and must never affect the calling application. In most situations, the calling application is trying to log an error condition of its own and does not expect to have error handling around its error handling logging. Most errors are prone to be in the Appender code when the message is to be written. Figure 8-3 shows that all Appenders inherit from AppenderSkeleton. ApenderSkeleton uses the helper class OnlyOnceErrorHandler to handle its errors, so unless the Appender overrides this functionality, it will get OnlyOnceErrorHandler to handle its errors as well. OnlyOnceErrorHandler implements IErrorHandler and actually writes the errors using another helper class called LogLog. LogLog just writes the message out to the Console.Error or Trace methods. If the fact that you cannot see these messages disturbs you, then you have three options. First, you could implement your own version of IErrorHandler. Second, you could create an application to capture the Debug messages. Third, you could just download DebugView from http://www.sysinternals.com, which is a nice graphical tool that intercepts the system debug messages for Windows. Similar tools are already built into the Linux OS. Custom FiltersWith the power of Filter Chaining, you rarely would need to create a custom Filter. However, you may want to do so for performance reasons. Chaining too many Filters together does decrease performance. The power of the Level Filtering and string Regex Filtering on the message already provides ample flexibility. The only flexibility not available with LevelRangeFilter and LevelMatchFilter is the ability to specify a list of Levels. While not earth-shattering, it is a good study in creating custom Filters.The SpecifiedLevelFilter will allow you to specify a comma-separated list of Levels and whether you should accept a match or decline it. Listing 8.22 would be the configuration file syntax. Listing 8.22. SpecifiedLevelFilter's Configuration File Information<filter type="log4net.Filter.SpecifiedLevelFilter"> <param name="Levels" value="WARN,INFO"/> <param name="AcceptOnMatch" value="true"/> </filter> Using the LevelRange as a template, first change the code for retrieving the Levels to use the comma-separated Levels (Listing 8.23) for the Filter. Listing 8.23. C# Code for SpecifiedLevelFilter's Levelspublic string Levels { get { System.Text.StringBuilder sb = new System.Text.StringBuilder(); foreach(Level l in m_levels) { sb.AppendFormat("{0},", l.Name); } sb.Remove(sb.Length, 1); return sb.ToString(); } set { string [] sLevels = value.Split(new char[]{','}); m_levels = new Level[sLevels.Length]; int i =0; LevelMap lm = new LevelMap(); lm.Add(Level.DEBUG); lm.Add(Level.ERROR); lm.Add(Level.FATAL); lm.Add(Level.WARN); foreach(string s in sLevels) { m_levels[i]= lm[s.ToUpper()]; i = i +1; } } } Listing 8.23 is a little messy, but basically it uses String.Split to retrieve just the Levels, does a look-up on the Level using LevelMap, and creates an array of Levels. Next (Listing 8.24) is the overridden Decide method from FilterSkeleton. Listing 8.24. The Decision logic for SpecifiedLevelFilteroverride public FilterDecision Decide(LoggingEvent loggingEvent) { if (loggingEvent == null) { throw new ArgumentNullException("loggingEvent"); } foreach(Level level in m_levels) { if(loggingEvent.Level == level) { if (m_acceptOnMatch) { // this Filter set up to bypass later Filters and always return // accept if level in range return FilterDecision.ACCEPT; } else { return FilterDecision.NEUTRAL; } } } return FilterDecision.DENY; } This decision defaults to Deny. If any one of the Levels is found in the Level array populated from the config file (Listing 8.22), then if accept on match is set to true, the Filter will return ACCEPT, and the message will be logged. Custom LayoutWith XML and Pattern Layouts, you can do almost anything. However, just to show how to create a custom Layout, I will create (actually port the Log4j version of) an HTMLLayout. This Layout could be useful to use as an output with a daily RollingFileAppender on a server. The HTML is static, so the pages do not even need to be served.Following the similar Architecture, the Layout has to inherit from LayoutSkeleton. This means we have to implement a header, footer, and format and decide whether we should ignore exception information. We really should not ignore exception information, as it is very useful. The header is simple; it is just the HTML table in Listing 8.25. Listing 8.25. Header Table for HTMLLayout<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4 ![]() <html> <head> <title>Log4Net Log Messages</title> <style type="text/css"> <!-- body, table {font-family: arial,sans-serif; font-size: x-small;} th {background: #336699; color: #FFFFFF; text-align: left;} --> </style> </head> <body bgcolor="#FFFFFF" topmargin="6" leftmargin="6"> <hr size="1" noshade> Log session start time 2/4/2004 12:18:28 AM<br> <br> <table cellspacing="0" cellpadding="4" border="1" bordercolor="#224466" width="100%"> <tr> <th>Time</th> <th>Thread</th> <th>Level</th> <th>Category</th> <th>Message</th> </tr> The footer just closes the table, as in Listing 8.26. Listing 8.26. Footer for HTMLLayout</table> <br> </body> </html> The overridden Format Method fills in the LoggingEvent information; this C# code is in Listing 8.27. Listing 8.27. C# Formating Code for HTMLLayoutpublic override System.String Format(LoggingEvent event_Renamed) { if (sbuf.Capacity > MAX_CAPACITY) { sbuf = new System.Text.StringBuilder(BUF_SIZE); } else { sbuf.Length = 0; } sbuf.Append(SystemInfo.NewLine + "<tr>" + SystemInfo.NewLine); sbuf.Append("<td>"); sbuf.Append(event_Renamed.TimeStamp); sbuf.Append("</td>" + SystemInfo.NewLine); sbuf.Append("<td title=\" + event_Renamed.ThreadName + " thread\">"); sbuf.Append(Transform.escapeTags(event_Renamed.ThreadName)); sbuf.Append("</td>" + SystemInfo.NewLine); sbuf.Append("<td title=\"Level\">"); if (event_Renamed.Level == Level.DEBUG) { sbuf.Append("<font color=\"#339933\">"); sbuf.Append(event_Renamed.Level); sbuf.Append("</font>"); } else if (event_Renamed.Level >= Level.WARN) { sbuf.Append("<font color=\"#993300\"><strong>"); sbuf.Append(event_Renamed.Level); sbuf.Append("</strong></font>"); } else { sbuf.Append(event_Renamed.Level); } sbuf.Append("</td>" + SystemInfo.NewLine); sbuf.Append("<td title=\" + event_Renamed.LoggerName + " category\">"); sbuf.Append(Transform.escapeTags(event_Renamed.LoggerName)); sbuf.Append("</td>" + SystemInfo.NewLine); if (locationInfo) { LocationInfo locInfo = event_Renamed.LocationInformation; sbuf.Append("<td>"); sbuf.Append(Transform.escapeTags(locInfo.FileName)); sbuf.Append(':'); sbuf.Append(locInfo.LineNumber); sbuf.Append("</td>" + SystemInfo.NewLine); } sbuf.Append("<td title=\"Message\">"); sbuf.Append(Transform.escapeTags(event_Renamed.Rendered Message)); sbuf.Append("</td>" + SystemInfo.NewLine); sbuf.Append("</tr>" + SystemInfo.NewLine); if (event_Renamed.NestedContext != null) { sbuf.Append("<tr><td bgcolor=\"#EEEEEE\" style=\"font-size : xx-small;\" ![]() sbuf.Append("NDC: " + Transform.escapeTags(event_Renamed.NestedContext)); sbuf.Append("</td></tr>" + SystemInfo.NewLine); } System.String s = event_Renamed.GetExceptionStrRep(); if (s != null) { sbuf.Append("<tr><td bgcolor=\"#993300\" style=\"color:White; font-size : ![]() appendThrowableAsHTML(s, sbuf); sbuf.Append("</td></tr>" + SystemInfo.NewLine); } return sbuf.ToString(); } The results are seen in Figure 8-7. Figure 8-7. HTMLLayout Output.[View full size image] ![]() Custom AppenderThere are a number of Appenders that would be interesting to implement for Log4Net. I considered a Telnet Appender but thought that would be unnecessarily confusing for just showing how to extend the architecture. AsFigure 8-3, you can see that we should start by inheriting the Appender from TextWriterAppender like the FileAppender. In fact, we can even use the FileAppender as a template and just change a little code.Listing 8.28 shows the actual code to open the IsolatedFileStorageStream wrapped in a StreamWriter in the already existing Open method. Listing 8.28. Custom Code for IsolatedFileStorageAppenderif(append) { m_isfs = new IsolatedStorageFileStream(fileName, FileMode.Append, FileAccess.Write, FileShare.ReadWrite, m_isf); } else { m_isfs = new IsolatedStorageFileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.ReadWrite, m_isf); } SetQWForFiles(new StreamWriter(m_isfs, m_encoding)); Very little code was changed from the FileAppender. The biggest difference from FileAppender is that it does not allow a directory specification but uses Listing 8.29 to get the directory to write to. Listing 8.29. Code Changes to FileAppender to Create IsolatedFileStorageAppenderm_isf = IsolatedStorageFile.GetUserStoreForAssembly(); .... protected static string ConvertToJustName(string path) { if (path == null) { throw new ArgumentNullException("path"); } if(path.LastIndexOf("//") > 0) { FileInfo fi = new FileInfo(path); path = fi.Name; } return path; } Listing 8.12 in the WMI section to use IsolatedFileStorageAppender and saved it to a new IFSTesterWindowsApp example. After running the IFSTesterWindowsApp, you should be able to browse to C:\Documents and Settings\[USERNAME]\Local Settings\Application Data\IsolatedStorage and see the log-file.txt output.NOTEThis file may be in a series of strangely named folders due to the way the IsolatedStorage works. Custom RendererCustom Renderers are very useful for logging the state of a Custom Object. To illustrate this, Listing 8.30 shows Log4Net logging a Strongly Typed Dataset. Listing 8.30. Logging a Strongly Typed Datasetprivate void button1_Click(object sender, System.EventArgs e) { _logger.Debug(this.dataset11); } The config file for the application wanting to render a Dataset is shown in Listing 8.31. Listing 8.31. Renderer Configuration Information<log4net> <renderer renderingClass="log4net.ObjectRenderer.DataSetRenderer" renderedClass="DatasetRenderTest.DataSet1, DatasetRenderTest" /> <appender name="FileAppender" type="log4net.Appender.FileAppender" > <param name="File" value="log-file.txt" /> <param name="AppendToFile" value="true" /> <layout type="log4net.Layout.SimpleLayout" /> </appender> <root> <level value="DEBUG" /> <appender-ref ref="FileAppender" /> </root> </log4net> In order to create a Custom Render, you must implement IObjectRenderer. The class that implements the render can exist either in Log4Net or in your own custom assembly that Log4Net will call. This is done by specifying the assembly and type in the config file in Listing 8.31. Log4Net will call the DoRender method required by the IObjectRenderer. This is where you filter on your type. So if the Object is a System.Data.DataSet, we will operate on it; otherwise, we can only call the ToString method because it is guaranteed to exist since everything inherits from System.Object. Listing 8.32 is the code called if the Object is a Dataset. Listing 8.32. C# Dataset Renderer Codevirtual protected string RenderDataSet(RendererMap rendererMap, DataSet dataset) { StringBuilder buffer = new StringBuilder(dataset.DataSetName + "{"); buffer.Append(NewLine); foreach(DataTable dt in dataset.Tables) { buffer.Append("\t" + dt.TableName + "{"); buffer.Append(NewLine); buffer.Append("\t\t"); foreach(DataColumn column in dt.Columns) { buffer.Append(column.ColumnName + ","); } buffer.Remove(buffer.Length-1, 1); buffer.Append(NewLine); foreach(DataRow row in dt.Rows) { buffer.Append("\t\t"); foreach(Object o in row.ItemArray) { buffer.Append(o.ToString() + ","); } buffer.Remove(buffer.Length-1, 1); buffer.Append(NewLine); } buffer.Append("\t\t}"); buffer.Append(NewLine); } return buffer.Append("}").ToString(); } Very simply, this code loops through a DataSet's tables, rows, and items, printing the values into a comma-separated file. This is a simple example, but it can be a powerful way to persist objects and messages in your distributed application to get an idea of the state of the system.As you can see, extending Log4Net is easy and somewhat intuitive just by looking at how other already implemented classes in the framework are designed. |