This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.


MIND


This article assumes you're familiar with ISAPI, Windows NT 4.0 and C++.
Download the code (76KB)
View Figure 4 from the magazine.

Using a Web Server for System Performance Monitoring and Management
Michael Swartzendruber

How can a weasel help you keep an eye on system performance? The WEASEL (Web Enabled Access to System Event Logs) system introduced here shows how you can put system monitoring tools into ISAPI DLLs.
There are lots of uses for Web-based software, from Jane Doe’s home page to the ultrasophisticated e-commerce sites now cropping up. I’d like to show you another interesting way you can put a Web server to work for you: as a system monitoring and management tool.
      A Web server has many uses as a component of a network-based system management solution. Generally, network management tools are designed to monitor the health of servers and other hardware on an intranet. But if your intranet is complex you could end up needing many management tools to observe the network. Furthermore, most of these tools are full-blown applications, with limited utility for road warriors or off-site support staff.
      These days, it’s safe to assume that everyone has some form of Web browser on their machine. If you could Webify these management tools to take advantage of these browsers, then everyone could benefit. That is what this article is about. Specifically, I’ll introduce the notion of using server-side code (in the form of ISAPI DLLs) to perform system monitoring and management tasks. These DLLs can be written to perform virtually any task, and can query any host on the network—they’re not limited to the local host. Since these code modules are implemented as server-side code on a Web hub, the user is no longer limited to using vendor-specific or proprietary management tools. All the user needs to monitor and manage any system is a Web browser like Microsoft Internet Explorer.
      By the time you complete this article, you should have enough information to start implementing your own solution for monitoring the servers on your intranet through a Web interface.

WEASEL
      The examples in this article are based on a sample app I built called WEASEL (Web Enabled Access to System Event Logs). My server-side code will retrieve the Windows NT® event log from any Windows NT-based host on an intranet. The log records from the event log will be marked up using HTML so they can be viewed with any browser capable of displaying tables. To accomplish this task, the project includes a small number of concrete classes that can be used and extended for any other similar purpose.
      The ISAPI DLL class for WEASEL is defined in CWebEventProbe.h and implemented in CWebEventProbe.cpp (see Figures 1 and 2 ). Experienced ISAPI developers will see a pretty run-of-the-mill implementation here. Most of the action takes place in the SendEventLog method. However, outside of a few parameter validations, this method passes off all the actual work to subordinate classes. The operations of these classes will be described later.
      So, for now, the ISAPI portion of WEASEL serves as little more than an I/O point for this module, taking arguments from the browser page and forwarding these arguments to other classes for actual processing. As this processing is completed, the subordinate classes return the results to the caller through the CHttpServerContext pointer passed into the SendEventLog method.

Splitting Logs
      I found that (at least to the uninitiated) interacting with the Windows NT event log is a convoluted process. There are a few reasons for this:

  • The need to use the registry as a pointer to various information about the log file records
  • The normalization of the log file (more on this later)
  • The lack of a single source that provides a complete explanation of how this portion of the Windows NT API operates
      The information included here about using the EventLog API should give you a head start. On the surface, the EventLog API is pretty simple. You just open a connection to a log file, count the number of records in it, and then loop through each of the event log records. And in all fairness, this part of the task is pretty easy. The work required to do these simple operations is encapsulated in a class specifically designed to handle these chores (the class is defined in CEventLog.h and implemented in CEventLog.cpp—see Figures 3 and 4). The ISAPI DLL invokes methods in the CEventLog class to perform these steps before sending the log back to the requester. Interested readers will discover that the calls to CEventLog::OpenEventLog and CEventLog::GetRecordCount do little more than wrap the Windows NT API calls for these tasks.
      Once a connection to the Windows NT event log has been formed and the number of records to be read is known, it’s a simple exercise to enter a read loop to get each record in the log. Lest you be lulled into a sense of complacency, though, the real fun begins once you have a record in scope. I’ll start my explanation at the CEventLog::ReadRecord method in CEventLog.cpp.
      The first thing you must account for is the fact that an event log record is a variable-size structure. For each record to be read, a "dummy read" must be issued to determine just how many bytes are required to hold the current event log record. The CEventLog::ReadRecord call performs this step by passing a value of 0 for the dwReadSize parameter. This invokes a call to the Windows NT API ReadEventLog, passing 0 for the size of the available output buffer. It essentially asks this API for the number of bytes required to hold the event log record.
      This is a common approach used by other Windows NT API calls that can return a variable length of data. Now that you know how many bytes the event log record requires, an appropriate amount of space can be allocated. Then you issue another call to the ReadEventLog API, this time passing the preallocated buffer and the size of this buffer. This triggers an actual read of the event log record, placing the data into the process space.

