Skip to content

Instantly share code, notes, and snippets.

@khpeet
Created July 11, 2023 13:47
Show Gist options
  • Save khpeet/257fcef97e3d404b71528aa32c6ad545 to your computer and use it in GitHub Desktop.
Save khpeet/257fcef97e3d404b71528aa32c6ad545 to your computer and use it in GitHub Desktop.
ServiceNow XMLStats Synthetic Polling
/* <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> */
/* Imports */
/* <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> */
const assert = require('assert'),
{ promisify } = require('util'),
{ Parser, processors } = require('xml2js'),
got = require('got'),
{ Cookie, CookieJar } = require('tough-cookie')
/* <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> */
/* Config */
/* <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> */
var SNOW_URL = $secure.SNOW_URL; //base ServiceNow URL
var SNOW_USER = $secure.SNOW_USER;
var SNOW_PASS = $secure.SNOW_PASS;
var NR_INGEST_KEY = $secure.NR_INGEST_KEY;
/* <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> */
/* Constants */
/* <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> */
const TYPES = { // Available metric types
GAUGE: 'gauge',
COUNT: 'count',
SUMMARY: 'summary',
},
FREQUENCY = 300000, // This value needs to match the monitor frequency - ms
PREFIX = "servicenow", // Prefix to all metric names reported to New Relic
DEBUG = false // Log output for debugging
/* Add your handler function(s) to this array for it to be invoked by the main collector logic. */
const HANDLERS = [
collectServletMetrics,
collectSemaphoreMetrics
]
/* Add any includes that you require for your handlers here. These will be included in the call to the XMLstats interface as filters: xmlstats.do?includes=servlet,sessionsummary,semaphores */
const INCLUDES = [
'semaphores',
'servlet',
'sessionsummary',
]
/* <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> */
/* Utility functions */
/* <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> */
/**
* Create a metric in a form suitable for the metric API.
*
* @param timestamp the timestamp for the metric
* @param name the metric name
* @param type the metric type
* @param value the value for the metric
* @param attributes the dimensional attributes to add on the metric
*/
function createMetric(timestamp, name, type, value, attributes) {
const metric = {
timestamp,
name: `${PREFIX}.${name}`,
type,
value,
attributes,
}
if (type === TYPES.COUNT || type === TYPES.SUMMARY) {
metric['interval.ms'] = FREQUENCY
}
return metric
}
/**
* Add a metric to the set of metrics whose value is the value of key in obj.
* If key is not in obj, do nothing. This is a convenience method so the
* collection handlers aren't littered with `if` checks.
*
* @param metricSet a set of metric objects formatted for the metric API
* @param timestamp the timestamp for the metric
* @param obj the object to inspect for the named metric key/value pair
* @param key the key to look for in obj
* @param type the metric type
* @param attributes the dimensional attributes to add on the metric
* @param name the metric name or null to use the key as the name
*/
function addMetric(metricSet, timestamp, obj, key, type, attributes, name = null) {
if (typeof obj[key] !== 'undefined') {
const val = Array.isArray(obj[key]) ? obj[key][0] : obj[key]
metricSet.push(createMetric(timestamp, name || key, type, val, attributes))
}
}
/**
* Add a dimension key/value pair to the set of dimensions whose value is the
* value of key in obj. If key is not in obj, do nothing. This is a convenience
* method so the collection handlers aren't littered with `if` checks.
*
* @param dimensions an object of key/value pairs representing dimensions to add to a metric
* @param obj the object to inspect for the named metric key/value pair
* @param key the key to look for in obj
* @param name the attribute name or null to use the key as the name
*/
function addDimension(dimensions, obj, key, name = null) {
if (typeof obj[key] !== 'undefined') {
const val = Array.isArray(obj[key]) ? obj[key][0] : obj[key]
dimensions[name || key] = val
}
}
/**
* Push the given array of metrics to the metrics API.
*
* @param metrics an array of metrics suitable for the metrics API
*/
async function pushMetrics(metrics) {
DEBUG && console.log(metrics)
const {
body,
statusCode
} = await got.post('https://metric-api.newrelic.com/metric/v1', {
json: [{ metrics }],
headers: { 'Api-Key': NR_INGEST_KEY},
responseType: 'json',
})
if (statusCode !== 202) {
throw new Error('metrics could not be posted')
}
if (!body.requestId) {
throw new Error('invalid response: no requestId')
}
console.log(`posted ${metrics.length} metrics with requestId: ${body.requestId}`)
}
/* <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> */
/* Handler functions */
/* */
/* This is where you would add logic to collect additional metrics. Handler */
/* functions have the signature (timestamp, xmlstatsJson) -> []metrics. */
/* */
/* You should not need to modify anything else in this script except the */
/* handlers in this section. Everything else should be handled for you. */
/* <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> */
/**
* Parse the servlet metrics out of the full JSON results.
*
* @param ts the timestamp to use for creating metrics
* @param doc the JSON document converted from the xmlstats.do call
* @returns array of metrics
*/
function collectServletMetrics(ts, doc) {
const metrics = [],
dimensions = {}
addDimension(dimensions, doc, 'servlet.info')
addDimension(dimensions, doc, 'servlet.node_id')
addDimension(dimensions, doc, 'servlet.hostname')
addDimension(dimensions, doc, 'servlet.port')
addMetric(metrics, ts, doc, 'servlet.cache.flushes', TYPES.COUNT, dimensions)
addMetric(metrics, ts, doc, 'servlet.uptime', TYPES.COUNT, dimensions)
addMetric(metrics, ts, doc, 'servlet.transactions', TYPES.COUNT, dimensions)
addMetric(metrics, ts, doc, 'servlet.errors.handled', TYPES.COUNT, dimensions)
addMetric(metrics, ts, doc, 'servlet.processor.transactions', TYPES.COUNT, dimensions)
addMetric(metrics, ts, doc, 'servlet.cancelled.transactions', TYPES.COUNT, dimensions)
addMetric(metrics, ts, doc, 'servlet.active.sessions', TYPES.GAUGE, dimensions)
const sessions = doc['sessionsummary']
if (sessions && sessions.$) {
const $ = sessions.$
addMetric(metrics, ts, $, 'logged_in', TYPES.GAUGE, dimensions, 'servlet.sessions.logged_in')
addMetric(metrics, ts, $, 'end_user', TYPES.GAUGE, dimensions, 'servlet.sessions.end_user')
addMetric(metrics, ts, $, 'total', TYPES.GAUGE, dimensions, 'servlet.sessions.total')
}
function addMetricGroup(obj, key, name) {
const aggregations = {
one: '1m',
five: '5m',
fifteen: '15m',
hour: '1h',
daily: '1d'
}
if (!obj[key]) {
return
}
for (aggr in aggregations) {
if (!obj[key][aggr]) {
continue
}
const aggrObj = obj[key][aggr]
if (!aggrObj.$) {
continue
}
const basename = `${name}.duration.${aggregations[aggr]}`
addMetric(metrics, ts, aggrObj.$, 'count', TYPES.GAUGE, dimensions, `${name}.${aggregations[aggr]}`)
addMetric(metrics, ts, aggrObj.$, 'mean', TYPES.GAUGE, dimensions, `${basename}.mean`)
addMetric(metrics, ts, aggrObj.$, 'median', TYPES.GAUGE, dimensions, `${basename}.median`)
addMetric(metrics, ts, aggrObj.$, 'ninetypercent', TYPES.GAUGE, dimensions, `${basename}.ninetypercent`)
}
}
const servletMetrics = doc['servlet.metrics']
if (servletMetrics) {
addMetricGroup(servletMetrics, 'transactions', 'servlet.transactions.server')
addMetricGroup(servletMetrics, 'client_transactions', 'servlet.transactions.client')
addMetricGroup(servletMetrics, 'client_network_times', 'servlet.transactions.client_network')
addMetricGroup(servletMetrics, 'client_browser_times', 'servlet.transactions.client_browser')
addMetricGroup(servletMetrics, 'amb_transactions', 'servlet.transactions.amb_server')
}
return metrics
}
function collectSemaphoreMetrics(ts, doc) {
const metrics = [];
let nodeId, hostname = null;
if (doc['servlet.node_id']) {
nodeId = doc['servlet.node_id'];
}
if (doc['servlet.hostname']) {
hostname = doc['servlet.hostname'];
}
const semaphoreMetrics = doc['semaphores'];
if (semaphoreMetrics && semaphoreMetrics.length > 0) {
for (s of semaphoreMetrics) {
if (s.$) {
let $ = s.$;
addMetric(metrics, ts, $, 'available', TYPES.GAUGE, {'servlet.hostname': hostname, 'servlet.node_id': nodeId, 'semaphore.name': $.name}, 'semaphores.available')
addMetric(metrics, ts, $, 'max_queue_depth', TYPES.GAUGE, {'servlet.hostname': hostname, 'servlet.node_id': nodeId, 'semaphore.name': $.name}, 'semaphores.max_queue_depth')
addMetric(metrics, ts, $, 'queue_depth', TYPES.GAUGE, {'servlet.hostname': hostname, 'servlet.node_id': nodeId, 'semaphore.name': $.name}, 'semaphores.queue_depth')
addMetric(metrics, ts, $, 'rejected_executions', TYPES.GAUGE, {'servlet.hostname': hostname, 'servlet.node_id': nodeId, 'semaphore.name': $.name}, 'semaphores.rejected_executions')
addMetric(metrics, ts, $, 'maximum_concurrency', TYPES.GAUGE, {'servlet.hostname': hostname, 'servlet.node_id': nodeId, 'semaphore.name': $.name}, 'semaphores.maximum_concurrency')
}
}
}
return metrics;
}
/* <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> */
/* Collector Logic */
/* <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> */
/**
* Hit the xmlstats.do endpoint to gather all the metrics for the specified
* namespaces and invoke all the handlers.
*/
async function collectXmlStats() {
const xmlStatsUrl = SNOW_URL + `xmlstats.do?include=${INCLUDES.join(',')}`,
cookieJar = await getCookies(xmlStatsUrl),
response = await got(xmlStatsUrl, { cookieJar }),
parser = new Parser({
explicitArray: false,
valueProcessors: [processors.parseNumbers, processors.parseBooleans],
attrValueProcessors: [processors.parseNumbers, processors.parseBooleans],
}),
result = await parser.parseStringPromise(response.body)
if (!result.xmlstats) {
console.warn('no xmlstats element detected in xmlstats.do response')
return []
}
const xmlstats = result.xmlstats,
now = new Date().getTime()
let metrics = []
DEBUG && console.log(xmlstats);
HANDLERS.forEach(fn => {
metricSet = fn(now, xmlstats)
if (metricSet && metricSet.length > 0) {
metrics = [ ...metrics, ...metricSet ]
}
})
return metrics
}
/* <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> */
/* Login Logic */
/* <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> */
/**
* Grab the current cookies for the current page loaded in the browser.
*
* @param url url for the cookie jar
* @returns an array of cookies as JSON objects
*/
async function getCookies(url) {
const cookies = await $webDriver.manage().getCookies(),
cookieJar = new CookieJar();
setCookie = promisify(cookieJar.setCookie.bind(cookieJar));
for (let index = 0; index < cookies.length; index += 1) {
const ck = new Cookie({ ...cookies[index], key: cookies[index].name })
await setCookie(ck, url)
}
return cookieJar
}
/**
* Login to the SNOW instance using the SNOW URL, user, and password
* secure credentials.
*/
async function login() {
await $webDriver.get(SNOW_URL)
const userName = await $webDriver.findElement($selenium.By.id('user_name'))
await userName.clear()
await userName.sendKeys(SNOW_USER)
const password = await $webDriver.findElement($selenium.By.id('user_password'))
await password.clear()
await password.sendKeys(SNOW_PASS)
await $webDriver.findElement($selenium.By.id('sysverb_login')).click()
}
/* <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> */
/* Main Logic */
/* <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> */
/**
* Run the synthetic logic.
*/
async function main() {
/* Login to the SNOW server */
await login()
/* Hit the `xmlstats.do` endpoint to collect the stats */
const metrics = await collectXmlStats();
if (!metrics || metrics.length === 0) {
console.warn('no metrics collected')
return
}
/* Post the metrics back to New Relic */
await pushMetrics(metrics)
}
/* Invoke the synthetic logic */
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment