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.
-
Download the latest release from the PythonCollector page.
-
Run the following command to install the ZenPack:
zenpack --install ZenPacks.zenoss.PythonCollector-<version>.egg
-
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.
- Navigate to Advanced > Settings > ZenPacks.
- Click into the ZenPacks.training.NWS ZenPack.
- Check ZenPacks.zenoss.PythonCollector in the list of dependencies.
- Click Save.
- Export the ZenPack.
Create the alerts plugin
Follow these steps to create the Alerts
data source plugin:
-
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.
-
Logging
The first thing we do is import
logging
and createLOG
as our logger. It’s important that the name of the logger in thelogging.getLogger()
begins withzen.
. 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 thezenpython
collector service. -
Alerts
classUnlike 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.
-
config_key
Class MethodThe
config_key
method must have the@classmethod
decorator. It is passeddatsource
, andcontext
. Thedatasource
argument will be the actual datasource that the user configures in the monitoring templates section of the web interface. It has properties such aseventClass
,severity
, and as you can see agetCycleTime()
method that returns the interval at which it should be polled. Thecontext
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 thezenpython
service. Thezenpython
service will create one task for each unique value returned fromconfig_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 removecontext.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
inconfig_key
so we get one task per station.The value returned by
config_key
will be used whenzenpython
logs. So adding something likenws-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 byzenhub
. So you must restartzenhub
if you make changes to theconfig_key
method. This also means that if there’s an exception in theconfig_key
method it will appear in thezenhub
log, notzenpython
. -
params
class methodThe
params
method must have the@classmethod
decorator. It is passed the samedatasource
andcontext
arguments asconfig_key
.The purpose of the
params
method is to copy information from the Zenoss database into theconfig.datasources[*]
that will be passed as an argument to thecollect
method. Since thecollect
method is run byzenpython
it won’t have direct access to the database, so it relies on theparams
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
, andstation_id
properties. These will be used in thecollect
method.Just like the
config_key
method,params
will only be executed byzenhub
. So be sure to restartzenhub
if you make changes, and look in thezenhub
log for errors. -
collect
methodThe
collect
method does all of the real work. It will be called once per cycletime. It gets passed aconfig
argument which for the most part has two useful properties:config.id
andconfig.datasources
.config.id
will be the device’s id, andconfig.datasources
is a list of the datasources that need to be collected.You’ll see in the
collect
method that each datasource inconfig.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 theparams
method returned.Within the body of the collect method we see that we create a new
data
variable usingdata = 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 thetype
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.
-
-
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.
-
Edit
$ZP_DIR/zenpack.yaml
and add thetemplates
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.
-
The highest-level element (based on indentation) is
/NWS/templates/Station
. This means to create aStation
monitoring template in the/NWS
device class.The monitoring template must be called
Station
because that is thelabel
for theNwsStation
class to which we want the template bound. -
The
description
is for documentation purposes and should describe the purpose of the monitoring template. -
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. -
Next we have
datasources
with a singlealerts
datasource defined.The
alerts
datasource only has three properties:type
: This is what makeszenpython
collect the dataplugin_classname
: This is the fully-qualified class name for thePythonDataSource
plugin we created that will be responsible for collecting the datasource.cycletime
: The interval in seconds at which this datasource should be collected.
-
-
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
-
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
-
Go to the following URL for the current severe weather map of the United States: https://www.weather.gov
-
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.
-
Update
zNwsStates
on theapi.weather.gov
device to add one of the states that has an active weather alert. For example, “ND”, for North Dakota. -
Remodel the
api.weather.gov
device then verify that the new location is modeled. -
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.