Thursday, May 10, 2007

I've written previously about using log4net to log system events for diagnostic purposes in development and live environments and how its configuration is so flexible the output can be sent anywhere. Well, recently, I've been developing a client-server application that uses WSE 3.0 to communicate using web messages and to make the initial development simpler we opted to host the server component in a Windows Form so it can be easily started and stopped. I figured that since we had already littered the server with appropriate logging that we might as well show this in the server's GUI and watch the live diagnostic information as messages are received and processed.

ServerLogViewerDemo.gif

Due to log4net's clean, modular design it is quite simple to achieve what I need.

First, we need to configure the app.config to send log entries to a MemoryAppender by adding a new appender to the log4net section

<appender name="LogViewerDemo" type="log4net.Appender.MemoryAppender">
  <layout type="log4net.Layout.PatternLayout">
    <conversionPattern value="%date [%thread] %-5level %logger [%property{NDC}] - %message%newline" />
  </layout>
</appender>

and specifying the appender name in the root section:

<appender-ref ref="LogViewerDemo" />

Next we write a UserControl containing a RichTextBox and a Timer. Every second, the timer will ask log4net for the MemoryAppender with the name 'LogViewerDemo' and output any new events received since the last timer tick to the RichTextBox.

Obtaining the MemoryAppender:

Hierarchy loggerHierarchy = LogManager.GetRepository() as Hierarchy;

if (loggerHierarchy != null)
{
   logMemoryAppender = loggerHierarchy.Root.GetAppender(appenderName) as MemoryAppender;
}

Fetching the log events:

LoggingEvent[] events = logMemoryAppender.GetEvents();
logMemoryAppender.Clear();

foreach (LoggingEvent logEvent in events)
{
   WriteLogEventToLogTextBox(logEvent);
}

Finally, all that is left is to append the event details to the end of the RichTextBox having first coloured the text red, orange or black to indicate the logging levels of 'Error', 'Warning' and 'anything else' respectively.

Log4NetViewerDemo.zip (13.91 KB)

Thursday, May 10, 2007 10:09:07 PM (GMT Daylight Time, UTC+01:00)

In any non-trivial project, especially when the application is split across several tiers, I find adding a few logging statements has a real benefit. I have used log4net in the last four enterprise-level projects I've worked on and it is a marked improvement on the standard System.Diagnostics.Trace and Debug classes.

log4Net is an open source logging tool that allows all aspects of logging to be modified by changing the config file, which can be done after the code has been built and is in production. This config file controls which messages are logged, the format of messages and where log messages are sent.

For example, once the application is in production you may decide to have certain messages, for example errors and warnings, emailed to the developer and system admin whilst all other informational messages are logged to a text file. (This is the approach I used for PartyHandbook so I get immediate notification when an exception is thrown or a warning generated). Setting this up is just a matter of adding a few lines to the config file and can be configured such that the application doesn't even need a restart. See http://logging.apache.org/log4net/release/config-examples.html#smtpappender for details of the email appender.

The logging facilities are also useful in tracking down specific bugs in the live system. Imagine you have a system performing complicated calculations and you've received a report that the resulting value is not as you'd expect. Assuming sufficient logging has been placed in the application, this would be as simple as turning on 'Debug' level log messages for the MyProduct.ServerServices.ComplicatedCalculationModule and we would see the calculation steps for the live system without having to recreate the data and start the debugger up.

It does take a bit of practice to get into the habit of adding log statements in the right places and in sufficient quantity to provide the required level of verbosity but they can prove very useful in diagnosing problems and also providing another level of documentation to the code.

The areas I tend to add logging statements are:

  • In top-level exception handlers. E.g. in Global.asax Page_Error method or in a try catch block wrapped around the Application.Run statement in the static Main method of a windows application or service.
  • In security authentication code to log failed login attempts
  • In session handling code to determine when web sessions or nhibernate sessions are started and stopped
  • To record configuration settings read in from the config file (e.g. the database connection string used)
  • To record things that happen across process boundaries. E.g. web service messages that are sent and received
  • User actions. E.g. when user runs a system command by selecting a menu option or clicking a toolbar button.

Several other open source tools such as nHibernate also uses log4net to record a whole host of useful information regarding database persistence (E.g. the SQL statements are being sent to the database) so they can be configured in a similar manner.

Getting Started

