The Details
The script begins with a statement that includes the Perl for Win32 EventLog module. This step lets you use the EventLog functions and methods. Next, the code declares the @EventTypes array that contains the valid event types for NT.
The %LegalParams hash identifies valid configuration file parameters. (You can see an example configuration file with additional usage instructions on the Windows NT Magazine Web site at http://www.winntmag.com.) Next, the script fetches and processes the first command-line argument ($ARGV\[0]). If the user types a question mark, the script calls the PrintHelp subroutine. If the user types something other than a question mark, the program assumes the input is a configuration filename and assigns it to the $configfile scalar. If the user doesn't provide an argument, the script uses the default configuration file, elparser.ini.
After processing the command-line argument, the script opens the configuration file and uses a while loop to parse it. I use two Perl regular expressions to do the parsing. (Regular expressions express string pattern matches. For more information, consult the references in the online sidebar, "Perl Resources," at http://www.winntmag.com.) The first regular expression skips over commented lines (lines beginning with #) or blank lines. The second regular expression matches and parses the remaining configuration file lines. When it finds a successful match, the regular expression remembers the parts of the string that matched, and stores them in the special read-only variables, $1, $2, and so on. The script assigns the remembered parts to the scalars $param and $value, verifies that each parameter is valid as defined in the %LegalParams hash, and if it is, creates a new scalar using $$param as the new scalar's name. Notice that I use the keys of the %LegalParams hash to dynamically create scalars of the same name. The elsif works similarly to create the @servers array. I take the %LegalParams key "server," attach an @ symbol as a prefix, append an "s", and automatically create an array named @servers.
The code at B in Listing 1 sets up the output report. The script retrieves and formats the current date and time. It uses this information to create a unique output report filename, such as elparser_103197@1300.txt. Next, the code opens the file and prints the configuration data to it. The last line at B calculates the time at which the code stops searching through a log based on the user-specified $timewindow (in seconds). The code takes the user's value and subtracts it from the current time (measured as the number of seconds since 01 Jan 70) to come up with the cutoff time.
At C, the code begins looping through the list of servers. It prints a simple string to the console that will provide visual feedback that the script is working. The script can run for hours or even days, depending on the configuration, the number of servers, and the state of the target logs. The script initializes five variables that track the status of each server's search: $eventsfound holds the number of successful matches, $numevents tracks the number of events in the opened log, $oldestevent holds the record number of the oldest entry currently in the opened logs, $EventObj is the handle to the currently opened log, and @EventList is the array that holds each matched record if the user specified verbose mode.
At D, the EventLog module's Open function obtains a handle to the target server's event log. If the Open succeeds, it returns a valid handle in $EventObj. Subsequent calls to the EventLog methods reference this handle. For example, GetNumber returns the number of event records in the opened log. This value prevents attempts to read beyond the end of a log. If GetNumber returns 0, you have an error (possibly resulting from a failed Open, an empty log, or some other error). In any case, the script writes the error status to the report and proceeds to the next server. The error in the output report tells the user to investigate.
If GetNumber succeeds, GetOldest returns the record number of the oldest entry currently in the target event log. The sum of the two values $numevents and $oldestevent lets the script compute the most recent record number, which is the first record you want to read. The way NT manages events makes this approach necessary. Each NT event has a corresponding record number that the EventLog service assigns to it. This number begins with 1 when you start the server for the first time or when you purge the log using the Event Viewer's Clear All Events option. From this point forward, the record numbers increase by one for each event written into the log. Over time, the older events are overwritten based on the Event Log Wrapping selection. When you obtain the number of events currently in the log and the record number of the oldest event, you can easily calculate the record number of the most recent event by adding these two values together, as the call to the Read method at E does.
The Read method expects three arguments: a read flag that defines the read mode and direction, a record offset that tells the Read method which event log record to start reading from (the code uses this value only with the EVENTLOG_SEEK_READ read flag), and a variable to hold the returned event information. The read flag can be any combination of the flags defined in Table 1, page 213.
I use the (EVENTLOG_SEEK_READ | EVENTLOG_BACKWARDS_READ) flags and the sum of $numevents and $oldestevent as the record offset to set the initial read location at the beginning of the log. The inner while loop, which traverses a single log, executes next. Notice that subsequent calls to Read use the (EVENTLOG_SEQUENTIAL_READ | EVENTLOG_BACKWARDS_READ) flags with a record offset of 0, which tells Read to proceed to the next event record in reverse chronological order.
Through each iteration of the while loop, the program fetches the next event record, extracts the returned event record information, tests for a match, increments the $eventsfound counter when a successful match is found, copies the resulting event data to a buffer in case the user specified verbose mode, and decrements $numevents before proceeding to the next record. The script traverses the log until it reads an event that is older than $timewindow (calculated at the end of B) or it runs out of events (it has reached the beginning of the log). At this point, the script exits the while loop, writes the findings to the output report, and proceeds to the next server.
Screen 1 shows the resulting output report based on the configuration file. If you run the script in non-verbose mode, you receive the same information without the three event records.
Make It Work for You
As you can see, Perl for Win32 makes working with the NT event logs a snap. You can quickly modify the elparser.pl script to support many different search and reporting scenarios.