Creating Monitors

Introduction

With Runbook, we have kept the creation of Monitors as simple as possible by making the Monitors modular. Creating a new Monitor doesn't require you to edit the main web.py file. Instead, you create several new files that are dynamically loaded into the application.

When creating a new Monitor, it is important to define a short-name for the Monitor. This short-name will be used to uniquely identify the Monitor throughout the various modules you create.

Example: An HTTP GET-based Monitor that searches for a specific keyword has a short-name of http-keyword. This short-name is unique for this type of Monitor and all files created for this Monitor reference the short-name.

In this guide, we will use the http-keyword Monitor as a reference. This Monitor is a non Webhook-based Monitor and is a good example of how simple it is to create a Monitor.

Creating a new Monitor

Step 1: The Monitor web form

Runbook is written using the Flask framework. A common utility for creating web forms within Flask is wtforms. We utilize wtforms for all web forms within the Runbook GUI, including the forms that create Monitors.

For this document, we will create a new Monitor named some-monitor; the first step of creating a Monitor is to create the web form needed for users. To start the web form we will create a directory in web/monitorforms/ called some-monitor. Within that directory we will create a __init__.py file containing a class that defines the required form fields.

$ mkdir src/web/monitorforms/some-monitor
$ vi src/web/monitorforms/some-monitor/__init__.py

Within this file is the actual wtforms code. You can use the http-keyword Monitor as an example.

There are a couple of guidelines when creating a web form for Monitors.

  • The class must be named CheckForm

When the main web component loads the form it will dynamically load the CheckForm class. This dynamic loading relies on the short-name and is pulled from the URL the user navigates to.

  • The class should inherit either DatacenterCheckForm or BaseCheckForm

When creating a web form the CheckForm should inherit either the BaseCheckForm or the DatacenterCheckForm classes. You can import these classes with the following commands.

BaseCheckForm

from ..base import BaseCheckForm

DatacenterCheckForm

from ..datacenter import DatacenterCheckForm

The BaseCheckForm class contains the name form field and is the lowest level of wtform classes that should be used with Monitor forms.

The DatacenterCheckForm class imports the TimerCheckForm which also imports the BaseCheckForm. If you import the DatacenterCheckForm class you will inherit the name, timer, and datacenter fields.

Most Monitors should inherit the DatacenterCheckForm. Only Monitors that do not run in an interval such as Webhook Monitors should inherit the BaseCheckForm directly.

Example Monitor form

The below example will show a Monitor form class that has two fields, one for an IP and one for a Port.

from wtforms import Form, TextField
from wtforms.validators import DataRequired, IPAddress, NumberRange
from ..datacenter import DatacenterCheckForm

class CheckForm(DatacenterCheckForm):
  ''' Class that creates a Check form for the dashboard '''
  ip = TextField("IP", validators=[IPAddress(message='Does not match IP address format')])
  port = TextField("Port", validators=[NumberRange(message='Port must be a number between 1 and 65535')])

if __name__ == '__main__':
  pass

If we wanted to add a second field that contains a hostname we could add it by just adding another TextField object to the class.

class CheckForm(DatacenterCheckForm):
  ''' Class that creates a Check form for the dashboard '''
  ip = TextField("IP", validators=[IPAddress(message='Does not match IP address format')])
  port = TextField("Port", validators=[NumberRange(message='Port must be a number between 1 and 65535')])
  hostname = TextField("Hostname", validators=[DataRequired(message='Hostname is a required field')])

Step 2: The Monitor form HTML

Step #1 defines what fields should be present in the form. Step #2 actually renders the web form page. The easiest way to create a new web form page is to simply copy an existing template and modify the input fields; a good reference would be the http-keyword template.

Example form field

The below is an example form input field written in HTML and Jinja2. Jinja2 is the templating language used in Flask.

<div class="form-group">
<label for="Host" class="col-sm-4 control-label">Domain to request</label>
  <div class="col-sm-8">
  {% if data['edit'] %}
  {{ form.host(class_="form-control", value=data['monitor']['data']['host']) }}
  {% else %}
  {{ form.host(class_="form-control", placeholder="example.com") }}
  {% endif %}
  </div>
</div>

As you can see Jinja2 offers the ability to use if statements within the template. In the above example, if the page is in edit mode the value of data['edit'] will be True and the form will be pre-filled with the value of data['monitor']['data']['host']. If the value of data['edit'] is False the form field will be created and the placeholder value will be displayed.

This HTML is an example of how the templates above render when data['edit'] is False.

