Examine specific events

Last month, I started my examination of the Perl for Win32 EventLog module. I wrote a simple script that writes user-defined events into the Windows NT Application Log. This month, I continue my examination with a reasonably robust script,, that searches NT event logs on one or more servers for a user-specified event.

You can use for troubleshooting distributed applications such as Exchange or Windows Internet Naming Service (WINS). When an Exchange router or WINS replication partner fails, events are written into the event logs of peer systems. You can search through all the Exchange routers' application logs or all the WINS servers' system logs to quickly pinpoint the faulty system. Looking through multiple event logs with Event Viewer is a time-consuming process. The utility will do the work for you.

You can also use as a report generator that verifies the completion of a task across the enterprise. For example, I'm a member of a team that uses a similar script to monitor the successful completion of backups for hundreds of servers. We've scheduled the script to run every morning to check the results of the previous night's backup based on events written into each backup server's Application Log. Systems administrators verify the output report each morning and take any necessary corrective action. The systems administrators have an exception report waiting when they arrive in the morning. They don't have to connect to each system via a GUI to determine the outcome of the previous night's backup.

The Big Picture
Listing 1, page 214, shows the complete code for This script searches the Application, Security, or System Log on any number of hosts for a specific event as defined in the script's configuration file; provides an optional input file command line argument that lets you have multiple configuration files to support different event search criteria; includes a time component that tells the script how far back in time to search; and provides a verbose or non-verbose output report mode. The verbose report includes the total number of event matches along with a printout of each record that matched. The non-verbose report provides only the total number of matches.

Let's walk through to get an idea of how it works. The code at callout A in Listing 1 is the initialization block, which initializes data structures and reads in the default or user-specified configuration file. The code at B generates a unique output report filename, opens the output report file, and writes header information to it. It calculates the time cutoff value that tells the script when to stop searching. The code at C comprises the outer loop that traverses the list of servers. In this section, the code attempts to open and set the initial pointer into the target event log. It also writes the results to the output report.

When has successfully opened a log, the while loop at F reads each event sequentially, extracts the data from the returned event record, and tests for a match. The code at G is a format definition, which uses Perl's powerful format facility to specify a template for the output report. The code at H is the PrintHelp subroutine, which simply prints how-to information to the user's screen if the user enters a question mark as the first command-line argument.

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 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 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 script to support many different search and reporting scenarios.