From 254479e289b60014f83992380872f157c860cd42 Mon Sep 17 00:00:00 2001 From: Dave Horton Date: Thu, 22 Sep 2022 23:44:58 +0200 Subject: [PATCH] Feature/app call count tracking (#52) * add call count tracking at the app level (optional) * update call_counts_app schema * update rtpengine-utils --- app.js | 6 ++++ lib/call-session.js | 29 ++++++++++--------- lib/middleware.js | 37 ++++++++++++++++-------- lib/utils.js | 68 +++++++++++++++++++++++++++++++++++++++++++-- package-lock.json | 68 ++++++++++++++++++++++----------------------- package.json | 6 ++-- 6 files changed, 147 insertions(+), 67 deletions(-) diff --git a/app.js b/app.js index 5e99edb..454cffe 100644 --- a/app.js +++ b/app.js @@ -17,6 +17,9 @@ const opts = Object.assign({ }, {level: process.env.JAMBONES_LOGLEVEL || 'info'}); const logger = require('pino')(opts); const { + writeCallCount, + writeCallCountSP, + writeCallCountApp, writeCdrs, queryCdrs, writeAlerts, @@ -65,6 +68,9 @@ const activeCallIds = new Map(); srf.locals = {...srf.locals, stats, + writeCallCount, + writeCallCountSP, + writeCallCountApp, writeCdrs, writeAlerts, AlertType, diff --git a/lib/call-session.js b/lib/call-session.js index 5da18db..491da26 100644 --- a/lib/call-session.js +++ b/lib/call-session.js @@ -1,7 +1,7 @@ const Emitter = require('events'); const sdpTransform = require('sdp-transform'); const SrsClient = require('@jambonz/siprec-client-utils'); -const {makeRtpEngineOpts, makeAccountCallCountKey, makeSPCallCountKey} = require('./utils'); +const {makeRtpEngineOpts, nudgeCallCounts} = require('./utils'); const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar'); const {SipError, stringifyUri, parseUri} = require('drachtio-srf'); const debug = require('debug')('jambonz:sbc-outbound'); @@ -71,10 +71,7 @@ class CallSession extends Emitter { this.activeCallIds = this.srf.locals.activeCallIds; this.writeCdrs = this.srf.locals.writeCdrs; - this.incrKey = req.srf.locals.realtimeDbHelpers.incrKey; this.decrKey = req.srf.locals.realtimeDbHelpers.decrKey; - this.callCountKey = makeAccountCallCountKey(req.locals.account_sid); - this.callCountKeySP = makeSPCallCountKey(req.locals.service_provider_sid); const {performLcr, lookupCarrierBySid, lookupSipGatewaysByCarrier} = this.srf.locals.dbHelpers; this.performLcr = performLcr; @@ -462,17 +459,19 @@ class CallSession extends Emitter { await other.destroy(); } catch (err) {} - Promise.all([ - this.decrKey(this.callCountKey), - this.decrKey(this.callCountKeySP) - ]) - .then(([calls, callsSP]) => { - this.logger.info({calls, callsSP}, 'decremented call counts after call completion'); - return [calls, callsSP]; - }) - .catch((err) => { - this.logger.error({err}, 'error decrementing call counts after call completion'); - }); + const trackingOn = process.env.JAMBONES_TRACK_ACCOUNT_CALLS || + process.env.JAMBONES_TRACK_SP_CALLS || + process.env.JAMBONES_TRACK_APP_CALLS; + + if (process.env.JAMBONES_HOSTING || trackingOn) { + const {writeCallCount, writeCallCountSP, writeCallCountApp} = this.req.srf.locals; + await nudgeCallCounts(this.logger, { + service_provider_sid: this.service_provider_sid, + account_sid: this.account_sid, + application_sid: this.application_sid + }, this.decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp}) + .catch((err) => this.logger.error(err, 'Error decrementing call counts')); + } /* write cdr for connected call */ if (this.req.locals.cdr) { diff --git a/lib/middleware.js b/lib/middleware.js index df5e75a..f996012 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -1,7 +1,7 @@ const debug = require('debug')('jambonz:sbc-outbound'); const parseUri = require('drachtio-srf').parseUri; const Registrar = require('@jambonz/mw-registrar'); -const {selectHostPort, makeAccountCallCountKey, makeSPCallCountKey} = require('./utils'); +const {selectHostPort, nudgeCallCounts} = require('./utils'); const FS_UUID_SET_NAME = 'fsUUIDs'; module.exports = (srf, logger, opts) => { @@ -87,21 +87,27 @@ module.exports = (srf, logger, opts) => { }; const checkLimits = async(req, res, next) => { - const {logger, account_sid, service_provider_sid} = req.locals; - const {writeAlerts, AlertType} = req.srf.locals; + const {logger, account_sid, service_provider_sid, application_sid} = req.locals; + const trackingOn = process.env.JAMBONES_TRACK_ACCOUNT_CALLS || + process.env.JAMBONES_TRACK_SP_CALLS || + process.env.JAMBONES_TRACK_APP_CALLS; + if (!process.env.JAMBONES_HOSTING && !trackingOn) { + logger.debug('tracking is off, skipping call limit checks'); + return next(); // skip + } + + const {writeCallCount, writeCallCountSP, writeCallCountApp, writeAlerts, AlertType} = req.srf.locals; - const keyAccount = makeAccountCallCountKey(account_sid); - const keySP = makeSPCallCountKey(service_provider_sid); try { - /* increment the call count */ - const [calls, callsSP] = await Promise.all([incrKey(keyAccount), incrKey(keySP)]); - logger.info({calls, callsSP}, 'incremented call counts'); - /* decrement count if INVITE is later rejected */ res.once('end', async({status}) => { if (status > 200) { - const [calls, callsSP] = await Promise.all([decrKey(keyAccount), decrKey(keySP)]); - logger.info({calls, callsSP}, `decremented call counts after ${status} response`); + nudgeCallCounts(logger, { + service_provider_sid, + account_sid, + application_sid + }, decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp}) + .catch((err) => logger.error(err, 'Error decrementing call counts')); const tags = ['accepted:no', `sipStatus:${status}`]; stats.increment('sbc.originations', tags); } @@ -111,6 +117,13 @@ module.exports = (srf, logger, opts) => { } }); + /* increment the call count */ + const {callsSP, calls} = await nudgeCallCounts(logger, { + service_provider_sid, + account_sid, + application_sid + }, incrKey, {writeCallCountSP, writeCallCount, writeCallCountApp}); + /* compare to account's limit, though avoid db hit when call count is low */ const minLimit = process.env.MIN_CALL_LIMIT ? parseInt(process.env.MIN_CALL_LIMIT) : @@ -134,7 +147,7 @@ module.exports = (srf, logger, opts) => { return req.srf.endSession(req); } } - else if (process.env.JAMBONES_TRACK_ACCOUNT_CALLS || process.env.JAMBONES_TRACK_SP_CALLS) { + else if (trackingOn) { const {account_limit, sp_limit} = await queryCallLimits(service_provider_sid, account_sid); if (process.env.JAMBONES_TRACK_ACCOUNT_CALLS && account_limit > 0 && calls > account_limit) { logger.info({calls, account_limit}, 'checkLimits: account limits exceeded'); diff --git a/lib/utils.js b/lib/utils.js index f52039a..d335afc 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -77,8 +77,9 @@ const pingMsTeamsGateways = (logger, srf) => { }); }; -const makeAccountCallCountKey = (sid) => `${sid}:outcalls:account`; -const makeSPCallCountKey = (sid) => `${sid}:outcalls:sp`; +const makeAccountCallCountKey = (sid) => `outcalls:account:${sid}`; +const makeSPCallCountKey = (sid) => `outcalls:sp:${sid}`; +const makeAppCallCountKey = (sid) => `outcalls:app:${sid}`; const equalsIgnoreOrder = (a, b) => { if (a.length !== b.length) return false; @@ -111,6 +112,66 @@ const createHealthCheckApp = (port, logger) => { }); }; +const nudgeCallCounts = async(logger, sids, nudgeOperator, writers) => { + const {service_provider_sid, account_sid, application_sid} = sids; + const {writeCallCount, writeCallCountSP, writeCallCountApp} = writers; + const nudges = []; + const writes = []; + + logger.debug(sids, 'nudgeCallCounts'); + + if (process.env.JAMBONES_TRACK_SP_CALLS) { + const key = makeSPCallCountKey(service_provider_sid); + nudges.push(nudgeOperator(key)); + } + else { + nudges.push(() => Promise.resolve(null)); + } + + if (process.env.JAMBONES_TRACK_ACCOUNT_CALLS || process.env.JAMBONES_HOSTING) { + const key = makeAccountCallCountKey(account_sid); + nudges.push(nudgeOperator(key)); + } + else { + nudges.push(() => Promise.resolve(null)); + } + + if (process.env.JAMBONES_TRACK_APP_CALLS && application_sid) { + const key = makeAppCallCountKey(application_sid); + nudges.push(nudgeOperator(key)); + } + else { + nudges.push(() => Promise.resolve(null)); + } + + try { + const [callsSP, calls, callsApp] = await Promise.all(nudges); + logger.debug({ + calls, callsSP, callsApp, + service_provider_sid, account_sid, application_sid}, 'call counts after adjustment'); + if (process.env.JAMBONES_TRACK_SP_CALLS) { + writes.push(writeCallCountSP({service_provider_sid, calls_in_progress: callsSP})); + } + + if (process.env.JAMBONES_TRACK_ACCOUNT_CALLS || process.env.JAMBONES_HOSTING) { + writes.push(writeCallCount({service_provider_sid, account_sid, calls_in_progress: calls})); + } + + if (process.env.JAMBONES_TRACK_APP_CALLS && application_sid) { + writes.push(writeCallCountApp({service_provider_sid, account_sid, application_sid, calls_in_progress: callsApp})); + } + + /* write the call counts to the database */ + Promise.all(writes).catch((err) => logger.error({err}, 'Error writing call counts')); + + return {callsSP, calls, callsApp}; + } catch (err) { + logger.error(err, 'error incrementing call counts'); + } + + return {callsSP: null, calls: null, callsApp: null}; +}; + module.exports = { makeRtpEngineOpts, selectHostPort, @@ -119,5 +180,6 @@ module.exports = { makeSPCallCountKey, equalsIgnoreOrder, systemHealth, - createHealthCheckApp + createHealthCheckApp, + nudgeCallCounts }; diff --git a/package-lock.json b/package-lock.json index 28aa43f..18068c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,10 @@ "@jambonz/http-health-check": "^0.0.1", "@jambonz/mw-registrar": "0.2.2", "@jambonz/realtimedb-helpers": "^0.4.29", - "@jambonz/rtpengine-utils": "^0.3.1", + "@jambonz/rtpengine-utils": "^0.3.4", "@jambonz/siprec-client-utils": "^0.1.4", "@jambonz/stats-collector": "^0.1.6", - "@jambonz/time-series": "^0.2.2", + "@jambonz/time-series": "^0.2.4", "cidr-matcher": "^2.1.1", "debug": "^4.3.4", "drachtio-fn-b2b-sugar": "^0.0.12", @@ -617,12 +617,12 @@ } }, "node_modules/@jambonz/rtpengine-utils": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@jambonz/rtpengine-utils/-/rtpengine-utils-0.3.1.tgz", - "integrity": "sha512-vf0TedWgDhOvlsbGcjrHmSDwyz/85fZ8TjxE50AjD4ueSw/zULwl7wezn3wTz70oTwbzAPYYHnjhyQJns9cwPg==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@jambonz/rtpengine-utils/-/rtpengine-utils-0.3.4.tgz", + "integrity": "sha512-7I7Q7O/TuWgk8Z0OX/qGrQZczeDt9pDLnGSwy88NTv5lAbrOYmPyNTC/8VJGfbHISfvBaPYT88+wQn2RIpXRPg==", "dependencies": { "debug": "^4.3.1", - "rtpengine-client": "^0.3.2", + "rtpengine-client": "^0.3.6", "ws": "^8.5.0" } }, @@ -644,9 +644,9 @@ } }, "node_modules/@jambonz/time-series": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.2.2.tgz", - "integrity": "sha512-QGljKP25R93nDenkuX7bXr7tZfsrRpHTK4ygUWzpRWIe44trY2wUcaTc+HevBSjCSshbOkB/Nn9Tzz19JTzO3w==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.2.4.tgz", + "integrity": "sha512-0rvELi9V/qdyPpM/2LidTd2EDN34iCU6U8ccuLUgqV05FaH/EW0qN0RFaVXVlFK6wCDZqEOBynd7x05De3dncw==", "dependencies": { "debug": "^4.3.1", "influx": "^5.9.3" @@ -1015,9 +1015,9 @@ ] }, "node_modules/bencode": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bencode/-/bencode-2.0.2.tgz", - "integrity": "sha512-0ilVjnE2diLdbec/3KN14SP0KE85wh8v/FceNRMbAB2ioc3yTj9tgqdoK9tFEH++TZ10JreTS29qTwg7+SpTiQ==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bencode/-/bencode-2.0.3.tgz", + "integrity": "sha512-D/vrAD4dLVX23NalHwb8dSvsUsxeRPO8Y7ToKA015JQYq69MLDOMkC0uGZYA/MPpltLO8rt8eqFC2j8DxjTZ/w==" }, "node_modules/bent": { "version": "7.3.12", @@ -4241,9 +4241,9 @@ } }, "node_modules/rtpengine-client": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/rtpengine-client/-/rtpengine-client-0.3.2.tgz", - "integrity": "sha512-RbttcmoQxtYU7VCHZR2RHE3eMFShZqq8cMRfPAGw42o4TDdET4Ih4OC0NicVyVRpzlRlzfysTvFWQNXVth03wQ==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/rtpengine-client/-/rtpengine-client-0.3.6.tgz", + "integrity": "sha512-V+o1j0F1bQekFifvkdB9LwdvRX8neMQ6HlG6TG6jFYFCvT1iMrh6BXaKIvn8JvJ4KcwHwTMJGJQSXpLmwa8KsQ==", "dependencies": { "bencode": "^2.0.1", "uuid": "^8.2.0", @@ -4251,9 +4251,9 @@ } }, "node_modules/rtpengine-client/node_modules/ws": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", - "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", "engines": { "node": ">=8.3.0" }, @@ -5571,12 +5571,12 @@ } }, "@jambonz/rtpengine-utils": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@jambonz/rtpengine-utils/-/rtpengine-utils-0.3.1.tgz", - "integrity": "sha512-vf0TedWgDhOvlsbGcjrHmSDwyz/85fZ8TjxE50AjD4ueSw/zULwl7wezn3wTz70oTwbzAPYYHnjhyQJns9cwPg==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@jambonz/rtpengine-utils/-/rtpengine-utils-0.3.4.tgz", + "integrity": "sha512-7I7Q7O/TuWgk8Z0OX/qGrQZczeDt9pDLnGSwy88NTv5lAbrOYmPyNTC/8VJGfbHISfvBaPYT88+wQn2RIpXRPg==", "requires": { "debug": "^4.3.1", - "rtpengine-client": "^0.3.2", + "rtpengine-client": "^0.3.6", "ws": "^8.5.0" } }, @@ -5598,9 +5598,9 @@ } }, "@jambonz/time-series": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.2.2.tgz", - "integrity": "sha512-QGljKP25R93nDenkuX7bXr7tZfsrRpHTK4ygUWzpRWIe44trY2wUcaTc+HevBSjCSshbOkB/Nn9Tzz19JTzO3w==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.2.4.tgz", + "integrity": "sha512-0rvELi9V/qdyPpM/2LidTd2EDN34iCU6U8ccuLUgqV05FaH/EW0qN0RFaVXVlFK6wCDZqEOBynd7x05De3dncw==", "requires": { "debug": "^4.3.1", "influx": "^5.9.3" @@ -5882,9 +5882,9 @@ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "bencode": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bencode/-/bencode-2.0.2.tgz", - "integrity": "sha512-0ilVjnE2diLdbec/3KN14SP0KE85wh8v/FceNRMbAB2ioc3yTj9tgqdoK9tFEH++TZ10JreTS29qTwg7+SpTiQ==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bencode/-/bencode-2.0.3.tgz", + "integrity": "sha512-D/vrAD4dLVX23NalHwb8dSvsUsxeRPO8Y7ToKA015JQYq69MLDOMkC0uGZYA/MPpltLO8rt8eqFC2j8DxjTZ/w==" }, "bent": { "version": "7.3.12", @@ -8311,9 +8311,9 @@ "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==" }, "rtpengine-client": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/rtpengine-client/-/rtpengine-client-0.3.2.tgz", - "integrity": "sha512-RbttcmoQxtYU7VCHZR2RHE3eMFShZqq8cMRfPAGw42o4TDdET4Ih4OC0NicVyVRpzlRlzfysTvFWQNXVth03wQ==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/rtpengine-client/-/rtpengine-client-0.3.6.tgz", + "integrity": "sha512-V+o1j0F1bQekFifvkdB9LwdvRX8neMQ6HlG6TG6jFYFCvT1iMrh6BXaKIvn8JvJ4KcwHwTMJGJQSXpLmwa8KsQ==", "requires": { "bencode": "^2.0.1", "uuid": "^8.2.0", @@ -8321,9 +8321,9 @@ }, "dependencies": { "ws": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", - "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", "requires": {} } } diff --git a/package.json b/package.json index 5092fab..513beee 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "description": "jambonz session border controller application for outbound calls", "scripts": { "start": "node app", - "test": "NODE_ENV=test HTTP_PORT=3050 JAMBONZ_HOSTING=1 JAMBONES_NETWORK_CIDR=127.0.0.1/32 JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_LOGLEVEL=info DRACHTIO_SECRET=cymru DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 JAMBONES_RTPENGINES=127.0.0.1:12222 node test/ ", + "test": "NODE_ENV=test HTTP_PORT=3050 JAMBONES_HOSTING=1 JAMBONES_NETWORK_CIDR=127.0.0.1/32 JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_LOGLEVEL=error DRACHTIO_SECRET=cymru DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 JAMBONES_RTPENGINES=127.0.0.1:12222 node test/ ", "coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test", "jslint": "eslint app.js lib" }, @@ -31,10 +31,10 @@ "@jambonz/http-health-check": "^0.0.1", "@jambonz/mw-registrar": "0.2.2", "@jambonz/realtimedb-helpers": "^0.4.29", - "@jambonz/rtpengine-utils": "^0.3.1", + "@jambonz/rtpengine-utils": "^0.3.4", "@jambonz/siprec-client-utils": "^0.1.4", "@jambonz/stats-collector": "^0.1.6", - "@jambonz/time-series": "^0.2.2", + "@jambonz/time-series": "^0.2.4", "cidr-matcher": "^2.1.1", "debug": "^4.3.4", "drachtio-fn-b2b-sugar": "^0.0.12",