.NET Instrumentation Workshop
Home About Workshops Articles Writing Talks Books Contact

8.7 The Right Way To Write To The Event Log

The right way to use the event log is to create a resource-only DLL containing the format strings for the messages, register the DLL for the source and then use the unmanaged ReportEvent (or failing that, EventLog.WriteEvent) to log the event. I will take you step by step through this process.

8.7.1 Message Compiler

The input for the message compiler are text scripts. These can be a bit confusing on initial sight because they are not just used for event log messages. In addition to the event log format strings, they can also be used to create format strings with placeholders that have format specifiers (as explained later, these cannot be used for the event log), they are also used to create C++ manifest constants for error codes which brings in the concepts of facility and severity (again, things which are not used by the event log). And, of course, all the strings are localisable.

From the input script, the message compiler will generate a .bin binary resource file, a resource compiler script that can be used to compile the resource to a format that the C++ linker can use to bind the resource to a DLL, and it will also generate a C++ header file with the manifest resource definitions. An example of such a header file is the winerror.h file in the Platform/Windows SDK which defines standard error values for Windows. Near the top of this file you'll find the following comment:

// Values are 32 bit values laid out as follows:
//
// 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
// 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
// +---+-+-+-----------------------+-------------------------------+
// |Sev|C|R| Facility | Code |
// +---+-+-+-----------------------+-------------------------------+
//
// where
//
// Sev - is the severity code
//
// 00 - Success
// 01 - Informational
// 10 - Warning
// 11 - Error
//
// C - is the Customer code flag
//
// R - is a reserved bit
//
// Facility - is the facility code
//
// Code - is the facility's status code

These values, the severity, the facility and the code, have nothing to do with the event log. The event log does not use facilities, instead it has categories. The severity is not determines by the event, it is specified by the event source through ReportEvent. I will explain the reason for these items in a later section, but note that for the event log you should simply ignore them.

The message compiler (mc.exe) is a very old utility: it originated on OS/2, so its pedigree goes back before NT. The documentation in the MSDN library for this tool is rather scant, I can only assume this is because Microsoft does not want you to use it and are trying to put as many obstacles in your way. The good news is that the Platform SDK had the source code for the message compiler as an example; but the bad news is that Microsoft removed it several years ago. However, those of us with old copies of the Platform SDK still have access to it and can use the source code to find out exactly how the compiler works.

The script essentially has two sections. In the first half you define the symbols that you will use in the second half of the file. The second half is where you define the events and the event messages. Each message looks like this

SYMBOL=value
SYMBOL=value
event message
.

The message compiler defines specific symbols that you can use and if the line does not begin with one of those symbols it is assumed to be the start of the event message. The event message finishes with a line with a single period. If you provide a symbol then it acts as a toggle, so that later messages will use the same value unless you provide a new value for the symbol.

The symbols you can use are:

Symbol Description
MessageId The number of the event. If you miss this out the message compiler will increment the number of the last message defined
Severity One of the severities defined in the SeverityNames collection (see below). In general you should use Success, or omit this item altogether. This has no connection at all with the severity of an event log message.
Facility A general categorization of the message. This is one of the values defined in the FacilityNames collection (see below). Again, this is not used by the event log, so you should miss it out altogether.
SymbolicName This is the name used for the constant for the message ID in the C++ headers file. You will only eant to use this if you intend to write C++ code that generates event log messages, but bear in mind the comments given later.
Language This is the locale of the message and is one of the values from the LanguageNames collection (see below). This is the most important symbol.

The message compiler application create 'collections' with possible values that you can use for some of these symbols, when you first start it, the compiler will fill these collections with default values (as shown in the table below), however, you can add your own values to these collections with code like this:

CollectionName=(value).

This will add the item value to the collection CollectionName. If you want to add multiple values into a collection you can have more than one line in the format shown above, or you can use a multiline format like this:

CollectionName=(value1
value2
value3
value4)

Note that the values are separated by new lines and that the last value is terminated by the closing parenthesis.

The following table lists the collections that you can alter (the first, however, only ever has one value). The only one that you should be interested in is LanguageNames.

