Integrate AppNeta with Slack Using Our API
by May 22, 2018

Filed under: Performance Monitoring, Product News

AppNeta Performance Manager (APM) provides several ways for customers to integrate monitoring data with other systems, including the RESTful API Observer endpoint that allows you to send event notifications to your own receiver. With this ability, the opportunity for flexible integration abounds. Justin previously discussed how to leverage the API to integrate with ServiceNow. Today, we’ll take a look at how to integrate with Slack.

Slack is a collaboration hub for workplace communications. According to a Gartner report, “with more than 70% market share, ARR growth of more than 100% and 6 million daily active users, Slack has a leading position in the [workstream collaboration] market”.

By posting APM events to a public Slack channel, team members are kept informed about performance events in real-time, within their primary communication space, while using the APM UI to help further investigate or report on the infrastructure’s status. To increase productivity, you could send operational events to relevant Slack channels and filter to only display key events.

There are a number of ways to integrate with Slack. The method we explore is a Slack app built as an internal integrationfor a single workspace. We’ll use the incoming webhook method, which is a simple way for us to push the external APM Observer events into a Slack channel in real time. From this base integration, you could grow in complexity by using the other Slack APIs to pick and choose your method of integration, or wrapping it as a Slack app with authentication for deployment to other workspaces.

In this post we cover three areas to tie things together:

  • a Slack environment to which we send the events
  • an Observer application listening for APM events that will send them to the Slack channel
  • the APM Observer endpoint that will cause events to be posted to your Observer application.
A note before we get into the walkthrough: I wrote the Observer application as a proof of value for a client and there are a few areas I would refactor for a production version. I’ll mention those throughout the post.

The Slack environment:

If you don’t already have a Slack environment, you can get started at slack.com and create a workspace for testing.

The first step is to generate the webhook URL for the destination workspace that will be used in the observer application.

1. From your workspace, expand the Apps menu

2. Browse for and install the Incoming WebHooks app

3. On the Incoming WebHooks app start page, click Add Configuration

4. Select the channel where your events will be posted (or create a new channel), then click Add Incoming WebHooks integration

5. Copy the Webhook URL, as you’ll use this later in the observer code

The Observer application:

For this post, we’ll use a Python application that sits in the cloud to relay events from APM to our Slack channel.

You can use whatever language or framework is familiar to you, as long as you can somehow receive the event JSON string and POST it to the Slack channel WebHook. Your application can be hosted anywhere that works for you, provided no firewall rules block the inbound event posting from APM and that you can send POSTs to the WebHook server and port.

Environment

The Python environment used in this test is Python 3.5.2 and most of the other packages used are installed with the base Python. The only additional package that you’ll need is Flask, which you can install via the command line:

sudo -H pip3 install Flask-Cors

The Flask packages in the test environment:

Flask==1.0.1
Flask-Celery==2.4.3
Flask-Cors==3.0.4
Flask-Script==2.0.6

Code imports and global setup

Here is the Python setup code that imports packages, sets up the Flask web app and sets the WebHook URL for later use in the script:

Here is the raw text if you wish to copy/paste:
#!/usr/bin/env python3

from __future__ import print_function
from flask import Flask, request
from flask_cors import CORS
from datetime import datetime
import logging
import traceback
import argparse
import pprint
import json
import requests

from slackmessage import SlackMessage

app = Flask('__name__')
cors = CORS(app)

datFile = "" # data file, as a global name

url = 'https://hooks.slack.com/services/'

Main Web app route

The core web app is created through the use of a Flask route() decorator that binds a function to a URL.

The route endpoint that’ll receive the APM posted events will be:

“http://<Your Server’s Public IP>:<port>/event”

Here is the code for that route endpoint:

