AppNeta is one of Inc. Magazine’s Best Workplaces 2019
We're Hiring
Get our latest news, blogs and product updates

Integrate AppNeta with Salesforce Chatter Feed Using Our API
by Mike Marzec Mike Marzec on

AppNeta Performance Manager 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. In a past post, I looked at how to integrate AppNeta Performance Manager events into Slack channels. Justin previously discussed how to leverage the API to integrate with ServiceNow. Today, we’ll take a look at how to integrate with Salesforce Chatter feeds.

What is Salesforce.com (SFDC) Chatter?

Salesforce Chatter is a social sharing platform built into the Salesforce environment, which enables collaboration between groups and individuals. For companies that leverage the Salesforce CRM solution, Chatter provides a way to stay in-product for communications and collaboration throughout the work day and has many rich features due to its integration within Salesforce.

Why would an AppNeta customer want to integrate with SFDC Chatter?

By posting APM events to a Chatter feed, individuals or team members in technical roles, sales teams, customer care groups, front-line support, etc, can be kept informed about performance events in real-time, within their primary Salesforce communication space. They can then switch the AppNeta Performance Manager UI to help further investigate or report on their infrastructure’s status. To increase productivity, as a best practice you would send filtered key operational events to relevant Chatter feed. This helps facilitate the move to a single Salesforce view of your customer or your environment.

There are a number of ways to integrate with SFDC Chatter.

I’ve used the Connected App for this integration because it was fairly simple to set up and allows for explicit control over the app permissions.

Step by step integration walkthrough

Setup of a Salesforce Connected App

The flow we use here is accomplished via the SFDC user interface (UI). If you prefer to automate or have automation in place, there is an alternative way to create a connected app using a continuous integration (CI) tool, such as Travis.

Using the SFDC web UI, login to your Salesforce environment and navigate to the Setup Home page by selecting the configuration gear icon in the top right corner then select Setup:

login to your Salesforce environment and navigate to the Setup Home page by selecting the configuration gear icon in the top right corner then select Setup

In Setup -> Home, in the search bar, enter ‘app manager’:

Select the App Manager link from the search results:

Select the App Manager link from the search results:

In the Setup -> Lightning Experience App Manager select ‘New Connected App’:

In the Setup -> Lightning Experience App Manager select ‘New Connected App’:

Under the API (Enable OAuth Settings) section, make the following changes:

Under the API (Enable OAuth Settings) section, make the following changes:

Scroll to the bottom of this setup panel and select “save”. You’ll get a notice about the time it takes to deploy the new Connected App before you’re able to use it:

Scroll to the bottom of this setup panel and select “save”. You’ll get a notice about the time it takes to deploy the new Connected App before you’re able to use it

Once you select “Continue”, it’ll take a few minutes to get the info and then you’ll see:

Once you select “Continue”, it’ll take a few minutes to get the info and then you’ll see:

Note the Consumer Key and Consumer Secret, which you’ll need later on.

To get your security token, which is also needed for the connection, navigate to your user Avatar -> Settings:

To get your security token, which is also needed for the connection, navigate to your user Avatar -> Settings:

Under “My Personal Information”, select “Reset My Security Token”:

Under “My Personal Information”, select “Reset My Security Token”:

You’ll get your new token emailed to you:

You’ll get your new token emailed to you:

Code Development

Simple Python Connectivity Test

At this point, you can set up basic Python code to test the connection, as follows. Replace the text marked with angled brackets “<...>” with your info from the earlier steps. For example, if your Consumer Key is 1234 then you’ll use: "client_id": "1234".

View code if you wish to copy/paste:
#!/usr/bin/env python3

import requests

params = {
    "grant_type": "password",
    "client_id": "", # Consumer Key
    "client_secret": "", # Consumer Secret
    "username": "", # The email you use to login
    "password": "" # Concatenate your password and your security token
}
r = requests.post("https://login.salesforce.com/services/oauth2/token", params=params)
access_token = r.json().get("access_token")
instance_url = r.json().get("instance_url")
print("Access Token:", access_token)
print("Instance URL", instance_url)
print("Raw result:", r)

Running the above code in a command window yields the successful connection info:

scripts$ ./sfdc_test.py

Access Token:
Instance URL https://na57.salesforce.com
Raw result:

scripts$

You’ll get a one-time Access Token to use in submitting subsequent requests during your session. We’ll use that later on.

I used the following reference code for testing Salesforce authentication via the REST API: https://jereze.com/code/authentification-salesforce-rest-api-python/

Chatter Feed

In the Trailblazer Development environment, the Chatter Feed is already available in some of your SFDC apps or you could edit the default home page to include it. We’ll use the default Chatter feed that is in the SFDC Apps because it’s simpler and leverages default SFDC settings.

You can verify that the Chatter feed is in an SFDC App of interest by launching the App by selecting the SFDC App Launcher:

You can verify that the Chatter feed is in an SFDC App of interest by launching the App by selecting the SFDC App Launcher:

Launch the App you want to check. For example, we’ll select the Service app:

Launch the App you want to check. For example, we’ll select the Service app:

Notice that the Service App already has the Chatter app tab:

Notice that the Service App already has the Chatter app tab:

The next thing to do is post to the feed. We’ll dig into the Python script, next.

Python For APM-SFDC Integration

Basic test

The following rudimentary script will test the authentication and sending of a Chatter Feed posting, which you can verify in SFDC in the Chatter app tab.

Be sure to replace the sections marked in angled brackets “<>” with your own info, from earlier.

View test code for posting feed: Sfdc_test.py if you wish to copy/paste:

#!/usr/bin/env python3

import requests
import json

#
# Get an Access Token for later use...
#
params = {
    "grant_type": "password",
    "client_id": "", # Consumer Key
    "client_secret": "", # Consumer Secret
    "username": "", # The email you use to login
    "password": "" # Concatenate your password and your security token
}
r = requests.post("https://login.salesforce.com/services/oauth2/token", params=params)
access_token = r.json().get("access_token")
instance_url = r.json().get("instance_url")
print("Access Token:", access_token)
print("Instance URL", instance_url)
print("Raw result:", r)

#
# This helper function is copied from:
# https://jereze.com/code/authentification-salesforce-rest-api-python/
#
def sf_api_call(action, parameters = {}, method = 'get', data = {}):
    """
    Helper function to make calls to Salesforce REST API.
    Parameters: action (the URL), URL params, method (get, post or patch), data for POST/PATCH.
    """
    headers = {
        'Content-type': 'application/json',
        'Accept-Encoding': 'gzip',
        'Authorization': 'Bearer %s' % access_token
    }
    if method == 'get':
        r = requests.request(method, instance_url+action, headers=headers, params=parameters, timeout=30)
    elif method in ['post', 'patch']:
        r = requests.request(method, instance_url+action, headers=headers, json=data, params=parameters, timeout=10)
    else:
        # other methods not implemented in this example
        raise ValueError('Method should be get or post or patch.')
    print('Debug: API %s call: %s' % (method, r.url) )
    if r.status_code < 300:
        if method=='patch':
            return None
        else:
            return r.json()
    else:
        raise Exception('API error when calling %s : %s' % (r.url, r.content))


#
# This is an extremely simple test stanza that makes a call to the helper function sf_api_call()
# and prints the result of the POST
# For a simple test, use "me" as subjectId, below
#

print(
	json.dumps(
		sf_api_call('/services/data/v44.0/chatter/feed-elements',
			data = {
			   "body" : {
			    	"messageSegments" : [
						{
							"markupType" : "Paragraph",
							"type" : "MarkupBegin"
						},
						{
							"markupType" : "Bold",
							"type" : "MarkupBegin"
						},
						{
							"text" : "Event: ",
							"type" : "Text"
						},
						{
							"markupType" : "Bold",
							"type" : "MarkupEnd"
						},
						{
							"text" : "Summary of event goes here.",
							"type" : "Text"
						},
						{
							"markupType" : "Paragraph",
							"type" : "MarkupEnd"
						},
				        {
							"type" : "Mention",
							"username" : "<youl@somewhere.com>"
				        }
			         ]
			       },
			   "feedElementType" : "FeedItem",
			   "subjectId" : "me"
			},
			method = 'post'
		),
		indent = 2
	)
)

When you run the test Chatter post script, you should see the results in the Chatter tab:

When you run the test Chatter post script, you should see the results in the Chatter tab:

Integration with AppNeta Observer API

In this section, we complete the integration using the AppNeta observer. This will allow you to receive AppNeta Performance Manager Events at a registered endpoint, an application, which will receive, reformat and send the messages to the SFDC Chatter feed.

We’ll leverage the AppNeta event listener code that was used in the earlier Blog post, which integrated with Slack messaging. I’ve removed the core Slack processing code, so we’re able to focus on getting events into SFDC Chatter for this blog post.

With the integration completed, using the code listing below, the events can be seen in SFDC Chatter:

With the integration completed, using the code listing below, the events can be seen in SFDC Chatter:

You’ve now completed the integration of AppNeta Performance Manager Events into an SFDC Chatter channel. Enhancements would include enhancing the Python event observer to make it more production ready, filtering events forwarded into SFDC Chatter to satisfy key performance indicators (KPI) of interest, modifying the code to push to specific Chatter channels, i.e. to further customize the Chatter channel in SFDC and reflect that in the gateway code.

Further information on the AppNeta Performance Manager API event integration is available at https://docs.appneta.com/event-integration.html

Further general information about the AppNeta Performance Manager API is available at https://docs.appneta.com/api.html

View APM Event Listener Code 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
import sys

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

datFile = "" # data file, as a global name
msgService = "" # service to use for posting - can be Salesforce (SFDC) or Slack (Slack)
sfdcAccessToken = "" # Salesforce access token
sfdcInstanceUrl = "" # Salesforce instance URL

@app.route('/',methods=['GET','POST','OPTIONS'])
def receive_root_events():
    try:
        data = request.get_data()

        if request.content_length < 20000 and request.content_length != 0:
            logging.info( "receive_root_events: received request: {0}".format( pprint.pformat(str(data)) ) )
        else:
            logging.info( "receive_root_events: 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_root_events: Exception {0}".format( traceback.format_exc() ) )
        return '{"status": 500}\n'

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

@app.route('/event',methods=['POST'])
def receive_apm_events():
    global datFile

    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().")

        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 post{0}Message()".format(service))
            if SFDC in str(service):
                postSfdcMessage( data )
# Slack processing code removed for clarity: focus on SFDC processing.

            logging.info("receive_apm_events: try: post post{0}Message()".format(service))

            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'

def postSfdcMessage(data):
    """Post the given JSON APM Event data to a Salesforce Chatter Feed"""
    try:
        item = dict(data)
        postMessage = "Sequencer {0} event {1} \n".format(item["sequencerHost"], item["description"])
        # conditional processing of event attributes based on event type...
        if 'SQA' in str(item["type"]):
            postMessage = postMessage+"Quality: {0} \nSequencerHost: {1} \nTarget: {2}".format(
                item["pathServiceQuality"],
                item["sequencerHost"],
                item["target"])

        if 'TEST_EVENT' in str(item["type"]):
            postMessage = postMessage+"TestStatus: {0} \nTestId: {1} \nTarget: {2}".format(
                item["testStatus"],
                item["testId"],
                item["target"])

        if 'SEQUENCER_EVENT' in str(item["type"]):
            postMessage = postMessage+"SequencerHost: {0} \nSequencerStatus: {1}".format(
                item["sequencerHost"],
                item["sequencerStatus"])

        r = sf_api_call(str("/services/data/v44.0/chatter/feed-elements"),
            data = {
               "body" : {
                    "messageSegments" : [
                        {
                            "text" : postMessage,
                            "type" : "Text"
                        }
                     ]
                   },
               "feedElementType" : "FeedItem",
               "subjectId" : "me"
            },
            method = 'post'
        )

        logging.info("Posted to Salesforce Chatter: r.json: {0}".format(r))

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

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

def sfdcGetAccessToken(accessDict):
    global sfdcAccessToken
    global sfdcInstanceUrl

    #
    # Get an Access Token for later use...
    #
    logging.info( "sfdcGetAccessToken: SFDC access stanza is of type {0} with value: {1}".format(type(accessDict), accessDict ) )
    r = requests.post("https://login.salesforce.com/services/oauth2/token", params=accessDict)

    sfdcAccessToken = r.json().get("access_token")
    sfdcInstanceUrl = r.json().get("instance_url")
    logging.info("sfdcGetAccessToken: SFDC Access Token: {0}".format(sfdcAccessToken))
    logging.info("sfdcGetAccessToken: SFDC Instance URL {0}".format(sfdcInstanceUrl))
    logging.info("sfdcGetAccessToken: SFDC Raw result: {0}".format(r))

#
# This helper function is copied from: https://jereze.com/code/authentification-salesforce-rest-api-python/
#
def sf_api_call(action, parameters = {}, method = 'get', data = {}):
    global sfdcInstanceUrl

    """
    Helper function to make calls to Salesforce REST API.
    Parameters: action (the URL), URL params, method (get, post or patch), data for POST/PATCH.
    """

    headers = {
        'Content-type': 'application/json',
        'Accept-Encoding': 'gzip',
        'Authorization': 'Bearer %s' % sfdcAccessToken
    }
    if method == 'get':
        r = requests.request(method, sfdcInstanceUrl+action, headers=headers, params=parameters, timeout=30)
    elif method in ['post', 'patch']:
        r = requests.request(method, sfdcInstanceUrl+action, headers=headers, json=data, params=parameters, timeout=10)
    else:
        # other methods not implemented in this example
        raise ValueError('Method should be get or post or patch.')
    print('Debug: API %s call: %s' % (method, r.url) )
    if r.status_code < 300:
        if method=='patch':
            return None
        else:
            return r.json()
    else:
        raise Exception('API error when calling %s : %s' % (r.url, r.content))

