Feature/app call count tracking (#52)

* add call count tracking at the app level (optional)

* update call_counts_app schema

* update rtpengine-utils
This commit is contained in:
Dave Horton
2022-09-22 23:44:58 +02:00
committed by GitHub
parent a10a311dcb
commit 254479e289
6 changed files with 147 additions and 67 deletions
+6
View File
@@ -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,
+14 -15
View File
@@ -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) {
+25 -12
View File
@@ -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');
+65 -3
View File
@@ -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
};
+34 -34
View File
@@ -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": {}
}
}
+3 -3
View File
@@ -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",