Here is the raw text if you wish to copy/paste:
@app.route('/event',methods=['POST'])                                                                                                                                         
def receive_apm_events():
    logging.info("\n"
        "====================================="
        "\n"
        "receive_apm_events: New event POSTed:"
        )
    try:
        logging.info("receive_apm_events: try: pre request.get_json().")
        data = request.get_json()
        logging.info("receive_apm_events: try: post request.get_json().")

        global datFile
        if request.content_length &lt; 20000 and request.content_length != 0:
            logging.info("receive_apm_events: post request: data: {0}".format( pprint.pformat(str(data)) ) )

            
            logging.info("receive_apm_events: try: pre postSlackMessage()")
            postSlackMessage( data )
            logging.info("receive_apm_events: try: post postSlackMessage()")

            with open(datFile, 'a') as f:
                json.dump(data, f)
                f.write('\n')
        else:
            logging.info( "receive_apm_events: post: Request too long: was {0} bytes.".format(request.content_length) )
            content = '{{"status": 413, "content_length": {0}, "content": "{1}"}}'.format(request.content_length, data)
            return content, 413 
    except:
        logging.info( "receive_apm_events: Exception {0}".format( traceback.format_exc() ) )
        return '{"status": 500}\n'

    return '{"status": 200}\n'

Slack Message class

I’ve created a Slack Message class to hold the attributes contained in a Slack message with attachments. This Slack message type provides the ability to add fields depending on what type of observer event is received.

We’ll use the Slack Message class in the mapping function, described below, with accessor functions, an inner Field utility class used to add fields to the message, which is part of the Slack message with attachments type.

A refactoring might be to additionally create various APM event type classes, use an adapter pattern to provide the mapping and perhaps a factory pattern to create various types of Slack and/or APM event type objects.
Copy/paste Slack Message class:
class SlackMessage(object):
	
	#Class variables

	#Constructor
	def __init__(self):
		"""Sets up the Slack message"""
		#Init any class variables

		#Init the instance
		self.reset()

	def reset(self):
		self._slackMessage=''
		self._fallback=''
		self._colour='#36a64f'
		self._pretext=''
		self._authorName=''
		self._authorLink=''
		self._authorIcon=''
		self._title=''
		self._titleLink=''
		self._text=''
		self._fields=[]
		self._ts=''

	def fallback(self, fb):
		"""Required plain-text summary of the attachment."""
		self._fallback=fb

	def getFallback(self):
		"""Returns required plain-text summary of attachment."""
		return self._fallback

	def colour(self, rgb):
		"""Required plain-text summary of the attachment."""
		self._colour=rgb

	def getColour(self):
		"""Returns the colour value"""
		return self._colour

	def pretext(self, pretxt):
		"""Optional text that appears above the attachment block"""
		self._pretext=pretxt

	def getPreText(self):
		"""Returns the optionial text that appears above the attachment block"""
		return self._pretext

	def authorName(self, auth):
		"""Author of the posting."""
		self._authorName=auth

	def getAuthorName(self):
		"""Returns the author's name."""
		return self._authorName

	def authorLink(self, link):
		"""Author's URL or other link."""
		self._authorLink=link

	def getAuthorLink(self):
		"""Returns the author's URL link."""
		return self._authorLink

	def authorIcon(self, icon):
		"""A URL link to the Author's icon."""
		self._authorIcon=icon

	def getAuthorIcon(self):
		"""Returns the author's icon."""
		return self._authorIcon

	def title(self, t):
		"""A title for the attachment."""
		self._title=t

	def getTitle(self):
		"""Returns the title for the attachment."""
		return self._title

	def titleLink(self, link):
		"""A URL link to associate with the title of the attachment."""
		self._titleLink=link

	def getTitleLink(self):
		"""Returns the URL link associated with the title of the attachment."""
		return self._titleLink

	def text(self, txt):
		"""Optional text that appears within the attachment"""
		self._text=txt

	def getText(self):
		"""Returns the optional text that appears within the attachment."""
		return self._text

	def timeStamp(self, ts):
		"""The timestamp of the event."""
		self._ts=ts

	def getTimestamp(self):
		"""Returns the timestamp of the event."""
		return self._ts

	class Field:
		"""A slack field contains a title, value and boolean to toggle short form"""
		def __init__(self, slackmsg, title, value, short):
			self._slackMessage = slackmsg
			self._title = title
			self._value = value
			self._short = short
			#self._field = { "title": self._title, "value": self._value, "short": self._short }
			#return self._field
		def getFieldJson(self):
			fieldDict = dict([ ('title', self._title), ('value', self._value), ('short', self._short) ])
			return fieldDict

	def addField(self, title, value, short):
		"""Add a field to the attachment"""
		self._newField = self.Field(self, title, value, short)
		self._fields.append( self._newField.getFieldJson() )

	def getFields(self):
		"""Returns the fields dictionary associated with this slack message."""
		return self._fields

	def getSlackMessage(self):
		"""Get the slack message to be posted"""
		self._slackMessage =  {
				"attachments": [
					{
						"fallback": self.getFallback(),
						"color": self.getColour(),
						"pretext": self.getPreText(),
						"author_name": self.getAuthorName(),
						"author_link": self.getAuthorLink(),
						"author_icon": self.getAuthorIcon(),
						"title": self.getTitle(),
						"title_link": self.getTitleLink(),
						"text": self.getText(),
						"fields": self.getFields(),
						"footer": "AppNeta Event",
						"footer_icon": "https://platform.slack-edge.com/img/default_application_icon.png",
						"ts": self.getTimestamp()
					}
				]
			}
		return self._slackMessage

