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.
The Slack environment:
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.
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:
#!/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://: /event”
Here is the code for that route endpoint:
@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 < 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.
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.
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
Filed Under: Performance Monitoring, Product News