<div class="form-group">
<label for="Host" class="col-sm-4 control-label">Domain to request</label>
<div class="col-sm-8">
<input class="form-control" id="host" name="host" placeholder="example.com" type="text" value="">
</div>
</div>

some-monitor.js

When the Monitor page is loaded via the main web.py, a .js file of the same name is also loaded. This file is used for the JavaScript required to activate popover help text. However, it should also be utilized for any other JavaScript-related code that needs to be imported at the footer of the Monitor page.

Even if popover text or any other JavaScript code is not utilized for this Monitor, it is required that a .js file is present. You can simply create a blank file if necessary.

$ touch static/templates/monitors/some-monitor.js

Processing the form

As a development team, our goal is to ensure that everything is modular. When you create a new Monitor you do not need to create code to process the web form inputs. This is done automatically via the web application. It is important, however, to understand how this processing takes place.

When the web app processes the new Monitor, the details will be stored into the monitors table in RethinkDB, which is a JSON-based database. When you edit a Monitor the web application will query RethinkDB and store the details of that Monitor into data['monitor']. Below is an example of the structure of both the database and data['monitor'].

data['monitor'] = {
  "ctype":  "http-keyword" ,
  "data": {
    "datacenter": [
      "dc1queue" ,
      "dc2queue"
    ] ,
    "host":  "example.com" ,
    "keyword":  "Test" ,
    "name":  "Status Check" ,
    "present":  "True" ,
    "reactions": [
      "c1c0240e-1333-1333-1333-122131112" ,
      "c107250a-1333-1333-13313-abcdefghi73"
    ] ,
    "regex":  "True" ,
    "timer":  "5mincheck" ,
    "url": "http://127.0.0.1/blah.html",
  } ,
  "failcount": 1583 ,
  "id":  "adfasdlkjfsdkljasdf98f0-1a2dbdea2675" ,
  "name":  "Example HTTP" ,
  "status":  "true" ,
  "uid":  "sdfsaasdfjlaksdfaskj369-15888dd98382" ,
  "url":  "asdfweqrue0rj2302309rur20cdsa09dafw09iacs09caswekflkwjqfklwejfjf.qwerzPHUz7heZ6VxA"
}

When a Monitor form is submitted, the web application will process the form and define the ctype, name, failcount, status, uid, and url keys. The application will then take all of the form's inputs and put them into a dictionary under the data key. This system gives us the ability to create new Monitors without having to redefine or customize anything outside of the web form itself.

In simpler terms, the data key can change between Monitor types. The other fields in data['monitor'] are meta fields that exist for every monitor.

Step 3: Creating the actual Monitor code

Steps #1 and #2 were specifically related to creating the web elements of a Monitor. Step #3 is the creation of the actual Monitor module itself. There are two main types of Monitors in Runbook, Webhook-based Monitors and non Webhook-based Monitors.

Webhook-based Monitors

An example of an Webhook-based Monitor would be the datadog-webhook Monitor. The end point for Webhook-based Monitors is /api/<short-name>/<monitor id>. The short-name in our example would be some-monitor and the ID would be the id key for the Monitor in the database. When this end point is called, the web application will try to load a python module monitorapis/<short-name>. If the module does not exist there is an error, if the module does exist then the web application will call the webCheck method from that module.

Example Webhook Monitor

The following is an example of a simple Webhook-based Monitor that always marks the Monitor false when called.

def webCheck(request, monitor, urldata, rdb):
  ''' Process the webbased api call '''
  replydata = {
    'headers': { 'Content-type' : 'application/json' }
    }
  jdata = request.json

  ## Delete the Monitor
  monitor.get(urldata['cid'], rdb)
  if jdata['check_key'] == monitor.url and urldata['atype'] == monitor.ctype:
    monitor.healthcheck = "false"
    result = monitor.webCheck(rdb)
  replydata['data'] = "{'success':'True'}"
  return replydata

When the webCheck method is called it will be given 4 arguments; request, monitor, urldata and rdb. The request argument is the full request object from Flask. This contains all POST data and headers of the Webhook request. The monitor argument is an object for the Monitor class. In the example above we use the get, healthcheck and webCheck methods from this class.

The urldata argument is a dictionary that contains data from the URL making the request. The dictionary contains cid, atype, check_key and action. The cid is the Monitor ID value passed from the URL, this is not a validated ID and should be treated the same as any user input. The atype value is the type of Webhook being requested, this is essentially the ctype key in the Monitor's meta data. The check_key is an optional URL parameter. If it exists in the URL it can be compared with monitor.url as a validator, this is essentually an API Key. The action key is also an optional URL parameter, and is used in webhook requests to specify false or true requests.