SymbolDescription
MessageIdTypedef A single value that determines the type of the constants in the headers file. If you do not specify this then the type will be DWORD, if none of the message descriptions use SymbolicName then this value is not used.
SeverityNames This is a collection of items in the form <name>=<value>:<symbol> where <name> is the name used as the Severity for the event, <value> is a number between 0 and 3 and <symbol> is the name of the symbol that will be added to the header file. The default values are Success (0x0), Informational (0x1), Warning (0x2) and Error (0x3), any value that you add to this collection will override the default.
FacilityNames This is a collection of items in the form <name>=<value>:<symbol> where <name> is the name used as the Facility for the event, <value> is a number between 0 and 0xfff and <symbol> is the name of the symbol that will be added to the header file. This collection will contain a facility called Application with a value of 0x0.
LanguageNames This is a collection of items in the form <name>=<value>:<symbol> where <name> is the name used as the Language for the event, <value> is the locale ID, a number between 0 and 0xffff and <symbol> is the name of the binary resource file that will be created. The default language is English (0x0901)

Note that every event must have a message for each LanguageName that you define. So if you define two languages then you have to provide two messages for each event.

Finally, bear in mind that semicolon (;) is the comment character as far as the message compiler is concerned. However, whatever you provide as a comment will be written to the header file verbatim. Since you are unlikely to use the header file this does not matter, but in the rare cases that you will use the header bear in mind that it is your responsibility to provide something that is acceptable to the C++ compiler. So if you provide a comment in the .mc file you will also need to provide the C++ comment symbol (that is, your comment must be prefixed with ;//).

To test this out, type the following into a file (calc.mc)

LanguageNames=(British=0x809:MSG00809)
LanguageNames=(French=0x40c:MSG0040c)

MessageId=0x1
Language=British
Calculation
.
Language=English
Calculation
.
Language=French
Calcul
.

MessageId=0x1000
Language=British
Divided %1 by nought
.
Language=English
Divided %1 by zero
.
Language=French
division de %1 par zéro
.

As mentioned above, the default language is US English, but the message compiler simply calls it English. (Huh? not US English, has anyone explained to Microsoft where the language came from?) This means that you can use English without declaring US English. To distinguish between messages generated for US English and real English I have used zero for the former and nought for the latter.

Compile this script. The Visual Studio command line will set up a path that includes the message compiler (mc.exe). The Windows SDK (which you have to install to get the .NET 3.0 SDK) also has this tool and the command line installed for the SDK will also set up a path for the message compiler. To compile it, just pass the name of the message compiler script to the compiler (mc calc.mc).

Take a look at the output: calc.h, calc.rc, MSG0040c.bin, MSG00001.bin and MSG00809.bin. The first file is a C++ header file that you will not use. The other four are important for the event log resource file: they are used to create an unmanaged resource.

8.7.2 Message File Resources

The FormatMessage function needs an RT_MESSAGETABLE unmanaged resource. To do this you compile a resource compiler script referring to the binary resources the message compiler created. This can be used to generate a compiled resource (.res) that can be bound to a DLL. Take a look at the resource script created by the message compiler (calc.rc):

LANGUAGE 0xc,0x4
1 11 MSG0040c.bin
LANGUAGE 0x9,0x1
1 11 MSG00001.bin
LANGUAGE 0x9,0x8
1 11 MSG00809.bin

As the name suggests, LANGUAGE defines the language of the resources that follow (until LANGUAGE is used again). The next line defines a custom (user defined) resource. Win32 resources have three properties: type, ID and language, the ID can be a string name or a numeric ID. The language ID is a global setting (until it is changed) as you have already seen. The other two properties, and the resource itself, are given on a single line. The first number is the identifier for the resource (in this case a numeric ID is used), the second number is an identifier for the resource type (RT_MESSAGETABLE is type 11). The last item on each line is the name of the file that contains the binary resource. In effect, all these commands do is associate the binary resource with an ID and a type for a specific locale.

It is important to point out here that a language ID is a 16-bit value that is a combination of the primary language and the sub-language as defined in winnt.h. This header file gives the English language ID as 0x9 and for French it gives 0x0c. However, these numbers are misleading because the primary language makes up the bottom ten bits of the language ID (whereas these numbers suggest that they contribute 8 bits). The sub-language provides the top 6 bits. The French sub-language is given in winnt.h as 0x1, British is 0x2 and US is 0x1, that is, when shifted ten bits, thee are actually 0x0400, 0x0800 and 0x0100.

Compiling these resources is straightforward using the resource compiler (rc.exe): you simply pass the name of the resource script to the compiler and this creates a compiled resource (.res) file. The resource compiler is provided with Visual Studio, with the .NET 2.0 SDK and the Windows SDK (and hence the paths should be set up in the command lines created for them).

Compile the resource:

rc calc.rc

8.7.3 Binding to a DLL

The FormatMessage function uses compiled resources through a HMODULE handle created by a call to LoadLibrary. This means that the file can be an EXE or a DLL, but usually it is a DLL. Creating a resource-only DLL is simple, however, it all depends on what tools you have. If you have C++ installed (Visual Studio, or the Windows SDK) then you can call link.exe to create a resource-only DLL. If you just have the .NET 2.0 SDK then you have to fool the compiler to create a resource-only DLL by telling it to compile an empty file and provide the name of the compiled Win32 resource file. You can use either the C# compiler or the assembly linker to do this.

Here are the options, first, the C++ linker (the best option):

link /DLL /noentry /machine:X86 /out:calc.dll calc.res

Next, the assembly linker:

copy nul dummy.txt
al /win32res:calc.res /t:lib /out:calc.dll /embed:dummy.txt,dummy
del dummy.txt

Finally, the C# compiler:

copy nul dummy.cs
csc /win32res:calc.res /t:library /out:calc.dll dummy.cs
del dummy.cs

Both the assembly linker and the C# compiler need to compile something, they refuse to create a DLL with only unmanaged resources. In the first case the assembly linker needs to have a managed resource and one way to do this is to embed an unmanaged resource. To do this an empty dummy file is created by copying the nul device to the file. The C# compiler requires that you pass a file with C# code for it to compile, however, that file too, can be completely empty. Thus, this code creates an empty file called dummy.cs which is then compiled.

All three of these will bind the resource to the DLL.

The phrase "bind the resource" implies that the compiler/linker simply copies the resource into the appropriate place in the final file. However, these tools do more than this.
Review what we have done so far: the message compiler creates a binary resource, this is then compiled to a compiled resource which is then bound to the DLL, that is, placed in the .rsrc section of the library. The .rsrc section is a nested tree of directories and items, this is a flexible format that can have 231 levels, but Win32 resources use only three levels (in this order): type, ID, language. That is, there will be a table with entries for each of the resource types, and an offset to a directory for each type; each of these directories will have an entry for each resource of that type, which again has an offset to a third directory; this final directory has an entry for each language that was used and these entries will have an offset to the actual resource.
The .res file is not formatted in this way. It has an entry for each resource and each entry has the type ID, the resource ID and the language. Thus the compiler/linker when it binds the resource will have to sort the resources it finds in the .res file into tables. The following picture illustrates this:



The image on the left is a schematic of the data in a .res file, as you can see there is no sorting, the file just contains an entry for each resource. The image in the centre is a schematic of the data when it has been bound to a .rsrc section. This has been sorted and arranged in directories with 'leaf' items. Since there are two resource types it means that there are two ID tables, each type entry will point to the appropriate ID table. Since there are three IDs there are three language tables, each ID item points to the appropriate language table. Each language table points to an item descriptor. As you can see from the left hand image, in this example there are five resources so there are five descriptors (Item 1 - Item 5). Each of these descriptors contains the RVA and size of the resource in the file. In this example I have assumed that the resources are referred using integer IDs. If string names are used then there will be a table in the .rsrc section with the names.

Once you have created a file with an RT_MESSAGETABLE resource you can now go on to the next step to install the resource file.

8.7.3 Creating the Event Source

Earlier you used the EventLog.CreateEventSource static method to create the event source registry entries. This method is overloaded and one version takes a parameter of the type EventSourceCreationData, here is the public interface:

public class EventSourceCreationData
{
   public EventSourceCreationData(string source, string logName);
   public int CategoryCount { get; set; }
   public string CategoryResourceFile { get; set; }
   public string LogName { get; set; }
   public string MachineName { get; set; }
   public string MessageResourceFile { get; set; }
   public string ParameterResourceFile { get; set; }
   public string Source { get; set; }
}  
Notice that in addition to the log, source and machine name, there is a property for the message resource file (which provides the EventMessageFile registry value that you saw earlier), MessageResourceFile. I will return to this class in a later section, but for now we have enough to register the message resource file that we have created.

Create a file to register (and delete) the event source (app.cs), notice that, once again, since we are only writing test messages, this code will create a new log file (this means that we can delete the log to remove the mess that we have created). In practice, you will usually register your source for the Application log.

using System;
using System.Diagnostics;
using System.Collections.Specialized;

class App
{
   const string sourceName = "Acme_Source";
   const string logName = "TestLog";
   const string msgFileName = "calc.dll";

   static void Main(string[] args)
   {
      StringCollection sysLogs = new StringCollection();
      sysLogs.AddRange(new string[] {"application", "system", "security"});
      if (args.Length > 0)
      {
         if (EventLog.SourceExists(sourceName))
         {
            string log = EventLog.LogNameFromSourceName(sourceName, ".");
            EventLog.DeleteEventSource(sourceName);
            if (!sysLogs.Contains(log.ToLower()))
            {
               EventLog.Delete(log);
            }
         }
         return;
      }

      if (!EventLog.SourceExists(sourceName))
      {
         EventSourceCreationData escd = new EventSourceCreationData(sourceName, logName);
         escd.MessageResourceFile = msgFileName;
         EventLog.CreateEventSource(escd);
         Console.WriteLine("Event source created - restart application");
         return;
      }
   }
}

Compile this code (csc app.cs). Run it without a parameter, then start the registry editor (regedit). Navigate to the registry key for the event log:

HKLM\Services\CurrentControlSet\Services\EventLog

You'll see that there is a new log called TestLog and beneath that is a source called Acme_Source. Open this key and there you'll see a EventMessageFile value with a full path to the event message file. The code gave the name of the file without a path so the path has been added by CreateEventSource. If you are writing an application installation program you will have to make sure that the file is installed in a suitable folder.

It is worth pointing out that the message resource could be bound to the process you created, app.exe, however, in most cases this is not a good idea. The reason is that the message resource file must be available to use FormatMessage to provide the formatted message. If you will access the event log from a remote machine then the resource file must be distributed to another machine, and if the resource is bound to a file which contains code, then that code file will be distributed on to another machine making that code available elsewhere. In most cases you will not want to do this (and anyway, such an action will most likely violate your EULA).

As you can see, to create a message resource file involves several steps, so I have created a makefile to allow you to perform all the steps of creating the message resource, compiling the application and registering the source, all in one go. The makefile can be downloaded from here. Note that if you rebuild the DLL you must make sure that the event log viewer is not running.

Next you will want to report an event. This is a two stage process using the EventLog's methods. First you have to create an EventInstance object, then you have to call WriteEvent. The public interface of EventInstance looks like this:

public class EventInstance
{
   public EventInstance(long instanceId, int categoryId);
   public EventInstance(long instanceId, int categoryId, EventLogEntryType entryType);
 
   public int CategoryId { get; set; }
   public EventLogEntryType EntryType { get; set; }
   public long InstanceId { get; set; }
}

Personally I cannot see the point of this class. All it does is encapsulates the event ID (called InstanceID here), the category and the event type. Where's the replacement strings? Where's the binary data? To supply those you have to pass them to WriteEvent. The overloads of this method are:

[ComVisible(false)]
public void WriteEvent(EventInstance instance, params object[] values);
[ComVisible(false)]
public void WriteEvent(EventInstance instance, byte[] data, params object[] values);
public static void WriteEvent(string source, EventInstance instance, params object[] values);
public static void WriteEvent(string source, EventInstance instance, byte[] data, params object[] values);

It's clear from these methods that the designer intended you to call the method as if it has a variable number of parameters (it doesn't, the params modifier just tells the compiler to create an object array from the parameters passed by the developer). This is why the array of replacement strings is not a member of EventInstance, but the binary data could have been. However, if the designer of this method had not used EventInstance then all that the developer would have to do is add three more parameters to the method, so for the instance methods that would have meant at most a method with five parameters, which is acceptable.