Here is Where Some of the Fun Begins
      Once you have the raw data from the event log, it still needs to be manipulated a little more before it is useful. To help accomplish this task, I have an EventLogRecord class (declared in CEventLogRecord.h and implemented in CEventLogRecord.cpp—see Figures 5 and 6 ) that knows how to set its internal members from an EVENTLOGRECORD variable-size structure. When Windows NT has given you an EVENTLOGRECORD, pass this record into the CEventLogRecord class for further processing.
      The CEventLogRecord::CopyFrom method (in CEventLogRecord.cpp) is where the data gets repackaged. Some of the copying is rather straightforward, as can be seen by the section of this method where members of the EVENTLOGRECORD structure are copied directly into class members. Things get more interesting when dealing with the variable-length section of the EVENTLOGRECORD.
      First, I copy off a variable number of bytes from the record into the CEventLogRecord class member byte arrays m_oUserSID and m_oData. These are pretty straightforward copies of byte data because the EVENTLOGRECORD provides an offset to start the copy from and the number of bytes to copy. Next, I copy off the SourceName (I'll need this later) from the EVENTLOGRECORD. The SourceName occurs just after the end of the actual EVENTLOGRECORD, so I'll set my offset pointer to that location and then scan bytes (characters) until I encounter a NULL terminator. After this, I'll increment the pointer offset, scanning past any additional NULLs that may be embedded in the record.
      Now, I'm at the place in the record where I can find the portion of the record that holds the computer name. I perform a copy loop until I find the NULL terminator at the end of the computer name. Finally, I have to copy a variable number of strings. The CEventLogRecord class uses a string array to store these. The CopyFrom method has one more loop that is designed to copy the number of NULL-separated strings that the EVENTLOGRECORD structure says it contains.
      Now that I've gotten this far, all I have to do is denormalize the data. Then I can return the event log record to the ISAPI DLL for display purposes. But, I'm getting a little ahead of myself here; I'm not quite done massaging the event log record data.

How Normal is This Data?
      For those of you who aren't familiar with the practice of data normalization, I'll give you a brief explanation. This will be useful as I describe what happens next. Normalization is a process of optimizing storage space through various techniques. There are several different forms of optimization, with the most common techniques referred to as ranging from First Normal Form up to Fifth Normal Form.
      The event log seems to have been designed with Second Normal Form in mind. Second Normal Form is a process intent on removing redundant or repeating data from the primary storage area (the event log file in this case) and storing a key in place of the repeating data. The key can then be used to look up the repeating data when it is required.
      This makes the size of the event log file more manageable because it does not store repeating data. In my opinion, the architects of the Windows NT event log system used a reasonable approach to storing this data without using any more disk space than is absolutely required. This is a technique you may want to keep in your own personal bag of tricks should you ever need to create some kind of logging system. This also means that (and this is a bit more important in terms of WEASEL) my work in manipulating the event log record is not finished, because I have to use keys that are embedded in the event log record and system registry) to reconstruct the entire event log record for display.

Where Did I Put My Keys?
      One of the data elements I captured from the EVENTLOGRECORD was the source name. This value directs me to a particular subkey in the registry. Each registered source of event log records is required to put an entry in the system registry in the following path:


 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\EventLog\
As shown in Figure 7, the source name from the event log record points to one of the keys under this path. Each of these source keys contains a number of value entries. The ones that are most significant for now are CategoryMessageFile and EventMessageFile. The values for these entries indicate a file name containing a message string that is assigned to the event ID or category ID. In rough terms, these files have a set of string resources embedded in them. These resources have numeric IDs associated with them. Therefore, to get any particular message string, I need to know the file name and the string ID.
      In the case of the Windows NT event log, the file name is found in the registry key paths and the string IDs are found in the event log record. These two items together represent the key that I need to complete my preparation of the event log record for display.
      This work is done in the CEventLog::GetRecordInfo method in CEventLog.cpp. Here I invoke the actions of the CRegistry class, which provides wrapper services around the Windows NT registry. I use this class to connect to the registry as well as to read the values of certain keys. In this case I use it to extract the file names for the EventMessageFile and the CategoryMessageFile keys under the Source key that is indicated by the event log record.
      In some cases, there may not be full path information to the files of interest. Instead, the path may contain strings that look like this:

 %SystemRoot%\System32\MsObjs.dll 