# =================================================================
# Script Entry...
# =================================================================
if __name__ == '__main__':

    # ---------------------------
    # Get command line arguments
    # ---------------------------
    parser = argparse.ArgumentParser(
        description='Parameters for running the event catcher'
    )

    parser.add_argument(
        "--loglevel",
        type=str,
        help="The logging level. One of: [DEBUG | INFO | WARN | CRITICAL | FATAL]")

    parser.add_argument(
        "--datafile",
        type=str,
        help="The filename for storing the raw event data")

    parser.add_argument(
        "--service",
        type=str,
        help="Post to this service: Salesforce or Slack. One of: [SFDC | Slack]")

    parser.add_argument(
        "--sfdcAccessFile",
        type=str,
        help="This is a file containing the stanza string that with the Salesforce access parameters for obtaining an access token.")

    args = parser.parse_args()

    if args.loglevel:
        logLevel = getattr(logging, str(args.loglevel) ) # dynamic using string
    else:
        logLevel = logging.INFO
    print( "\tWe'll use logging level {0}".format(logLevel) )

    logFile = 'PostListener.{0}'.format(str(datetime.now().strftime("%Y%m%d.%H%M%s")))

    logging.basicConfig(
        format="%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s]  %(message)s",
        handlers=[
            logging.FileHandler("{0}.log".format(logFile))
            #logging.StreamHandler()
        ],
        level=logLevel
    )

    if args.datafile:
        datFile = str(args.datafile).format(str(datetime.now().strftime("%Y%m%d.%H%M%s")))
    else:
        datFile = 'apmEventData'.format(str(datetime.now().strftime("%Y%m%d.%H%M%s")))

    if args.service:
        service = str(args.service) # dynamic using string
    else:
        service = "SFDC"
    print( "\tWe'll use the {0} service to post messages.".format(service) )

    if args.sfdcAccessFile and 'SFDC' in str(service):
        with open(args.sfdcAccessFile, 'r') as f:
            sfdcAccessStanza = f.read()
            logging.info( "main: SFDC read of security file {0} has security stanza {1}".format( args.sfdcAccessFile, sfdcAccessStanza ) )
            sfdcGetAccessToken(json.loads(sfdcAccessStanza))
            f.close
    else:
        if 'SFDC' in str(service):
            print( "\tYou MUST have an SFDC access stanza if using Salesforce" )
            sys.exit()

    # ---------------------------
    # Run, run...!
    # ---------------------------

    app.run(host='0.0.0.0',debug=False,port=8181)

# =================================================================

View shell run-time control script if you wish to copy/paste:

  #!/bin/bash

#
# This is a wrapper around the Python script
# It allows for command line customization
#
export DATA_FILE=apmEventData
export LOG_LEVEL="DEBUG"
export SCRIPT_HOME=~/your path to the script
export SCRIPT_NAME=PostListener.py

export SERVICE="SFDC"
export SFDC_FILE=sfdcAccessStanza.txt

$SCRIPT_HOME/$SCRIPT_NAME \
	--loglevel $LOG_LEVEL \
	--datafile $DATA_FILE \
	--sfdcAccessFile $SFDC_FILE \
	--service $SERVICE

Separating the SFDC access parameters into a file is better than hard-coding the values in the script. However, production scripts/code should use other methods for credential storage, including encryption, etc. This is for proof of concept testing only. As noted earlier, replace the values in angled brackets “<>” with your own values.

View SFDC Access Stanza File sfdcAccessStanza.txt:
  
  params = {
    "grant_type": "password",
    "client_id": "",
    "client_secret": "",
    "username": "",
    "password": ""
}

Filed Under: networking technology, product news

Tags: API, application performance, application performance interface, cloud applications, cloud apps, network performance, network performance monitoring, salesforce, UCaaS, unified communications

Categories

Get a Demo

Dive into our end-user performance monitoring solution to see the value of end-to-end visibility for yourself.

Get a Demo

You might be interested in:

Go back to AppNeta Blog