Again, it appears that the designer of the EventLog class does not really know what he is doing, since out of all the possible designs, he chose the worst one.

Now add the following to the end of the Main method.

using (EventLog el = new EventLog())
{
   el.Source = sourceName;
   EventInstance ei = new EventInstance(0x1000, 1, EventLogEntryType.Error);
   el.WriteEvent(ei, "42");
}

First, note that the event ID is 0x1000, this is the value that I gave it in the message compiler script file; I gave it this value for a reason. The category value I use is 1, but note from the message compiler script that I also defined a 'message' with this ID in the message compiler script. The message indicates that something performed a divide by zero error: the message text is Divided %1 by nought. (Or zero if you use a US machine.) So this message is clearly an error message. This code initializes an EventInstance with these values and passes them to WriteEvent. Since the message has a parameter (%1) you have to provide a replacement string and in this case I provide the string 42

Compile this code and run it. Since the event source should have already been registered when you run the application it should report an event. Now start the event log viewer, click on the TestLog entry on the left hand side and you'll see that one error message has been reported, double click on this. Here are the relevant values for a US machine:

Source: Acme_Source
Category: (1)
Type: Error
EventID: 4096
Description: Divided 42 by zero

Notice that the description has been formatted correctly, replacing the %1 with the value 42. However, the Category is not correct, it gives the category value and not the category string. Let's fix that. Close down the properties dialog and then close the event log viewer, de-register the event source by calling the application with a parameter.

