diff --git a/app.js b/app.js index 3de49ba..1efe416 100644 --- a/app.js +++ b/app.js @@ -5,6 +5,7 @@ assert.ok(process.env.JAMBONES_MYSQL_HOST && process.env.JAMBONES_MYSQL_DATABASE, 'missing JAMBONES_MYSQL_XXX env vars'); assert.ok(process.env.DRACHTIO_PORT || process.env.DRACHTIO_HOST, 'missing DRACHTIO_PORT env var'); assert.ok(process.env.DRACHTIO_SECRET, 'missing DRACHTIO_SECRET env var'); +assert.ok(process.env.JAMBONES_RTPENGINES, 'missing DRACHTIO_SECRET env var'); const Srf = require('drachtio-srf'); const srf = new Srf(); @@ -12,7 +13,16 @@ const opts = Object.assign({ timestamp: () => {return `, "time": "${new Date().toISOString()}"`;} }, {level: process.env.JAMBONES_LOGLEVEL || 'info'}); const logger = require('pino')(opts); +const StatsCollector = require('jambonz-stats-collector'); +srf.locals.stats = new StatsCollector(logger); srf.locals.getFeatureServer = require('./lib/fs-tracking')(srf, logger); +const {getRtpEngine} = require('jambonz-rtpengine-utils')(process.env.JAMBONES_RTPENGINES.split(','), { + emitter: srf.locals.stats +}); +srf.locals.getRtpEngine = getRtpEngine; +srf.locals.activeCallIds = new Set(); +logger.info('starting..'); + const { lookupAuthHook, lookupSipGatewayBySignalingAddress @@ -28,7 +38,7 @@ srf.locals.dbHelpers = { lookupAuthHook, lookupSipGatewayBySignalingAddress }; -const {challengeDeviceCalls} = require('./lib/middleware')(srf, logger); +const {challengeDeviceCalls, initLocals} = require('./lib/middleware')(srf, logger); const CallSession = require('./lib/call-session'); if (process.env.DRACHTIO_HOST) { @@ -47,11 +57,16 @@ if (process.env.NODE_ENV === 'test') { } // challenge calls from devices, let calls from sip gateways through -srf.use('invite', [challengeDeviceCalls]); +srf.use('invite', [initLocals, challengeDeviceCalls]); srf.invite((req, res) => { const session = new CallSession(logger, req, res); session.connect(); }); -module.exports = {srf}; +srf.use((req, res, next, err) => { + logger.error(err, 'hit top-level error handler'); + res.send(500); +}); + +module.exports = {srf, logger}; diff --git a/lib/call-session.js b/lib/call-session.js index e4d633d..00c4283 100644 --- a/lib/call-session.js +++ b/lib/call-session.js @@ -1,8 +1,7 @@ const Emitter = require('events'); -const {getAppserver, isWSS, makeRtpEngineOpts} = require('./utils'); +const {isWSS, makeRtpEngineOpts} = require('./utils'); const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar'); const {parseUri, SipError} = require('drachtio-srf'); -const {getRtpEngine} = require('jambonz-rtpengine-utils')(process.env.JAMBONES_RTPENGINES.split(',')); const debug = require('debug')('jambonz:sbc-inbound'); class CallSession extends Emitter { @@ -13,13 +12,21 @@ class CallSession extends Emitter { this.srf = req.srf; this.logger = logger.child({callId: req.get('Call-ID')}); + this.getRtpEngine = req.srf.locals.getRtpEngine; this.getFeatureServer = req.srf.locals.getFeatureServer; + this.stats = this.srf.locals.stats; + this.activeCallIds = this.srf.locals.activeCallIds; + } async connect() { - const engine = getRtpEngine(this.logger); + this.activeCallIds.add(this.req.get('Call-ID')); + this.stats.gauge('sbc.sip.calls.count', this.activeCallIds.size); + const engine = this.getRtpEngine(this.logger); if (!engine) { this.logger.info('No available rtpengines, rejecting call!'); + const tags = ['accepted:no', 'sipStatus:480', `originator:${this.req.locals.originator}`]; + this.stats.increment('sbc.terminations', tags); return this.res.send(480); } debug(`got engine: ${JSON.stringify(engine)}`); @@ -31,6 +38,8 @@ class CallSession extends Emitter { const featureServer = this.getFeatureServer(); if (!featureServer) { this.logger.info('No available feature servers, rejecting call!'); + const tags = ['accepted:no', 'sipStatus:480', `originator:${this.req.locals.originator}`]; + this.stats.increment('sbc.terminations', tags); return this.res.send(480); } this.logger.debug(`using feature server ${featureServer}`); @@ -79,6 +88,8 @@ class CallSession extends Emitter { } } + if (this.req.canceled) throw new Error('call canceled'); + debug(`sending INVITE to ${proxy} with ${uri}`); const {uas, uac} = await this.srf.createB2BUA(this.req, this.res, uri, { proxy, @@ -102,16 +113,24 @@ class CallSession extends Emitter { this.logger.info('call connected'); debug('call connected'); this.emit('connected'); + const tags = ['accepted:yes', 'sipStatus:200', `originator:${this.req.locals.originator}`]; + this.stats.increment('sbc.terminations', tags); this._setHandlers({uas, uac}); return; } catch (err) { this.rtpEngineResource.destroy(); + this.activeCallIds.delete(this.req.get('Call-ID')); + this.stats.gauge('sbc.sip.calls.count', this.activeCallIds.size); if (err instanceof SipError) { + const tags = ['accepted:no', `sipStatus:${err.status}`, `originator:${this.req.locals.originator}`]; + this.stats.increment('sbc.terminations', tags); this.logger.info(`call failed with ${err.status}`); return this.emit('failed'); } - this.logger.error(err, 'unexpected error routing inbound call'); + else if (err.message !== 'call canceled') { + this.logger.error(err, 'unexpected error routing inbound call'); + } } } @@ -123,6 +142,8 @@ class CallSession extends Emitter { dlg.on('destroy', () => { this.logger.info('call ended'); this.rtpEngineResource.destroy(); + this.activeCallIds.delete(this.req.get('Call-ID')); + this.stats.gauge('sbc.sip.calls.count', this.activeCallIds.size); }); //re-invite diff --git a/lib/fs-tracking.js b/lib/fs-tracking.js index 8a44c6d..4bbe4f8 100644 --- a/lib/fs-tracking.js +++ b/lib/fs-tracking.js @@ -6,6 +6,7 @@ module.exports = (srf, logger) => { logger = logger || noopLogger; let dynamic = true; let idx = 0; + const stats = srf.locals.stats; srf.options((req, res) => { res.send(200); @@ -14,9 +15,9 @@ module.exports = (srf, logger) => { const status = req.get('X-FS-Status'); const calls = req.has('X-FS-Calls') ? parseInt(req.get('X-FS-Calls')) : 0; if (status === 'open') { - if (!contacts) { + if (!contacts.has(uri)) { logger.info(`adding feature server at ${uri}`); - //stats.gauge('sbc.featureservers', contacts.size + 1); + stats.gauge('sbc.featureservers.count', contacts.size + 1); } logger.debug(`Feature server at ${uri} has ${calls} calls`); contacts.set(uri, {pingTime: new Date(), calls: calls}); @@ -25,7 +26,7 @@ module.exports = (srf, logger) => { if (contacts.includes(uri)) { logger.info(`removing feature server at ${uri}`); contacts.delete(uri); - //stats.gauge('sbc.featureservers', contacts.size + 1); + stats.gauge('sbc.featureservers.count', contacts.size + 1); } } } @@ -55,7 +56,7 @@ module.exports = (srf, logger) => { const keys = [ ...contacts.keys() ]; logger.debug({keys}, `there are ${keys.length} feature servers online`); - //stats.gauge('sbc.featureservers', contacts.size); + stats.gauge('sbc.featureservers.count', contacts.size); }, CHECK_INTERVAL * 1000); } diff --git a/lib/middleware.js b/lib/middleware.js index 5b3a9cb..2f91272 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -3,9 +3,25 @@ const debug = require('debug')('jambonz:sbc-inbound'); module.exports = function(srf, logger) { const {lookupSipGatewayBySignalingAddress, lookupAuthHook} = srf.locals.dbHelpers; const authenticator = require('jambonz-http-authenticator')(lookupAuthHook, logger, {blacklistUnknownRealms: true}); + const {stats, activeCallIds} = srf.locals; + + function initLocals(req, res, next) { + req.locals = req.locals || {}; + const callId = req.get('Call-ID'); + req.on('cancel', () => { + logger.info({callId}, 'caller hungup before connecting to feature server'); + req.canceled = true; + const tags = ['canceled:yes', 'sipStatus:487']; + if (req.locals.originator) tags.push(`originator:${req.locals.originator}`); + stats.increment('sbc.terminations', tags); + activeCallIds.delete(callId); + stats.gauge('sbc.sip.calls.count', activeCallIds.size); + }); + stats.increment('sbc.invites', ['direction:inbound']); + next(); + } async function challengeDeviceCalls(req, res, next) { - req.locals = req.locals || {}; try { const gateway = await lookupSipGatewayBySignalingAddress(req.source_address, req.source_port); if (!gateway) { @@ -19,12 +35,14 @@ module.exports = function(srf, logger) { if (gateway.application_sid) req.locals.application_sid = gateway.application_sid; next(); } catch (err) { + stats.increment('sbc.terminations', ['sipStatus:500']); logger.error(err, `${req.get('Call-ID')} Error looking up related info for inbound call`); res.send(500); } } return { + initLocals, challengeDeviceCalls }; }; diff --git a/package-lock.json b/package-lock.json index bf32bd9..2d035c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "sbc-inbound", - "version": "0.2.1", + "version": "0.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -325,6 +325,15 @@ "integrity": "sha512-3/qRXczDi2Cdbz6jE+W3IflJOutRVica8frpBn14de1mBOkzDo+6tY33kNhvkw54Kn3PzRRD2VnGbGPcTAk4sw==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "blue-tape": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/blue-tape/-/blue-tape-1.0.0.tgz", @@ -1073,6 +1082,12 @@ "flat-cache": "^2.0.1" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "optional": true + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -1382,6 +1397,14 @@ "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==", "dev": true }, + "hot-shots": { + "version": "6.8.7", + "resolved": "https://registry.npmjs.org/hot-shots/-/hot-shots-6.8.7.tgz", + "integrity": "sha512-XH8iezBSZgVw2jegu96pUfF1Zv0VZ/iXjb7L5yE3F7mn7/bdhf4qeniXjO0wQWeefe433rhOsazNKLxM+XMI9w==", + "requires": { + "unix-dgram": "2.0.x" + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -1824,9 +1847,9 @@ } }, "jambonz-rtpengine-utils": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/jambonz-rtpengine-utils/-/jambonz-rtpengine-utils-0.0.5.tgz", - "integrity": "sha512-tkAwXkV6nYVBvF/epcIoNOUX8NDepFo25WP4a2DBu0+g8e76MzDChXGuziYRGswL3TFZxd6faTbU44rg+UcoMA==", + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/jambonz-rtpengine-utils/-/jambonz-rtpengine-utils-0.0.9.tgz", + "integrity": "sha512-G1d0ZHv5n5cYat0weDtgXyxrkXtZXg5GhSNCt9ekv2S1Yox3afzPdENLcL9eqltytHoBU/kJTnMQR7+8BTjFTQ==", "requires": { "debug": "^4.1.1", "rtpengine-client": "0.0.9" @@ -1842,6 +1865,14 @@ } } }, + "jambonz-stats-collector": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jambonz-stats-collector/-/jambonz-stats-collector-0.0.1.tgz", + "integrity": "sha512-SjldpRYr5fRn/gXb1w9T9YvTiV+5cenVyNQjZfjOGBpGUlcHQomK84oxgdIiaLbGXEg7VxM5E9kupEjoF6hKfg==", + "requires": { + "hot-shots": "^6.8.7" + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2172,6 +2203,12 @@ "lru-cache": "^4.1.3" } }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "optional": true + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4838,6 +4875,16 @@ } } }, + "unix-dgram": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unix-dgram/-/unix-dgram-2.0.3.tgz", + "integrity": "sha512-Bay5CkSLcdypcBCsxvHEvaG3mftzT5FlUnRToPWEAVxwYI8NI/8zSJ/Gknlp86MPhV6hBA8I8TBsETj2tssoHQ==", + "optional": true, + "requires": { + "bindings": "^1.3.0", + "nan": "^2.13.2" + } + }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", diff --git a/package.json b/package.json index 962893e..f05945f 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,11 @@ "debug": "^4.1.1", "drachtio-fn-b2b-sugar": "0.0.12", "drachtio-srf": "^4.4.28", + "hot-shots": "^6.8.7", "jambonz-db-helpers": "^0.3.2", "jambonz-http-authenticator": "0.1.1", - "jambonz-rtpengine-utils": "0.0.5", + "jambonz-rtpengine-utils": "0.0.9", + "jambonz-stats-collector": "0.0.1", "pino": "^5.14.0", "rtpengine-client": "^0.0.8" },