Developer's Guide

  • Docs Home
  • Community Home

5. Create a Performance Collector

A performance data collector gathers the current statistics of items such as the amount of space used in a file system. The data can be collected using either a script or an SNMP command. For our Filesystem data, we must create a new data collector called Filesystem (this is a special name) that will return a property called usedBlocks (another special name).

If your operating system's MIB provides a usedBlocks (or something named like that) value, then we can make use of existing Zenoss infrastructure and just collect that data using SNMP. Otherwise, you need to create a script to take the total size of the filesystem (totalBlocks) and subtract the freeBlocks value. Unfortunately, AIX only provides freeBlocks, so we need to create a command.

With the Zenoss 2.4 release, we can also create a command parser to gather our performance information. This new functionality allows you to write simple code to gather performance data and graph the results.

5.1. Performance Data Collector Code

Multiple collectors for different components of a system can be created, or one huge collectors for everything can be created. Smaller collectors are preferred for maintenance reasons. The following collector is for calculating file system free space, and would live in the libexec/ directory of your ZenPack.

#!/usr/bin/env python

"""Gather used disk space statistics for AIX"""

import sys
import re
from subprocess import *

base_fs_table_oid= "1.3.6.1.4.1.2.6.191.6.2.1"

def process_disk_stats( device, community, totalBlocks_oid, freeBlocks_oid ):
    """Gather OID info and sanitize it"""

    cmd= "snmpwalk -v1 -c %s -On %s %s %s" % ( community, device, \
          totalBlocks_oid, freeBlocks_oid )
    proc= Popen( cmd, shell=True, stdout=PIPE, stderr=PIPE )

    #
    # Check to make sure that we don't have any hangups in
    # executing our smidump
    #
    if not proc.stdout:
        print "Couldn't open pipe to stdout for %s" % cmd
        return

    if not proc.stderr:
        print "Couldn't open pipe to stderr for %s" % cmd
        return

    ( line1, line2 )= proc.stdout.readlines()
    totalBlocks= line1.split()[-1]
    freeBlocks= line2.split()[-1]
    usedBlocks= totalBlocks - freeBlocks

    return totalBlocks, freeBlocks, usedBlocks


if __name__ == "__main__":
    if len(sys.argv) < 4:
        print "Need device, community and fs_index arguments!"
        sys.exit(1)

    (device, community, fs_index )= sys.argv[1:]

    totalBlocks_oid= ".".join( base_fs_table_oid, 5, fs_index )
    freeBlocks_oid= ".".join( base_fs_table_oid, 6, fs_index )

    totalBlocks, freeBlocks, usedBlocks= process_disk_stats( device, \
         community, totalBlocks_oid, freeBlocks_oid )
    print "totalBlocks:%s freeBlocks:%s usedBlocks:%s" % ( totalBlocks, \
         freeBlocks, usedBlocks )
    sys.exit(0)

5.2. Writing Your Own Command Parser

Zencommand may be used to execute commands on remote hosts using the SSH protocol. This provides secure and flexible performance monitoring for Unix-style systems such as AIX, Solaris, OS X (Darwin) and Linux servers.

When the remote host has commands that show data in a format already understood by Zencommand (such as Nagios or Cacti plugins), Zencommand can process the results and update the ZODB. However, if you are monitoring servers that have not had these commands installed, you need to extend Zencommand with new parsers to understand the results.

The basic data flow for Zencommand is this:

  1. A collector starts Zencommand with a collector name, like localhost or collector2.

  2. Zencommand contacts zenhub and loads the commands to be run against the devices for that configuration. The command configuration includes details such as "use SSH" to run the command on the remote box and credentials to allow access to the remote host. The command configuration also includes a specification for the parser to use on the data that is returned by the command.

  3. Zencommand runs the command on the remote host, and when the command finishes, a parser is created and the results are passed to the processResults() method of the parser. The processResults() method is passed the command configuration fetched from ZenHub, and an object into which parsed results will be placed. The parser is also used to copy any data needed by Zencommand during the parsing.

  4. Zencommand takes the returned Python dictionary from the parser and updates the ZODB.

Consider the Unix df command. It can be used to determine free disk space on a device's file systems. Here's a typical output format from Linux:

Filesystem           1K-blocks      Used Available Use% Mounted on
/dev/sda6             57669700  34162636  20577616  63% /
/dev/sda7             71133144  28824804  38694924  43% /home
/dev/mmcblk1           3924476       536   3923940   1% /media/disk

The Zenoss data modeler (zenmodeler) will have created components under this device for the file systems. The mapping of data to the component must use the mount point information. ZenHub must copy mount point information from the model data stored in the ZODB into the configuration for this command. To know what data may be needed for parsing, ZenHub creates the parser that will be used by Zencommand, and calls the dataForParser() method. Remember, this happens in ZenHub, and not Zencommand, and it happens before any command is run.

The result of dataForParser() is a Python dictionary that is stored as data in the command configuration passed to Zencommand. When the parser is invoked in Zencommand, it will have access to this information.

Note

  • After the parser digests the results of running the command, it can produce performance information and events.

  • The result object is a simple Python class that contains two lists, one called values and the other called events.

  • The events item contains a dictionary of string to value mappings which are turned into events. Zencommand will update the event with the device name, but the rest of the fields (such as component, severity, etc) are up to the parser to fill in.

  • The values item is a list of two-element tuples. The first element is the data point, and the second is a value, which is a Python number or None. None is always ignored.

  • Every command run by Zencommand comes with a list of data points that correspond to that command. In our df example, the datapoints may include percentUsed and blocksFree, along with any thresholds or parser-specific data, such as mount point.

  • Thresholds will be tested by Zencommand, and threshold events automatically generated.

  • The command's exit code is available at parse time, too.

Parsers will be available to Zenoss when they are placed in the $ZENHOME/Products/ZenRRD/parsers directory or in a ZenPack's $ZENHOME/ZenPacks.pkg.zpid-version_id-py2.4.egg/Zenpacks/pkg/zpid/parsers directory. Each parser should be a sub-class of the Products.ZenRRD.CommandParser.CommandParser class.

A command like df is a very common case. Unix commands will often emit easily-parsed, line-oriented records. There are some useful subclasses of CommandParser that perform much of the parsing if you provide these parsers with the right details, such as regular expressions. They are:

Table 12.2. CommandParser Helper Parsers

NameDescription
componentScanner

A regular expression that finds details about a component that can be used to map back to the component known to the Zenoss model. It must return a match named component using the Python regular expression syntax ?P<component>

scanners

An iterable list of regular expressions that will pull out numerical values from the output of the command.

componentScanValue

The data to be copied to the data point needed to match the component to the output results.


Let's examine what these values might be for our df command, and its example output.

  1. For the componentScanner, we want to find the mount-point data and extract it, so that we can match the Unix file separator ('/') to the component file system that has the id "_". We can use something like:

    % (?P<component>/.*)$

  2. For the scanners, we'll use a tuple of regular expressions to pull out the numerical values we want:

    ( r' (?P<availableBlocks>\d+) +(?P<percentUsed>\d+)%' )
  3. For the componentScanValue, we'll specify mount so that the mount point information is copied to the command configuration by ZenHub and matched against the component value parsed by the componentScanner regular expression.