Now add the following lines to the code that registers the event source:

if (!EventLog.SourceExists(sourceName))
{
   EventSourceCreationData escd = new EventSourceCreationData(sourceName, logName);
   escd.MessageResourceFile = msgFileName;
   escd.CategoryResourceFile = msgFileName;
   escd.CategoryCount = 1;
   EventLog.CreateEventSource(escd);
   Console.WriteLine("Event source created - restart application");
   return;
}
 
Here you specify the DLL that has the message resources used for the category string, and you specify how many category strings the file contains. You can use a separate message file if you wish, but in this example I have decided to combine the string resources in one file. The reason why the first message in the file is 0x1000, is because these values indicate to the event log viewer that the first resource in the message file is a category string and any resources after that can be used by FormatMessage.

Now compile this application, run it to register the source and then run it again to report the event. Finally open the event log viewer and confirm that now the category is given as the string Calculation.

Close down the event log viewer and unregister the event source.

You have now done all that is necessary to generate localised event log messages. It wasn't difficult was it? I fail to understand why Microsoft decided to provide the WriteEntry method that did this so badly. The only excuses that I can provide is that the designer of the EventLog is either ignorant of how to log events, or is just plain lazy and simply copied the class from unmanaged Visual Basic.

8.8 More About Reporting Events