This is a 30 second guide to using log4net in a project.

  • Get hold of log4net.dll. This can be downloaded from http://logging.apache.org/log4net/downloads.html
  • Add a reference to the DLL from your project
  • Add the following line as the first line in your application (in the Main( ) method for windows applications or in your global.asax file):

XmlConfigurator.Configure();

  • Add the following line to the top of the class you'd like to log:

private static readonly ILog m_Log = LogManager.GetLogger( typeof(MyClass) );

  • Add logging statements to your code:

m_Log.Debug( "This is my log message" )

  • Add this to your App.config configuration file.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net" />
  </configSections>
  <log4net>
    <appender name="FileAppender" type=" log4net.Appender.FileAppender">
      <file value="MyApp.log" />
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%date [%thread] %-5level %logger {%identity} - %message%newline" />
      </layout>
    </appender>
    <root>
      <level value="ALL" />
      <appender-ref ref="FileAppender" />
    </root>
  </log4net>
</configuration>

When you execute your application you will see a new file in the same directory as the executable called 'MyApp.log'. It will contain an entry similar to this:

2006-10-12 14:00:10,883 [18076] DEBUG MyNamespace.MyClass {} – This is my log message

The full manual is available at http://logging.apache.org/log4net/release/manual/introduction.html and there are many examples of different configuration options at http://logging.apache.org/log4net/release/manual/configuration.html

Notes

The recommendation is to create a static member variable containing the logger object for each class where you want to log output. The logger will be obtained using the LogManager.GetLogger(type) method passing in the class's type.

The easiest way to get up and running and initialise log4net is to call XmlConfigurator.Configure(). However, any changes made to the config file whilst the application is running won't be re-read. If this is desirable the log4net config file can be placed in a separate file and the ConfigureAndWatch() method can be used instead. http://logging.apache.org/log4net/release/faq.html#config-reload

The easiest way to add the log to a class is to use Resharper live templates. Create a template with the abbreviation 'log' and use the following for the template text:

private static readonly ILog m_Log = LogManager.GetLogger(typeof ($TYPE$));

$TYPE$ is a template variable that will get its value from the name of the class where the line of code will be placed.

Once setup, you can create a logger by typing log and pressing tab.

Thursday, May 10, 2007 7:20:20 AM (GMT Daylight Time, UTC+01:00)
 Wednesday, May 09, 2007

I recently wrote a simple class for generating diagnostic information that should prove useful in tracking down errors when the system goes live. It details the operating system and .NET version installed on the current machine, the version and location of the entry assembly and the currently logged in OS user:

ENTRY ASSEMBLY
--------------

File                           : C:\Projects\SystemDiagnosticDemo\SystemDiagnosticDemo\bin\Debug\SystemDiagnosticDemo.exe
FileVersion                    : 1.0.0.123
Product                        : SystemDiagnosticDemo
ProductVersion                 : 1.0.0.123

SERVER
------

Machine Name                   : STUART-LAPTOP
Operating System               : Microsoft Windows NT 5.1.2600 Service Pack 2
Processor Count                : 1
.NET Version                   : 2.0.50727.42
OS User                        : STUART-LAPTOP\Stuart

 

As part of the automated build, the nant script creates an AssemblyInfo.cs file containing the Cruisecontrol build number. Cruisecontrol is also setup to tag the source code in Subversion with the same build number to give us full traceability. This means we can determine the exact source code that went into a particular dll and bring up the build log containing the unit test results.

The assembly metadata is retrieved using System.Diagnostics.FileVersionInfo.GetVersionInfo() method on the entry assembly file's location:

Assembly assembly = Assembly.GetEntryAssembly();
string assemblyFileName = assembly.Location;
FileVersionInfo versionInfo = FileVersionInfo.GetVersionInfo(assemblyFileName);

string fileName = versionInfo.FileName;
string fileVersion = versionInfo.FileVersion;
string product = versionInfo.ProductName;
string productVersion = versionInfo.ProductVersion;

and the environment information comes from the static properties on the System.Environment class:

string machineName = Environment.MachineName;
string operatingSystem = Environment.OSVersion.ToString();
int processorCount = Environment.ProcessorCount;
string dotNetVersion = Environment.Version.ToString();
string osUser = String.Format(@"{0}\{1}", Environment.UserDomainName, Environment.UserName);

The demo source code is available here:

SystemDiagnosticDemo.zip (11.09 KB)
Wednesday, May 09, 2007 8:34:31 PM (GMT Daylight Time, UTC+01:00)