Skip to content

Datasource plugin (events)

Now that we have one or more locations modeled on our api.weather.gov device, we’ll want to start monitoring each location. Using PythonCollector we have the ability to create events, record datapoints and even update the model. We’ll start with an example that creates events from weather alert data.

The idea will be that we’ll create events for locations that have outstanding weather alerts such as tornado warnings. We’ll try to capture severity data so tornado warnings are higher severity events than something like a frost advisory.

Using PythonCollector

Before using a Python plugin in our ZenPack, we must make sure we install the PythonCollector ZenPack, and make it a requirement for our ZenPack.

The PythonCollector ZenPack adds the capability to write high performance datasources in Python. They will be collected by the zenpython service that comes with the PythonCollector ZenPack. I’d recommend reading the PythonCollector documentation for more information. Note that commercial versions of Zenoss include PythonCollector.

Installing PythonCollector

The first thing we’ll need to do is to make sure the PythonCollector ZenPack is installed on our system. If it isn’t, follow these instructions to install it.

  1. Download the latest release from the PythonCollector page.

  2. Run the following command to install the ZenPack:

    zenpack --install ZenPacks.zenoss.PythonCollector-<version>.egg
    
  3. Restart Zenoss.

Add PythonCollector dependency

Since we’re going to be using PythonCollector capabilities in our ZenPack we must now update our ZenPack to define the dependency.

Follow these instructions to define the dependency.

  1. Navigate to Advanced > Settings > ZenPacks.
  2. Click into the ZenPacks.training.NWS ZenPack.
  3. Check ZenPacks.zenoss.PythonCollector in the list of dependencies.
  4. Click Save.
  5. Export the ZenPack.

Create the alerts plugin