There is a lot more to message reporting than was given in the last section. In this section I will explain to you some of the more esoteric, and less documented features.

8.8.1 Severity in the Message Compiler Script

Recall that winerror.h documented a severity for the message and in the table of items you can use in the script I gave the following description for the values you can use for Severity:

The default values are Success (0x0), Informational (0x1), Warning (0x2) and Error (0x3), any value that you add to this collection will override the default.

Note that they are similar, but not the same as, the values you can use for EventLogEntryType. This is purely coincidental. The severity has nothing to do with the type of message that is reported. It is used to generate the correct formatted error code for COM errors and will provide the top two bits of the error code. However, it has a side effect of providing the top two bits of the event ID, so if you use a Severity other than Success then thyeevent ID will be different to the one you defined.

To see how this works, change the message compiler script that you used in the last section so that the message has a Severity of Error:

MessageId=0x1000
Severity=Error

Now compile the message resource file and run the application (twice, the first time to register the event source). Run the event log viewer, what do you see for the description of the event? Here's what I get:

The description for Event ID ( 4096 ) in Source ( Acme_Source ) cannot be found. The local computer may not have the necessary registry information or message DLL files to display messages from a remote computer. You may be able to use the /AUXSOURCE= flag to retrieve this description; see Help and Support for details. The following information is part of the event: 42.

This indicates that the event log viewer cannot find the format string for the message. The reason is that by using a severity you have changed the resource ID. Error sets the top two bits, so the event ID is now 0xc0001000. Change the code accordingly:

EventInstance ei = new EventInstance(0xc0001000, 1, EventLogEntryType.Error);

Close down the event log viewer and compile this code, then run it. Open the event log viewer and what do you see?

Source: Acme_Source
Category: Calculation
Type: Error
EventID: 4096
Description: Divided 42 by zero

