From ca53e4fb7fe2113d6672cecc33e5b0f5faef5743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20H=C3=B6rtnagl?= Date: Mon, 20 Apr 2020 06:22:20 +0200 Subject: [PATCH 1/8] Prometheus exporter (initial version) --- prom.md | 59 +++++++++ server/lib/promExporter.js | 238 +++++++++++++++++++++++++++++++++++++ server/package.json | 1 + server/server.js | 4 + 4 files changed, 302 insertions(+) create mode 100644 prom.md create mode 100644 server/lib/promExporter.js diff --git a/prom.md b/prom.md new file mode 100644 index 0000000..47ae230 --- /dev/null +++ b/prom.md @@ -0,0 +1,59 @@ +# Prometheus exporter + +The goal of this version is to offer a few basic metrics for +initial testing. The set of supported metrics can be extended. + +The current implementation is +[unconventional](https://prometheus.io/docs/instrumenting/writing_exporters) +in that it creates new metrics each time but does not register a +custom collector. Reasons are that the exporter should +[clear out metrics](https://github.com/prometheus/client_python/issues/182) +for closed connections but that `prom-client` +[does not yet support](https://github.com/siimon/prom-client/issues/241) +custom collectors. + +This version has been ported from an earlier Python version that was not part +of `multiparty-meeting` but connected as an interactive client. + +## Configuration + +| `.env` | description | +|--------|-------| +| `DEBUG` | e.g. `*WARN*,*ERROR*,*:prom` for debugging | +| `PROM_DEIDENTIFY` | if set, deidentify IP addresses | +| `PROM_NUMERIC` | if set, show numerical IP addresses | +| `PROM_PORT` | if set, enable exporter on this port | +| `PROM_QUIET` | if set, include fewer labels | + +## License + +MIT License (without copyright notice) + +## Metrics + +| metric | value | +|--------|-------| +| `edumeet_peers`| | +| `edumeet_rooms`| | +| `mediasoup_consumer_byte_count_bytes`| [`byteCount`](https://mediasoup.org/documentation/v3/mediasoup/rtc-statistics/#Consumer-Statistics) | +| `mediasoup_consumer_score`| [`score`](https://mediasoup.org/documentation/v3/mediasoup/rtc-statistics/#Consumer-Statistics) | +| `mediasoup_producer_byte_count_bytes`| [`byteCount`](https://mediasoup.org/documentation/v3/mediasoup/rtc-statistics/#Producer-Statistics) | +| `mediasoup_producer_score`| [`score`](https://mediasoup.org/documentation/v3/mediasoup/rtc-statistics/#Producer-Statistics) | + +## Architecture + +``` ++-----------+ +---------------------------------------------+ +| workers | | server observer API | +| | sock | +------o------+----o-----+ +| +------+ | int. server | exporter | +| | | | | | +| mediasoup | | express socket.io | net | express | ++-----+-----+ +----+---------+-----+-----+-------+-----+----+ + ^ min-max ^ 443 ^ 443 ^ sock ^ PROM_PORT + | RTP | HTTPS | ws | | HTTP + | | | | | + | +-+---------+-+ +------+------+ +---+--------+ + +---------------+ app | | int. client | | Prometheus | + +-------------+ +-------------+ +------------+ +``` diff --git a/server/lib/promExporter.js b/server/lib/promExporter.js new file mode 100644 index 0000000..ccb2562 --- /dev/null +++ b/server/lib/promExporter.js @@ -0,0 +1,238 @@ +const { Resolver } = require('dns').promises; +const express = require('express'); +const mediasoup = require('mediasoup'); +const prom = require('prom-client'); + +const Logger = require('./Logger'); +const Peer = require('./Peer'); +const Room = require('./Room'); + +const logger = new Logger('prom'); +const resolver = new Resolver(); + +const workers = new Map(); + +const label_names = [ + 'pid', 'room_id', 'peer_id', 'display_name', 'user_agent', 'transport_id', + 'proto', 'local_addr', 'remote_addr', 'id', 'kind', 'codec', 'type' +]; + +const metadata = { + 'byteCount': { metric_type: prom.Counter, unit: 'bytes' }, + 'score': { metric_type: prom.Gauge } +} + +common_labels = function(both, fn) { + for (let [room_id, room] of rooms) { + for (let [peer_id, peer] of peers) { + if (fn(peer)) { + let display_name = peer._displayName; + + let user_agent = peer._socket.client.request.headers['user-agent']; + let kind = both.kind; + let codec = both.rtpParameters.codecs[0].mimeType.split('/')[1]; + return { room_id, peer_id, display_name, user_agent, kind, codec }; + } + } + } + throw new Error('cannot find generic labels'); +} + +set_value = function(key, m, labels, v) { + logger.debug(`set_value key=${key} v=${v}`); + switch (metadata[key].metric_type) { + case prom.Counter: + m.inc(labels, v); + break; + case prom.Gauge: + m.set(labels, v); + break; + default: + throw new Error(`unexpected metric: ${metric}`); + } +} + + +addr = async function(ip, port) { + if ('PROM_DEIDENTIFY' in process.env) { + let a = ip.split('.') + for (let i = 0; i < a.length - 2; i++) { + a[i] = 'xx'; + } + return a.join('.'); + } + else if ('PROM_NUMERIC' in process.env) { + return ip; + } + else { + try { + let a = await resolver.reverse(ip); + ip = a[0]; + } + catch (err) { + logger.error(`reverse DNS query failed: ${ip} ${err.code}`); + } + return `${ip}:${port}`; + } +} + +quiet = function(s) { + return 'PROM_QUIET' in process.env ? '' : s; +} + +collect = async function(registry, rooms, peers) { + + metrics = function(subsystem) { + let namespace = 'mediasoup'; + let metrics = new Map(); + for (let key in metadata) { + value = metadata[key]; + let name = key.split(/(?=[A-Z])/).join('_').toLowerCase(); + let unit = value.unit; + let metric_type = value.metric_type; + let s = `${namespace}_${subsystem}_${name}`; + if (unit) { + s += `_${unit}`; + } + m = new metric_type({name: s, help: `${subsystem}.${key}`, + labelNames: label_names, registers: [registry]}); + metrics.set(key, m); + } + return metrics; + } + + logger.debug('collect'); + const m_rooms = new prom.Gauge({name: 'edumeet_rooms', help: '#rooms', + registers: [registry]}); + m_rooms.set(rooms.size); + const m_peers = new prom.Gauge({name: 'edumeet_peers', help: '#peers', + labelNames: ['room_id'], registers: [registry]}); + for (let [room_id, room] of rooms) { + m_peers.labels(room_id).set(Object.keys(room._peers).length); + } + + const m_consumer = metrics('consumer'); + const m_producer = metrics('producer'); + for (let [pid, worker] of workers) { + logger.debug(`visiting worker ${pid}`); + for (let router of worker._routers) { + logger.debug(`visiting router ${router.id}`); + for (let [transport_id, transport] of router._transports) { + logger.debug(`visiting transport ${transport_id}`); + let transport_j = await transport.dump(); + if (transport_j.iceState != 'completed') { + logger.debug(`skipping transport ${transport_id}}: ${transport_j.iceState}`); + continue; + } + let ice_selected_tuple = transport_j.iceSelectedTuple; + let proto = ice_selected_tuple.protocol + let local_addr = await addr(ice_selected_tuple.localIp, + ice_selected_tuple.localPort); + let remote_addr = await addr(ice_selected_tuple.remoteIp, + ice_selected_tuple.remotePort); + for (let [producer_id, producer] of transport._producers) { + logger.debug(`visiting producer ${producer_id}`); + let { room_id, peer_id, display_name, user_agent, kind, codec } = + common_labels(producer, peer => peer._producers.has(producer_id)); + let a = await producer.getStats(); + for (let x of a) { + let type = x.type; + let labels = { + 'pid': pid, + 'room_id': room_id, + 'peer_id': peer_id, + 'display_name': display_name, + 'user_agent': user_agent, + 'transport_id': quiet(transport_id), + 'proto': proto, + 'local_addr': local_addr, + 'remote_addr': remote_addr, + 'id': quiet(producer_id), + 'kind': kind, + 'codec': codec, + 'type': type + } + for (let [key, m] of m_producer) { + set_value(key, m, labels, x[key]); + } + } + } + for (let [consumer_id, consumer] of transport._consumers) { + logger.debug(`visiting consumer ${consumer_id}`); + let { room_id, peer_id, display_name, user_agent, kind, codec } = + common_labels(consumer, peer => peer._consumers.has(consumer_id)); + let a = await consumer.getStats(); + for (let x of a) { + if (x.type == 'inbound-rtp') { + continue; + } + let type = x.type; + let labels = { + 'pid': pid, + 'room_id': room_id, + 'peer_id': peer_id, + 'display_name': display_name, + 'user_agent': user_agent, + 'transport_id': quiet(transport_id), + 'proto': proto, + 'local_addr': local_addr, + 'remote_addr': remote_addr, + 'id': quiet(consumer_id), + 'kind': kind, + 'codec': codec, + 'type': type + } + for (let [key, m] of m_consumer) { + set_value(key, m, labels, x[key]); + } + } + } + } + } + } +} + +module.exports = async function(rooms, peers) { + try { + logger.debug(`PROM_DEIDENTIFY=${process.env.PROM_DEIDENTIFY}`); + logger.debug(`PROM_NUMERIC=${process.env.PROM_NUMERIC}`); + logger.debug(`PROM_PORT=${process.env.PROM_PORT}`); + logger.debug(`PROM_QUIET=${process.env.PROM_QUIET}`); + let s_port = process.env.PROM_PORT; + if (!s_port) { + logger.info('exporter disabled'); + } + else { + let n_port = Number(s_port); + if (Number.isNaN(n_port)) { + throw new TypeError(`PROM_PORT has illegal value: ${s_port}`); + } + + mediasoup.observer.on('newworker', worker => { + logger.debug(`observing newworker ${worker.pid} #${workers.size}`); + workers.set(worker.pid, worker); + worker.observer.on('close', () => { + logger.debug(`observing close worker ${worker.pid} #${workers.size - 1}`); + workers.delete(worker.pid); + }); + }); + + let app = express(); + app.get('/', async (req, res) => { + logger.debug(`GET ${req.originalUrl}`); + let registry = new prom.Registry(); + await collect(registry, rooms, peers); + res.set('Content-Type', registry.contentType); + let data = registry.metrics(); + res.end(data); + }); + let server = app.listen(n_port, () => { + address = server.address(); + logger.info(`listening ${address.address}:${address.port}`); + }); + } + } + catch (err) { + logger.error(err); + } +} diff --git a/server/package.json b/server/package.json index d8b314d..d156a62 100644 --- a/server/package.json +++ b/server/package.json @@ -31,6 +31,7 @@ "passport": "^0.4.0", "passport-lti": "0.0.7", "pidusage": "^2.0.17", + "prom-client": ">=12.0.0", "redis": "^2.8.0", "socket.io": "^2.3.0", "spdy": "^4.0.1", diff --git a/server/server.js b/server/server.js index 70cb5bc..9253bec 100755 --- a/server/server.js +++ b/server/server.js @@ -34,6 +34,7 @@ const expressSession = require('express-session'); const RedisStore = require('connect-redis')(expressSession); const sharedSession = require('express-socket.io-session'); const interactiveServer = require('./lib/interactiveServer'); +const promExporter = require('./lib/promExporter'); /* eslint-disable no-console */ console.log('- process.env.DEBUG:', process.env.DEBUG); @@ -132,6 +133,9 @@ async function run() // Open the interactive server. await interactiveServer(rooms, peers); + // start Prometheus exporter + await promExporter(rooms, peers); + if (typeof(config.auth) === 'undefined') { logger.warn('Auth is not configured properly!'); From 53e6ae68c9ff8ea9904984dd067df1b6a6af1ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20H=C3=B6rtnagl?= Date: Fri, 24 Apr 2020 07:36:11 +0200 Subject: [PATCH 2/8] documentation (firewall) --- prom.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/prom.md b/prom.md index 47ae230..37a2f50 100644 --- a/prom.md +++ b/prom.md @@ -25,6 +25,11 @@ of `multiparty-meeting` but connected as an interactive client. | `PROM_PORT` | if set, enable exporter on this port | | `PROM_QUIET` | if set, include fewer labels | +If `multiparty-meeting` was installed with +[`mm-absible`](https://github.com/misi/mm-ansible) +it may be necessary to open the `iptables` firewall that was established +with `ferm` for incoming TCP traffic on `PROM_PORT`. + ## License MIT License (without copyright notice) From 00f1ec792975f7b91ceca04abde0c24901732038 Mon Sep 17 00:00:00 2001 From: christian2 Date: Fri, 1 May 2020 11:41:18 +0200 Subject: [PATCH 3/8] align coding conventions --- server/lib/promExporter.js | 388 ++++++++++++++++++------------------- server/server.js | 4 +- 2 files changed, 196 insertions(+), 196 deletions(-) diff --git a/server/lib/promExporter.js b/server/lib/promExporter.js index ccb2562..d2989d3 100644 --- a/server/lib/promExporter.js +++ b/server/lib/promExporter.js @@ -13,226 +13,226 @@ const resolver = new Resolver(); const workers = new Map(); const label_names = [ - 'pid', 'room_id', 'peer_id', 'display_name', 'user_agent', 'transport_id', - 'proto', 'local_addr', 'remote_addr', 'id', 'kind', 'codec', 'type' + 'pid', 'room_id', 'peer_id', 'display_name', 'user_agent', 'transport_id', + 'proto', 'local_addr', 'remote_addr', 'id', 'kind', 'codec', 'type' ]; const metadata = { - 'byteCount': { metric_type: prom.Counter, unit: 'bytes' }, - 'score': { metric_type: prom.Gauge } + 'byteCount': { metric_type: prom.Counter, unit: 'bytes' }, + 'score': { metric_type: prom.Gauge } } common_labels = function(both, fn) { - for (let [room_id, room] of rooms) { - for (let [peer_id, peer] of peers) { - if (fn(peer)) { - let display_name = peer._displayName; + for (let [room_id, room] of rooms) { + for (let [peer_id, peer] of peers) { + if (fn(peer)) { + let display_name = peer._displayName; - let user_agent = peer._socket.client.request.headers['user-agent']; - let kind = both.kind; - let codec = both.rtpParameters.codecs[0].mimeType.split('/')[1]; - return { room_id, peer_id, display_name, user_agent, kind, codec }; - } - } - } - throw new Error('cannot find generic labels'); + let user_agent = peer._socket.client.request.headers['user-agent']; + let kind = both.kind; + let codec = both.rtpParameters.codecs[0].mimeType.split('/')[1]; + return { room_id, peer_id, display_name, user_agent, kind, codec }; + } + } + } + throw new Error('cannot find generic labels'); } set_value = function(key, m, labels, v) { - logger.debug(`set_value key=${key} v=${v}`); - switch (metadata[key].metric_type) { - case prom.Counter: - m.inc(labels, v); - break; - case prom.Gauge: - m.set(labels, v); - break; - default: - throw new Error(`unexpected metric: ${metric}`); - } + logger.debug(`set_value key=${key} v=${v}`); + switch (metadata[key].metric_type) { + case prom.Counter: + m.inc(labels, v); + break; + case prom.Gauge: + m.set(labels, v); + break; + default: + throw new Error(`unexpected metric: ${metric}`); + } } addr = async function(ip, port) { - if ('PROM_DEIDENTIFY' in process.env) { - let a = ip.split('.') - for (let i = 0; i < a.length - 2; i++) { - a[i] = 'xx'; - } - return a.join('.'); - } - else if ('PROM_NUMERIC' in process.env) { - return ip; - } - else { - try { - let a = await resolver.reverse(ip); - ip = a[0]; - } - catch (err) { - logger.error(`reverse DNS query failed: ${ip} ${err.code}`); - } - return `${ip}:${port}`; - } + if ('PROM_DEIDENTIFY' in process.env) { + let a = ip.split('.') + for (let i = 0; i < a.length - 2; i++) { + a[i] = 'xx'; + } + return a.join('.'); + } + else if ('PROM_NUMERIC' in process.env) { + return ip; + } + else { + try { + let a = await resolver.reverse(ip); + ip = a[0]; + } + catch (err) { + logger.error(`reverse DNS query failed: ${ip} ${err.code}`); + } + return `${ip}:${port}`; + } } quiet = function(s) { - return 'PROM_QUIET' in process.env ? '' : s; + return 'PROM_QUIET' in process.env ? '' : s; } collect = async function(registry, rooms, peers) { - metrics = function(subsystem) { - let namespace = 'mediasoup'; - let metrics = new Map(); - for (let key in metadata) { - value = metadata[key]; - let name = key.split(/(?=[A-Z])/).join('_').toLowerCase(); - let unit = value.unit; - let metric_type = value.metric_type; - let s = `${namespace}_${subsystem}_${name}`; - if (unit) { - s += `_${unit}`; - } - m = new metric_type({name: s, help: `${subsystem}.${key}`, - labelNames: label_names, registers: [registry]}); - metrics.set(key, m); - } - return metrics; - } + metrics = function(subsystem) { + let namespace = 'mediasoup'; + let metrics = new Map(); + for (let key in metadata) { + value = metadata[key]; + let name = key.split(/(?=[A-Z])/).join('_').toLowerCase(); + let unit = value.unit; + let metric_type = value.metric_type; + let s = `${namespace}_${subsystem}_${name}`; + if (unit) { + s += `_${unit}`; + } + m = new metric_type({name: s, help: `${subsystem}.${key}`, + labelNames: label_names, registers: [registry]}); + metrics.set(key, m); + } + return metrics; + } - logger.debug('collect'); - const m_rooms = new prom.Gauge({name: 'edumeet_rooms', help: '#rooms', - registers: [registry]}); - m_rooms.set(rooms.size); - const m_peers = new prom.Gauge({name: 'edumeet_peers', help: '#peers', - labelNames: ['room_id'], registers: [registry]}); - for (let [room_id, room] of rooms) { - m_peers.labels(room_id).set(Object.keys(room._peers).length); - } + logger.debug('collect'); + const m_rooms = new prom.Gauge({name: 'edumeet_rooms', help: '#rooms', + registers: [registry]}); + m_rooms.set(rooms.size); + const m_peers = new prom.Gauge({name: 'edumeet_peers', help: '#peers', + labelNames: ['room_id'], registers: [registry]}); + for (let [room_id, room] of rooms) { + m_peers.labels(room_id).set(Object.keys(room._peers).length); + } - const m_consumer = metrics('consumer'); - const m_producer = metrics('producer'); - for (let [pid, worker] of workers) { - logger.debug(`visiting worker ${pid}`); - for (let router of worker._routers) { - logger.debug(`visiting router ${router.id}`); - for (let [transport_id, transport] of router._transports) { - logger.debug(`visiting transport ${transport_id}`); - let transport_j = await transport.dump(); - if (transport_j.iceState != 'completed') { - logger.debug(`skipping transport ${transport_id}}: ${transport_j.iceState}`); - continue; - } - let ice_selected_tuple = transport_j.iceSelectedTuple; - let proto = ice_selected_tuple.protocol - let local_addr = await addr(ice_selected_tuple.localIp, - ice_selected_tuple.localPort); - let remote_addr = await addr(ice_selected_tuple.remoteIp, - ice_selected_tuple.remotePort); - for (let [producer_id, producer] of transport._producers) { - logger.debug(`visiting producer ${producer_id}`); - let { room_id, peer_id, display_name, user_agent, kind, codec } = - common_labels(producer, peer => peer._producers.has(producer_id)); - let a = await producer.getStats(); - for (let x of a) { - let type = x.type; - let labels = { - 'pid': pid, - 'room_id': room_id, - 'peer_id': peer_id, - 'display_name': display_name, - 'user_agent': user_agent, - 'transport_id': quiet(transport_id), - 'proto': proto, - 'local_addr': local_addr, - 'remote_addr': remote_addr, - 'id': quiet(producer_id), - 'kind': kind, - 'codec': codec, - 'type': type - } - for (let [key, m] of m_producer) { - set_value(key, m, labels, x[key]); - } - } - } - for (let [consumer_id, consumer] of transport._consumers) { - logger.debug(`visiting consumer ${consumer_id}`); - let { room_id, peer_id, display_name, user_agent, kind, codec } = - common_labels(consumer, peer => peer._consumers.has(consumer_id)); - let a = await consumer.getStats(); - for (let x of a) { - if (x.type == 'inbound-rtp') { - continue; - } - let type = x.type; - let labels = { - 'pid': pid, - 'room_id': room_id, - 'peer_id': peer_id, - 'display_name': display_name, - 'user_agent': user_agent, - 'transport_id': quiet(transport_id), - 'proto': proto, - 'local_addr': local_addr, - 'remote_addr': remote_addr, - 'id': quiet(consumer_id), - 'kind': kind, - 'codec': codec, - 'type': type - } - for (let [key, m] of m_consumer) { - set_value(key, m, labels, x[key]); - } - } - } - } - } - } + const m_consumer = metrics('consumer'); + const m_producer = metrics('producer'); + for (let [pid, worker] of workers) { + logger.debug(`visiting worker ${pid}`); + for (let router of worker._routers) { + logger.debug(`visiting router ${router.id}`); + for (let [transport_id, transport] of router._transports) { + logger.debug(`visiting transport ${transport_id}`); + let transport_j = await transport.dump(); + if (transport_j.iceState != 'completed') { + logger.debug(`skipping transport ${transport_id}}: ${transport_j.iceState}`); + continue; + } + let ice_selected_tuple = transport_j.iceSelectedTuple; + let proto = ice_selected_tuple.protocol + let local_addr = await addr(ice_selected_tuple.localIp, + ice_selected_tuple.localPort); + let remote_addr = await addr(ice_selected_tuple.remoteIp, + ice_selected_tuple.remotePort); + for (let [producer_id, producer] of transport._producers) { + logger.debug(`visiting producer ${producer_id}`); + let { room_id, peer_id, display_name, user_agent, kind, codec } = + common_labels(producer, peer => peer._producers.has(producer_id)); + let a = await producer.getStats(); + for (let x of a) { + let type = x.type; + let labels = { + 'pid': pid, + 'room_id': room_id, + 'peer_id': peer_id, + 'display_name': display_name, + 'user_agent': user_agent, + 'transport_id': quiet(transport_id), + 'proto': proto, + 'local_addr': local_addr, + 'remote_addr': remote_addr, + 'id': quiet(producer_id), + 'kind': kind, + 'codec': codec, + 'type': type + } + for (let [key, m] of m_producer) { + set_value(key, m, labels, x[key]); + } + } + } + for (let [consumer_id, consumer] of transport._consumers) { + logger.debug(`visiting consumer ${consumer_id}`); + let { room_id, peer_id, display_name, user_agent, kind, codec } = + common_labels(consumer, peer => peer._consumers.has(consumer_id)); + let a = await consumer.getStats(); + for (let x of a) { + if (x.type == 'inbound-rtp') { + continue; + } + let type = x.type; + let labels = { + 'pid': pid, + 'room_id': room_id, + 'peer_id': peer_id, + 'display_name': display_name, + 'user_agent': user_agent, + 'transport_id': quiet(transport_id), + 'proto': proto, + 'local_addr': local_addr, + 'remote_addr': remote_addr, + 'id': quiet(consumer_id), + 'kind': kind, + 'codec': codec, + 'type': type + } + for (let [key, m] of m_consumer) { + set_value(key, m, labels, x[key]); + } + } + } + } + } + } } module.exports = async function(rooms, peers) { - try { - logger.debug(`PROM_DEIDENTIFY=${process.env.PROM_DEIDENTIFY}`); - logger.debug(`PROM_NUMERIC=${process.env.PROM_NUMERIC}`); - logger.debug(`PROM_PORT=${process.env.PROM_PORT}`); - logger.debug(`PROM_QUIET=${process.env.PROM_QUIET}`); - let s_port = process.env.PROM_PORT; - if (!s_port) { - logger.info('exporter disabled'); - } - else { - let n_port = Number(s_port); - if (Number.isNaN(n_port)) { - throw new TypeError(`PROM_PORT has illegal value: ${s_port}`); - } + try { + logger.debug(`PROM_DEIDENTIFY=${process.env.PROM_DEIDENTIFY}`); + logger.debug(`PROM_NUMERIC=${process.env.PROM_NUMERIC}`); + logger.debug(`PROM_PORT=${process.env.PROM_PORT}`); + logger.debug(`PROM_QUIET=${process.env.PROM_QUIET}`); + let s_port = process.env.PROM_PORT; + if (!s_port) { + logger.info('exporter disabled'); + } + else { + let n_port = Number(s_port); + if (Number.isNaN(n_port)) { + throw new TypeError(`PROM_PORT has illegal value: ${s_port}`); + } - mediasoup.observer.on('newworker', worker => { - logger.debug(`observing newworker ${worker.pid} #${workers.size}`); - workers.set(worker.pid, worker); - worker.observer.on('close', () => { - logger.debug(`observing close worker ${worker.pid} #${workers.size - 1}`); - workers.delete(worker.pid); - }); - }); + mediasoup.observer.on('newworker', worker => { + logger.debug(`observing newworker ${worker.pid} #${workers.size}`); + workers.set(worker.pid, worker); + worker.observer.on('close', () => { + logger.debug(`observing close worker ${worker.pid} #${workers.size - 1}`); + workers.delete(worker.pid); + }); + }); - let app = express(); - app.get('/', async (req, res) => { - logger.debug(`GET ${req.originalUrl}`); - let registry = new prom.Registry(); - await collect(registry, rooms, peers); - res.set('Content-Type', registry.contentType); - let data = registry.metrics(); - res.end(data); - }); - let server = app.listen(n_port, () => { - address = server.address(); - logger.info(`listening ${address.address}:${address.port}`); - }); - } - } - catch (err) { - logger.error(err); - } + let app = express(); + app.get('/', async (req, res) => { + logger.debug(`GET ${req.originalUrl}`); + let registry = new prom.Registry(); + await collect(registry, rooms, peers); + res.set('Content-Type', registry.contentType); + let data = registry.metrics(); + res.end(data); + }); + let server = app.listen(n_port, () => { + address = server.address(); + logger.info(`listening ${address.address}:${address.port}`); + }); + } + } + catch (err) { + logger.error(err); + } } diff --git a/server/server.js b/server/server.js index 2c102c8..90e299a 100755 --- a/server/server.js +++ b/server/server.js @@ -133,8 +133,8 @@ async function run() // Open the interactive server. await interactiveServer(rooms, peers); - // start Prometheus exporter - await promExporter(rooms, peers); + // start Prometheus exporter + await promExporter(rooms, peers); if (typeof(config.auth) === 'undefined') { From 3717e41ac565c62c80595088b056ca4960c8bd4a Mon Sep 17 00:00:00 2001 From: christian2 Date: Fri, 1 May 2020 11:46:23 +0200 Subject: [PATCH 4/8] MIT License already mentioned in README.md --- prom.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/prom.md b/prom.md index 37a2f50..e5c3b39 100644 --- a/prom.md +++ b/prom.md @@ -30,10 +30,6 @@ If `multiparty-meeting` was installed with it may be necessary to open the `iptables` firewall that was established with `ferm` for incoming TCP traffic on `PROM_PORT`. -## License - -MIT License (without copyright notice) - ## Metrics | metric | value | From ebb728f4a8269561b0ed37ad2b8e4b96474c27a4 Mon Sep 17 00:00:00 2001 From: christian2 Date: Fri, 1 May 2020 15:37:03 +0200 Subject: [PATCH 5/8] remove extra identation --- server/lib/promExporter.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/server/lib/promExporter.js b/server/lib/promExporter.js index d2989d3..706e06a 100644 --- a/server/lib/promExporter.js +++ b/server/lib/promExporter.js @@ -25,15 +25,15 @@ const metadata = { common_labels = function(both, fn) { for (let [room_id, room] of rooms) { for (let [peer_id, peer] of peers) { - if (fn(peer)) { - let display_name = peer._displayName; + if (fn(peer)) { + let display_name = peer._displayName; - let user_agent = peer._socket.client.request.headers['user-agent']; - let kind = both.kind; - let codec = both.rtpParameters.codecs[0].mimeType.split('/')[1]; - return { room_id, peer_id, display_name, user_agent, kind, codec }; - } - } + let user_agent = peer._socket.client.request.headers['user-agent']; + let kind = both.kind; + let codec = both.rtpParameters.codecs[0].mimeType.split('/')[1]; + return { room_id, peer_id, display_name, user_agent, kind, codec }; + } + } } throw new Error('cannot find generic labels'); } From 9adcc807ddc50f74383995e63af20a1e0f2599af Mon Sep 17 00:00:00 2001 From: christian2 Date: Mon, 4 May 2020 09:47:10 +0200 Subject: [PATCH 6/8] bug fixes --- server/lib/promExporter.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/server/lib/promExporter.js b/server/lib/promExporter.js index 706e06a..af277eb 100644 --- a/server/lib/promExporter.js +++ b/server/lib/promExporter.js @@ -35,7 +35,7 @@ common_labels = function(both, fn) { } } } - throw new Error('cannot find generic labels'); + throw new Error('cannot find common labels'); } set_value = function(key, m, labels, v) { @@ -48,21 +48,20 @@ set_value = function(key, m, labels, v) { m.set(labels, v); break; default: - throw new Error(`unexpected metric: ${metric}`); + throw new Error(`unexpected metric: ${m}`); } } - addr = async function(ip, port) { if ('PROM_DEIDENTIFY' in process.env) { let a = ip.split('.') for (let i = 0; i < a.length - 2; i++) { a[i] = 'xx'; } - return a.join('.'); + return `${a.join('.')}:${port}`; } else if ('PROM_NUMERIC' in process.env) { - return ip; + return `${ip}:${port}`; } else { try { From 84f77f38139baf587f73d04e7d13477128c41d1a Mon Sep 17 00:00:00 2001 From: christian2 Date: Mon, 4 May 2020 10:57:24 +0200 Subject: [PATCH 7/8] employ config.js --- prom.md | 15 ++--- server/config/config.example.js | 9 +++ server/lib/promExporter.js | 115 +++++++++++++++----------------- server/server.js | 4 +- 4 files changed, 70 insertions(+), 73 deletions(-) diff --git a/prom.md b/prom.md index e5c3b39..51ba2e8 100644 --- a/prom.md +++ b/prom.md @@ -3,7 +3,7 @@ The goal of this version is to offer a few basic metrics for initial testing. The set of supported metrics can be extended. -The current implementation is +The current implementation is partly [unconventional](https://prometheus.io/docs/instrumenting/writing_exporters) in that it creates new metrics each time but does not register a custom collector. Reasons are that the exporter should @@ -17,18 +17,13 @@ of `multiparty-meeting` but connected as an interactive client. ## Configuration -| `.env` | description | -|--------|-------| -| `DEBUG` | e.g. `*WARN*,*ERROR*,*:prom` for debugging | -| `PROM_DEIDENTIFY` | if set, deidentify IP addresses | -| `PROM_NUMERIC` | if set, show numerical IP addresses | -| `PROM_PORT` | if set, enable exporter on this port | -| `PROM_QUIET` | if set, include fewer labels | +See `prometheus` in `server/config/config.example.js` for options and +applicable defaults. If `multiparty-meeting` was installed with [`mm-absible`](https://github.com/misi/mm-ansible) -it may be necessary to open the `iptables` firewall that was established -with `ferm` for incoming TCP traffic on `PROM_PORT`. +it may be necessary to open the `iptables` firewall for incoming TCP traffic +on the allocated port (see `/etc/ferm/ferm.conf`). ## Metrics diff --git a/server/config/config.example.js b/server/config/config.example.js index 12bf938..6ff279a 100644 --- a/server/config/config.example.js +++ b/server/config/config.example.js @@ -342,4 +342,13 @@ module.exports = maxIncomingBitrate : 1500000 } } + // Prometheus exporter + /* + prometheus: { + deidentify: false, // deidentify IP addresses + numeric: false, // show numeric IP addresses + port: 8889, // allocated port + quiet: false // include fewer labels + } + */ }; diff --git a/server/lib/promExporter.js b/server/lib/promExporter.js index af277eb..e54f241 100644 --- a/server/lib/promExporter.js +++ b/server/lib/promExporter.js @@ -52,33 +52,6 @@ set_value = function(key, m, labels, v) { } } -addr = async function(ip, port) { - if ('PROM_DEIDENTIFY' in process.env) { - let a = ip.split('.') - for (let i = 0; i < a.length - 2; i++) { - a[i] = 'xx'; - } - return `${a.join('.')}:${port}`; - } - else if ('PROM_NUMERIC' in process.env) { - return `${ip}:${port}`; - } - else { - try { - let a = await resolver.reverse(ip); - ip = a[0]; - } - catch (err) { - logger.error(`reverse DNS query failed: ${ip} ${err.code}`); - } - return `${ip}:${port}`; - } -} - -quiet = function(s) { - return 'PROM_QUIET' in process.env ? '' : s; -} - collect = async function(registry, rooms, peers) { metrics = function(subsystem) { @@ -191,46 +164,64 @@ collect = async function(registry, rooms, peers) { } } -module.exports = async function(rooms, peers) { - try { - logger.debug(`PROM_DEIDENTIFY=${process.env.PROM_DEIDENTIFY}`); - logger.debug(`PROM_NUMERIC=${process.env.PROM_NUMERIC}`); - logger.debug(`PROM_PORT=${process.env.PROM_PORT}`); - logger.debug(`PROM_QUIET=${process.env.PROM_QUIET}`); - let s_port = process.env.PROM_PORT; - if (!s_port) { - logger.info('exporter disabled'); +module.exports = async function(rooms, peers, config) { + + addr = async function(ip, port) { + if (config.deidentify) { + let a = ip.split('.') + for (let i = 0; i < a.length - 2; i++) { + a[i] = 'xx'; + } + return `${a.join('.')}:${port}`; + } + else if (config.numeric) { + return `${ip}:${port}`; } else { - let n_port = Number(s_port); - if (Number.isNaN(n_port)) { - throw new TypeError(`PROM_PORT has illegal value: ${s_port}`); + try { + let a = await resolver.reverse(ip); + ip = a[0]; } - - mediasoup.observer.on('newworker', worker => { - logger.debug(`observing newworker ${worker.pid} #${workers.size}`); - workers.set(worker.pid, worker); - worker.observer.on('close', () => { - logger.debug(`observing close worker ${worker.pid} #${workers.size - 1}`); - workers.delete(worker.pid); - }); - }); - - let app = express(); - app.get('/', async (req, res) => { - logger.debug(`GET ${req.originalUrl}`); - let registry = new prom.Registry(); - await collect(registry, rooms, peers); - res.set('Content-Type', registry.contentType); - let data = registry.metrics(); - res.end(data); - }); - let server = app.listen(n_port, () => { - address = server.address(); - logger.info(`listening ${address.address}:${address.port}`); - }); + catch (err) { + logger.error(`reverse DNS query failed: ${ip} ${err.code}`); + } + return `${ip}:${port}`; } } + + quiet = function(s) { + return config.quiet ? '' : s; + } + + try { + logger.debug(`config.deidentify=${config.deidentify}`); + logger.debug(`config.numeric=${config.numeric}`); + logger.debug(`config.port=${config.port}`); + logger.debug(`config.quiet=${config.quiet}`); + + mediasoup.observer.on('newworker', worker => { + logger.debug(`observing newworker ${worker.pid} #${workers.size}`); + workers.set(worker.pid, worker); + worker.observer.on('close', () => { + logger.debug(`observing close worker ${worker.pid} #${workers.size - 1}`); + workers.delete(worker.pid); + }); + }); + + let app = express(); + app.get('/', async (req, res) => { + logger.debug(`GET ${req.originalUrl}`); + let registry = new prom.Registry(); + await collect(registry, rooms, peers); + res.set('Content-Type', registry.contentType); + let data = registry.metrics(); + res.end(data); + }); + let server = app.listen(config.port || 8889, () => { + address = server.address(); + logger.info(`listening ${address.address}:${address.port}`); + }); + } catch (err) { logger.error(err); } diff --git a/server/server.js b/server/server.js index 90e299a..82aa9ab 100755 --- a/server/server.js +++ b/server/server.js @@ -134,7 +134,9 @@ async function run() await interactiveServer(rooms, peers); // start Prometheus exporter - await promExporter(rooms, peers); + if (config.prometheus) { + await promExporter(rooms, peers, config.prometheus); + } if (typeof(config.auth) === 'undefined') { From f6c76f372a6061caaf75903f3bdc926624e2e990 Mon Sep 17 00:00:00 2001 From: christian2 Date: Mon, 4 May 2020 14:22:31 +0200 Subject: [PATCH 8/8] satisfy ESLint --- server/lib/promExporter.js | 410 +++++++++++++++++++++---------------- server/server.js | 5 +- 2 files changed, 236 insertions(+), 179 deletions(-) diff --git a/server/lib/promExporter.js b/server/lib/promExporter.js index e54f241..cb34f21 100644 --- a/server/lib/promExporter.js +++ b/server/lib/promExporter.js @@ -4,225 +4,281 @@ const mediasoup = require('mediasoup'); const prom = require('prom-client'); const Logger = require('./Logger'); -const Peer = require('./Peer'); -const Room = require('./Room'); const logger = new Logger('prom'); const resolver = new Resolver(); - const workers = new Map(); -const label_names = [ +const labelNames = [ 'pid', 'room_id', 'peer_id', 'display_name', 'user_agent', 'transport_id', 'proto', 'local_addr', 'remote_addr', 'id', 'kind', 'codec', 'type' ]; const metadata = { - 'byteCount': { metric_type: prom.Counter, unit: 'bytes' }, - 'score': { metric_type: prom.Gauge } -} + 'byteCount' : { metricType: prom.Counter, unit: 'bytes' }, + 'score' : { metricType: prom.Gauge } +}; -common_labels = function(both, fn) { - for (let [room_id, room] of rooms) { - for (let [peer_id, peer] of peers) { - if (fn(peer)) { - let display_name = peer._displayName; +module.exports = async function(rooms, peers, config) +{ + const collect = async function(registry) + { + const newMetrics = function(subsystem) + { + const namespace = 'mediasoup'; + const metrics = new Map(); - let user_agent = peer._socket.client.request.headers['user-agent']; - let kind = both.kind; - let codec = both.rtpParameters.codecs[0].mimeType.split('/')[1]; - return { room_id, peer_id, display_name, user_agent, kind, codec }; - } - } - } - throw new Error('cannot find common labels'); -} + for (const key in metadata) + { + if (Object.prototype.hasOwnProperty.call(metadata, key)) + { + const value = metadata[key]; + const name = key.split(/(?=[A-Z])/).join('_') + .toLowerCase(); + const unit = value.unit; + const metricType = value.metricType; + let s = `${namespace}_${subsystem}_${name}`; -set_value = function(key, m, labels, v) { - logger.debug(`set_value key=${key} v=${v}`); - switch (metadata[key].metric_type) { - case prom.Counter: - m.inc(labels, v); - break; - case prom.Gauge: - m.set(labels, v); - break; - default: - throw new Error(`unexpected metric: ${m}`); - } -} + if (unit) + { + s += `_${unit}`; + } + const m = new metricType({ + name : s, help : `${subsystem}.${key}`, labelNames : labelNames, registers : [ registry ] }); -collect = async function(registry, rooms, peers) { - - metrics = function(subsystem) { - let namespace = 'mediasoup'; - let metrics = new Map(); - for (let key in metadata) { - value = metadata[key]; - let name = key.split(/(?=[A-Z])/).join('_').toLowerCase(); - let unit = value.unit; - let metric_type = value.metric_type; - let s = `${namespace}_${subsystem}_${name}`; - if (unit) { - s += `_${unit}`; - } - m = new metric_type({name: s, help: `${subsystem}.${key}`, - labelNames: label_names, registers: [registry]}); - metrics.set(key, m); - } - return metrics; - } - - logger.debug('collect'); - const m_rooms = new prom.Gauge({name: 'edumeet_rooms', help: '#rooms', - registers: [registry]}); - m_rooms.set(rooms.size); - const m_peers = new prom.Gauge({name: 'edumeet_peers', help: '#peers', - labelNames: ['room_id'], registers: [registry]}); - for (let [room_id, room] of rooms) { - m_peers.labels(room_id).set(Object.keys(room._peers).length); - } - - const m_consumer = metrics('consumer'); - const m_producer = metrics('producer'); - for (let [pid, worker] of workers) { - logger.debug(`visiting worker ${pid}`); - for (let router of worker._routers) { - logger.debug(`visiting router ${router.id}`); - for (let [transport_id, transport] of router._transports) { - logger.debug(`visiting transport ${transport_id}`); - let transport_j = await transport.dump(); - if (transport_j.iceState != 'completed') { - logger.debug(`skipping transport ${transport_id}}: ${transport_j.iceState}`); - continue; + metrics.set(key, m); } - let ice_selected_tuple = transport_j.iceSelectedTuple; - let proto = ice_selected_tuple.protocol - let local_addr = await addr(ice_selected_tuple.localIp, - ice_selected_tuple.localPort); - let remote_addr = await addr(ice_selected_tuple.remoteIp, - ice_selected_tuple.remotePort); - for (let [producer_id, producer] of transport._producers) { - logger.debug(`visiting producer ${producer_id}`); - let { room_id, peer_id, display_name, user_agent, kind, codec } = - common_labels(producer, peer => peer._producers.has(producer_id)); - let a = await producer.getStats(); - for (let x of a) { - let type = x.type; - let labels = { - 'pid': pid, - 'room_id': room_id, - 'peer_id': peer_id, - 'display_name': display_name, - 'user_agent': user_agent, - 'transport_id': quiet(transport_id), - 'proto': proto, - 'local_addr': local_addr, - 'remote_addr': remote_addr, - 'id': quiet(producer_id), - 'kind': kind, - 'codec': codec, - 'type': type - } - for (let [key, m] of m_producer) { - set_value(key, m, labels, x[key]); - } + } + + return metrics; + }; + + const commonLabels = function(both, fn) + { + for (const roomId of rooms.keys()) + { + for (const [ peerId, peer ] of peers) + { + if (fn(peer)) + { + const displayName = peer._displayName; + const userAgent = peer._socket.client.request.headers['user-agent']; + const kind = both.kind; + const codec = both.rtpParameters.codecs[0].mimeType.split('/')[1]; + + return { roomId, peerId, displayName, userAgent, kind, codec }; } } - for (let [consumer_id, consumer] of transport._consumers) { - logger.debug(`visiting consumer ${consumer_id}`); - let { room_id, peer_id, display_name, user_agent, kind, codec } = - common_labels(consumer, peer => peer._consumers.has(consumer_id)); - let a = await consumer.getStats(); - for (let x of a) { - if (x.type == 'inbound-rtp') { - continue; + } + throw new Error('cannot find common labels'); + }; + + const addr = async function(ip, port) + { + if (config.deidentify) + { + const a = ip.split('.'); + + for (let i = 0; i < a.length - 2; i++) + { + a[i] = 'xx'; + } + + return `${a.join('.')}:${port}`; + } + else if (config.numeric) + { + return `${ip}:${port}`; + } + else + { + try + { + const a = await resolver.reverse(ip); + + ip = a[0]; + } + catch (err) + { + logger.error(`reverse DNS query failed: ${ip} ${err.code}`); + } + + return `${ip}:${port}`; + } + }; + + const quiet = function(s) + { + return config.quiet ? '' : s; + }; + + const setValue = function(key, m, labels, v) + { + logger.debug(`setValue key=${key} v=${v}`); + switch (metadata[key].metricType) + { + case prom.Counter: + m.inc(labels, v); + break; + case prom.Gauge: + m.set(labels, v); + break; + default: + throw new Error(`unexpected metric: ${m}`); + } + }; + + logger.debug('collect'); + const mRooms = new prom.Gauge({ name: 'edumeet_rooms', help: '#rooms', registers: [ registry ] }); + + mRooms.set(rooms.size); + const mPeers = new prom.Gauge({ name: 'edumeet_peers', help: '#peers', labelNames: [ 'room_id' ], registers: [ registry ] }); + + for (const [ roomId, room ] of rooms) + { + mPeers.labels(roomId).set(Object.keys(room._peers).length); + } + + const mConsumer = newMetrics('consumer'); + const mProducer = newMetrics('producer'); + + for (const [ pid, worker ] of workers) + { + logger.debug(`visiting worker ${pid}`); + for (const router of worker._routers) + { + logger.debug(`visiting router ${router.id}`); + for (const [ transportId, transport ] of router._transports) + { + logger.debug(`visiting transport ${transportId}`); + const transportJson = await transport.dump(); + + if (transportJson.iceState != 'completed') + { + logger.debug(`skipping transport ${transportId}}: ${transportJson.iceState}`); + continue; + } + const iceSelectedTuple = transportJson.iceSelectedTuple; + const proto = iceSelectedTuple.protocol; + const localAddr = await addr(iceSelectedTuple.localIp, + iceSelectedTuple.localPort); + const remoteAddr = await addr(iceSelectedTuple.remoteIp, + iceSelectedTuple.remotePort); + + for (const [ producerId, producer ] of transport._producers) + { + logger.debug(`visiting producer ${producerId}`); + const { roomId, peerId, displayName, userAgent, kind, codec } = + commonLabels(producer, (peer) => peer._producers.has(producerId)); + const a = await producer.getStats(); + + for (const x of a) + { + const type = x.type; + const labels = { + 'pid' : pid, + 'room_id' : roomId, + 'peer_id' : peerId, + 'display_name' : displayName, + 'user_agent' : userAgent, + 'transport_id' : quiet(transportId), + 'proto' : proto, + 'local_addr' : localAddr, + 'remote_addr' : remoteAddr, + 'id' : quiet(producerId), + 'kind' : kind, + 'codec' : codec, + 'type' : type + }; + + for (const [ key, m ] of mProducer) + { + setValue(key, m, labels, x[key]); + } } - let type = x.type; - let labels = { - 'pid': pid, - 'room_id': room_id, - 'peer_id': peer_id, - 'display_name': display_name, - 'user_agent': user_agent, - 'transport_id': quiet(transport_id), - 'proto': proto, - 'local_addr': local_addr, - 'remote_addr': remote_addr, - 'id': quiet(consumer_id), - 'kind': kind, - 'codec': codec, - 'type': type - } - for (let [key, m] of m_consumer) { - set_value(key, m, labels, x[key]); + } + for (const [ consumerId, consumer ] of transport._consumers) + { + logger.debug(`visiting consumer ${consumerId}`); + const { roomId, peerId, displayName, userAgent, kind, codec } = + commonLabels(consumer, (peer) => peer._consumers.has(consumerId)); + const a = await consumer.getStats(); + + for (const x of a) + { + if (x.type == 'inbound-rtp') + { + continue; + } + const type = x.type; + const labels = + { + 'pid' : pid, + 'room_id' : roomId, + 'peer_id' : peerId, + 'display_name' : displayName, + 'user_agent' : userAgent, + 'transport_id' : quiet(transportId), + 'proto' : proto, + 'local_addr' : localAddr, + 'remote_addr' : remoteAddr, + 'id' : quiet(consumerId), + 'kind' : kind, + 'codec' : codec, + 'type' : type + }; + + for (const [ key, m ] of mConsumer) + { + setValue(key, m, labels, x[key]); + } } } } } } - } -} + }; -module.exports = async function(rooms, peers, config) { - - addr = async function(ip, port) { - if (config.deidentify) { - let a = ip.split('.') - for (let i = 0; i < a.length - 2; i++) { - a[i] = 'xx'; - } - return `${a.join('.')}:${port}`; - } - else if (config.numeric) { - return `${ip}:${port}`; - } - else { - try { - let a = await resolver.reverse(ip); - ip = a[0]; - } - catch (err) { - logger.error(`reverse DNS query failed: ${ip} ${err.code}`); - } - return `${ip}:${port}`; - } - } - - quiet = function(s) { - return config.quiet ? '' : s; - } - - try { + try + { logger.debug(`config.deidentify=${config.deidentify}`); logger.debug(`config.numeric=${config.numeric}`); logger.debug(`config.port=${config.port}`); logger.debug(`config.quiet=${config.quiet}`); - mediasoup.observer.on('newworker', worker => { + mediasoup.observer.on('newworker', (worker) => + { logger.debug(`observing newworker ${worker.pid} #${workers.size}`); workers.set(worker.pid, worker); - worker.observer.on('close', () => { + worker.observer.on('close', () => + { logger.debug(`observing close worker ${worker.pid} #${workers.size - 1}`); workers.delete(worker.pid); }); }); - let app = express(); - app.get('/', async (req, res) => { + const app = express(); + + app.get('/', async (req, res) => + { logger.debug(`GET ${req.originalUrl}`); - let registry = new prom.Registry(); - await collect(registry, rooms, peers); + const registry = new prom.Registry(); + + await collect(registry); res.set('Content-Type', registry.contentType); - let data = registry.metrics(); + const data = registry.metrics(); + res.end(data); }); - let server = app.listen(config.port || 8889, () => { - address = server.address(); + const server = app.listen(config.port || 8889, () => + { + const address = server.address(); + logger.info(`listening ${address.address}:${address.port}`); }); } - catch (err) { + catch (err) + { logger.error(err); } -} +}; diff --git a/server/server.js b/server/server.js index 82aa9ab..dbf9a8e 100755 --- a/server/server.js +++ b/server/server.js @@ -134,9 +134,10 @@ async function run() await interactiveServer(rooms, peers); // start Prometheus exporter - if (config.prometheus) { + if (config.prometheus) + { await promExporter(rooms, peers, config.prometheus); - } + } if (typeof(config.auth) === 'undefined') {