Created
July 11, 2023 13:47
-
-
Save khpeet/257fcef97e3d404b71528aa32c6ad545 to your computer and use it in GitHub Desktop.
ServiceNow XMLStats Synthetic Polling
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> */ | |
/* 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