Notice that the event ID is given as 4096 (ie 0x1000) but you logged the event ID 0xc0001000. In general you should use a severity of Success (ie don't use it in the message compiler script), since, as you have seen, it makes reporting events a little less intuitive and you have not gained anything. A similar thing can be said about the Facility, if you use a value other than Application then extra bits will be added to the event ID (bits 16 to 27). The facility adds nothing to the event log, it just confuses the issue.

So what are these things used for, the severity and the facility? The severity bits are important for COM codes because they are used by code that call COM objects to determine if the status code returned by COM method calls are successful or not. A severity of Success means that a status code can provide additional information about the method call. For example, an enumerator can provide objects, but if you call the enumerator after it has returned all objects then it needs to tell the client that there are no other objects available. This is not an error condition, but it is still different to returning a status code that indicating that objects will be returned.

The facility was intended for another purpose. The original idea was that it would be a 'handle' to some other error object that can provide additional information. Sometime during the evolution of COM Microsoft produced error objects. These implemented the IErrorInfo interface and were attached to the logical thread used by the method call (they were also localisable). At that point the facility became redundant, and these days the facility is simply an additional categorisation mechanism. In fact, facility also complicates things in COM code because if a COM object calls a Win32 API that returns a non-successful status, this should be sent back to the caller of the COM object with a facility of FACILITY_WIN32 (0x0007) that is, the top WORD is usually 0x8007 (to complicate things more, COM SCODEs use just the top bit for severity, set for error, unset for success).

So when defining event messages, you should define neither a Severity, nor a Facility.

Close down the event log viewer, unregister the event source, and change the message file and the application file back to what they were.

8.8.2 Formatting Parameters

If you look up FormatMessage in MSDN you'll see that it mentions that the placeholders can have format specifications in this form: %<n>!<f>! (the exclamation mark is literal) where <n> is a number between 1 and 99 (ie the index of the placeholder) and <f> is the format specifier. This would suggest that you can add formatting specifications to event log messages. The default format specification is !s!, that is, the insert string is, umm, a string. In fact, this facility is not intended for use by the event log.

Let's start by looking at the unmanaged API to report events:

BOOL ReportEvent(
   HANDLE hEventLog, WORD wType, WORD wCategory, DWORD dwEventID,
   PSID lpUserSid, WORD wNumStrings, DWORD dwDataSize,
   LPCTSTR* lpStrings, LPVOID lpRawData);

The replacement parameters are passed through the lpStrings parameter which is pointer to an array of string pointers of size wNumStrings. Recall that the actual replacement strings are written into the event log which implies that ReportEvent will dereference each of the wNumStrings pointers to get the individual characters that make up the strings. Thus, if you pass anything other than a string pointer through the array pointed to by lpStrings then when ReportEvent tries to 'dereference' that value an access violation will occur. This means that the only data that you can pass as replacement parameters are strings. So you cannot use the format specifiers.

I know I am in danger of labouring a point, but I cannot resist it. Take another look at WriteEvent:

[ComVisible(false)]
public void WriteEvent(EventInstance instance, params object[] values);
[ComVisible(false)]
public void WriteEvent(EventInstance instance, byte[] data, params object[] values);
public static void WriteEvent(string source, EventInstance instance, params object[] values);
public static void WriteEvent(string source, EventInstance instance, byte[] data, params object[] values);

The replacement strings are passed through the last parameter of each of these methods. The designer of this class does not know how many parameters that a message format string will have and so needs to allow any number of parameters to be passed. This can be achieved by an array parameter. However, to support the syntactic sugar of allowing a C# developer to provide these optional parameters as actual parameters the array parameter is declared as params. This is purely syntactic sugar. To see why, compare these entirely equivalent methods:

void SyntacticSugar(int one, params string[] optional);
void JustAsGood(int one, string[] optional);

Now look at how these methods are called:

obj.SyntacticSugar(1, "two", "three", "four");
obj.JustAsGood(1, new string[]{"two", "three", "four"});
obj.SyntacticSugar(2); /* no optional parameters */
obj.JustAsGood(2, new string[] /* or possibly null */);

Is the first method better? More readable? Not really, and worse, to support optional parameters a params parameter has to be the last parameter, but if a string array is used then it can be any parameter. Thus I would say that the method with params is not the best of the two. We are all capable developers, so we really don't need to have the array to be hidden from us; there really is no need for WriteEvent to have a params parameter.

Now look at the values parameter again. Look at the type: object[]. Why? We have already established that you can only pass strings to ReportEvent, so WriteEvent can only take strings. If you look at the implementation of WriteEvent using something like Reflector you'll see that the first thing that is done is the method allocates a string array and copy the parameters over. This is a waste of processing time and a waste of memory to allocate another array. Again, it shows that the writer of the method does not know how the event log API works.

So what is the point of the format specifiers in the message file? Well, this shows another use of FormatMessage. If you look again at the documentation you'll see that the last parameter is of type va_list*, that is, a variable sized list that can contain values of different types. (In fact, if you are writing 32-bit code you have the choice of either passing a pointer to a va_list or a pointer to an array of DWORD_PTR values, depending on the flags that you use in the first parameter; for 64-bit code you can only pass a pointer to a va_list.) Thus, if you call FormatMessage directly you can pass any types you like through this parameter. If the parameters originate from the event log, then those parameters will be string pointers, but if you create the va_list yourself, you can pass any types you like. FormatMessage is not just for the event log, and when used by the event log only a subset of this function's functionality is used. If you trawl through the resources of the system DLLs on your machine, you'll find that some messages will use format specifiers, and you will now know that these are not event log format strings, instead, they are messages that will be localised though FormatMessage and then the formatted string is displayed by the application's UI.

8.8.3 Parameter Message File

The EventLog class allows you to register an event source with a parameter resource file through the ParameterResourceFile member of the EventSourceCreationData class. This allows you to have localised parameters in the format string. An example will make this clearer. As mentioned before, the replacement parameters are given in the format %n where n is 1 to 99. The number n is an index in the array of parameters passed to ReportEvent or FormatMessage. You can use another syntax: %%n which represents another level of indirection. In effect, what happens is that %%n means, "replace this place holder with message string number n in the ParameterMessageFile". Again, localization is used, so the locale passed to FormatMessage will be the locale of the string resource in the parameter message file. Of course, the parameter message file strings must not contain placeholders because there is no mechanism to fill them.

To test this out, add the following to the message compiler script:

MessageId=0x2
Language=British
British
.
Language=English
US English
.
Language=French
French
.

(Remember that each message is terminated with a dot.) This defines a message number 2 that simply reflects the localisation language. Add a message that uses this:

MessageId=0x1001
Language=British
Language = %%2
.
Language=English
Language = %%2
.
Language=French
Language = %%2
.

Notice that it uses parameter placeholders. Now change the code so that it registers a parameter message file:

escd.MessageResourceFile = msgFileName;
escd.ParameterResourceFile = msgFileName;

Again, I have used the same resource file, but it could be a separate file. Now add a line to generate this message:

el.WriteEvent(ei, 42);
ei.InstanceId = 0x1001;
el.WriteEvent(ei);

The EventInstance is changed to make sure that it logs the new message, and then the message is generated. This does not pass any insert strings because none are needed. Make sure that the event source has been unregistered and then compile the message DLL, then compile the application, and finally run the application once to register the source.

Now run the application again and then start the event log viewer and navigate to the TestLog log. There should be two events, take a look at the last one. You should find something like this:

Description: Language = US English

Here you can see that the event log viewer interpreted the %%2 string as meaning "load the parameter message file, and replace the placeholder with the message with an ID of 2 using localisation". Bear in mind that the parameter placeholder has a value between 1 and 99. Category values are usually in this range too, so in general you will not want to use the same resource files for categories and parameter placeholder strings.

Now close down the event log viewer and run the application with a parameter to unregister the event source and log.

8.9 Installers

One of the underlying principles of .NET is that the installation of an application should touch as little as possible, and that if it does, the changes should be reversible. As I explained earlier on the page for performance counters the solution is an installer. This is a class that provides the infrastructure for transactional installation, that is, an installation either installs completely or not at all. The previous page explained that the framework provides a class called Installer that acts as a container for task-specific installer objects. These task-specific installer classes will install, or uninstall a particular feature. When you write installation code you derive a class from Installer and use its constructor to create instances of each of the task-specific installers and put them in the Installers collection. This 'container' class should be marked with the [RunInstaller(true)] attribute.

The code is then installed by running the InstallUtil tool on the assembly with the installer collection class (that is, the class marked with the [RunInstaller(true)] attribute). This tool creates an instance of TransactedInstaller and adds an instance of your installer collection class to its Installers collection. The TransactedInstaller class is used because if the installation fails then it will be rolled back with a call to Rollback which will call the Uninstall method of all the installers that have been run; this provides the transactional aspect.

The class you should use to install an event log is EventLogInstaller, the important members of this class are its properties:

public class EventLogInstaller : ComponentInstaller
{
   // Properties
   public int CategoryCount { get; set; }
   public string CategoryResourceFile { get; set; }
   public string Log { get; set; }
   public string MessageResourceFile { get; set; }
   public string ParameterResourceFile { get; set; }
   public string Source { get; set; }
   public UninstallAction UninstallAction { get; set; }

These properties allow you to add the sort of data that you would add through the EventSourceCreationData class. The only additional member that is UninstallAction, which indicates what happens when the application is uninstalled: should the event log and message resource files be removed, or should they be left in place? Bear in mind that if you remove an event source message resource files then there will be no format strings available for any messages that are still left in event logs on the machine. If the application put messages in the Application log (and there are reasons why this makes sense) then if the application's message resource files are removed it may mean that some messages will be unintelligible. If the application logs messages to a private event log (and there are valid reasons for not doing this) then if you use UninstallAction.Remove it means that all messages logged by the application will be removed from the machine and no attempt will be made to make a backup. This will mean that potentially useful information will have been removed. The problem here is that the removed application may have affected the way that other applications run, and without the removed application's messages the other applications' messages could be more difficult to understand. If you use UninstallAction.NoAction then when your application is uninstalled it means that something is left behind. This means that you will have violated one of .NET's tenets.

Unfortunately there is no best practice here, it really is a choice that you should make. My personal opinion is that you should leave the resource files, but that makes me uneasy enough to tell you that perhaps you shouldn't.

8.10 What Should You Log?

You should NOT log trivial messages to the event log. If you do this, then you deserve to have someone stand at your front door 24 hours a day pressing your door bell once a second - that is the best analogy I can present to you of how bad trivial messages are. Coincidentally it is also, in my opinion, what should be done to the author of the EventLog class for inflicting such a poor design on us.

One important point about trace messages (remember them?) is that as much information as possible is traced so that you have enough information to track down problems in your code. Tracing (either debug or trace mode) is completely opposite in nature to event log logging and this means that you should NEVER use the EventLogTraceListener class.

Never, never, never use the EventLogTraceListener class. It is very poor practice to use this and it will reflect very badly on your professionalism if your code uses it.

Assuming that you accept that there is no connection whatsoever between tracing and logging, now you have to decide what is suitable to be logged. The first question is: do you really need your own log? In most cases the answer is: if you are using the event log properly then in most cases you do not need a separate log. In the examples above I have used a separate log because the examples logged trivial messages and this prevented the example code from polluting the system's event logs. However, it does mean that the messages will be isolated, and you will not know if the problems reported by your application had any relationship to messages reported by other applications. This means that if you chose to report messages to a private log, these messages should not be ones that could be related to any other application: they should be isolated messages. However, note that you are not restricted to a single event source in your application, so you can have one source that logs to the Application log, and another that logs to your private log.

If your application is a service, then it will not have a user interface and it will be long lived, in this case logging messages indicating that the service started or stopped to the Application log is acceptable, and it can be useful: if your application stops and starts many times, and no other service has stopped during this period, then it means that there is a problem with your service.

Exceptional conditions should be logged (and most likely to the Application log), but note this means exceptional. A user aborting a transaction is not exceptional, it is something that happens frequently. You may decide that you want to record such an abort condition, in which case, the application's private log is a better place. Successful transactions are a much more frequent event and you may get thousands of these every day. Logging these in any event log is pointless because it swamps the event log, and the API is not designed to handle so many entries. If you want to keep a permanent record of these events then you should do so in an storage mechanism, one that is specially designed to handle a large number of records. Such a facility is a database. A similar thing can be said about logging work requests or things like users 'logging' into your application: these are private data that you should log in a database.

Intermediate results should never be logged to the event log, and should not be logged to a database either. These are items that are traced, and should only appear in debug builds. As you should be aware by now, tracing code should be disabled in release builds. As mentioned above, do not use the EventLogTraceListener class, and do not use the EventLog class to log what are essentially trace messages.

Anyone that has access to the event log has access to all event logs, so you should be careful from a security point of view. Do not log information that can be considered a secret (eg "User RichardGrimes changed his password to 'aubergine'."). Also, from the perspective of your coveted intellectual property, be careful about leaking information about how your application works. The information that you put in the event log is not information that you, the developer, will read, instead, it is information that your customer will read, so only log messages that you want them to read.

Finally, it is worth pointing out that items in the event log cannot be altered using the API. This means that there is no official mechanism to modify or delete an entry. The data remains in the log until the log is cleared, or the event is overwritten. Once you have logged something inappropriate the only way to remedy the situation is to clear the log, something that only a machine administrator should do, and only then, when it is absolutely necessary.

I hope that you enjoy this tutorial and value the knowledge that you will gain from it. I am always pleased to hear from people who use this tutorial (contact me). If you find this tutorial useful then please also email your comments to mvpga@microsoft.com.

Errata

If you see an error on this page, please contact me and I will fix the problem.

Page Nine

This page is (c) 2007 Richard Grimes, all rights reserved