Follow these steps to create the Alerts data source plugin:

  1. Create $ZP_DIR/dsplugins.py with the following contents.

    """Monitors current conditions using the NWS API."""
    
    # Logging
    import logging
    LOG = logging.getLogger('zen.NWS')
    
    # stdlib Imports
    import json
    import time
    from datetime import datetime
    from dateutil import parser
    
    # Twisted Imports
    from twisted.internet.defer import inlineCallbacks, returnValue
    from twisted.web.client import getPage
    
    # PythonCollector Imports
    from ZenPacks.zenoss.PythonCollector.datasources.PythonDataSource import (
        PythonDataSourcePlugin,
        )
    
    
    class Alerts(PythonDataSourcePlugin):
    
        """NWS alerts data source plugin."""
    
        @classmethod
        def config_key(cls, datasource, context):
            return (
                context.device().id,
                datasource.getCycleTime(context),
                context.id,
                'nws-alerts',
                )
    
        @classmethod
        def params(cls, datasource, context):
            return {
                'county': context.county,
                'station_name': context.title,
                }
    
        @inlineCallbacks
        def collect(self, config):
            data = self.new_data()
    
            for datasource in config.datasources:
                try:
                    response = yield getPage(
                        'https://api.weather.gov/alerts/active?zone={query}'
                        .format(
                            query=datasource.params['county']))
    
                    response = json.loads(response)
                except Exception:
                    LOG.exception(
                        "%s: failed to get alerts data for %s",
                        config.id,
                        datasource.params['station_name'])
                    continue
    
                for rawAlert in response.get('features'):
                    alert = rawAlert['properties']
                    severity = None
                    expires = parser.parse(alert['expires'])
                    if datetime.timetuple(expires) <= time.gmtime():
                        severity = 0
                    elif alert['certainty'] == 'Likely':
                        severity = 4
                    else:
                        severity = 3
    
                    data['events'].append({
                        'device': config.id,
                        'component': datasource.component,
                        'severity': severity,
                        'eventKey': 'nws-alert-{}'.format(alert['event'].replace(' ', '')),
                        'eventClassKey': 'nws-alert',
    
                        'summary': alert['headline'],
                        'message': alert['description'],
    
                        'nws-sender': alert['senderName'],
                        'nws-date': alert['effective'],
                        'nws-expires': alert['expires'],
                        'nws-category': alert['category'],
                        'nws-instruction': alert['instruction'],
                        'nws-type': alert['event'],
                        })
    
            returnValue(data)
    

    Let’s walk through this code to explain what is being done.

    1. Logging

      The first thing we do is import logging and create LOG as our logger. It’s important that the name of the logger in the logging.getLogger() begins with zen.. You will not see your logs otherwise.

      The stdlib and Twisted imports are almost identical to what we used in the modeler plugin, and they’re used for the same purposes.

      Finally we import PythonDataSourcePlugin from the PythonCollector ZenPack. This is the class our data source plugin will extend, and basically allows us to write code that will be executed by the zenpython collector service.

    2. Alerts class

      Unlike our modeler plugin, there’s no need to make the plugin class’ name the same as the filename. As we’ll see later when we’re setting up the monitoring template that will use this plugin, there’s no specific name for the file or the class required because we configure where to find the plugin in the datasource configuration within the monitoring template.

    3. config_key Class Method

      The config_key method must have the @classmethod decorator. It is passed datsource, and context. The datasource argument will be the actual datasource that the user configures in the monitoring templates section of the web interface. It has properties such as eventClass, severity, and as you can see a getCycleTime() method that returns the interval at which it should be polled. The context argument will be the object to which the monitoring template and datasource is bound. In our case this will be a station object such as Dallas Fort Worth International Airport.

      The purpose of the config_key method is to split monitoring configuration into tasks that will be executed by the zenpython service. The zenpython service will create one task for each unique value returned from config_key. It should be used to optimize the way data is collected. In some cases it is possible to make a single query to an API to get back data for many components. In these cases it would be wise to remove context.id from the config_key so we get one task for all components.

      In our case, the NWS API must be queried once per location so it makes more sense to put context.id in config_key so we get one task per station.

      The value returned by config_key will be used when zenpython logs. So adding something like nws-alerts to the end makes it easy to see logs related to collecting alerts in the log file.

      The config_key method will only be executed by zenhub. So you must restart zenhub if you make changes to the config_key method. This also means that if there’s an exception in the config_key method it will appear in the zenhub log, not zenpython.

    4. params class method

      The params method must have the @classmethod decorator. It is passed the same datasource and context arguments as config_key.

      The purpose of the params method is to copy information from the Zenoss database into the config.datasources[*] that will be passed as an argument to the collect method. Since the collect method is run by zenpython it won’t have direct access to the database, so it relies on the params method to provide it with any information it will need to collect.

      In our case you can see that we’re copying the context’s county, and station_id properties. These will be used in the collect method.

      Just like the config_key method, params will only be executed by zenhub. So be sure to restart zenhub if you make changes, and look in the zenhub log for errors.

    5. collect method

      The collect method does all of the real work. It will be called once per cycletime. It gets passed a config argument which for the most part has two useful properties: config.id and config.datasources. config.id will be the device’s id, and config.datasources is a list of the datasources that need to be collected.

      You’ll see in the collect method that each datasource in config.datasources has some useful properties. datasource.component will be the id of the component against which the datasource is run, or blank in the case of a device-level monitoring template. datasource.params contains whatever the params method returned.

      Within the body of the collect method we see that we create a new data variable using data = self.new_data(). data is a place where we stick all of the collected events, values and maps. data looks like the following:

      data = {
          'events': [],
          'values': defaultdict(<type 'dict'>, {}),
          'maps': [],
      }
      

      Next we iterate over every configured datasource. For each one we make a call to the NWS’s Alerts API, then iterate over each alert in the response creating an event for each.

      The following standard fields are being set for every event. You should read Zenoss’ event management documentation if the purpose of any of these fields is not clear. I highly recommend setting all of these fields to an appropriate value for any event you send into Zenoss to improve the ability of Zenoss and Zenoss’ operators to manage the events.

      • device: Mandatory. The device id related to the event.
      • component: Optional. The component id related to the event.
      • severity: Mandatory. The severity for the event.
      • eventKey: Optional. A further uniqueness key for the event. Used for de-duplication and clearing.
      • eventClassKey: Optional. An identifier for the type of event. Used during event class mapping.
      • summary: Mandatory: A (hopefully) short summary of the event. Truncated to 128 characters.
      • message: Optional: A longer text description of the event. Not truncated.

      You will also see many nws-* fields being added to the event. Zenoss allows arbitrary fields on events so it can be a good practice to add any further information you get about the event in this way. It can make understanding and troubleshooting the resulting event easier.

      Finally we return data with all of events we appended to it. zenpython will take care of getting the events sent from this point.

  2. Restart Zenoss.

    After adding a new datasource plugin you must restart Zenoss. While developing it’s enough to just restart zenhub with the following command.

    serviced service restart zenhub
    

    That’s it. The datasource plugin has been created. Now we just need to do some Zenoss configuration to allow us to use it.

Configure monitoring templates

