Reading the Event Log

[Previous] [Next]

The Event Viewer snap-in that ships with Windows is usually sufficient for most event-reading needs. However, Windows does provide functions that allow your own applications to access event logs. There are lots of possibilities for a feature like this—for example, you could write an application that sends an e-mail when it detects that an event entry of a certain ID is being added to an event log.

To get started reading the event log, your application must first retrieve a handle to a log by calling OpenEventLog:

 HANDLE OpenEventLog(     PCTSTR pszUNCServerName,     PCTSTR pszLogName); 

The pszUNCServerName parameter identifies the machine containing the event log you wish to access. The pszLogName parameter identifies the specific log on the server machine. Once you have a valid handle to the log file, you are prepared to read events. As always, you should close the handle when you are finished accessing the log file by calling CloseEventLog:

 BOOL CloseEventLog(HANDLE hEventLog); 

Reading events requires a call to ReadEventLog:

 BOOL ReadEventLog(    HANDLE hEventLog,    DWORD  dwReadFlags,     DWORD  dwRecordOffset,     PVOID  pvBuffer,     DWORD  nNumberOfBytesToRead,    PDWORD pnBytesRead,     PDWORD pnMinNumberOfBytesNeeded); 

The hEventLog parameter is the handle returned from OpenEventLog. The dwReadFlags parameter identifies whether you will read the log sequentially or start reading from a specific record. Table 6-5 lists the possible flags for dwReadFlags. A common value to pass is EVENTLOG_FORWARDS_READ | EVENTLOG_SEQUENTIAL_READ.

Table 6-5. Flags that can be passed for ReadEventLog's dwReadFlags parameter

Flag Description
EVENTLOG_SEEK_READ Allows you to specify a zero-based index in the log file for the event that you want to begin reading from. You specify the index with the dwRecordOffset parameter. You must select either seek or sequential reading.
EVENTLOG_SEQUENTIAL_READ Indicates that you will be reading the event log sequentially, starting with the event record that follows the most recently read record. This is the most commonly selected read type for the event log.
EVENTLOG_FORWARDS_READ Indicates that you will be reading forward through the event log file. This flag can be used for either sequential or seek reads.
EVENTLOG_BACKWARDS_READ Indicates that you will be reading backward through the event log file. This flag can be used with either sequential or seek reads.

To receive event log data, you supply a buffer pointer (pvBuffer) and a buffer size in bytes (nNumberOfBytesToRead) to ReadEventLog. If your buffer is not large enough to read the next record in the log, the function will fail, and GetLastError will report ERROR_INSUFFICIENT_BUFFER. The variable pointed to by the pnMinNumberOfBytesNeeded parameter will contain the number of bytes needed to read a single record. However, if your buffer is large enough for one or more records, ReadEventLog will fill your buffer with the data for as many records as will completely fit in your buffer. I suggest your application make a single call to ReadEventLog to find out the necessary buffer size for reading a single event, allocate the buffer, and then call ReadEventLog again to read the log. Although you can read multiple events at a time, doing so is not significantly more efficient, and it complicates your parsing code because the data returned is of variable length.

If no more records are left to read, ReadEventLog will return FALSE, and GetLastError will report ERROR_HANDLE_EOF.