Because of this, you have to either figure out the system root directory and tokenize this path yourself, or you can make a call to the little-known Windows NT API ExpandEnvironmentStrings, which converts the %SystemRoot% section of the path into the actual path for that value. Now, armed with the full path, I can attempt to load the file containing the message string so that I can extract that string for display purposes.
      When I translated the EVENTLOGRECORD structure earlier, I copied a variable number of strings into the CEventLogRecord class. This is where they come back into use. Typically, the string loaded from the file will be a format string; in other words, it will have any number of %s arguments embedded in it for the purposes of a sprintf call. The strings that were stored in the EVENTLOGRECORD are the arguments to that sprintf call. Therefore, I have to form a va_arg-style arglist from this list of strings. Once I've done that, I can pass them off as an argument in a call to FormatMessage. This call gets the message string from the file indicated in the EventMessageFile value, performs the sprintf operation against that string and the variable number of arguments I pass to it, and presents the final display message. The rest of this function performs some error trapping, and will provide default messages for display should something fail.
      This scheme does present a bit of a problem: when you are probing the event log of a foreign host, the event log may indicate sources or DLLs that aren't loaded on the local host. This can cause this mechanism to fail because it can't load the message files for display. I must admit that I don't have that particular problem worked out yet.

Are We There Yet?
      Essentially, all that remains now is to put the event log record into a displayable form. I have some simple HTML pagination methods that I'll employ to do this work. After each record has been completely prepared for display, I'll send it into the paginator methods. These methods have the simple task of transforming the event log record into an HTML table row. This marked up version of the event log record is sent back to the requesting browser through the CHttpServerContext that was passed into the SendEventLog method of the ISAPI DLL.
      In the first incarnation of WEASEL, it became apparent that getting an entire event log of any type could be an expensive operation due to a couple of factors. One is the cost of iterating through the event log itself. The second expense was having the browser paint a table containing an entire event log. To get around this problem, I added the ability to page through the event log; the weasel.html form lets you select the number of records that you would like to have fetched and displayed in the browser (see Figure 8).
      To support this functionality, it became necessary to store some context information for each user's previous requests so that when they entered a Get Next Page request, the WEASEL DLL could know the source server and log type for the request. In addition, there would have to be some information maintained for the current log file "page," and record depth information so that each subsequent request would retrieve and display the same number of records.

Figure 8: The Weasel Test Page
Figure 8: The Weasel Test Page

      The solution to this problem was implemented using HTML hidden variables. So the last task weasel.dll performs for each request is to store some context information that it returns to the user who made the request. After the event records have been retrieved and formatted, the DLL will store the parameters for the fetch in the reply page. These context variables are then wrapped in a simple form that the user can click to get the next set of records.
      Hidden variables are very useful for this type of feature. And implementing them in this fashion prevented the problems I experienced in trying to shove too much data into the browser. This is also a good improvement because the user has the ability to page through the data in a manner that lets them analyze the log content in a piecemeal fashion, as opposed to being presented with the entire log at once.

Installation and Use
      This step couldn't be easier. All you need to do is build the ISAPI DLL from the source provided. After you do that, simply place the DLL into the Scripts home directory of your Web server. Next, you'll need to place weasel.html and the three GIF files into your Wwwroot path, or in any other aliased path you'd like. Now just invoke weasel.dll's functionality through the weasel.html page. That's all there is to it.
      To use this system, simply type in the name of the computer whose event log you'd like to read, and push the Get Log button. After a few seconds, you should see the results coming back into your browser (see Figure 9).

Wrapping Up
      Now, what's the WEASEL good for? Well, if you don't have access to the Event Viewer program and you need to view a log from some host on your network, this system can get you there.
      But this is only the beginning of what can be done when you put system monitoring tools into ISAPI DLLs. It isn't too hard to imagine SNMP functionality packaged in an ISAPI DLL. Other useful ideas include using ISAPI DLLs to monitor file system space, check the state of other processes, or even pull across other ASCII log files when directory browsing is disabled. These are just a few of the management-style tasks I can think of; I'm sure you can come up with more.
      A Web server gives you another tool for looking at various Windows NT-based hosts on a network. And since the code is all located on the server, your client requirements are reduced to any mainstream Web browser and an HTML file. Finally, since it is server-based, you can also implement any level of security you deem necessary.

From the September 1998 issue of Microsoft Interactive Developer.