Rather than use the web interface to manually create a monitoring template, we’ll specify it in our zenpack.yaml instead.

  1. Edit $ZP_DIR/zenpack.yaml and add the templates section below to the existing /NWS device class.

    device_classes:
      /NWS:
        templates:
          Station:
            description: Weather monitoring using the NWS API.
            targetPythonClass: ZenPacks.training.NWS.NwsStation
    
            datasources:
              alerts:
                type: Python
                plugin_classname: ZenPacks.training.NWS.dsplugins.Alerts
                cycletime: "600"
    

    At least some of this should be self-explanatory. The YAML vocabulary has been designed to be as intuitive and concise as possible. Let’s walk through it.

    1. The highest-level element (based on indentation) is /NWS/templates/Station. This means to create a Station monitoring template in the /NWS device class.

      The monitoring template must be called Station because that is the label for the NwsStation class to which we want the template bound.

    2. The description is for documentation purposes and should describe the purpose of the monitoring template.

    3. The targetPythonClass is a hint to what type of object the template is meant to be bound to. Currently this is only used to determine if users should be allowed to manually bind the template to device classes or devices. Providing a valid component type like we’ve done prevents users from making this mistake.

    4. Next we have datasources with a single alerts datasource defined.

      The alerts datasource only has three properties:

      • type: This is what makes zenpython collect the data
      • plugin_classname: This is the fully-qualified class name for the PythonDataSource plugin we created that will be responsible for collecting the datasource.
      • cycletime: The interval in seconds at which this datasource should be collected.
  2. Reinstall the ZenPack to add the monitoring templates.

    Some sections of zenpack.yaml such as zProperties and templates only get created when the ZenPack is installed.

    Run the usual command to reinstall the ZenPack in development mode.

    zenpack --link --install $ZP_TOP_DIR
    
  3. Navigate to Advanced > Monitoring Templates in the web interface to verify that the Station monitoring template has been created as defined.

Test monitoring weather alerts

  1. Go to the following URL for the current severe weather map of the United States: https://www.weather.gov

  2. Click on one of the colored areas. Orange and red are more exciting. This will take you to the text of the warning. It should reference city or county names.

  3. Update zNwsStates on the api.weather.gov device to add one of the states that has an active weather alert. For example, “ND”, for North Dakota.

  4. Remodel the api.weather.gov device then verify that the new location is modeled.

  5. Run the following command to collect from api.weather.gov.

    zenpython run -v10 --device=api.weather.gov
    

    There will be a lot of output from this command, but we’re mainly looking for an event to be sent for the weather alert. It will look similar to the following output:

    2019-05-29 16:19:04,832 DEBUG zen.zenpython: Queued event (total of 93) {'nws-type': u'Flash Flood Watch', 'nws-expires': u'2019-05-29T17:00:00-05:00', 'severity': 3, 'nws-category': u'Met', 'eventClassKey': 'nws-alert', 'component': 'KBVO', 'monitor': 'localhost', 'agent': 'zenpython', 'summary': u'Flash Flood Watch issued May 29 at 4:28AM CDT until May 30 at 7:00AM CDT by NWS Tulsa OK', 'nws-sender': u'NWS Tulsa OK', 'manager': '6328c71e5335', 'nws-date': u'2019-05-29T04:28:00-05:00', 'rcvtime': 1559146744.832561, 'device_guid': '021a3692-2a94-412d-a221-aa579ec2fecf', 'device': 'api.weather.gov', 'message': u'The Flash Flood Watch continues for\n\n* Portions of Arkansas and Oklahoma...including the following\ncounties...in Arkansas...Benton...Carroll...Crawford...\nFranklin...Madison...Sebastian and Washington AR. in\nOklahoma...Adair...Cherokee...Choctaw...Craig...Creek...\nDelaware...Haskell...Latimer...Le Flore...Mayes...McIntosh...\nMuskogee...Nowata...Okfuskee...Okmulgee...Osage...Ottawa...\nPawnee...Pittsburg...Pushmataha...Rogers...Sequoyah...Tulsa...\nWagoner and Washington OK.\n\n* Through Thursday morning\n\n* Widespread showers and thunderstorms will expand across the\nregion on today and continue into tonight. The heaviest rainfall\nis expected to be southeast of Interstate 44 with the heaviest\ntotals from southeast Oklahoma into northwest Arkansas.\nWidespread 1 to 2 inch rain amounts with local amounts over 3\ninches appear likely within the heaviest rainfall band. The\nArkansas River remaining in historic flood category will inhibit\nsmall creeks and tributaries from draining as they normally\nwould. Despite the lack of preceding rainfall, rapid onset flash\nflooding will be possible given this condition. Cities through\nwest central Arkansas near the Arkansas river may experience\nflooding in areas that typically remain dry and there is\npotential for catastrophic flooding should the heavier totals be\nrealized near the larger Arkansas river channel. Flooding should\nbe considered a high risk weather impact today through tonight.', 'nws-instruction': u'If you are in the watch area, keep informed, and be ready for\nquick action if flash flooding is observed or if a warning is\nissued.\n\nA flash flood watch means rapidly rising water or flooding is\npossible within the watch area.', 'eventKey': 'nws-alert-FlashFloodWatch'}
    

    You can use a commmand like the following to see just these log lines:

    zenpython run -v10 --device=api.weather.gov | grep 'Queued' | grep 'nws'
    

    You should now be able to confirm that this event was created in the Zenoss event console.