Monday, August 01, 2005

MASTER THE MYSTERIOUS EVENTLOGRECORD

When dealing with the Windows event log, a developer usually only wants to write events to the log. The Event Viewer, on the other hand, is good at displaying event log messages and lets you view, sort, or filter these messages from a local or remote machine. However, sometimes it's necessary to read events from the log. For instance, if you're dealing with end users and you want the users to be able to click on an icon and have all relevant event log entries sent to you via e-mail.


OVERVIEW

If you look at the documentation for the event-logging API, it appears that reading entries from the log is as simple as this.



DWORD nBytesRead, nBytesMin;
EVENTLOGRECORD record;
HANDLE hEntry = OpenEventLog( NULL, "Application");

While(ReadEventLog(hEvent, EVENTLOG_SEQUENTIAL_READ |
EVENTLOG_FORWARDS_READ,
0, &record, sizeof(EVENTLOGRECORD),
&nBytesRead, &nBytesMin)){
//Do something with record
}

READING AN EVENT

There are a number of problems with the preceding code, which stem from the fact that EVENTLOGRECORD is a dynamic structure. If you look at the documentation for EVENTLOGRECORD, you'll see the following:



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, *PEVENTLOGRECORD;

All of the fields in comments are just examples. Yet if you try to do something like this:



_tcslen(record.SourceName);

you'll get a compile error. All of the fields after DataOffset are one big buffer, and you have to treat it as such to work with it. For instance, to get the length of the SourceName field, you must do the following.



//precord is declared as a pointer to a byte buffer.
DWORD length = _tcslen((TCHAR*)precord + 56);

The magic number 56 happens to be the total number of bytes preceding the SourceName field. So if you want to get the value of the ComputerName field, you use the following code:



DWORD offset = 56 + _tcslen((TCHAR*)precord + 56) + 1;
//The +1 is for the NULL byte
DWORD length = _tcslen((TCHAR*)precord + offset)

The other major problem is that there isn't a way to tell how big the EVENTLOGRECORD is ahead of time without attempting to read it. This means you have to call ReadEventLog log twice: the first time with parameters that you know will fail and the second time with the correct parameters. If you look at the last parameter, nBytesMin, this variable will contain the number of bytes necessary to read the requested number of records. If you didn't catch that, you can read multiple records with each call to ReadEventLog. Here's some code that works:



DWORD nBytesRead = 0;
DWORD nBytesMin = 0;
DWORD nBytesToRead = 0;
BYTE *precord = NULL;
BYTE fake[1];

HANDLE hEntry = OpenEventLog( NULL, "Application");


//This one fails but gives us the event record size
ReadEventLog(hEvent, EVENTLOG_SEQUENTIAL_READ |
EVENTLOG_FORWARDS_READ,
0, fake, 1,&nBytesRead, &nBytesMin);

nBytesToRead = nBytesMin;
precord = new BYTE[nBytesToRead];
ReadEventLog(hEvent, EVENTLOG_SEQUENTIAL_READ |
EVENTLOG_FORWARDS_READ, 0,
precord, nBytesToRead, &nBytesRead,
&nBytesMin);//read one record

Now we extract some fields.

CString SourceName(precord + 56);
CString ComputerName(precord + 56 + SourceName.GetLength() + 1);

SUMMARY

Even though the event log API isn't the most difficult to master, it isn't well documented or obvious. Yet once you master the EVENTLOGRECORD structure, it's smooth sailing.

1 comment:

Anonymous said...

the first interesting description about the buggy structure EVENTLOGRECORD.