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:
//
// 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
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:
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:
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.
| Symbol | Description |
|---|---|
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=(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):
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:
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):
Next, the assembly linker:
al /win32res:calc.res /t:lib /out:calc.dll /embed:dummy.txt,dummy
del dummy.txt
Finally, the C# compiler:
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 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; }
}
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.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:
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 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:
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.
{
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:
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:
{
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;
}
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:
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:
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:
Close down the event log viewer and compile this code, then run it. Open the event log viewer and what do you see?
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:
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:
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 JustAsGood(int one, string[] optional);
Now look at how these methods are called:
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:
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:
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.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:
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:
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:
{
// 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