The rdb object is a connection object to the RethinkDB database store.

If the POST data in the above example contains a JSON string that has a key check_key and that key is the same as the monitor.url objects value, and the atype value is the same as the monitor.cytype objects value, then the monitor.healthcheck object will be set to false and the monitor.webCheck method will be called. This method will send a health check message to the backend bridge process. This process will process the false Monitor and perform necessary Reactions.

To get started with a new Webhook-based Monitor you will first need to create a new directory with the short-name under the web/monitorapis directory and then create an __init__.py file that contains the Webhook processing code.

$ mkdir web/monitorapis/some-monitor
$ vi web/monitorapis/some-monitor/__init__.py

Non Webhook-based Monitors

Non Webhook-based Monitors are Monitors that are run via monitors. These Monitors are executed from Runbook. You can think of these as external Monitors. At the moment of this writing, most of these have to do with checking a server/application externally. Using the http-keyword Monitor as an example is the best place to start. All Monitors that run through monitors are Python modules placed into the checks/ directory.

Much like using the wtforms module in step #1 to create a new Monitor, simply create a directory with the short-name and a __init__.py file.

$ mkdir monitors/checks/some-monitor
$ vi monitors/checks/some-monitor/__init__.py

The only requirement for this Monitor is to have a single method called check() defined. The check() method is a keyword arguments defined method. When src/monitors/worker.py calls the check() method, it specifies values for jdata and logger.

Example of jdata

Below is an example of what the jdata dictionary contains.

jdata = {
  "status": "false",
  "uid": "1232131231231231231-111-15888dd98382",
  "zone": "Digital Ocean - sfo1",
  "cid": "232132312312312313123-aea-qer2-vs4e3",
  "url": "Twerewu230432423owrjewoj3fw3r-.2342432fserw323eaew1234567890204zT6el98CmmI2X30SwCo",
  "ctype": "http-keyword",
  "failcount": "412",
  "time_tracking": {
    "control": 1411488928.422103,
    "ez_key": "key@example.com",
    "env": "Prod"
  },
  "data": {
    "regex": "True",
    "datacenter": [
      "dc2queue",
      "dc1queue"
    ],
    "name": "Some Monitor",
    "keyword": "Test",
    "reactions": [
      "1232432jsad-aefawewr2-adsfa-q23261c5",
      "asfkldjsafj0eq2.-23rq23=afsedfadc359"
    ],
    "url": "http://example.com/hello.txt",
    "timer": "5mincheck",
    "host": "example.com",
    "present": "True"
  },
  "name": "Some Monitor"
}

The key item of this Monitor is the jdata['data'] dictionary. The jdata['data'] dictionary holds all of the form values from steps #1 and #2. For most Monitors, the details are located in this dictionary.

What to do after the health check is performed

The actual code to perform the health check really depends on the health check itself, but once you determine if the check was "true" the check function should return True. If the monitor is determined "false" the return value should be False.

Example Health Check: http-get-statuscode

The following Monitor code is from the http-get-statuscode Monitor and can be used as a guide on how to write a monitors-based Monitor.

def check(**kwargs):
    """ Perform a http get request and validate the return code """
    jdata = kwargs['jdata']
    logger = kwargs['logger']
    headers = {'host': jdata['data']['host']}
    timeout = 3.00
    url = jdata['data']['url']
    try:
        result = requests.get(
            url, timeout=timeout, headers=headers, verify=False)
    except Exception as e:
        line = 'http-get-statuscode: Reqeust to {0} sent for monitor {1} - ' \
               'had an exception: {2}'.format(url, jdata['cid'], e)
        logger.error(line)
        return False
    rcode = str(result.status_code)
    if rcode in jdata['data']['codes']:
        line = 'http-get-statuscode: Reqeust to {0} sent for monitor {1} - ' \
               'Successful'.format(url, jdata['cid'])
        logger.info(line)
        return True
    else:
        line = 'http-get-statuscode: Reqeust to {0} sent for monitor {1} - ' \
               'Failure'.format(url, jdata['cid'])
        logger.info(line)
        return False
Example Health Check: always-true

The following Monitor code will always return True, which means the Monitor itself will always be true.

def check(**kwargs):
  ''' Always return true '''
  return True

Getting help

At this point you should at least have a Monitor that mostly works. If you're stuck and need some help, feel free to drop by our chat page.