After you successfully call ReadEventLog, your buffer will contain one or more EVENTLOGRECORD structures. This structure is of variable length and is declared as follows:

 typedef struct _EVENTLOGRECORD {    DWORD Length;     DWORD Reserved;     DWORD RecordNumber;     DWORD TimeGenerated;     DWORD TimeWritten;     DWORD EventID;     WORD  EventType;     WORD  NumStrings;     WORD  EventCategory;     WORD  ReservedFlags;     DWORD ClosingRecordNumber;     DWORD StringOffset;     DWORD UserSidLength;     DWORD UserSidOffset;     DWORD DataLength;     DWORD DataOffset;     //     // Then follow:     //     // TCHAR SourceName[]     // TCHAR Computername[]     // SID UserSid     // TCHAR Strings[]     // BYTE Data[]     // CHAR Pad[]     // DWORD Length;     //  } EVENTLOGRECORD;  

A precious few members of this structure are straightforward, and you should recognize them immediately as the EventID, EventType, and EventCategory fields. But the other members of this structure get more complex from here.

Let's start with the TimeGenerated and TimeWritten values. As you might have guessed, they correspond to the date and time the event was generated and written to the log, respectively. However, the format of the time value is not one commonly used with Win32 API functions, so it may seem awkward at first. Here is what the documentation has to say about this time format: "This time is measured in the number of seconds elapsed since 00:00:00 January 1, 1970, Universal Coordinated Time." This format is similar to that for the C runtime's time_t type, except that the time values for events or "event times" are unsigned. What does this mean? It means that you will have to jump through a couple of hoops to get event time values into a useful time format such as the SYSTEMTIME structure.

If you are familiar with the different time structures supported by Windows, you might recognize the event time format as being similar to the FILETIME format. The FILETIME format, however, is defined as a 64-bit value representing the number of 100-nanosecond intervals since 00:00:00 January 1, 1601. Since Windows provides useful functions for converting from FILETIME to SYSTEMTIME, our best move is to convert our event time to a FILETIME value. Somewhere in the process we should adjust for local time. (Remember that event times are Universal Coordinated Time.) The following code wraps all of this logic in a simple function:

 void EventTimeToLocalSystemTime(DWORD dwEventTime,     SYSTEMTIME* pstTime) {    SYSTEMTIME st1970;    // Create a FILETIME for 00:00:00 January 1, 1970    st1970.wYear         = 1970;    st1970.wMonth        = 1;    st1970.wDay          = 1;    st1970.wHour         = 0;    st1970.wMinute       = 0;    st1970.wSecond       = 0;    st1970.wMilliseconds = 0;    union {       FILETIME ft;       LONGLONG ll;    } u1970;    SystemTimeToFileTime(&st1970, &u1970.ft);    union {       FILETIME ft;       LONGLONG ll;    } uUCT;    // Scale from seconds to 100-nanoseconds    uUCT.ll = 0;    uUCT.ft.dwLowDateTime = dwEventTime;    uUCT.ll *= 10000000;    uUCT.ll += u1970.ll;    FILETIME   ftLocal;    FileTimeToLocalFileTime(&uUCT.ft, &ftLocal);    FileTimeToSystemTime(&ftLocal, pstTime); } 

Well, now that the mystery of the time values is solved, let's move on to the SourceName and Computername members. These are both string values that are somewhat awkward to retrieve. Although the EVENTLOGRECORD structure provides offsets to some of the variably positioned items in the structure, the designers of the system apparently didn't feel the need to be consistent. As a result, SourceName is simply defined as the zero-terminated string immediately following the DataOffset member of the structure. Similarly, the Computername string starts on the first character following the SourceName string. I have thrown together a couple of useful macros (included in EventMonitor.cpp on the companion CD) to ease the pain of extracting these values from the EVENTLOGRECORD structure. These macros will work with source modules built using either Unicode or ANSI strings.

Fortunately the EVENTLOGRECORD structure includes the UserSidOffset and StringOffset values for accessing the UserSid and Strings members gracefully. These offsets start from the beginning of the structure and are measured in bytes. The UserSid is a SID structure identifying the user for which the event was logged. In Chapter 9, I discuss how to convert a SID structure into a human-readable user name. The Strings array is an array of pointers to the expansion strings, which were passed to the ppszStrings parameter of ReportEvent.

Converting a Message ID to a Human-Readable String

You are probably wondering how you are supposed to get the text for the event category and the detailed description of the event. Knowing what you know about event reporting, you could infer that it would be possible to look up the event source in the registry, load the appropriate message DLLs, and extract the detailed category and message from the resource manually—but that would be unnecessarily messy, wouldn't it? Well, it would, but the method I described is the only way to retrieve the event text. Fortunately the system does implement a handy function named FormatMessage that extracts the resource text, but the rest of the task is up to us. The EventMonitor sample application, discussed at the end of this chapter, shows how to use FormatMessage for event reading. The function is defined as follows:

 DWORD FormatMessage(    DWORD  dwFlags,    PCVOID pSource,    DWORD  dwMessageId,    DWORD  dwLanguageId,    PTSTR  pBuffer,    DWORD  nSize,    va_list *Arguments);  

I would have liked to see a pair of functions defined as follows:

 PTSTR GetEventCategory(    PTSTR           pszLog,    PEVENTLOGRECORD pelr); PTSTR GetEventMessage(    PTSTR           pszLog,    PEVENTLOGRECORD pelr); 

These functions would take a pointer to a log file and an event record from that log file, and then return a buffer containing the requested text. The returned buffer could be freed using LocalFree in the tradition of the FormatMessage function. However, the system doesn't provide us with these lovely functions. So I took it upon myself to implement them as a macro and a function that wrap my own FormatEventMessage function. The complete code for this function is available in EventMonitor.cpp on the companion CD. Let me point out some highlights.

You probably remember from our earlier discussion of reporting events that the message DLLs are stored in registry values named EventMessageFile, ParameterMessageFile, or CategoryMessageFile, depending on which message you are attempting to find. If the event source in question is named MySource and is logged to the Application log, the registry looks like this:

 HKEY_LOCAL_MACHINE    SYSTEM       CurrentControlSet          Services             EventLog                Application                   MySource                      CategoryMessageFile                      EventMessageFile                      ParameterMessageFile 

It is your application's job, when reading events, to locate the appropriate registry value and parse the string for the one or many message DLLs that contain the desired message string. Remember, you must pass the semicolon-delimited string of message files retrieved from the registry to ExpandEnvironmentStrings before using the string to load the modules into your address space, because the registry value type for these values is REG_EXPAND_SZ.

After you have retrieved your list of message DLLs and EXEs, you must load each one in turn (from left to right) into your process's address space using LoadLibraryEx. Use LoadLibraryEx rather than LoadLibrary, because LoadLibraryEx allows you to load a module as a resource-only module by passing the LOAD_LIBRARY_AS_DATAFILE flag. After you have retrieved an instance handle from LoadLibraryEx, you pass it, along with a message ID and expansion strings, from the EVENTLOGRECORD structure to FormatMessage. If the call to FormatMessage succeeds, your message has been located, and you can unload the library and return the message text. If it does not succeed, you must unload the message DLL, and then load and try the next message DLL. If you will be extracting message text for more than a single event, you might find it advantageous to optimize your code to avoid repeated loading and unloading of message DLLs.

Although FormatMessage automatically expands your strings into the message, it does not automatically expand messages from the ParameterMessageFile DLLs into your message. Your code must manually scan for instances of "%%" in the string returned from FormatMessage and, using the same algorithm, replace them with the text extracted from the ParameterMessageFile DLLs. The EventMonitor sample application in this chapter shows how to do all of this properly.

One final point about FormatMessage before we move on: If the message text calls for more strings than the number of strings you pass to FormatMessage, the function will blindly attempt to access the nonexistent strings. Most likely, this will cause an access violation, meaning that you either need to parse the string and precount the number of strings expected, which ensures that the correct number of strings is passed, or—and this is the easier approach to take—wrap the call to FormatMessage in a structured exception-handling frame that handles the access violation gracefully. Remember that the system makes no promises about the correctness of the events reported by other applications.

While we are on the topic of robust event viewing, I should point out that it is also possible for an event source to have an invalid or nonexistent event message file. Your code should deal gracefully with situations like these. For example, the Event Viewer snap-in shows an improperly logged event, as shown earlier and in Figure 6-8.

Figure 6-8. An event in Event Viewer where message file information could not be found

As you can see, event reading is not a trivial problem. Because of the complexity of this task, you might find it helpful to adapt your event reading code from the sample code for this chapter.

Before I move on to the next section, I feel compelled to mention the BackupEventLog, OpenBackupEventLog, and ClearEventLog functions, not because they directly relate to event reading, but because many event-reading tools use these functions to offer additional functionality. These functions are prototyped as follows:

 BOOL BackupEventLog(    HANDLE hEventLog,     PCTSTR pszBackupFileName);   HANDLE OpenBackupEventLog(     PCTSTR pszUNCServerName,    PCTSTR pszFileName);   BOOL ClearEventLog(    HANDLE hEventLog,    PCTSTR pszBackupFileName); 

BackupEventLog creates a file using the provided filename and then copies the contents of event log (identified by the hEventLog parameter) to this new file. This function allows an administrator to save the history of events.

To open the event history and read its events, an application calls OpenBackupEventLog, which returns an event log handle (just like the OpenEventLog function discussed earlier). Using this handle, you can call the other familiar event log functions to retrieve the stored event entries. When you are finished retrieving the entries, you close the event log handle by calling CloseEventLog. Backing up an event log can be very helpful for archiving and later examining the event log, and it eases the customer's task of wrapping up a log and shipping it back to you for troubleshooting.

The last function, ClearEventLog, simply erases all the event entries from a log file opened by OpenEventLog or OpenBackupEventLog. For convenience, this function allows you to back up the log file before erasing the entries. You can pass NULL for the pszBackupFileName parameter to clear the log file without producing a backup file.

Event Notification

You can have the system notify you as events are added to an event log. For example, if your favorite chat server logs an event for every client connection as well as every client disconnection, you could write a utility to wait for notification of these events and maintain a running log of connections to your chat server. To receive event log notifications, you must call the NotifyChangeEventLog function:

 BOOL NotifyChangeEventLog(    HANDLE hEventLog,    HANDLE hEvent); 

The hEventLog parameter is the handle returned from a call to OpenEventLog, and the hEvent parameter is the handle of a previously created event kernel object. When the system detects a change to the event log, the system automatically signals the event kernel object. A thread in your application will detect the signaled event kernel object and then do whatever processing it desires to the event log.

You should know several facts about NotifyChangeEventLog. First, once you have associated an event object with an event log, there is no way to turn off notification to that event object short of calling CloseEventLog. This is typically not a problem, however, since you can simply choose to pass your event object handle to CloseHandle and create a new event kernel object if needed.

Second, the system signals the event by calling PulseEvent when a change is made to the event log. This means that you do not have to reset the event and that you must have a thread waiting on the event consistently; otherwise, you're likely to miss some notifications.

Third, the system does not promise PulseEvent for every event record added to the event log. Rather, it pulses your event roughly every 5 seconds if one or more changes were made to the event log during that 5-second period. So if you are waiting on the event object, and then read the log as a result of a pulse, you should not assume that only one event has been added, and you should read until you have reached the end of the log.

Last, the system pulses the event when any event record is added to any log file, regardless of which log you specified when calling NotifyChangeEventLog. So your thread might wake up when an event is added to the System log even though you wanted only notifications from the Application log. Your application must be able to gracefully handle receipt of a notification even when nothing has changed in the log.

The EventMonitor Sample Application

The EventMonitor sample application ("06 EventMonitor.exe") demonstrates how to read event records from an event log. In addition, the sample application calls the NotifyChangeEventLog function and updates its display as new entries are added to the event log. The source code and resource files for the application are in the 06-EventMonitor directory on the companion CD. When the user executes the EventMonitor sample application, the dialog box shown in Figure 6-9 appears.

By default, EventMonitor shows the contents of the local machine's Application event log. But you can easily select a different machine or log and then click the Monitor button to dump and monitor the newly selected machine's log. The sample only allows you to view the System, Security, and Application logs, but the source code can easily be modified so that the application monitors any custom logs you may create.

click to view at full size.

Figure 6-9. The dialog box for the EventMonitor sample application

When you start monitoring an event log, EventMonitor creates an entry in its list box for every entry in the selected log. Once the list box is full, EventMonitor calls NotifyChangeEventLog and has a thread that waits for new event log entries to appear. As new entries appear in the system's event log, EventMonitor retrieves the new entries and appends them to the list box as well. The last thing that EventMonitor demonstrates is how to convert category and message IDs into the proper string text. As you select entries in the list box, EventMonitor loads in the proper message file or files, grabs the appropriate string text, and displays the string text in the read-only edit box at the bottom of the dialog box.



Programming Server-Side Applications for Microsoft Windows 2000
Programming Server-Side Applications for Microsoft Windows 2000 (Microsoft Programming)
ISBN: 0735607532
EAN: 2147483647
Year: 2000
Pages: 126

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net