APM event to Slack message mapping function

The postSlackMessage() function maps the incoming APM event data fields to Slack Message format and posts it to the Slack Message URL.

Here is the raw text if you wish to copy/paste:
def postSlackMessage(data):
    """Post the given JSON APM Event data to a Slack Channel"""
    try:
        item = dict(data)

        sm = SlackMessage()
        sm.fallback("Sequencer {0} event {1}".format(item["sequencerHost"], item["description"]) )
        sm.colour('#36a64f')
        sm.pretext("{0} : {1}".format(item["sequencerHost"], item["description"]) )
        sm.authorName('APM Observer Test')
        sm.authorLink('http://appneta.com/')
        sm.title( item["description"] )
        sm.text('')
        sm.addField("Type", item["type"], True)

        # conditional processing of event attributes based on event type...
        if 'SQA' in str(item["type"]):
            sm.addField("Quality", item["pathServiceQuality"], True)
            sm.addField("SequencerHost", item["sequencerHost"], True)
            sm.addField("Target", item["target"], True)

        if 'TEST_EVENT' in str(item["type"]):
            sm.addField("TestStatus", item["testStatus"], True)
            sm.addField("TestId", item["testId"], True)
            sm.addField("Target", item["target"], True)
            
        if 'SEQUENCER_EVENT' in str(item["type"]):
            sm.addField("SequencerHost", item["sequencerHost"], True)
            sm.addField("SequencerStatus", item["sequencerStatus"], True)

        sm.timeStamp(item["eventTime"])

        r = requests.post(url, data=json.dumps(sm.getSlackMessage()))
        logging.info("Posted to SLACK with status code {0} and reason code {1}".format(r.status_code, r.reason))

    except:
        logging.info( "postSlackMessage: Exception {0}".format( traceback.format_exc() ) )

    return '{"status": 200}\n'

Registering our observer application with the API:

We need to register our application with APM so it can start receiving event messages from our environment:

1. Login to AppNeta Performance Manager at https://login.appneta.com/

2. Hover over the gear icon and click the API link (in the General section near the bottom of the menu)

3. Expand the observer section and click on POST /v3/observer

4. Copy and paste the model JSON into the body section under Parameters. Update the JSON string to include your Slack WebHook URL from earlier. Select the type of APM events you wish to receive by changing the parameter value from false to true.

  • testEvents: Notify when a diagnostic test completes or otherwise halts
  • seqEvents: Notify when connectivity to an appliance or software sequencer is lost or reestablished
  • sqaEvents: Notify when a service quality condition is violated or cleared
  • blacklisted: If the target URL has been blacklisted due to some problem

Make sure the endpoint includes the port, if other than port 80, and that any firewall rules are in place allowing inbound traffic from your organization’s APM server – see your URL once you’ve logged into APM for the source server.

5. Click the Try it out! button. If everything worked, you should see a 200 response code letting you know it was successful.

The final result:

If all goes well, you should start receiving APM events in your Slack channel in your workspace.

Next Steps:

Additional next steps include:

  • Adding authentication
  • Using other Slack APIs to integrate in a manner that best reflects your production environment
  • Filtering events in your relay to send only key events of interest
  • Refactoring to make it a more robust production ready application