From 816753dd23f6a06789acddc7913cfcdecb634795 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Fri, 26 Jul 2024 10:58:41 +0200 Subject: [PATCH 01/33] added stats env variables --- doc/admin.md | 5 ++++- lib/commonConfig.js | 24 +++++++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/doc/admin.md b/doc/admin.md index 413da4384..303366469 100644 --- a/doc/admin.md +++ b/doc/admin.md @@ -162,11 +162,12 @@ support nulls or multi-attribute requests if they are encountered. #### `stats` It configures the periodic collection of statistics. Use `interval` in milliseconds to set the time between stats -writings. +writings. The `persistence` flag stores stats in the mongo backend. ```javascript stats: { interval: 100; + persistence: false; } ``` @@ -482,6 +483,8 @@ overrides. | IOTA_EXPLICIT_ATTRS | `explicitAttrs` | | IOTA_DEFAULT_ENTITY_NAME_CONJUNCTION | `defaultEntityNameConjunction` | | IOTA_RELAX_TEMPLATE_VALIDATION | `relaxTemplateValidation` | +| IOTA_STATS_INTERVAL | `stats.interval` | +| IOTA_STATS_PERSISTENCE | `stats.persistence` | Note: diff --git a/lib/commonConfig.js b/lib/commonConfig.js index f7a2ee78b..6657b2a4c 100644 --- a/lib/commonConfig.js +++ b/lib/commonConfig.js @@ -156,7 +156,9 @@ function processEnvironmentVariables() { 'IOTA_FALLBACK_TENANT', 'IOTA_FALLBACK_PATH', 'IOTA_LD_SUPPORT_NULL', - 'IOTA_LD_SUPPORT_DATASET_ID' + 'IOTA_LD_SUPPORT_DATASET_ID', + `IOTA_STATS_INTERVAL`, + 'IOTA_STATS_PERSISTENCE' ]; const iotamVariables = [ 'IOTA_IOTAM_URL', @@ -188,6 +190,10 @@ function processEnvironmentVariables() { 'IOTA_MONGO_USER', 'IOTA_MONGO_PASSWORD' ]; + const statsVariables = [ + 'IOTA_STATS_INTERVAL', + 'IOTA_STATS_PERSISTENCE', + ]; // Substitute Docker Secret Variables where set. protectedVariables.forEach((key) => { @@ -468,6 +474,22 @@ function processEnvironmentVariables() { ? config.defaultEntityNameConjunction : ':'; } + + // Stats Configuration + if (anyIsSet(statsVariables)) { + config.stats = config.stats || {}; + } + + if (process.env.IOTA_STATS_INTERVAL) { + const interval = parseInt(process.env.IOTA_STATS_INTERVAL); + if (!isNaN(interval) && interval > 0) { + config.stats.interval = interval; + } + } + + if (process.env.IOTA_STATS_PERSISTENCE) { + config.stats.persistence = process.env.IOTA_STATS_PERSISTENCE === 'true' + } } function setConfig(newConfig) { From 14bed96765233b8dd23940f9e91ddca84eefbd82 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Fri, 26 Jul 2024 11:40:29 +0200 Subject: [PATCH 02/33] Update CNR --- CHANGES_NEXT_RELEASE | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES_NEXT_RELEASE b/CHANGES_NEXT_RELEASE index 854a262e1..aa1a4378a 100644 --- a/CHANGES_NEXT_RELEASE +++ b/CHANGES_NEXT_RELEASE @@ -1,3 +1,4 @@ +- Add: environment variables `IOTA_STATS_INTERVAL` and `IOTA_STATS_PERSISTENCE` (#1627) - Fix: service header to use uppercase in case of update and delete (#1528) - Fix: Allow to send to CB batch update for multimeasures for NGSI-LD (#1623) - Add: new JEXL transformations for including into an array keys that have a certain value: valuePicker and valuePickerMulti From 67674d7260ff76e5609dcddd02e925e8434d5776 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:04:30 +0200 Subject: [PATCH 03/33] Update admin.md --- doc/admin.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/admin.md b/doc/admin.md index 4c313ff95..ac02294ae 100644 --- a/doc/admin.md +++ b/doc/admin.md @@ -162,7 +162,9 @@ support nulls or multi-attribute requests if they are encountered. #### `stats` It configures the periodic collection of statistics. Use `interval` in milliseconds to set the time between stats -writings. The `persistence` flag stores stats in the mongo backend. +writings. The `persistence` flag stores stats in a collection named `kpis` in the mongo backend. Each document in the +`kpis` collection will have a `timestamp` attribute with the stat collection time, and an additional attribute +for each of the stats created by calling the `statsRegistry.add` function. ```javascript stats: { From 679b7d1a67cbe655f2e899a40cf6585ad55e2c28 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:11:16 +0200 Subject: [PATCH 04/33] Update admin.md --- doc/admin.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/doc/admin.md b/doc/admin.md index ac02294ae..11e518e69 100644 --- a/doc/admin.md +++ b/doc/admin.md @@ -162,9 +162,10 @@ support nulls or multi-attribute requests if they are encountered. #### `stats` It configures the periodic collection of statistics. Use `interval` in milliseconds to set the time between stats -writings. The `persistence` flag stores stats in a collection named `kpis` in the mongo backend. Each document in the -`kpis` collection will have a `timestamp` attribute with the stat collection time, and an additional attribute -for each of the stats created by calling the `statsRegistry.add` function. +writings. + +By default, stats are logged to the standard log at level `INFO`. If the `persistence` flag is defined and set to +`true`, stats are also stored in a collection named `kpis` in the mongo backend. ```javascript stats: { @@ -173,6 +174,17 @@ stats: { } ``` +Each document in the `kpis` collection will have a `timestamp` attribute with the stat collection time, and an +additional attribute for each of the stats created by calling the `statsRegistry.add` function, e.g.: + +```json +{ + "timeinstant": new ISODate("2024-07-29T00:00:00.000Z"), + "deviceCreationRequests": 1334, + "deviceRemovalRequests": 454 +} +``` + #### `authentication` Stores the authentication data, for use in retrieving tokens for devices with a trust token (required in scenarios with From 0bb40cd2c38a34c0604aff07d866918c00fade4f Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:35:12 +0200 Subject: [PATCH 05/33] Update admin.md --- doc/admin.md | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/doc/admin.md b/doc/admin.md index 11e518e69..9b763aea6 100644 --- a/doc/admin.md +++ b/doc/admin.md @@ -164,16 +164,32 @@ support nulls or multi-attribute requests if they are encountered. It configures the periodic collection of statistics. Use `interval` in milliseconds to set the time between stats writings. -By default, stats are logged to the standard log at level `INFO`. If the `persistence` flag is defined and set to -`true`, stats are also stored in a collection named `kpis` in the mongo backend. +```javascript +stats: { + interval: 100; +} +``` + +By default, stats are logged to the standard log at level `INFO`. You can also have your stats written +to a collection named `kpis` in the mongo backend, if you do these two things: + +- Set the `stats.persistence` flag to `true`. ```javascript stats: { interval: 100; - persistence: false; + persistence: true; } ``` +- Schedule periodic collection of stats to mongo by calling `statsRegistry.addTimerAction`. + +```js +statsRegistry.addTimerAction(statsRegistry.mongodbPersistence, function callback() { + // ... called after timer is enabled ... +}); +``` + Each document in the `kpis` collection will have a `timestamp` attribute with the stat collection time, and an additional attribute for each of the stats created by calling the `statsRegistry.add` function, e.g.: @@ -181,7 +197,8 @@ additional attribute for each of the stats created by calling the `statsRegistry { "timeinstant": new ISODate("2024-07-29T00:00:00.000Z"), "deviceCreationRequests": 1334, - "deviceRemovalRequests": 454 + "deviceRemovalRequests": 454, + "measureRequests": 4432 } ``` From 04bacb1478751708edd2adbab2e425240ccfa5cb Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Tue, 30 Jul 2024 17:28:31 +0200 Subject: [PATCH 06/33] added prometheus exporter --- lib/services/common/alarmManagement.js | 7 +++++++ lib/services/ngsi/ngsiService.js | 4 +++- lib/services/northBound/northboundServer.js | 21 +++++++++++++++++++ lib/services/stats/statsRegistry.js | 23 +++++++++++++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/lib/services/common/alarmManagement.js b/lib/services/common/alarmManagement.js index fa6e18beb..84d6fb72c 100644 --- a/lib/services/common/alarmManagement.js +++ b/lib/services/common/alarmManagement.js @@ -22,6 +22,7 @@ */ let alarmRepository = {}; +const statsRegistry = require('../../stats/statsRegistry'); const logger = require('logops'); const context = { op: 'IoTAgentNGSI.Alarms' @@ -41,6 +42,9 @@ function raise(alarmName, description) { }; logger.error(context, 'Raising [%s]: %j', alarmName, description); + // NOTE: statsRegistry.add is actually synchronous (does not return a promise), + // we can call it synchronously. + statsRegistry.add('raiseAlarm', 1, function () {}); } } @@ -53,6 +57,9 @@ function release(alarmName) { if (alarmRepository[alarmName]) { delete alarmRepository[alarmName]; logger.error(context, 'Releasing [%s]', alarmName); + // NOTE: statsRegistry.add is actually synchronous (does not return a promise), + // we can call it synchronously. + statsRegistry.add('releaseAlarm', 1, function () {}); } } diff --git a/lib/services/ngsi/ngsiService.js b/lib/services/ngsi/ngsiService.js index e91799886..b42330fea 100644 --- a/lib/services/ngsi/ngsiService.js +++ b/lib/services/ngsi/ngsiService.js @@ -26,6 +26,7 @@ const async = require('async'); const apply = async.apply; +const statsRegistry = require('../stats/statsRegistry'); const intoTrans = require('../common/domain').intoTrans; const fillService = require('./../common/domain').fillService; const errors = require('../../errors'); @@ -67,7 +68,8 @@ function init() { * @param {String} token User token to identify against the PEP Proxies (optional). */ function sendUpdateValue(entityName, attributes, typeInformation, token, callback) { - entityHandler.sendUpdateValue(entityName, attributes, typeInformation, token, callback); + const newCallback = statsRegistry.withStats('updateEntityOk', 'updateEntityError', callback); + entityHandler.sendUpdateValue(entityName, attributes, typeInformation, token, newCallback); } /** diff --git a/lib/services/northBound/northboundServer.js b/lib/services/northBound/northboundServer.js index bd22879d2..27655c790 100644 --- a/lib/services/northBound/northboundServer.js +++ b/lib/services/northBound/northboundServer.js @@ -33,12 +33,32 @@ const intoTrans = domainUtils.intoTrans; const deviceProvisioning = require('./deviceProvisioningServer'); const deviceUpdating = require('./deviceProvisioningServer'); const groupProvisioning = require('./deviceGroupAdministrationServer'); +const statsRegistry = require('./stats/statsRegistry'); const logger = require('logops'); const context = { op: 'IoTAgentNGSI.NorthboundServer' }; const bodyParser = require('body-parser'); +/** + * Predefined http handler that returns current prometheus metrics + */ +/* eslint-disable-next-line no-unused-vars */ +function prometheusHandler(req, res) { + const metrics = new Array(); + const globalStats = statsRegistry.getGlobalStats(); + for (const key in globalStats) { + if (hasOwnProperty(globalStats, key)) { + metrics.push('# HELP ' + key); + metrics.push('# TYPE ' + key + ' counter'); + metrics.push(key + ' ' + globalStats[key]); + metrics.push('\n'); + } + } + res.set('Content-Type', 'text/plain; version=0.0.4; charset=utf-8'); + res.status(200).send(metrics.join('\n')); +} + function start(config, callback) { let baseRoot = '/'; @@ -83,6 +103,7 @@ function start(config, callback) { northboundServer.router.get('/version', middlewares.retrieveVersion); northboundServer.router.put('/admin/log', middlewares.changeLogLevel); northboundServer.router.get('/admin/log', middlewares.getLogLevel); + northboundServer.router.get('/metrics', prometheusHandler); northboundServer.app.use(baseRoot, northboundServer.router); contextServer.loadContextRoutes(northboundServer.router); diff --git a/lib/services/stats/statsRegistry.js b/lib/services/stats/statsRegistry.js index 6900c9940..cda551780 100644 --- a/lib/services/stats/statsRegistry.js +++ b/lib/services/stats/statsRegistry.js @@ -183,6 +183,28 @@ function mongodbPersistence(currentValues, globalValues, callback) { dbService.db.collection('kpis').insertOne(statStamp, callback); } +/** + * Wraps a callback with stats, incrementing the given counters + * depending on the parameters passed to the callback: + * + * - If the callback receives an error, the errCounter is incremented. + * - If the callback receives no error, the okCounter is incremented. + * + * @param {String} okCounter Name of the counter to increment on success. + * @param {String} errCounter Name of the counter to increment on error. + * @param {Function} callback Callback to wrap. Its first argument is expected + * to be an error object + */ +function withStats(okCounter, errCounter, callback) { + function accounting(...args) { + const counter = args.length > 0 && args[0] ? errCounter : okCounter; + add(counter, 1, function () { + callback(...args); + }); + } + return accounting; +} + exports.add = add; exports.getCurrent = getCurrent; exports.getGlobal = getGlobal; @@ -194,3 +216,4 @@ exports.clearTimers = clearTimers; exports.addTimerAction = addTimerAction; exports.logStats = logStats; exports.mongodbPersistence = mongodbPersistence; +exports.withStats = withStats; From 1664ca30cfd3f014748236fe6c625881bade1179 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Tue, 30 Jul 2024 18:09:00 +0200 Subject: [PATCH 07/33] fix include path --- lib/services/common/alarmManagement.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/common/alarmManagement.js b/lib/services/common/alarmManagement.js index 84d6fb72c..bd628865c 100644 --- a/lib/services/common/alarmManagement.js +++ b/lib/services/common/alarmManagement.js @@ -22,7 +22,7 @@ */ let alarmRepository = {}; -const statsRegistry = require('../../stats/statsRegistry'); +const statsRegistry = require('../stats/statsRegistry'); const logger = require('logops'); const context = { op: 'IoTAgentNGSI.Alarms' From ad0cab2d327c6134863e199998b8e133630294a7 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Tue, 30 Jul 2024 18:13:03 +0200 Subject: [PATCH 08/33] fix paths --- lib/services/northBound/deviceProvisioningServer.js | 4 ++-- lib/services/northBound/northboundServer.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/services/northBound/deviceProvisioningServer.js b/lib/services/northBound/deviceProvisioningServer.js index 510b9802f..1a3914942 100644 --- a/lib/services/northBound/deviceProvisioningServer.js +++ b/lib/services/northBound/deviceProvisioningServer.js @@ -23,8 +23,8 @@ const async = require('async'); const restUtils = require('./restUtils'); -const statsRegistry = require('./../stats/statsRegistry'); -const deviceService = require('./../devices/deviceService'); +const statsRegistry = require('../stats/statsRegistry'); +const deviceService = require('../devices/deviceService'); const intoTrans = require('../common/domain').intoTrans; const logger = require('logops'); const errors = require('../../errors'); diff --git a/lib/services/northBound/northboundServer.js b/lib/services/northBound/northboundServer.js index 27655c790..8578de7a4 100644 --- a/lib/services/northBound/northboundServer.js +++ b/lib/services/northBound/northboundServer.js @@ -33,7 +33,7 @@ const intoTrans = domainUtils.intoTrans; const deviceProvisioning = require('./deviceProvisioningServer'); const deviceUpdating = require('./deviceProvisioningServer'); const groupProvisioning = require('./deviceGroupAdministrationServer'); -const statsRegistry = require('./stats/statsRegistry'); +const statsRegistry = require('../stats/statsRegistry'); const logger = require('logops'); const context = { op: 'IoTAgentNGSI.NorthboundServer' From 539a6f0b45809dc72eeac6e08766eaba3ab3bc4b Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Tue, 30 Jul 2024 18:18:16 +0200 Subject: [PATCH 09/33] fix stats collection --- lib/services/northBound/northboundServer.js | 28 +++++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/lib/services/northBound/northboundServer.js b/lib/services/northBound/northboundServer.js index 8578de7a4..a8c0300b3 100644 --- a/lib/services/northBound/northboundServer.js +++ b/lib/services/northBound/northboundServer.js @@ -45,18 +45,24 @@ const bodyParser = require('body-parser'); */ /* eslint-disable-next-line no-unused-vars */ function prometheusHandler(req, res) { - const metrics = new Array(); - const globalStats = statsRegistry.getGlobalStats(); - for (const key in globalStats) { - if (hasOwnProperty(globalStats, key)) { - metrics.push('# HELP ' + key); - metrics.push('# TYPE ' + key + ' counter'); - metrics.push(key + ' ' + globalStats[key]); - metrics.push('\n'); + statsRegistry.getAllGlobal(function (err, globalStats) { + if (err) { + logger.error(context, 'Error retrieving global stats: %s', err); + res.status(500).send('Error retrieving global stats'); + return; } - } - res.set('Content-Type', 'text/plain; version=0.0.4; charset=utf-8'); - res.status(200).send(metrics.join('\n')); + const metrics = new Array(); + for (const key in globalStats) { + if (hasOwnProperty(globalStats, key)) { + metrics.push('# HELP ' + key); + metrics.push('# TYPE ' + key + ' counter'); + metrics.push(key + ' ' + globalStats[key]); + metrics.push('\n'); + } + } + res.set('Content-Type', 'text/plain; version=0.0.4; charset=utf-8'); + res.status(200).send(metrics.join('\n')); + }); } function start(config, callback) { From 89a74e791287cd64e96d34eeb180eb6cdf6359b1 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Tue, 30 Jul 2024 18:34:51 +0200 Subject: [PATCH 10/33] unconditionally publish global stats --- doc/admin.md | 17 +++++++++++------ lib/fiware-iotagent-lib.js | 28 +++++++++++++++------------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/doc/admin.md b/doc/admin.md index 9b763aea6..515225edf 100644 --- a/doc/admin.md +++ b/doc/admin.md @@ -170,10 +170,10 @@ stats: { } ``` -By default, stats are logged to the standard log at level `INFO`. You can also have your stats written -to a collection named `kpis` in the mongo backend, if you do these two things: +By default, stats are logged to the standard log at level `INFO`. You can also have your stats written to a collection +named `kpis` in the mongo backend, if you do these two things: -- Set the `stats.persistence` flag to `true`. +- Set the `stats.persistence` flag to `true`. ```javascript stats: { @@ -182,11 +182,11 @@ stats: { } ``` -- Schedule periodic collection of stats to mongo by calling `statsRegistry.addTimerAction`. +- Schedule periodic collection of stats to mongo by calling `statsRegistry.addTimerAction`. ```js statsRegistry.addTimerAction(statsRegistry.mongodbPersistence, function callback() { - // ... called after timer is enabled ... + // ... called after timer is enabled ... }); ``` @@ -202,6 +202,10 @@ additional attribute for each of the stats created by calling the `statsRegistry } ``` +Global stats are also unconditionally exposed at the `/metrics` path in the northbound API, using the +[prometheus text-based format](https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md#text-based-format), +regardless of the value of `stats.interval`, so they can be scraped by a prometheus server + #### `authentication` Stores the authentication data, for use in retrieving tokens for devices with a trust token (required in scenarios with @@ -459,7 +463,8 @@ IotAgents, as all Express applications that use the body-parser middleware, have size that the application will handle. This default limit for ioiotagnets are 1Mb. So, if your IotAgent receives a request with a body that exceeds this limit, the application will throw a “Error: Request entity too large”. -The 1Mb default can be changed setting the `expressLimit` configuration parameter (or equivalente `IOTA_EXPRESS_LIMIT` environment variable). +The 1Mb default can be changed setting the `expressLimit` configuration parameter (or equivalente `IOTA_EXPRESS_LIMIT` +environment variable). ### Configuration using environment variables diff --git a/lib/fiware-iotagent-lib.js b/lib/fiware-iotagent-lib.js index 90a847313..6179329b8 100644 --- a/lib/fiware-iotagent-lib.js +++ b/lib/fiware-iotagent-lib.js @@ -46,21 +46,23 @@ const context = { }; function activateStatLogs(newConfig, callback) { + // global stats are always initialized, since they + // will be collected regardless of `stats.interval` + logActions = [ + apply(statsRegistry.globalLoad, { + deviceCreationRequests: 0, + deviceRemovalRequests: 0, + measureRequests: 0, + updateEntityOk: 0, + updateEntityError: 0 + }) + ]; + // However, local stats will only be periodically + // logged / cleared if `stats.interval` is set if (newConfig.stats && newConfig.stats.interval) { - async.series( - [ - apply(statsRegistry.globalLoad, { - deviceCreationRequests: 0, - deviceRemovalRequests: 0, - measureRequests: 0 - }), - apply(statsRegistry.addTimerAction, statsRegistry.logStats) - ], - callback - ); - } else { - callback(); + logActions.push(apply(statsRegistry.addTimerAction, statsRegistry.logStats)); } + async.series(logActions, callback); } /** From 908d6f3196b2a330312c4cf30c92d3500d88b455 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Tue, 30 Jul 2024 18:46:28 +0200 Subject: [PATCH 11/33] fixed reference to hasOwnProperty --- lib/services/northBound/northboundServer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/northBound/northboundServer.js b/lib/services/northBound/northboundServer.js index a8c0300b3..d23b85ef9 100644 --- a/lib/services/northBound/northboundServer.js +++ b/lib/services/northBound/northboundServer.js @@ -53,7 +53,7 @@ function prometheusHandler(req, res) { } const metrics = new Array(); for (const key in globalStats) { - if (hasOwnProperty(globalStats, key)) { + if (globalStats.hasOwnProperty(key)) { metrics.push('# HELP ' + key); metrics.push('# TYPE ' + key + ' counter'); metrics.push(key + ' ' + globalStats[key]); From 22dfddb6381b2b690b8c03b3e82f106f5774fc96 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Tue, 30 Jul 2024 18:52:03 +0200 Subject: [PATCH 12/33] improve metrics format --- lib/services/northBound/northboundServer.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/services/northBound/northboundServer.js b/lib/services/northBound/northboundServer.js index d23b85ef9..9431c240b 100644 --- a/lib/services/northBound/northboundServer.js +++ b/lib/services/northBound/northboundServer.js @@ -54,10 +54,9 @@ function prometheusHandler(req, res) { const metrics = new Array(); for (const key in globalStats) { if (globalStats.hasOwnProperty(key)) { - metrics.push('# HELP ' + key); + metrics.push('# HELP ' + key + ' global metric for ' + key); metrics.push('# TYPE ' + key + ' counter'); metrics.push(key + ' ' + globalStats[key]); - metrics.push('\n'); } } res.set('Content-Type', 'text/plain; version=0.0.4; charset=utf-8'); From b7bad16d9cb5f16166d4e68deae8711cfc0dfd0c Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Tue, 30 Jul 2024 19:18:12 +0200 Subject: [PATCH 13/33] fix some typos and add all global stats --- doc/admin.md | 2 +- lib/fiware-iotagent-lib.js | 6 ++++-- lib/services/ngsi/ngsiService.js | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/doc/admin.md b/doc/admin.md index 515225edf..e24db92b6 100644 --- a/doc/admin.md +++ b/doc/admin.md @@ -204,7 +204,7 @@ additional attribute for each of the stats created by calling the `statsRegistry Global stats are also unconditionally exposed at the `/metrics` path in the northbound API, using the [prometheus text-based format](https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md#text-based-format), -regardless of the value of `stats.interval`, so they can be scraped by a prometheus server +regardless of the value of `stats.interval`, so they can be scraped by a prometheus server. #### `authentication` diff --git a/lib/fiware-iotagent-lib.js b/lib/fiware-iotagent-lib.js index 6179329b8..8858c5b39 100644 --- a/lib/fiware-iotagent-lib.js +++ b/lib/fiware-iotagent-lib.js @@ -53,8 +53,10 @@ function activateStatLogs(newConfig, callback) { deviceCreationRequests: 0, deviceRemovalRequests: 0, measureRequests: 0, - updateEntityOk: 0, - updateEntityError: 0 + raiseAlarm: 0, + releaseAlarm: 0, + updateEntityRequestsOk: 0, + updateEntityRequestsError: 0 }) ]; // However, local stats will only be periodically diff --git a/lib/services/ngsi/ngsiService.js b/lib/services/ngsi/ngsiService.js index b42330fea..4b57c53c1 100644 --- a/lib/services/ngsi/ngsiService.js +++ b/lib/services/ngsi/ngsiService.js @@ -68,7 +68,7 @@ function init() { * @param {String} token User token to identify against the PEP Proxies (optional). */ function sendUpdateValue(entityName, attributes, typeInformation, token, callback) { - const newCallback = statsRegistry.withStats('updateEntityOk', 'updateEntityError', callback); + const newCallback = statsRegistry.withStats('updateEntityRequestsOk', 'updateEntityRequestsError', callback); entityHandler.sendUpdateValue(entityName, attributes, typeInformation, token, newCallback); } From 390a0fc6b2012174cb88dbf674d931933695fc6a Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Tue, 30 Jul 2024 19:21:49 +0200 Subject: [PATCH 14/33] remove superflouos comments --- lib/services/common/alarmManagement.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/services/common/alarmManagement.js b/lib/services/common/alarmManagement.js index bd628865c..fad0cc147 100644 --- a/lib/services/common/alarmManagement.js +++ b/lib/services/common/alarmManagement.js @@ -42,8 +42,6 @@ function raise(alarmName, description) { }; logger.error(context, 'Raising [%s]: %j', alarmName, description); - // NOTE: statsRegistry.add is actually synchronous (does not return a promise), - // we can call it synchronously. statsRegistry.add('raiseAlarm', 1, function () {}); } } @@ -57,8 +55,6 @@ function release(alarmName) { if (alarmRepository[alarmName]) { delete alarmRepository[alarmName]; logger.error(context, 'Releasing [%s]', alarmName); - // NOTE: statsRegistry.add is actually synchronous (does not return a promise), - // we can call it synchronously. statsRegistry.add('releaseAlarm', 1, function () {}); } } From 34b47e75d096d1f70dfc91377d6830a318b16f89 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Tue, 30 Jul 2024 19:24:33 +0200 Subject: [PATCH 15/33] remove noise from the PR --- lib/services/northBound/deviceProvisioningServer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/services/northBound/deviceProvisioningServer.js b/lib/services/northBound/deviceProvisioningServer.js index 1a3914942..510b9802f 100644 --- a/lib/services/northBound/deviceProvisioningServer.js +++ b/lib/services/northBound/deviceProvisioningServer.js @@ -23,8 +23,8 @@ const async = require('async'); const restUtils = require('./restUtils'); -const statsRegistry = require('../stats/statsRegistry'); -const deviceService = require('../devices/deviceService'); +const statsRegistry = require('./../stats/statsRegistry'); +const deviceService = require('./../devices/deviceService'); const intoTrans = require('../common/domain').intoTrans; const logger = require('logops'); const errors = require('../../errors'); From e549c8c9676606363b72f1dcc4f21088df6405c4 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Tue, 30 Jul 2024 19:59:35 +0200 Subject: [PATCH 16/33] add support for openmetrics --- doc/admin.md | 5 ++- lib/services/northBound/northboundServer.js | 42 +++++++++++++++++++-- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/doc/admin.md b/doc/admin.md index e24db92b6..a502903e5 100644 --- a/doc/admin.md +++ b/doc/admin.md @@ -203,8 +203,9 @@ additional attribute for each of the stats created by calling the `statsRegistry ``` Global stats are also unconditionally exposed at the `/metrics` path in the northbound API, using the -[prometheus text-based format](https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md#text-based-format), -regardless of the value of `stats.interval`, so they can be scraped by a prometheus server. +[openmetrics text-based format](https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md), +regardless of the value of `stats.interval`, so they can be scraped by a prometheus server or other compatible metrics +collector. #### `authentication` diff --git a/lib/services/northBound/northboundServer.js b/lib/services/northBound/northboundServer.js index 9431c240b..a159f1817 100644 --- a/lib/services/northBound/northboundServer.js +++ b/lib/services/northBound/northboundServer.js @@ -41,10 +41,10 @@ const context = { const bodyParser = require('body-parser'); /** - * Predefined http handler that returns current prometheus metrics + * Predefined http handler that returns current openmetrics data */ /* eslint-disable-next-line no-unused-vars */ -function prometheusHandler(req, res) { +function openmetricsHandler(req, res) { statsRegistry.getAllGlobal(function (err, globalStats) { if (err) { logger.error(context, 'Error retrieving global stats: %s', err); @@ -59,7 +59,41 @@ function prometheusHandler(req, res) { metrics.push(key + ' ' + globalStats[key]); } } - res.set('Content-Type', 'text/plain; version=0.0.4; charset=utf-8'); + // Expositions MUST END WUTH '#EOF' + // (see https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md) + metrics.push('# EOF'); + // Content-Type MUST BE 'text/plain; version=0.0.4; charset=utf-8' + // unless the collector requests openmetrics text format with Accept header, + // in which case it MUST BE 'application/openmetrics-text; version=1.0.0; charset=utf-8' + let contentType = 'text/plain; version=0.0.4; charset=utf-8'; + if (req.headers.accept) { + const parts = req.headers.accept.split(';').map((part) => part.trim().toLowerCase()); + switch (parts[0]) { + case 'application/openmetrics-text': + let version = null; + for (let i = 1; i < parts.length; i++) { + if (parts[i].startsWith('version=')) { + version = parts[i].substring(8); + break; + } + } + if (version && version != '1.0.0') { + logger.error(context, 'Unsupported openmetrics version: %s', version); + res.status(406).send('Unsupported openmetrics version'); + return; + } + contentType = 'application/openmetrics-text; version=1.0.0; charset=utf-8'; + break; + case 'text/plain': + // nop + break; + default: + logger.error(context, 'Unsupported accept header: %s', req.headers.accept); + res.status(406).send('Unsupported accept header'); + return; + } + } + res.set('Content-Type', contentType); res.status(200).send(metrics.join('\n')); }); } @@ -108,7 +142,7 @@ function start(config, callback) { northboundServer.router.get('/version', middlewares.retrieveVersion); northboundServer.router.put('/admin/log', middlewares.changeLogLevel); northboundServer.router.get('/admin/log', middlewares.getLogLevel); - northboundServer.router.get('/metrics', prometheusHandler); + northboundServer.router.get('/metrics', openmetricsHandler); northboundServer.app.use(baseRoot, northboundServer.router); contextServer.loadContextRoutes(northboundServer.router); From 5142a8b8eb07ad8717634a7921fb90548f7e0c18 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Tue, 30 Jul 2024 20:09:41 +0200 Subject: [PATCH 17/33] improve processing of accepts header --- lib/services/northBound/northboundServer.js | 46 ++++++++++----------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/lib/services/northBound/northboundServer.js b/lib/services/northBound/northboundServer.js index a159f1817..63329b266 100644 --- a/lib/services/northBound/northboundServer.js +++ b/lib/services/northBound/northboundServer.js @@ -66,32 +66,30 @@ function openmetricsHandler(req, res) { // unless the collector requests openmetrics text format with Accept header, // in which case it MUST BE 'application/openmetrics-text; version=1.0.0; charset=utf-8' let contentType = 'text/plain; version=0.0.4; charset=utf-8'; - if (req.headers.accept) { - const parts = req.headers.accept.split(';').map((part) => part.trim().toLowerCase()); - switch (parts[0]) { - case 'application/openmetrics-text': - let version = null; - for (let i = 1; i < parts.length; i++) { - if (parts[i].startsWith('version=')) { - version = parts[i].substring(8); - break; - } + switch (req.accepts(['text/plain', 'application/openmetrics-text'])) { + case 'application/openmetrics-text': + const parts = req.headers.accept.split(';').map((part) => part.trim().toLowerCase()); + let version = null; + for (let i = 1; i < parts.length; i++) { + if (parts[i].startsWith('version=')) { + version = parts[i].substring(8); + break; } - if (version && version != '1.0.0') { - logger.error(context, 'Unsupported openmetrics version: %s', version); - res.status(406).send('Unsupported openmetrics version'); - return; - } - contentType = 'application/openmetrics-text; version=1.0.0; charset=utf-8'; - break; - case 'text/plain': - // nop - break; - default: - logger.error(context, 'Unsupported accept header: %s', req.headers.accept); - res.status(406).send('Unsupported accept header'); + } + if (version && version != '1.0.0') { + logger.error(context, 'Unsupported openmetrics version: %s', version); + res.status(406).send('Unsupported openmetrics version'); return; - } + } + contentType = 'application/openmetrics-text; version=1.0.0; charset=utf-8'; + break; + case 'text/plain': + // nop + break; + default: + logger.error(context, 'Unsupported accept header: %s', req.headers.accept); + res.status(406).send('Unsupported accept header'); + return; } res.set('Content-Type', contentType); res.status(200).send(metrics.join('\n')); From 3cd0463a8d53a4afe4feae7562e9a4370fb0c9c3 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Tue, 30 Jul 2024 20:39:28 +0200 Subject: [PATCH 18/33] Improve content-type negotation for openmetrics --- lib/services/northBound/northboundServer.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/services/northBound/northboundServer.js b/lib/services/northBound/northboundServer.js index 63329b266..0656f397c 100644 --- a/lib/services/northBound/northboundServer.js +++ b/lib/services/northBound/northboundServer.js @@ -59,23 +59,22 @@ function openmetricsHandler(req, res) { metrics.push(key + ' ' + globalStats[key]); } } - // Expositions MUST END WUTH '#EOF' + // Expositions MUST END WITH '#EOF' // (see https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md) metrics.push('# EOF'); // Content-Type MUST BE 'text/plain; version=0.0.4; charset=utf-8' // unless the collector requests openmetrics text format with Accept header, // in which case it MUST BE 'application/openmetrics-text; version=1.0.0; charset=utf-8' let contentType = 'text/plain; version=0.0.4; charset=utf-8'; + let acceptVersion = null; + // Workaround for express.accepts not recognizing `version=...;' + // in the Accept header, which is used by openmetrics collectors + if (req.headers.accept && req.headers.accept.contains('version=1.0.0')) { + acceptVersion = '1.0.0'; + req.headers.accept = req.headers.accept.replace(/version=1.0.0;?/, ''); + } switch (req.accepts(['text/plain', 'application/openmetrics-text'])) { case 'application/openmetrics-text': - const parts = req.headers.accept.split(';').map((part) => part.trim().toLowerCase()); - let version = null; - for (let i = 1; i < parts.length; i++) { - if (parts[i].startsWith('version=')) { - version = parts[i].substring(8); - break; - } - } if (version && version != '1.0.0') { logger.error(context, 'Unsupported openmetrics version: %s', version); res.status(406).send('Unsupported openmetrics version'); From 600791f0468c9fe4bf63f2a45b042d1f6fbcf030 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Tue, 30 Jul 2024 20:54:11 +0200 Subject: [PATCH 19/33] improve header negotiation --- lib/services/northBound/northboundServer.js | 28 +++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/services/northBound/northboundServer.js b/lib/services/northBound/northboundServer.js index 0656f397c..a1557abe7 100644 --- a/lib/services/northBound/northboundServer.js +++ b/lib/services/northBound/northboundServer.js @@ -66,16 +66,30 @@ function openmetricsHandler(req, res) { // unless the collector requests openmetrics text format with Accept header, // in which case it MUST BE 'application/openmetrics-text; version=1.0.0; charset=utf-8' let contentType = 'text/plain; version=0.0.4; charset=utf-8'; - let acceptVersion = null; - // Workaround for express.accepts not recognizing `version=...;' - // in the Accept header, which is used by openmetrics collectors - if (req.headers.accept && req.headers.accept.contains('version=1.0.0')) { - acceptVersion = '1.0.0'; - req.headers.accept = req.headers.accept.replace(/version=1.0.0;?/, ''); + // Workaround for `express.accepts` not supporting generic key-value pairs + // (other then ;q=...) in the Accept headers. + const kvPairs = {}; + if (req.headers.accept) { + const parts = req.headers.accept.split(';'); + let regular = []; + for (let i = 0; i < parts.length; i++) { + const trimmed = parts[i].trim(); + // any `key=value` pair not matching the standard + // `q=...` format, goes to `kvPairs` + if (trimmed.contains('=') && !trimmed.startswith('q=')) { + const kv = trimmed.split('=', 2); + if (kv.length == 2) { + kvPairs[kv[0]] = kv[1]; + } + } else { + other.push(trimmed); + } + } + req.headers.accept = regular.join(';'); } switch (req.accepts(['text/plain', 'application/openmetrics-text'])) { case 'application/openmetrics-text': - if (version && version != '1.0.0') { + if (kvPairs.version && kvPairs.version != '1.0.0') { logger.error(context, 'Unsupported openmetrics version: %s', version); res.status(406).send('Unsupported openmetrics version'); return; From 90bf909020669dca60f424045fa388d128b57024 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Tue, 30 Jul 2024 20:56:30 +0200 Subject: [PATCH 20/33] optimize not getting stats if header is wrong --- lib/services/northBound/northboundServer.js | 84 ++++++++++----------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/lib/services/northBound/northboundServer.js b/lib/services/northBound/northboundServer.js index a1557abe7..3b9513f85 100644 --- a/lib/services/northBound/northboundServer.js +++ b/lib/services/northBound/northboundServer.js @@ -45,6 +45,48 @@ const bodyParser = require('body-parser'); */ /* eslint-disable-next-line no-unused-vars */ function openmetricsHandler(req, res) { + // Content-Type MUST BE 'text/plain; version=0.0.4; charset=utf-8' + // unless the collector requests openmetrics text format with Accept header, + // in which case it MUST BE 'application/openmetrics-text; version=1.0.0; charset=utf-8' + let contentType = 'text/plain; version=0.0.4; charset=utf-8'; + // Workaround for `express.accepts` not supporting generic key-value pairs + // (other then ;q=...) in the Accept headers. + const kvPairs = {}; + if (req.headers.accept) { + const parts = req.headers.accept.split(';'); + let regular = []; + for (let i = 0; i < parts.length; i++) { + const trimmed = parts[i].trim(); + // any `key=value` pair not matching the standard + // `q=...` format, goes to `kvPairs` + if (trimmed.contains('=') && !trimmed.startswith('q=')) { + const kv = trimmed.split('=', 2); + if (kv.length == 2) { + kvPairs[kv[0]] = kv[1]; + } + } else { + other.push(trimmed); + } + } + req.headers.accept = regular.join(';'); + } + switch (req.accepts(['text/plain', 'application/openmetrics-text'])) { + case 'application/openmetrics-text': + if (kvPairs.version && kvPairs.version != '1.0.0') { + logger.error(context, 'Unsupported openmetrics version: %s', version); + res.status(406).send('Unsupported openmetrics version'); + return; + } + contentType = 'application/openmetrics-text; version=1.0.0; charset=utf-8'; + break; + case 'text/plain': + // nop + break; + default: + logger.error(context, 'Unsupported accept header: %s', req.headers.accept); + res.status(406).send('Unsupported accept header'); + return; + } statsRegistry.getAllGlobal(function (err, globalStats) { if (err) { logger.error(context, 'Error retrieving global stats: %s', err); @@ -62,48 +104,6 @@ function openmetricsHandler(req, res) { // Expositions MUST END WITH '#EOF' // (see https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md) metrics.push('# EOF'); - // Content-Type MUST BE 'text/plain; version=0.0.4; charset=utf-8' - // unless the collector requests openmetrics text format with Accept header, - // in which case it MUST BE 'application/openmetrics-text; version=1.0.0; charset=utf-8' - let contentType = 'text/plain; version=0.0.4; charset=utf-8'; - // Workaround for `express.accepts` not supporting generic key-value pairs - // (other then ;q=...) in the Accept headers. - const kvPairs = {}; - if (req.headers.accept) { - const parts = req.headers.accept.split(';'); - let regular = []; - for (let i = 0; i < parts.length; i++) { - const trimmed = parts[i].trim(); - // any `key=value` pair not matching the standard - // `q=...` format, goes to `kvPairs` - if (trimmed.contains('=') && !trimmed.startswith('q=')) { - const kv = trimmed.split('=', 2); - if (kv.length == 2) { - kvPairs[kv[0]] = kv[1]; - } - } else { - other.push(trimmed); - } - } - req.headers.accept = regular.join(';'); - } - switch (req.accepts(['text/plain', 'application/openmetrics-text'])) { - case 'application/openmetrics-text': - if (kvPairs.version && kvPairs.version != '1.0.0') { - logger.error(context, 'Unsupported openmetrics version: %s', version); - res.status(406).send('Unsupported openmetrics version'); - return; - } - contentType = 'application/openmetrics-text; version=1.0.0; charset=utf-8'; - break; - case 'text/plain': - // nop - break; - default: - logger.error(context, 'Unsupported accept header: %s', req.headers.accept); - res.status(406).send('Unsupported accept header'); - return; - } res.set('Content-Type', contentType); res.status(200).send(metrics.join('\n')); }); From e2b07ffc7ab0678ef0cf178b0089134965d77948 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Tue, 30 Jul 2024 21:07:24 +0200 Subject: [PATCH 21/33] improved negotation --- lib/services/northBound/northboundServer.js | 36 +++++++++++++-------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/lib/services/northBound/northboundServer.js b/lib/services/northBound/northboundServer.js index 3b9513f85..c9867c94a 100644 --- a/lib/services/northBound/northboundServer.js +++ b/lib/services/northBound/northboundServer.js @@ -51,28 +51,38 @@ function openmetricsHandler(req, res) { let contentType = 'text/plain; version=0.0.4; charset=utf-8'; // Workaround for `express.accepts` not supporting generic key-value pairs // (other then ;q=...) in the Accept headers. - const kvPairs = {}; + let version = null; + let charset = null; if (req.headers.accept) { const parts = req.headers.accept.split(';'); - let regular = []; + let unparsed = []; for (let i = 0; i < parts.length; i++) { - const trimmed = parts[i].trim(); - // any `key=value` pair not matching the standard - // `q=...` format, goes to `kvPairs` - if (trimmed.contains('=') && !trimmed.startswith('q=')) { - const kv = trimmed.split('=', 2); - if (kv.length == 2) { - kvPairs[kv[0]] = kv[1]; - } + const current = parts[i]; + const trimmed = current.trim(); + // prometheus exporters SHOULD recognize `version` + // and `charset` in ACCEPT header + if (trimmed.startswith('version=')) { + version = trimmed.substring(8); + } else if (trimmed.startsWith('charset=')) { + charset = trimmed.substring(8); } else { - other.push(trimmed); + unparsed.push(current); } } - req.headers.accept = regular.join(';'); + if (unparsed.length < parts.length) { + // Remove the parts not recognized by + // express.accepts + req.headers['Accept'] = unparsed.join(';'); + } + } + if (charset && charset != 'utf-8') { + logger.error(context, 'Unsupported charset: %s', charset); + res.status(406).send('Unsupported charset'); + return; } switch (req.accepts(['text/plain', 'application/openmetrics-text'])) { case 'application/openmetrics-text': - if (kvPairs.version && kvPairs.version != '1.0.0') { + if (version && version != '1.0.0') { logger.error(context, 'Unsupported openmetrics version: %s', version); res.status(406).send('Unsupported openmetrics version'); return; From 863ab5eb8bde646e895978042927174d4ff371db Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Tue, 30 Jul 2024 21:10:38 +0200 Subject: [PATCH 22/33] fix typo --- lib/services/northBound/northboundServer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/northBound/northboundServer.js b/lib/services/northBound/northboundServer.js index c9867c94a..ce82a713c 100644 --- a/lib/services/northBound/northboundServer.js +++ b/lib/services/northBound/northboundServer.js @@ -61,7 +61,7 @@ function openmetricsHandler(req, res) { const trimmed = current.trim(); // prometheus exporters SHOULD recognize `version` // and `charset` in ACCEPT header - if (trimmed.startswith('version=')) { + if (trimmed.startsWith('version=')) { version = trimmed.substring(8); } else if (trimmed.startsWith('charset=')) { charset = trimmed.substring(8); From d0faa5dfc8c97ad5a6bcb45515f83824c3064c28 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Tue, 30 Jul 2024 21:15:06 +0200 Subject: [PATCH 23/33] fix accept headers --- lib/services/northBound/northboundServer.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/services/northBound/northboundServer.js b/lib/services/northBound/northboundServer.js index ce82a713c..011ba2032 100644 --- a/lib/services/northBound/northboundServer.js +++ b/lib/services/northBound/northboundServer.js @@ -70,9 +70,9 @@ function openmetricsHandler(req, res) { } } if (unparsed.length < parts.length) { - // Remove the parts not recognized by - // express.accepts - req.headers['Accept'] = unparsed.join(';'); + // Remove the parts not recognized by express.accepts + delete req.headers['accept']; + req.headers['accept'] = unparsed.join(';'); } } if (charset && charset != 'utf-8') { From 50f904e4e127afbfb76048c66dcf1060b1bb84ca Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Tue, 30 Jul 2024 21:31:20 +0200 Subject: [PATCH 24/33] improve negotiation --- lib/services/northBound/northboundServer.js | 29 +++++++++++++-------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/services/northBound/northboundServer.js b/lib/services/northBound/northboundServer.js index 011ba2032..327891151 100644 --- a/lib/services/northBound/northboundServer.js +++ b/lib/services/northBound/northboundServer.js @@ -45,12 +45,21 @@ const bodyParser = require('body-parser'); */ /* eslint-disable-next-line no-unused-vars */ function openmetricsHandler(req, res) { - // Content-Type MUST BE 'text/plain; version=0.0.4; charset=utf-8' - // unless the collector requests openmetrics text format with Accept header, - // in which case it MUST BE 'application/openmetrics-text; version=1.0.0; charset=utf-8' - let contentType = 'text/plain; version=0.0.4; charset=utf-8'; - // Workaround for `express.accepts` not supporting generic key-value pairs - // (other then ;q=...) in the Accept headers. + // Content-Type MUST BE 'application/openmetrics-text; version=1.0.0; charset=utf-8' + // for openmetrics collectors, see: + //https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#overall-structure + // However, it should be 'text/plain; version=0.0.4; charset=utf-8' + // for prometheus.compatible collectors, see: + // https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md + let contentType = 'application/openmetrics-text; version=1.0.0; charset=utf-8'; + // To differentiate both kinds of collectors, the `Accept` header is used. + // Openmetrics-based collectors use an `Accept` header such as: + // Accept: application/openmetrics-text; version=1.0.0; charset=utf-8'; + // See https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/18913 + // However, this is sort of a prometheus-specific extension to the `Accepts` + // header, that breaks the regexps in `express.accepts`. + // So we must parse these key-value pairs ourselves, and remove them from the + // header before handling it to `requests.accept`. let version = null; let charset = null; if (req.headers.accept) { @@ -59,8 +68,6 @@ function openmetricsHandler(req, res) { for (let i = 0; i < parts.length; i++) { const current = parts[i]; const trimmed = current.trim(); - // prometheus exporters SHOULD recognize `version` - // and `charset` in ACCEPT header if (trimmed.startsWith('version=')) { version = trimmed.substring(8); } else if (trimmed.startsWith('charset=')) { @@ -70,11 +77,11 @@ function openmetricsHandler(req, res) { } } if (unparsed.length < parts.length) { - // Remove the parts not recognized by express.accepts delete req.headers['accept']; req.headers['accept'] = unparsed.join(';'); } } + // charset MUST BE utf-8 if (charset && charset != 'utf-8') { logger.error(context, 'Unsupported charset: %s', charset); res.status(406).send('Unsupported charset'); @@ -82,15 +89,15 @@ function openmetricsHandler(req, res) { } switch (req.accepts(['text/plain', 'application/openmetrics-text'])) { case 'application/openmetrics-text': + // Version MUST BE 1.0.0 for openmetrics if (version && version != '1.0.0') { logger.error(context, 'Unsupported openmetrics version: %s', version); res.status(406).send('Unsupported openmetrics version'); return; } - contentType = 'application/openmetrics-text; version=1.0.0; charset=utf-8'; break; case 'text/plain': - // nop + contentType = 'text/plain; version=0.0.4; charset=utf-8'; break; default: logger.error(context, 'Unsupported accept header: %s', req.headers.accept); From 836d5e7158174f3f43c1ade604b9120a40658384 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Wed, 31 Jul 2024 13:18:05 +0200 Subject: [PATCH 25/33] improve comments --- lib/services/northBound/northboundServer.js | 28 ++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/services/northBound/northboundServer.js b/lib/services/northBound/northboundServer.js index 327891151..f0a162640 100644 --- a/lib/services/northBound/northboundServer.js +++ b/lib/services/northBound/northboundServer.js @@ -45,23 +45,23 @@ const bodyParser = require('body-parser'); */ /* eslint-disable-next-line no-unused-vars */ function openmetricsHandler(req, res) { - // Content-Type MUST BE 'application/openmetrics-text; version=1.0.0; charset=utf-8' - // for openmetrics collectors, see: - //https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#overall-structure - // However, it should be 'text/plain; version=0.0.4; charset=utf-8' - // for prometheus.compatible collectors, see: + // Content-Type: + // - For openmetrics collectors, it MUST BE 'application/openmetrics-text; version=1.0.0; charset=utf-8'. See: + // https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#overall-structure + // - For prometheus compatible collectors, it SHOULD BE 'text/plain; version=0.0.4; charset=utf-8'. See: // https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md let contentType = 'application/openmetrics-text; version=1.0.0; charset=utf-8'; - // To differentiate both kinds of collectors, the `Accept` header is used. - // Openmetrics-based collectors use an `Accept` header such as: - // Accept: application/openmetrics-text; version=1.0.0; charset=utf-8'; - // See https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/18913 - // However, this is sort of a prometheus-specific extension to the `Accepts` - // header, that breaks the regexps in `express.accepts`. - // So we must parse these key-value pairs ourselves, and remove them from the - // header before handling it to `requests.accept`. let version = null; let charset = null; + // To identify openmetrics collectors, we need to parse the `Accept` header. + // An openmetrics-based collectors SHOULD use an `Accept` header such as: + // `Accept: application/openmetrics-text; version=1.0.0; charset=utf-8' + // See: https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/18913 + // + // WORKAROUND: express version 4 does not parse properly the openmetrics Accept header, + // it won't match the regular expressions supported by `express.accepts`. + // So we must parse these key-value pairs ourselves, and remove them from the + // header before handling it to `requests.accept`. if (req.headers.accept) { const parts = req.headers.accept.split(';'); let unparsed = []; @@ -119,7 +119,7 @@ function openmetricsHandler(req, res) { } } // Expositions MUST END WITH '#EOF' - // (see https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md) + // See https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md metrics.push('# EOF'); res.set('Content-Type', contentType); res.status(200).send(metrics.join('\n')); From 31f1071daa3bc92e487d1ffe1658a9eec785a7eb Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Thu, 1 Aug 2024 19:29:12 +0200 Subject: [PATCH 26/33] added doc --- doc/admin.md | 10 +- doc/api.md | 307 ++++++++++++++++++++++++++++++++------------------- 2 files changed, 200 insertions(+), 117 deletions(-) diff --git a/doc/admin.md b/doc/admin.md index a502903e5..d437deaef 100644 --- a/doc/admin.md +++ b/doc/admin.md @@ -161,6 +161,11 @@ support nulls or multi-attribute requests if they are encountered. #### `stats` +**NOTE**: This configuration section is **deprecated**. Users interested in collecting stats are encouraged to scrape +the `/metrics` endpoint in the [northbound API](./api.md), with an +[openmetrics-compatible](https://github.com/OpenObservability/OpenMetrics/tree/mainpro) telemetry collector such as +[prometheus](https://prometheus.io/). + It configures the periodic collection of statistics. Use `interval` in milliseconds to set the time between stats writings. @@ -202,11 +207,6 @@ additional attribute for each of the stats created by calling the `statsRegistry } ``` -Global stats are also unconditionally exposed at the `/metrics` path in the northbound API, using the -[openmetrics text-based format](https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md), -regardless of the value of `stats.interval`, so they can be scraped by a prometheus server or other compatible metrics -collector. - #### `authentication` Stores the authentication data, for use in retrieving tokens for devices with a trust token (required in scenarios with diff --git a/doc/api.md b/doc/api.md index a0c9ddbac..ea9d08f38 100644 --- a/doc/api.md +++ b/doc/api.md @@ -69,6 +69,8 @@ - [Retrieve log level `GET /admin/log`](#retrieve-log-level-get-adminlog) - [About operations](#about-operations) - [List IoTA Information `GET /iot/about`](#list-iota-information-get-iotabout) + - [Metrics](#metrics) + - [Retrieve metrics `GET /metrics`](#retrieve-metrics-get-metrics) @@ -245,10 +247,13 @@ measure name, unless `explicitAttrs` is defined. Measures `id` or `type` names a ## Device autoprovision and entity creation For those agents that uses IoTA Node LIB version 3.4.0 or higher, you should consider that the entity is not created -automatically when a device is created. This means that all entities into the Context Broker are created when data -arrives from a device, no matter if the device is explicitly provisioned (via [device provisioning API](#create-device-post-iotdevices)) or autoprovisioned. +automatically when a device is created. This means that all entities into the Context Broker are created when data +arrives from a device, no matter if the device is explicitly provisioned (via +[device provisioning API](#create-device-post-iotdevices)) or autoprovisioned. -If for any reason you need the entity at CB before the first measure of the corresponding device arrives to the IOTAgent, you can create it in advance using the Context Broker [NGSI v2 API](https://github.com/telefonicaid/fiware-orion/blob/master/doc/manuals/orion-api.md). +If for any reason you need the entity at CB before the first measure of the corresponding device arrives to the +IOTAgent, you can create it in advance using the Context Broker +[NGSI v2 API](https://github.com/telefonicaid/fiware-orion/blob/master/doc/manuals/orion-api.md). ## Multientity support @@ -479,7 +484,8 @@ mappings of the provision. If `explicitAttrs` is provided both at device and con precedence. Additionally `explicitAttrs` can be used to define which measures (identified by their attribute names, not by their object_id) defined in JSON/JEXL array will be propagated to NGSI interface. -Note that when `explicitAttrs` is an array or a JEXL expression resulting in to Array, if this array is empty then `TimeInstant` is not propaged to CB. +Note that when `explicitAttrs` is an array or a JEXL expression resulting in to Array, if this array is empty then +`TimeInstant` is not propaged to CB. The different possibilities are summarized below: @@ -632,53 +638,52 @@ to incorporate new transformations from the IoT Agent configuration file in a fa Current common transformation set: -| JEXL Transformation | Equivalent JavaScript Function | -| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | -| jsonparse: (str) | `JSON.parse(str);` | -| jsonstringify: (obj) | `JSON.stringify(obj);` | -| indexOf: (val, char) | `String(val).indexOf(char);` | -| length: (val) | `String(val).length;` | -| trim: (val) | `String(val).trim();` | -| substr: (val, int1, int2) | `String(val).substr(int1, int2);` | -| addreduce: (arr) | arr.reduce((i, v) | i + v)); | -| lengtharray: (arr) | `arr.length;` | -| typeof: (val) | `typeof val;` | -| isarray: (arr) | `Array.isArray(arr);` | -| isnan: (val) | `isNaN(val);` | -| parseint: (val) | `parseInt(val);` | -| parsefloat: (val) | `parseFloat(val);` | -| toisodate: (val) | `new Date(val).toISOString();` | -| timeoffset:(isostr) | `new Date(isostr).getTimezoneOffset();` | -| tostring: (val) | `val.toString();` | -| urlencode: (val) | `encodeURI(val);` | -| urldecode: (val) | `decodeURI(val);` | -| replacestr: (str, from, to) | `str.replace(from, to);` | -| replaceregexp: (str, reg, to) | `str.replace(new RegExp(reg), to);` | -| replaceallstr: (str, from, to) | `str.replaceAll(from, to);` | -| replaceallregexp: (str, reg, to) | `str.replaceAll(new RegExp(reg,"g"), to);` | -| split: (str, ch) | `str.split(ch);` | -| joinarrtostr: (arr, ch) | `arr.join(ch);` | -| concatarr: (arr, arr2) | `arr.concat(arr2);` | -| mapper: (val, values, choices) | choices[values.findIndex((target) | target == val)]); | -| thmapper: (val, values, choices) | choices[values.reduce((acc,curr,i,arr) | (acc==0)||acc?acc:val<=curr?acc=i:acc=null,null)]; | -| bitwisemask: (i,mask,op,shf) | (op==="&"?parseInt(i)&mask: op==="|"?parseInt(i)|mask: op==="^"?parseInt(i)^mask:i)>>shf; | -| slice: (arr, init, end) | `arr.slice(init,end);` | -| addset: (arr, x) | { return Array.from((new Set(arr)).add(x)) } | -| removeset: (arr, x) | { let s = new Set(arr); s.delete(x); return Array.from(s) } | -| touppercase: (val) | `String(val).toUpperCase()` | -| tolowercase: (val) | `String(val).toLowerCase()` | -| round: (val) | `Math.round(val)` | -| floor: (val) | `Math.floor(val)` | -| ceil: (val) | `Math.ceil(val)` | -| tofixed: (val, decimals) | `Number.parseFloat(val).toFixed(decimals)` | -| gettime: (d) | `new Date(d).getTime()` | -| toisostring: (d) | `new Date(d).toISOString()` | -| localestring: (d, timezone, options) | `new Date(d).toLocaleString(timezone, options)` | -| now: () | `Date.now()` | -| hextostring: (val) | `new TextDecoder().decode(new Uint8Array(val.match(/.{1,2}/g).map(byte => parseInt(byte, 16))))` | -| valuePicker: (val,pick) | valuePicker: (val,pick) => Object.entries(val).filter(([_, v]) => v === pick).map(([k, _]) => k) | -| valuePickerMulti: (val,pick) | valuePickerMulti: (val,pick) => Object.entries(val).filter(([_, v]) => pick.includes(v)).map(([k, _]) => k) | - +| JEXL Transformation | Equivalent JavaScript Function | +| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | +| jsonparse: (str) | `JSON.parse(str);` | +| jsonstringify: (obj) | `JSON.stringify(obj);` | +| indexOf: (val, char) | `String(val).indexOf(char);` | +| length: (val) | `String(val).length;` | +| trim: (val) | `String(val).trim();` | +| substr: (val, int1, int2) | `String(val).substr(int1, int2);` | +| addreduce: (arr) | arr.reduce((i, v) | i + v)); | +| lengtharray: (arr) | `arr.length;` | +| typeof: (val) | `typeof val;` | +| isarray: (arr) | `Array.isArray(arr);` | +| isnan: (val) | `isNaN(val);` | +| parseint: (val) | `parseInt(val);` | +| parsefloat: (val) | `parseFloat(val);` | +| toisodate: (val) | `new Date(val).toISOString();` | +| timeoffset:(isostr) | `new Date(isostr).getTimezoneOffset();` | +| tostring: (val) | `val.toString();` | +| urlencode: (val) | `encodeURI(val);` | +| urldecode: (val) | `decodeURI(val);` | +| replacestr: (str, from, to) | `str.replace(from, to);` | +| replaceregexp: (str, reg, to) | `str.replace(new RegExp(reg), to);` | +| replaceallstr: (str, from, to) | `str.replaceAll(from, to);` | +| replaceallregexp: (str, reg, to) | `str.replaceAll(new RegExp(reg,"g"), to);` | +| split: (str, ch) | `str.split(ch);` | +| joinarrtostr: (arr, ch) | `arr.join(ch);` | +| concatarr: (arr, arr2) | `arr.concat(arr2);` | +| mapper: (val, values, choices) | choices[values.findIndex((target) | target == val)]); | +| thmapper: (val, values, choices) | choices[values.reduce((acc,curr,i,arr) | (acc==0)||acc?acc:val<=curr?acc=i:acc=null,null)]; | +| bitwisemask: (i,mask,op,shf) | (op==="&"?parseInt(i)&mask: op==="|"?parseInt(i)|mask: op==="^"?parseInt(i)^mask:i)>>shf; | +| slice: (arr, init, end) | `arr.slice(init,end);` | +| addset: (arr, x) | { return Array.from((new Set(arr)).add(x)) } | +| removeset: (arr, x) | { let s = new Set(arr); s.delete(x); return Array.from(s) } | +| touppercase: (val) | `String(val).toUpperCase()` | +| tolowercase: (val) | `String(val).toLowerCase()` | +| round: (val) | `Math.round(val)` | +| floor: (val) | `Math.floor(val)` | +| ceil: (val) | `Math.ceil(val)` | +| tofixed: (val, decimals) | `Number.parseFloat(val).toFixed(decimals)` | +| gettime: (d) | `new Date(d).getTime()` | +| toisostring: (d) | `new Date(d).toISOString()` | +| localestring: (d, timezone, options) | `new Date(d).toLocaleString(timezone, options)` | +| now: () | `Date.now()` | +| hextostring: (val) | `new TextDecoder().decode(new Uint8Array(val.match(/.{1,2}/g).map(byte => parseInt(byte, 16))))` | +| valuePicker: (val,pick) | valuePicker: (val,pick) => Object.entries(val).filter(([_, v]) => v === pick).map(([k, _]) => k) | +| valuePickerMulti: (val,pick) | valuePickerMulti: (val,pick) => Object.entries(val).filter(([_, v]) => pick.includes(v)).map(([k, _]) => k) | You have available this [JEXL interactive playground][99] with all the transformations already loaded, in which you can test all the functions described above. @@ -1193,13 +1198,20 @@ In this case a batch update (`POST /v2/op/update`) to CB will be generated with ## Command execution -This section reviews the end-to-end process to trigger and receive commands into devices. The URL paths and messages format is based on the [IoT Agent JSON](https://github.com/telefonicaid/iotagent-json). It may differ in the case of using any other IoT Agent. In that case, please refer to the specific IoTA documentation. +This section reviews the end-to-end process to trigger and receive commands into devices. The URL paths and messages +format is based on the [IoT Agent JSON](https://github.com/telefonicaid/iotagent-json). It may differ in the case of +using any other IoT Agent. In that case, please refer to the specific IoTA documentation. ### Triggering commands -This starts the process of sending data to devices. It starts by updating an attribute into the Context Broker defined as `command` in the [config group](#config-group-datamodel) or in the [device provision](#device-datamodel). Commands attributes are created using `command` as attribute type. Also, you can define the protocol you want the commands to be sent (HTTP/MQTT) with the `transport` parameter at the provisioning process. +This starts the process of sending data to devices. It starts by updating an attribute into the Context Broker defined +as `command` in the [config group](#config-group-datamodel) or in the [device provision](#device-datamodel). Commands +attributes are created using `command` as attribute type. Also, you can define the protocol you want the commands to be +sent (HTTP/MQTT) with the `transport` parameter at the provisioning process. -For a given device provisioned with a `ping` command defined, any update on this attribute "ping" at the NGSI entity in the Context Broker will send a command to your device. For instance, to send the `ping` command with value `Ping request` you could use the following operation in the Context Broker API: +For a given device provisioned with a `ping` command defined, any update on this attribute "ping" at the NGSI entity in +the Context Broker will send a command to your device. For instance, to send the `ping` command with value +`Ping request` you could use the following operation in the Context Broker API: ``` PUT /v2/entities//attrs/ping?type= @@ -1213,30 +1225,36 @@ PUT /v2/entities//attrs/ping?type= It is important to note that parameter `type`, with the entity type must be included. -Context Broker API is quite flexible and allows to update an attribute in several ways. Please have a look to the [Orion API]([http://telefonicaid.github.io/fiware-orion/api/v2/stable](https://github.com/telefonicaid/fiware-orion/blob/master/doc/manuals/orion-api.md)) for details. +Context Broker API is quite flexible and allows to update an attribute in several ways. Please have a look to the +[Orion API](<[http://telefonicaid.github.io/fiware-orion/api/v2/stable](https://github.com/telefonicaid/fiware-orion/blob/master/doc/manuals/orion-api.md)>) +for details. -**Important note**: don't use operations in the NGSI API with creation semantics. Otherwise, the entity/attribute will be created locally to Context Broker and the command will not progress to the device (and you will need to delete the created entity/attribute if you want to make it to work again). Thus, the following operations *must not* be used: +**Important note**: don't use operations in the NGSI API with creation semantics. Otherwise, the entity/attribute will +be created locally to Context Broker and the command will not progress to the device (and you will need to delete the +created entity/attribute if you want to make it to work again). Thus, the following operations _must not_ be used: -* `POST /v2/entities` -* `POST /v2/entities//attrs` -* `PUT /v2/entities//attrs` -* `POST /v2/op/entites` with `actionType` `append`, `appendStrict` or `replace` +- `POST /v2/entities` +- `POST /v2/entities//attrs` +- `PUT /v2/entities//attrs` +- `POST /v2/op/entites` with `actionType` `append`, `appendStrict` or `replace` ### Command reception -Once the command is triggered, it is send to the device. Depending on transport protocol, it is going to be sent to the device in a different way. After sending the command, the IoT Agent will have updated the value of `ping_status` to `PENDING` for entity into the Context Broker. Neither -`ping_info` nor `ping` will be updated. +Once the command is triggered, it is send to the device. Depending on transport protocol, it is going to be sent to the +device in a different way. After sending the command, the IoT Agent will have updated the value of `ping_status` to +`PENDING` for entity into the Context Broker. Neither `ping_info` nor `ping` will be updated. #### HTTP devices **Push commands** -Push commands are those that are sent to the device once the IoT Agent receives the request from the Context Broker. In order to -send push commands it is needed to set the `"endpoint": "http://[DEVICE_IP]:[PORT]/"` in the device or group provision. The device -is supposed to be listening for commands at that URL in a synchronous way. Make sure the device endpoint is reachable by the IoT -Agent. Push commands are only valid for HTTP devices. For MQTT devices it is not needed to set the `endpoint` parameter. +Push commands are those that are sent to the device once the IoT Agent receives the request from the Context Broker. In +order to send push commands it is needed to set the `"endpoint": "http://[DEVICE_IP]:[PORT]/"` in the device or group +provision. The device is supposed to be listening for commands at that URL in a synchronous way. Make sure the device +endpoint is reachable by the IoT Agent. Push commands are only valid for HTTP devices. For MQTT devices it is not needed +to set the `endpoint` parameter. -Considering using the IoTA-JSON Agent, and given the previous example, the device should receive a POST request to +Considering using the IoTA-JSON Agent, and given the previous example, the device should receive a POST request to `http://[DEVICE_IP]:[PORT]` with the following payload: ``` @@ -1248,22 +1266,29 @@ Content-Type: application/json **Poll commands** -Poll commands are those that are stored in the IoT Agent waiting to be retrieved by the devices. This kind of -commands are typically used for devices that doesn't have a public IP or the IP cannot be reached because of -power or netkork constrictions. The device connects to the IoT Agent periodically to retrieve commands. In order -to configure the device as poll commands you just need to avoid the usage of `endpoint` parameter in the device provision. +Poll commands are those that are stored in the IoT Agent waiting to be retrieved by the devices. This kind of commands +are typically used for devices that doesn't have a public IP or the IP cannot be reached because of power or netkork +constrictions. The device connects to the IoT Agent periodically to retrieve commands. In order to configure the device +as poll commands you just need to avoid the usage of `endpoint` parameter in the device provision. -Once the command request is issued to the IoT agent, the command is stored waiting to be retrieved by the device. In that moment, the status of the command is `"_status": "PENDING"`. +Once the command request is issued to the IoT agent, the command is stored waiting to be retrieved by the device. In +that moment, the status of the command is `"_status": "PENDING"`. -For HTTP devices, in order to retrieve a poll command, the need to make a GET request to the IoT Agent to the path used as `resource` in the provisioned group (`/iot/json` by default in IoTA-JSON if no `resource` is used) with the following parameters: +For HTTP devices, in order to retrieve a poll command, the need to make a GET request to the IoT Agent to the path used +as `resource` in the provisioned group (`/iot/json` by default in IoTA-JSON if no `resource` is used) with the following +parameters: -**FIXME [#1524](https://github.com/telefonicaid/iotagent-node-lib/issues/1524)**: `resource` different to the default one (`/iot/json` in the case of the [IoTA-JSON](https://github.com/telefonicaid/iotagent-json)) is not working at the present moment, but it will when this issue gets solved. +**FIXME [#1524](https://github.com/telefonicaid/iotagent-node-lib/issues/1524)**: `resource` different to the default +one (`/iot/json` in the case of the [IoTA-JSON](https://github.com/telefonicaid/iotagent-json)) is not working at the +present moment, but it will when this issue gets solved. -* `k`: API key of the device. -* `i`: Device ID. -* `getCmd`: This parameter is used to indicate the IoT Agent that the device is requesting a command. It is needed to set it to `1` +- `k`: API key of the device. +- `i`: Device ID. +- `getCmd`: This parameter is used to indicate the IoT Agent that the device is requesting a command. It is needed to + set it to `1` -Taking the previous example, and considering the usage of the IoTA-JSON Agent, the device should make the following request, being the response to this request a JSON object with the command name as key and the command value as value: +Taking the previous example, and considering the usage of the IoTA-JSON Agent, the device should make the following +request, being the response to this request a JSON object with the command name as key and the command value as value: **Request:** @@ -1276,15 +1301,20 @@ Accept: application/json **Response:**: ``` -200 OK -Content-type: application/json +200 OK +Content-type: application/json -{"ping":"Ping request"} +{"ping":"Ping request"} ``` -For IoT Agents different from IoTA-JSON it is exactly the same just changing in the request the resource by the corresponding resource employed by the agent (i.e., IoTA-UL uses `/iot/d` as default resource instead of `/iot/json`) and setting the correct `` and ``. The response will be also different depending on the IoT Agent employed. +For IoT Agents different from IoTA-JSON it is exactly the same just changing in the request the resource by the +corresponding resource employed by the agent (i.e., IoTA-UL uses `/iot/d` as default resource instead of `/iot/json`) +and setting the correct `` and ``. The response will be also different depending on the IoT Agent +employed. -**FIXME [#1524](https://github.com/telefonicaid/iotagent-node-lib/issues/1524)**: `resource` different to the default one (`/iot/json` in the case of the [IoTA-JSON](https://github.com/telefonicaid/iotagent-json)) is not working at the present moment, but it will when this issue gets solved. +**FIXME [#1524](https://github.com/telefonicaid/iotagent-node-lib/issues/1524)**: `resource` different to the default +one (`/iot/json` in the case of the [IoTA-JSON](https://github.com/telefonicaid/iotagent-json)) is not working at the +present moment, but it will when this issue gets solved. **Request** @@ -1298,61 +1328,61 @@ Content-Type: application/json **Response** ``` -200 OK -Content-type: application/json +200 OK +Content-type: application/json {"ping":"Ping request"} ``` +This is also possible for IoTA-UL Agent changing in the request the resource, setting the correct ``, +``, payload and headers. -This is also possible for IoTA-UL Agent changing in the request the resource, setting the correct ``, ``, payload and headers. - -Once the command is retrieved by the device the status is updated to `"_status": "DELIVERED"`. Note that status `DELIVERED` only make sense in the case of poll commands. In the case of push command it cannot happen. - +Once the command is retrieved by the device the status is updated to `"_status": "DELIVERED"`. Note that status +`DELIVERED` only make sense in the case of poll commands. In the case of push command it cannot happen. #### MQTT devices -For MQTT devices, it is not needed to declare an endpoint (i.e. if included in the provisioning request, it is not used). The device -is supposed to be subscribed to the following MQTT topic where the IoT Agent will publish the command: +For MQTT devices, it is not needed to declare an endpoint (i.e. if included in the provisioning request, it is not +used). The device is supposed to be subscribed to the following MQTT topic where the IoT Agent will publish the command: ``` ///cmd ``` -In the case of using the IoTA-JSON Agent, the device should subscribe to the previous topic where it is going to receive a message like -the following one when a command is triggered in the Context Broker like the previous step: +In the case of using the IoTA-JSON Agent, the device should subscribe to the previous topic where it is going to receive +a message like the following one when a command is triggered in the Context Broker like the previous step: ```json -{"ping":"Ping request"} +{ "ping": "Ping request" } ``` -Please note that the device should subscribe to the broker using the disabled clean session mode (enabled using -`--disable-clean-session` option CLI parameter in `mosquitto_sub`). This option means that all of the subscriptions for the device will -be maintained after it disconnects, along with subsequent QoS 1 and QoS 2 commands that arrive. When the device reconnects, it will -receive all of the queued commands. +Please note that the device should subscribe to the broker using the disabled clean session mode (enabled using +`--disable-clean-session` option CLI parameter in `mosquitto_sub`). This option means that all of the subscriptions for +the device will be maintained after it disconnects, along with subsequent QoS 1 and QoS 2 commands that arrive. When the +device reconnects, it will receive all of the queued commands. ### Command confirmation -Once the command is completely processed by the device, it should return the result of the command to the IoT -Agent. This result will be progressed to the Context Broker where it will be stored in the `_info` -attribute. The status of the command will be stored in the `_status` attribute (`OK` if everything -goes right). +Once the command is completely processed by the device, it should return the result of the command to the IoT Agent. +This result will be progressed to the Context Broker where it will be stored in the `_info` attribute. The +status of the command will be stored in the `_status` attribute (`OK` if everything goes right). -For the IoTA-JSON, the payload of the confirmation message must be a JSON object with name of the command as key -and the result of the command as value. For other IoT Agents, the payload must follow the corresponding protocol. -For a given `ping` command, with a command result `status_ok`, the response payload should be: +For the IoTA-JSON, the payload of the confirmation message must be a JSON object with name of the command as key and the +result of the command as value. For other IoT Agents, the payload must follow the corresponding protocol. For a given +`ping` command, with a command result `status_ok`, the response payload should be: ```JSON {"ping":"status_ok"} ``` -Eventually, once the device makes the response request the IoTA would update the attributes `ping_status` to -`OK` and `ping_info` to `status_ok` for the previous example. +Eventually, once the device makes the response request the IoTA would update the attributes `ping_status` to `OK` and +`ping_info` to `status_ok` for the previous example. #### HTTP -In order confirm the command execution, the device must make a POST request to the IoT Agent with the result -of the command as payload, no matter if it is a push or a poll command. Following with the IoTAgent JSON case, the request must be made to the `/iot/json/commands`, this way: +In order confirm the command execution, the device must make a POST request to the IoT Agent with the result of the +command as payload, no matter if it is a push or a poll command. Following with the IoTAgent JSON case, the request must +be made to the `/iot/json/commands`, this way: ``` POST /iot/json/commands?k=&i= @@ -1364,14 +1394,16 @@ Accept: application/json #### MQTT -The device should publish the result of the command (`{"ping":"status_ok"}` in the previous example) to a -topic following the next pattern: +The device should publish the result of the command (`{"ping":"status_ok"}` in the previous example) to a topic +following the next pattern: ``` ////cmdexe ``` -The IoTA is subscribed to that topic, so it gets the result of the command. When this happens, the status is updated to`"_status": "OK"`. Also the result of the command delivered by the device is stored in the `_info` attribute. +The IoTA is subscribed to that topic, so it gets the result of the command. When this happens, the status is updated +to`"_status": "OK"`. Also the result of the command delivered by the device is stored in the `_info` +attribute. ## Overriding global Context Broker host @@ -2224,6 +2256,57 @@ Example: } ``` +### Metrics + +The IoT Agent Library exposes a [openmetrics-compatible](https://github.com/OpenObservability/OpenMetrics) endpoint for +telemetry collectors to gather application statistics. + +#### Retrieve metrics `GET /metrics` + +_**Response code**_ + +- `200` `OK` if successful. +- `406` `Wrong Accept Header` If accept format is not supported. +- `500` `SERVER ERROR` if there was any error not contemplated above. + +_**Response body**_ + +Returns the current value of the server stats, + +- If `Accept` header contains `application/openmetrics-text`, the response has content-type + `application/openmetrics-text; version=1.0.0; charset=utf-8` +- Else, If `Accept` header is missing or supports `text/plain` (explicitly or by `*/*`) , the response has + content-type `text/plain; version=0.0.4; charset=utf-8` (legacy format for [prometheus](https://prometheus.io)) +- In any other case, returns an error message with `406` status. + +For the kind of metrics exposed by the application, the actual payload itself is completely the same for both +content-types, and follows the openmetrics specification, e.g: + +``` +# HELP deviceCreationRequests global metric for deviceCreationRequests +# TYPE deviceCreationRequests counter +deviceCreationRequests 0 +# HELP deviceRemovalRequests global metric for deviceRemovalRequests +# TYPE deviceRemovalRequests counter +deviceRemovalRequests 0 +# HELP measureRequests global metric for measureRequests +# TYPE measureRequests counter +measureRequests 0 +# HELP raiseAlarm global metric for raiseAlarm +# TYPE raiseAlarm counter +raiseAlarm 0 +# HELP releaseAlarm global metric for releaseAlarm +# TYPE releaseAlarm counter +releaseAlarm 0 +# HELP updateEntityRequestsOk global metric for updateEntityRequestsOk +# TYPE updateEntityRequestsOk counter +updateEntityRequestsOk 2 +# HELP updateEntityRequestsError global metric for updateEntityRequestsError +# TYPE updateEntityRequestsError counter +updateEntityRequestsError 5 +# EOF +``` + [1]: https://czosel.github.io/jexl-playground/#/?context=%7B%0A%20%20%22longitude%22%3A%205%2C%0A%20%20%22latitude%22%3A%2037%2C%0A%20%20%22level%22%3A223%0A%7D&input=%7Bcoordinates%3A%20%5Blongitude%2Clatitude%5D%2C%20type%3A%20'Point'%7D&transforms=%7B%0A%7D [2]: From 2d43858b3ed484dd48f935f49efdf18dcf07433b Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Thu, 1 Aug 2024 19:40:43 +0200 Subject: [PATCH 27/33] updated documetation --- CHANGES_NEXT_RELEASE | 3 ++- doc/admin.md | 51 ---------------------------------------- doc/deprecated.md | 1 + doc/devel/development.md | 16 +++++++------ 4 files changed, 12 insertions(+), 59 deletions(-) diff --git a/CHANGES_NEXT_RELEASE b/CHANGES_NEXT_RELEASE index aa1a4378a..e2bbbaf05 100644 --- a/CHANGES_NEXT_RELEASE +++ b/CHANGES_NEXT_RELEASE @@ -1,4 +1,5 @@ -- Add: environment variables `IOTA_STATS_INTERVAL` and `IOTA_STATS_PERSISTENCE` (#1627) +- Add: openmetrics-compatible `/metrics` endpoint in nortbound API +- Deprecate: push-based stats - Fix: service header to use uppercase in case of update and delete (#1528) - Fix: Allow to send to CB batch update for multimeasures for NGSI-LD (#1623) - Add: new JEXL transformations for including into an array keys that have a certain value: valuePicker and valuePickerMulti diff --git a/doc/admin.md b/doc/admin.md index d437deaef..494fecea5 100644 --- a/doc/admin.md +++ b/doc/admin.md @@ -5,7 +5,6 @@ - [loglevel](#loglevel) - [contextBroker](#contextbroker) - [server](#server) - - [stats](#stats) - [authentication](#authentication) - [deviceRegistry](#deviceregistry) - [mongodb](#mongodb) @@ -159,54 +158,6 @@ support nulls or multi-attribute requests if they are encountered. } ``` -#### `stats` - -**NOTE**: This configuration section is **deprecated**. Users interested in collecting stats are encouraged to scrape -the `/metrics` endpoint in the [northbound API](./api.md), with an -[openmetrics-compatible](https://github.com/OpenObservability/OpenMetrics/tree/mainpro) telemetry collector such as -[prometheus](https://prometheus.io/). - -It configures the periodic collection of statistics. Use `interval` in milliseconds to set the time between stats -writings. - -```javascript -stats: { - interval: 100; -} -``` - -By default, stats are logged to the standard log at level `INFO`. You can also have your stats written to a collection -named `kpis` in the mongo backend, if you do these two things: - -- Set the `stats.persistence` flag to `true`. - -```javascript -stats: { - interval: 100; - persistence: true; -} -``` - -- Schedule periodic collection of stats to mongo by calling `statsRegistry.addTimerAction`. - -```js -statsRegistry.addTimerAction(statsRegistry.mongodbPersistence, function callback() { - // ... called after timer is enabled ... -}); -``` - -Each document in the `kpis` collection will have a `timestamp` attribute with the stat collection time, and an -additional attribute for each of the stats created by calling the `statsRegistry.add` function, e.g.: - -```json -{ - "timeinstant": new ISODate("2024-07-29T00:00:00.000Z"), - "deviceCreationRequests": 1334, - "deviceRemovalRequests": 454, - "measureRequests": 4432 -} -``` - #### `authentication` Stores the authentication data, for use in retrieving tokens for devices with a trust token (required in scenarios with @@ -529,8 +480,6 @@ overrides. | IOTA_DEFAULT_ENTITY_NAME_CONJUNCTION | `defaultEntityNameConjunction` | | IOTA_RELAX_TEMPLATE_VALIDATION | `relaxTemplateValidation` | | IOTA_EXPRESS_LIMIT | `expressLimit` | -| IOTA_STATS_INTERVAL | `stats.interval` | -| IOTA_STATS_PERSISTENCE | `stats.persistence` | Note: diff --git a/doc/deprecated.md b/doc/deprecated.md index 2a7ccf476..d3160bba6 100644 --- a/doc/deprecated.md +++ b/doc/deprecated.md @@ -25,6 +25,7 @@ A list of deprecated features and the version in which they were deprecated foll - Support to legacy expressions (finally removed in 3.2.0) - Bidirectinal pluging (finally removed in 3.4.0) - appendMode configuration (`IOTA_APPEND_MODE` env var) (finally removed in 3.4.0) +- `config.stats` section, and push-mode statistics. The use of Node.js v14 is highly recommended. diff --git a/doc/devel/development.md b/doc/devel/development.md index 1bf2c1a29..f597eef8a 100644 --- a/doc/devel/development.md +++ b/doc/devel/development.md @@ -226,18 +226,20 @@ npm run prettier:text ### Stats Registry -The library provides a mechanism for the periodic reporting of stats related to the library's work. In order to activate -the use of the periodic stats, it must be configured in the config file, as described in the -[Configuration](../admin.md#configuration) section. - -The Stats Registry holds two dictionaries, with the same set of stats. For each stat, one of the dictionaries holds the -historical global value and the other one stores the value since the last value reporting (or current value). +The library provides a mechanism for the collection of stats related to the library's work. The Stats Registry holds a +dictionary with the historical global value of each stat. The stats library currently stores only the following values: - **deviceCreationRequests**: number of Device Creation Requests that arrived to the API (no matter the result). - **deviceRemovalRequests**: number of Removal Device Requests that arrived to the API (no matter the result). - **measureRequests**: number of times the ngsiService.update() function has been invoked (no matter the result). +- **raiseAlarm**: number of times the alarmManagement.raise() function has been invoked. +- **releaseAlarm**: number of times the alarmManagement.release() function has been invoked. +- **updateEntityRequestsOk**: number of times the ngsiService.sendUpdateValue() function has been invoked + successfully. +- **updateEntityRequestsError**: number of times the ngsiService.sendUpdateValue() function has been invoked and + failed. More values will be added in the future to the library. The applications using the library can add values to the Stats Registry just by using the following function: @@ -247,7 +249,7 @@ iotagentLib.statsRegistry.add('statName', statIncrementalValue, callback); ``` The first time this function is invoked, it will add the new stat to the registry. Subsequent calls will add the value -to the specified stat both to the current and global measures. The stat will be cleared in each interval as usual. +to the specified stat. ### Alarm module From f7eedbefba51d2f1a5bff6f23a4a7e00e94ef4ce Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Fri, 2 Aug 2024 10:55:40 +0200 Subject: [PATCH 28/33] remove unneeded changes --- doc/admin.md | 3 +- doc/api.md | 254 +++++++++++++++++++------------------------- lib/commonConfig.js | 25 +---- 3 files changed, 114 insertions(+), 168 deletions(-) diff --git a/doc/admin.md b/doc/admin.md index 494fecea5..41758343b 100644 --- a/doc/admin.md +++ b/doc/admin.md @@ -415,8 +415,7 @@ IotAgents, as all Express applications that use the body-parser middleware, have size that the application will handle. This default limit for ioiotagnets are 1Mb. So, if your IotAgent receives a request with a body that exceeds this limit, the application will throw a “Error: Request entity too large”. -The 1Mb default can be changed setting the `expressLimit` configuration parameter (or equivalente `IOTA_EXPRESS_LIMIT` -environment variable). +The 1Mb default can be changed setting the `expressLimit` configuration parameter (or equivalente `IOTA_EXPRESS_LIMIT` environment variable). ### Configuration using environment variables diff --git a/doc/api.md b/doc/api.md index ea9d08f38..e200749c8 100644 --- a/doc/api.md +++ b/doc/api.md @@ -247,13 +247,10 @@ measure name, unless `explicitAttrs` is defined. Measures `id` or `type` names a ## Device autoprovision and entity creation For those agents that uses IoTA Node LIB version 3.4.0 or higher, you should consider that the entity is not created -automatically when a device is created. This means that all entities into the Context Broker are created when data -arrives from a device, no matter if the device is explicitly provisioned (via -[device provisioning API](#create-device-post-iotdevices)) or autoprovisioned. +automatically when a device is created. This means that all entities into the Context Broker are created when data +arrives from a device, no matter if the device is explicitly provisioned (via [device provisioning API](#create-device-post-iotdevices)) or autoprovisioned. -If for any reason you need the entity at CB before the first measure of the corresponding device arrives to the -IOTAgent, you can create it in advance using the Context Broker -[NGSI v2 API](https://github.com/telefonicaid/fiware-orion/blob/master/doc/manuals/orion-api.md). +If for any reason you need the entity at CB before the first measure of the corresponding device arrives to the IOTAgent, you can create it in advance using the Context Broker [NGSI v2 API](https://github.com/telefonicaid/fiware-orion/blob/master/doc/manuals/orion-api.md). ## Multientity support @@ -484,8 +481,7 @@ mappings of the provision. If `explicitAttrs` is provided both at device and con precedence. Additionally `explicitAttrs` can be used to define which measures (identified by their attribute names, not by their object_id) defined in JSON/JEXL array will be propagated to NGSI interface. -Note that when `explicitAttrs` is an array or a JEXL expression resulting in to Array, if this array is empty then -`TimeInstant` is not propaged to CB. +Note that when `explicitAttrs` is an array or a JEXL expression resulting in to Array, if this array is empty then `TimeInstant` is not propaged to CB. The different possibilities are summarized below: @@ -638,52 +634,53 @@ to incorporate new transformations from the IoT Agent configuration file in a fa Current common transformation set: -| JEXL Transformation | Equivalent JavaScript Function | -| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | -| jsonparse: (str) | `JSON.parse(str);` | -| jsonstringify: (obj) | `JSON.stringify(obj);` | -| indexOf: (val, char) | `String(val).indexOf(char);` | -| length: (val) | `String(val).length;` | -| trim: (val) | `String(val).trim();` | -| substr: (val, int1, int2) | `String(val).substr(int1, int2);` | -| addreduce: (arr) | arr.reduce((i, v) | i + v)); | -| lengtharray: (arr) | `arr.length;` | -| typeof: (val) | `typeof val;` | -| isarray: (arr) | `Array.isArray(arr);` | -| isnan: (val) | `isNaN(val);` | -| parseint: (val) | `parseInt(val);` | -| parsefloat: (val) | `parseFloat(val);` | -| toisodate: (val) | `new Date(val).toISOString();` | -| timeoffset:(isostr) | `new Date(isostr).getTimezoneOffset();` | -| tostring: (val) | `val.toString();` | -| urlencode: (val) | `encodeURI(val);` | -| urldecode: (val) | `decodeURI(val);` | -| replacestr: (str, from, to) | `str.replace(from, to);` | -| replaceregexp: (str, reg, to) | `str.replace(new RegExp(reg), to);` | -| replaceallstr: (str, from, to) | `str.replaceAll(from, to);` | -| replaceallregexp: (str, reg, to) | `str.replaceAll(new RegExp(reg,"g"), to);` | -| split: (str, ch) | `str.split(ch);` | -| joinarrtostr: (arr, ch) | `arr.join(ch);` | -| concatarr: (arr, arr2) | `arr.concat(arr2);` | -| mapper: (val, values, choices) | choices[values.findIndex((target) | target == val)]); | -| thmapper: (val, values, choices) | choices[values.reduce((acc,curr,i,arr) | (acc==0)||acc?acc:val<=curr?acc=i:acc=null,null)]; | -| bitwisemask: (i,mask,op,shf) | (op==="&"?parseInt(i)&mask: op==="|"?parseInt(i)|mask: op==="^"?parseInt(i)^mask:i)>>shf; | -| slice: (arr, init, end) | `arr.slice(init,end);` | -| addset: (arr, x) | { return Array.from((new Set(arr)).add(x)) } | -| removeset: (arr, x) | { let s = new Set(arr); s.delete(x); return Array.from(s) } | -| touppercase: (val) | `String(val).toUpperCase()` | -| tolowercase: (val) | `String(val).toLowerCase()` | -| round: (val) | `Math.round(val)` | -| floor: (val) | `Math.floor(val)` | -| ceil: (val) | `Math.ceil(val)` | -| tofixed: (val, decimals) | `Number.parseFloat(val).toFixed(decimals)` | -| gettime: (d) | `new Date(d).getTime()` | -| toisostring: (d) | `new Date(d).toISOString()` | -| localestring: (d, timezone, options) | `new Date(d).toLocaleString(timezone, options)` | -| now: () | `Date.now()` | -| hextostring: (val) | `new TextDecoder().decode(new Uint8Array(val.match(/.{1,2}/g).map(byte => parseInt(byte, 16))))` | -| valuePicker: (val,pick) | valuePicker: (val,pick) => Object.entries(val).filter(([_, v]) => v === pick).map(([k, _]) => k) | -| valuePickerMulti: (val,pick) | valuePickerMulti: (val,pick) => Object.entries(val).filter(([_, v]) => pick.includes(v)).map(([k, _]) => k) | +| JEXL Transformation | Equivalent JavaScript Function | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | +| jsonparse: (str) | `JSON.parse(str);` | +| jsonstringify: (obj) | `JSON.stringify(obj);` | +| indexOf: (val, char) | `String(val).indexOf(char);` | +| length: (val) | `String(val).length;` | +| trim: (val) | `String(val).trim();` | +| substr: (val, int1, int2) | `String(val).substr(int1, int2);` | +| addreduce: (arr) | arr.reduce((i, v) | i + v)); | +| lengtharray: (arr) | `arr.length;` | +| typeof: (val) | `typeof val;` | +| isarray: (arr) | `Array.isArray(arr);` | +| isnan: (val) | `isNaN(val);` | +| parseint: (val) | `parseInt(val);` | +| parsefloat: (val) | `parseFloat(val);` | +| toisodate: (val) | `new Date(val).toISOString();` | +| timeoffset:(isostr) | `new Date(isostr).getTimezoneOffset();` | +| tostring: (val) | `val.toString();` | +| urlencode: (val) | `encodeURI(val);` | +| urldecode: (val) | `decodeURI(val);` | +| replacestr: (str, from, to) | `str.replace(from, to);` | +| replaceregexp: (str, reg, to) | `str.replace(new RegExp(reg), to);` | +| replaceallstr: (str, from, to) | `str.replaceAll(from, to);` | +| replaceallregexp: (str, reg, to) | `str.replaceAll(new RegExp(reg,"g"), to);` | +| split: (str, ch) | `str.split(ch);` | +| joinarrtostr: (arr, ch) | `arr.join(ch);` | +| concatarr: (arr, arr2) | `arr.concat(arr2);` | +| mapper: (val, values, choices) | choices[values.findIndex((target) | target == val)]); | +| thmapper: (val, values, choices) | choices[values.reduce((acc,curr,i,arr) | (acc==0)||acc?acc:val<=curr?acc=i:acc=null,null)]; | +| bitwisemask: (i,mask,op,shf) | (op==="&"?parseInt(i)&mask: op==="|"?parseInt(i)|mask: op==="^"?parseInt(i)^mask:i)>>shf; | +| slice: (arr, init, end) | `arr.slice(init,end);` | +| addset: (arr, x) | { return Array.from((new Set(arr)).add(x)) } | +| removeset: (arr, x) | { let s = new Set(arr); s.delete(x); return Array.from(s) } | +| touppercase: (val) | `String(val).toUpperCase()` | +| tolowercase: (val) | `String(val).toLowerCase()` | +| round: (val) | `Math.round(val)` | +| floor: (val) | `Math.floor(val)` | +| ceil: (val) | `Math.ceil(val)` | +| tofixed: (val, decimals) | `Number.parseFloat(val).toFixed(decimals)` | +| gettime: (d) | `new Date(d).getTime()` | +| toisostring: (d) | `new Date(d).toISOString()` | +| localestring: (d, timezone, options) | `new Date(d).toLocaleString(timezone, options)` | +| now: () | `Date.now()` | +| hextostring: (val) | `new TextDecoder().decode(new Uint8Array(val.match(/.{1,2}/g).map(byte => parseInt(byte, 16))))` | +| valuePicker: (val,pick) | valuePicker: (val,pick) => Object.entries(val).filter(([_, v]) => v === pick).map(([k, _]) => k) | +| valuePickerMulti: (val,pick) | valuePickerMulti: (val,pick) => Object.entries(val).filter(([_, v]) => pick.includes(v)).map(([k, _]) => k) | + You have available this [JEXL interactive playground][99] with all the transformations already loaded, in which you can test all the functions described above. @@ -1198,20 +1195,13 @@ In this case a batch update (`POST /v2/op/update`) to CB will be generated with ## Command execution -This section reviews the end-to-end process to trigger and receive commands into devices. The URL paths and messages -format is based on the [IoT Agent JSON](https://github.com/telefonicaid/iotagent-json). It may differ in the case of -using any other IoT Agent. In that case, please refer to the specific IoTA documentation. +This section reviews the end-to-end process to trigger and receive commands into devices. The URL paths and messages format is based on the [IoT Agent JSON](https://github.com/telefonicaid/iotagent-json). It may differ in the case of using any other IoT Agent. In that case, please refer to the specific IoTA documentation. ### Triggering commands -This starts the process of sending data to devices. It starts by updating an attribute into the Context Broker defined -as `command` in the [config group](#config-group-datamodel) or in the [device provision](#device-datamodel). Commands -attributes are created using `command` as attribute type. Also, you can define the protocol you want the commands to be -sent (HTTP/MQTT) with the `transport` parameter at the provisioning process. +This starts the process of sending data to devices. It starts by updating an attribute into the Context Broker defined as `command` in the [config group](#config-group-datamodel) or in the [device provision](#device-datamodel). Commands attributes are created using `command` as attribute type. Also, you can define the protocol you want the commands to be sent (HTTP/MQTT) with the `transport` parameter at the provisioning process. -For a given device provisioned with a `ping` command defined, any update on this attribute "ping" at the NGSI entity in -the Context Broker will send a command to your device. For instance, to send the `ping` command with value -`Ping request` you could use the following operation in the Context Broker API: +For a given device provisioned with a `ping` command defined, any update on this attribute "ping" at the NGSI entity in the Context Broker will send a command to your device. For instance, to send the `ping` command with value `Ping request` you could use the following operation in the Context Broker API: ``` PUT /v2/entities//attrs/ping?type= @@ -1225,36 +1215,30 @@ PUT /v2/entities//attrs/ping?type= It is important to note that parameter `type`, with the entity type must be included. -Context Broker API is quite flexible and allows to update an attribute in several ways. Please have a look to the -[Orion API](<[http://telefonicaid.github.io/fiware-orion/api/v2/stable](https://github.com/telefonicaid/fiware-orion/blob/master/doc/manuals/orion-api.md)>) -for details. +Context Broker API is quite flexible and allows to update an attribute in several ways. Please have a look to the [Orion API]([http://telefonicaid.github.io/fiware-orion/api/v2/stable](https://github.com/telefonicaid/fiware-orion/blob/master/doc/manuals/orion-api.md)) for details. -**Important note**: don't use operations in the NGSI API with creation semantics. Otherwise, the entity/attribute will -be created locally to Context Broker and the command will not progress to the device (and you will need to delete the -created entity/attribute if you want to make it to work again). Thus, the following operations _must not_ be used: +**Important note**: don't use operations in the NGSI API with creation semantics. Otherwise, the entity/attribute will be created locally to Context Broker and the command will not progress to the device (and you will need to delete the created entity/attribute if you want to make it to work again). Thus, the following operations *must not* be used: -- `POST /v2/entities` -- `POST /v2/entities//attrs` -- `PUT /v2/entities//attrs` -- `POST /v2/op/entites` with `actionType` `append`, `appendStrict` or `replace` +* `POST /v2/entities` +* `POST /v2/entities//attrs` +* `PUT /v2/entities//attrs` +* `POST /v2/op/entites` with `actionType` `append`, `appendStrict` or `replace` ### Command reception -Once the command is triggered, it is send to the device. Depending on transport protocol, it is going to be sent to the -device in a different way. After sending the command, the IoT Agent will have updated the value of `ping_status` to -`PENDING` for entity into the Context Broker. Neither `ping_info` nor `ping` will be updated. +Once the command is triggered, it is send to the device. Depending on transport protocol, it is going to be sent to the device in a different way. After sending the command, the IoT Agent will have updated the value of `ping_status` to `PENDING` for entity into the Context Broker. Neither +`ping_info` nor `ping` will be updated. #### HTTP devices **Push commands** -Push commands are those that are sent to the device once the IoT Agent receives the request from the Context Broker. In -order to send push commands it is needed to set the `"endpoint": "http://[DEVICE_IP]:[PORT]/"` in the device or group -provision. The device is supposed to be listening for commands at that URL in a synchronous way. Make sure the device -endpoint is reachable by the IoT Agent. Push commands are only valid for HTTP devices. For MQTT devices it is not needed -to set the `endpoint` parameter. +Push commands are those that are sent to the device once the IoT Agent receives the request from the Context Broker. In order to +send push commands it is needed to set the `"endpoint": "http://[DEVICE_IP]:[PORT]/"` in the device or group provision. The device +is supposed to be listening for commands at that URL in a synchronous way. Make sure the device endpoint is reachable by the IoT +Agent. Push commands are only valid for HTTP devices. For MQTT devices it is not needed to set the `endpoint` parameter. -Considering using the IoTA-JSON Agent, and given the previous example, the device should receive a POST request to +Considering using the IoTA-JSON Agent, and given the previous example, the device should receive a POST request to `http://[DEVICE_IP]:[PORT]` with the following payload: ``` @@ -1266,29 +1250,22 @@ Content-Type: application/json **Poll commands** -Poll commands are those that are stored in the IoT Agent waiting to be retrieved by the devices. This kind of commands -are typically used for devices that doesn't have a public IP or the IP cannot be reached because of power or netkork -constrictions. The device connects to the IoT Agent periodically to retrieve commands. In order to configure the device -as poll commands you just need to avoid the usage of `endpoint` parameter in the device provision. +Poll commands are those that are stored in the IoT Agent waiting to be retrieved by the devices. This kind of +commands are typically used for devices that doesn't have a public IP or the IP cannot be reached because of +power or netkork constrictions. The device connects to the IoT Agent periodically to retrieve commands. In order +to configure the device as poll commands you just need to avoid the usage of `endpoint` parameter in the device provision. -Once the command request is issued to the IoT agent, the command is stored waiting to be retrieved by the device. In -that moment, the status of the command is `"_status": "PENDING"`. +Once the command request is issued to the IoT agent, the command is stored waiting to be retrieved by the device. In that moment, the status of the command is `"_status": "PENDING"`. -For HTTP devices, in order to retrieve a poll command, the need to make a GET request to the IoT Agent to the path used -as `resource` in the provisioned group (`/iot/json` by default in IoTA-JSON if no `resource` is used) with the following -parameters: +For HTTP devices, in order to retrieve a poll command, the need to make a GET request to the IoT Agent to the path used as `resource` in the provisioned group (`/iot/json` by default in IoTA-JSON if no `resource` is used) with the following parameters: -**FIXME [#1524](https://github.com/telefonicaid/iotagent-node-lib/issues/1524)**: `resource` different to the default -one (`/iot/json` in the case of the [IoTA-JSON](https://github.com/telefonicaid/iotagent-json)) is not working at the -present moment, but it will when this issue gets solved. +**FIXME [#1524](https://github.com/telefonicaid/iotagent-node-lib/issues/1524)**: `resource` different to the default one (`/iot/json` in the case of the [IoTA-JSON](https://github.com/telefonicaid/iotagent-json)) is not working at the present moment, but it will when this issue gets solved. -- `k`: API key of the device. -- `i`: Device ID. -- `getCmd`: This parameter is used to indicate the IoT Agent that the device is requesting a command. It is needed to - set it to `1` +* `k`: API key of the device. +* `i`: Device ID. +* `getCmd`: This parameter is used to indicate the IoT Agent that the device is requesting a command. It is needed to set it to `1` -Taking the previous example, and considering the usage of the IoTA-JSON Agent, the device should make the following -request, being the response to this request a JSON object with the command name as key and the command value as value: +Taking the previous example, and considering the usage of the IoTA-JSON Agent, the device should make the following request, being the response to this request a JSON object with the command name as key and the command value as value: **Request:** @@ -1301,20 +1278,15 @@ Accept: application/json **Response:**: ``` -200 OK -Content-type: application/json +200 OK +Content-type: application/json -{"ping":"Ping request"} +{"ping":"Ping request"} ``` -For IoT Agents different from IoTA-JSON it is exactly the same just changing in the request the resource by the -corresponding resource employed by the agent (i.e., IoTA-UL uses `/iot/d` as default resource instead of `/iot/json`) -and setting the correct `` and ``. The response will be also different depending on the IoT Agent -employed. +For IoT Agents different from IoTA-JSON it is exactly the same just changing in the request the resource by the corresponding resource employed by the agent (i.e., IoTA-UL uses `/iot/d` as default resource instead of `/iot/json`) and setting the correct `` and ``. The response will be also different depending on the IoT Agent employed. -**FIXME [#1524](https://github.com/telefonicaid/iotagent-node-lib/issues/1524)**: `resource` different to the default -one (`/iot/json` in the case of the [IoTA-JSON](https://github.com/telefonicaid/iotagent-json)) is not working at the -present moment, but it will when this issue gets solved. +**FIXME [#1524](https://github.com/telefonicaid/iotagent-node-lib/issues/1524)**: `resource` different to the default one (`/iot/json` in the case of the [IoTA-JSON](https://github.com/telefonicaid/iotagent-json)) is not working at the present moment, but it will when this issue gets solved. **Request** @@ -1328,61 +1300,61 @@ Content-Type: application/json **Response** ``` -200 OK -Content-type: application/json +200 OK +Content-type: application/json {"ping":"Ping request"} ``` -This is also possible for IoTA-UL Agent changing in the request the resource, setting the correct ``, -``, payload and headers. -Once the command is retrieved by the device the status is updated to `"_status": "DELIVERED"`. Note that status -`DELIVERED` only make sense in the case of poll commands. In the case of push command it cannot happen. +This is also possible for IoTA-UL Agent changing in the request the resource, setting the correct ``, ``, payload and headers. + +Once the command is retrieved by the device the status is updated to `"_status": "DELIVERED"`. Note that status `DELIVERED` only make sense in the case of poll commands. In the case of push command it cannot happen. + #### MQTT devices -For MQTT devices, it is not needed to declare an endpoint (i.e. if included in the provisioning request, it is not -used). The device is supposed to be subscribed to the following MQTT topic where the IoT Agent will publish the command: +For MQTT devices, it is not needed to declare an endpoint (i.e. if included in the provisioning request, it is not used). The device +is supposed to be subscribed to the following MQTT topic where the IoT Agent will publish the command: ``` ///cmd ``` -In the case of using the IoTA-JSON Agent, the device should subscribe to the previous topic where it is going to receive -a message like the following one when a command is triggered in the Context Broker like the previous step: +In the case of using the IoTA-JSON Agent, the device should subscribe to the previous topic where it is going to receive a message like +the following one when a command is triggered in the Context Broker like the previous step: ```json -{ "ping": "Ping request" } +{"ping":"Ping request"} ``` -Please note that the device should subscribe to the broker using the disabled clean session mode (enabled using -`--disable-clean-session` option CLI parameter in `mosquitto_sub`). This option means that all of the subscriptions for -the device will be maintained after it disconnects, along with subsequent QoS 1 and QoS 2 commands that arrive. When the -device reconnects, it will receive all of the queued commands. +Please note that the device should subscribe to the broker using the disabled clean session mode (enabled using +`--disable-clean-session` option CLI parameter in `mosquitto_sub`). This option means that all of the subscriptions for the device will +be maintained after it disconnects, along with subsequent QoS 1 and QoS 2 commands that arrive. When the device reconnects, it will +receive all of the queued commands. ### Command confirmation -Once the command is completely processed by the device, it should return the result of the command to the IoT Agent. -This result will be progressed to the Context Broker where it will be stored in the `_info` attribute. The -status of the command will be stored in the `_status` attribute (`OK` if everything goes right). +Once the command is completely processed by the device, it should return the result of the command to the IoT +Agent. This result will be progressed to the Context Broker where it will be stored in the `_info` +attribute. The status of the command will be stored in the `_status` attribute (`OK` if everything +goes right). -For the IoTA-JSON, the payload of the confirmation message must be a JSON object with name of the command as key and the -result of the command as value. For other IoT Agents, the payload must follow the corresponding protocol. For a given -`ping` command, with a command result `status_ok`, the response payload should be: +For the IoTA-JSON, the payload of the confirmation message must be a JSON object with name of the command as key +and the result of the command as value. For other IoT Agents, the payload must follow the corresponding protocol. +For a given `ping` command, with a command result `status_ok`, the response payload should be: ```JSON {"ping":"status_ok"} ``` -Eventually, once the device makes the response request the IoTA would update the attributes `ping_status` to `OK` and -`ping_info` to `status_ok` for the previous example. +Eventually, once the device makes the response request the IoTA would update the attributes `ping_status` to +`OK` and `ping_info` to `status_ok` for the previous example. #### HTTP -In order confirm the command execution, the device must make a POST request to the IoT Agent with the result of the -command as payload, no matter if it is a push or a poll command. Following with the IoTAgent JSON case, the request must -be made to the `/iot/json/commands`, this way: +In order confirm the command execution, the device must make a POST request to the IoT Agent with the result +of the command as payload, no matter if it is a push or a poll command. Following with the IoTAgent JSON case, the request must be made to the `/iot/json/commands`, this way: ``` POST /iot/json/commands?k=&i= @@ -1394,16 +1366,14 @@ Accept: application/json #### MQTT -The device should publish the result of the command (`{"ping":"status_ok"}` in the previous example) to a topic -following the next pattern: +The device should publish the result of the command (`{"ping":"status_ok"}` in the previous example) to a +topic following the next pattern: ``` ////cmdexe ``` -The IoTA is subscribed to that topic, so it gets the result of the command. When this happens, the status is updated -to`"_status": "OK"`. Also the result of the command delivered by the device is stored in the `_info` -attribute. +The IoTA is subscribed to that topic, so it gets the result of the command. When this happens, the status is updated to`"_status": "OK"`. Also the result of the command delivered by the device is stored in the `_info` attribute. ## Overriding global Context Broker host diff --git a/lib/commonConfig.js b/lib/commonConfig.js index 70c285dbc..d16908615 100644 --- a/lib/commonConfig.js +++ b/lib/commonConfig.js @@ -157,9 +157,7 @@ function processEnvironmentVariables() { 'IOTA_FALLBACK_PATH', 'IOTA_LD_SUPPORT_NULL', 'IOTA_LD_SUPPORT_DATASET_ID', - 'IOTA_EXPRESS_LIMIT', - `IOTA_STATS_INTERVAL`, - 'IOTA_STATS_PERSISTENCE' + 'IOTA_EXPRESS_LIMIT' ]; const iotamVariables = [ 'IOTA_IOTAM_URL', @@ -191,10 +189,6 @@ function processEnvironmentVariables() { 'IOTA_MONGO_USER', 'IOTA_MONGO_PASSWORD' ]; - const statsVariables = [ - 'IOTA_STATS_INTERVAL', - 'IOTA_STATS_PERSISTENCE', - ]; // Substitute Docker Secret Variables where set. protectedVariables.forEach((key) => { @@ -475,28 +469,11 @@ function processEnvironmentVariables() { ? config.defaultEntityNameConjunction : ':'; } - if (process.env.IOTA_EXPRESS_LIMIT) { config.expressLimit = process.env.IOTA_EXPRESS_LIMIT; } else { config.expressLimit = config.expressLimit ? config.expressLimit : '1mb'; } - - // Stats Configuration - if (anyIsSet(statsVariables)) { - config.stats = config.stats || {}; - } - - if (process.env.IOTA_STATS_INTERVAL) { - const interval = parseInt(process.env.IOTA_STATS_INTERVAL); - if (!isNaN(interval) && interval > 0) { - config.stats.interval = interval; - } - } - - if (process.env.IOTA_STATS_PERSISTENCE) { - config.stats.persistence = process.env.IOTA_STATS_PERSISTENCE === 'true' - } } function setConfig(newConfig) { From 1b44fe3160df88db5a5554bf2f76272c72218eed Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Fri, 2 Aug 2024 10:59:29 +0200 Subject: [PATCH 29/33] reallocate metrics code --- lib/services/northBound/northboundServer.js | 88 +-------------------- lib/services/stats/statsRegistry.js | 80 +++++++++++++++++++ 2 files changed, 81 insertions(+), 87 deletions(-) diff --git a/lib/services/northBound/northboundServer.js b/lib/services/northBound/northboundServer.js index f0a162640..c5f2d8b7d 100644 --- a/lib/services/northBound/northboundServer.js +++ b/lib/services/northBound/northboundServer.js @@ -40,92 +40,6 @@ const context = { }; const bodyParser = require('body-parser'); -/** - * Predefined http handler that returns current openmetrics data - */ -/* eslint-disable-next-line no-unused-vars */ -function openmetricsHandler(req, res) { - // Content-Type: - // - For openmetrics collectors, it MUST BE 'application/openmetrics-text; version=1.0.0; charset=utf-8'. See: - // https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#overall-structure - // - For prometheus compatible collectors, it SHOULD BE 'text/plain; version=0.0.4; charset=utf-8'. See: - // https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md - let contentType = 'application/openmetrics-text; version=1.0.0; charset=utf-8'; - let version = null; - let charset = null; - // To identify openmetrics collectors, we need to parse the `Accept` header. - // An openmetrics-based collectors SHOULD use an `Accept` header such as: - // `Accept: application/openmetrics-text; version=1.0.0; charset=utf-8' - // See: https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/18913 - // - // WORKAROUND: express version 4 does not parse properly the openmetrics Accept header, - // it won't match the regular expressions supported by `express.accepts`. - // So we must parse these key-value pairs ourselves, and remove them from the - // header before handling it to `requests.accept`. - if (req.headers.accept) { - const parts = req.headers.accept.split(';'); - let unparsed = []; - for (let i = 0; i < parts.length; i++) { - const current = parts[i]; - const trimmed = current.trim(); - if (trimmed.startsWith('version=')) { - version = trimmed.substring(8); - } else if (trimmed.startsWith('charset=')) { - charset = trimmed.substring(8); - } else { - unparsed.push(current); - } - } - if (unparsed.length < parts.length) { - delete req.headers['accept']; - req.headers['accept'] = unparsed.join(';'); - } - } - // charset MUST BE utf-8 - if (charset && charset != 'utf-8') { - logger.error(context, 'Unsupported charset: %s', charset); - res.status(406).send('Unsupported charset'); - return; - } - switch (req.accepts(['text/plain', 'application/openmetrics-text'])) { - case 'application/openmetrics-text': - // Version MUST BE 1.0.0 for openmetrics - if (version && version != '1.0.0') { - logger.error(context, 'Unsupported openmetrics version: %s', version); - res.status(406).send('Unsupported openmetrics version'); - return; - } - break; - case 'text/plain': - contentType = 'text/plain; version=0.0.4; charset=utf-8'; - break; - default: - logger.error(context, 'Unsupported accept header: %s', req.headers.accept); - res.status(406).send('Unsupported accept header'); - return; - } - statsRegistry.getAllGlobal(function (err, globalStats) { - if (err) { - logger.error(context, 'Error retrieving global stats: %s', err); - res.status(500).send('Error retrieving global stats'); - return; - } - const metrics = new Array(); - for (const key in globalStats) { - if (globalStats.hasOwnProperty(key)) { - metrics.push('# HELP ' + key + ' global metric for ' + key); - metrics.push('# TYPE ' + key + ' counter'); - metrics.push(key + ' ' + globalStats[key]); - } - } - // Expositions MUST END WITH '#EOF' - // See https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md - metrics.push('# EOF'); - res.set('Content-Type', contentType); - res.status(200).send(metrics.join('\n')); - }); -} - function start(config, callback) { let baseRoot = '/'; @@ -170,7 +84,7 @@ function start(config, callback) { northboundServer.router.get('/version', middlewares.retrieveVersion); northboundServer.router.put('/admin/log', middlewares.changeLogLevel); northboundServer.router.get('/admin/log', middlewares.getLogLevel); - northboundServer.router.get('/metrics', openmetricsHandler); + northboundServer.router.get('/metrics', statsRegistry.openmetricsHandler); northboundServer.app.use(baseRoot, northboundServer.router); contextServer.loadContextRoutes(northboundServer.router); diff --git a/lib/services/stats/statsRegistry.js b/lib/services/stats/statsRegistry.js index cda551780..d4434ecda 100644 --- a/lib/services/stats/statsRegistry.js +++ b/lib/services/stats/statsRegistry.js @@ -183,6 +183,85 @@ function mongodbPersistence(currentValues, globalValues, callback) { dbService.db.collection('kpis').insertOne(statStamp, callback); } +/** + * Predefined http handler that returns current openmetrics data + */ +/* eslint-disable-next-line no-unused-vars */ +function openmetricsHandler(req, res) { + // Content-Type: + // - For openmetrics collectors, it MUST BE 'application/openmetrics-text; version=1.0.0; charset=utf-8'. See: + // https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#overall-structure + // - For prometheus compatible collectors, it SHOULD BE 'text/plain; version=0.0.4; charset=utf-8'. See: + // https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md + let contentType = 'application/openmetrics-text; version=1.0.0; charset=utf-8'; + let version = null; + let charset = null; + // To identify openmetrics collectors, we need to parse the `Accept` header. + // An openmetrics-based collectors SHOULD use an `Accept` header such as: + // `Accept: application/openmetrics-text; version=1.0.0; charset=utf-8' + // See: https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/18913 + // + // WORKAROUND: express version 4 does not parse properly the openmetrics Accept header, + // it won't match the regular expressions supported by `express.accepts`. + // So we must parse these key-value pairs ourselves, and remove them from the + // header before handling it to `requests.accept`. + if (req.headers.accept) { + const parts = req.headers.accept.split(';'); + let unparsed = []; + for (let i = 0; i < parts.length; i++) { + const current = parts[i]; + const trimmed = current.trim(); + if (trimmed.startsWith('version=')) { + version = trimmed.substring(8); + } else if (trimmed.startsWith('charset=')) { + charset = trimmed.substring(8); + } else { + unparsed.push(current); + } + } + if (unparsed.length < parts.length) { + delete req.headers['accept']; + req.headers['accept'] = unparsed.join(';'); + } + } + // charset MUST BE utf-8 + if (charset && charset != 'utf-8') { + logger.error(context, 'Unsupported charset: %s', charset); + res.status(406).send('Unsupported charset'); + return; + } + switch (req.accepts(['text/plain', 'application/openmetrics-text'])) { + case 'application/openmetrics-text': + // Version MUST BE 1.0.0 for openmetrics + if (version && version != '1.0.0') { + logger.error(context, 'Unsupported openmetrics version: %s', version); + res.status(406).send('Unsupported openmetrics version'); + return; + } + break; + case 'text/plain': + contentType = 'text/plain; version=0.0.4; charset=utf-8'; + break; + default: + logger.error(context, 'Unsupported accept header: %s', req.headers.accept); + res.status(406).send('Unsupported accept header'); + return; + } + const metrics = new Array(); + for (const key in globalStats) { + if (globalStats.hasOwnProperty(key)) { + metrics.push('# HELP ' + key + ' global metric for ' + key); + metrics.push('# TYPE ' + key + ' counter'); + metrics.push(key + ' ' + globalStats[key]); + } + } + // Expositions MUST END WITH '#EOF' + // See https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md + metrics.push('# EOF'); + res.set('Content-Type', contentType); + res.status(200).send(metrics.join('\n')); +} + /** * Wraps a callback with stats, incrementing the given counters * depending on the parameters passed to the callback: @@ -217,3 +296,4 @@ exports.addTimerAction = addTimerAction; exports.logStats = logStats; exports.mongodbPersistence = mongodbPersistence; exports.withStats = withStats; +exports.openmetricsHandler = openmetricsHandler; From 14cc8d60a061261d8c65c3de3423f190fe9ec78c Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:28:14 +0200 Subject: [PATCH 30/33] improve docs --- lib/services/stats/statsRegistry.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/services/stats/statsRegistry.js b/lib/services/stats/statsRegistry.js index d4434ecda..a594ae8bb 100644 --- a/lib/services/stats/statsRegistry.js +++ b/lib/services/stats/statsRegistry.js @@ -271,8 +271,9 @@ function openmetricsHandler(req, res) { * * @param {String} okCounter Name of the counter to increment on success. * @param {String} errCounter Name of the counter to increment on error. - * @param {Function} callback Callback to wrap. Its first argument is expected - * to be an error object + * @param {Function} callback Callback to wrap. It must be a function that can + * expect any number of parameters, but the first one must + * be an indication of the error occured, if any. */ function withStats(okCounter, errCounter, callback) { function accounting(...args) { From 683121f9a073b22b32f444c32612129bd6cfa71a Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:42:35 +0200 Subject: [PATCH 31/33] Update CHANGES_NEXT_RELEASE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fermín Galán Márquez --- CHANGES_NEXT_RELEASE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES_NEXT_RELEASE b/CHANGES_NEXT_RELEASE index e2bbbaf05..f42d6d674 100644 --- a/CHANGES_NEXT_RELEASE +++ b/CHANGES_NEXT_RELEASE @@ -1,4 +1,4 @@ -- Add: openmetrics-compatible `/metrics` endpoint in nortbound API +- Add: openmetrics-compatible `/metrics` endpoint in nortbound API (#1627) - Deprecate: push-based stats - Fix: service header to use uppercase in case of update and delete (#1528) - Fix: Allow to send to CB batch update for multimeasures for NGSI-LD (#1623) From b770380adfd70bfed1abcf906ed351778a01b7cd Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Fri, 2 Aug 2024 14:58:03 +0200 Subject: [PATCH 32/33] Remove push-based stats --- CHANGES_NEXT_RELEASE | 2 +- doc/deprecated.md | 1 + lib/fiware-iotagent-lib.js | 13 +- lib/model/dbConn.js | 5 +- lib/services/stats/statsRegistry.js | 132 +----------------- .../general/statistics-persistence_test.js | 121 ---------------- test/unit/general/statistics-service_test.js | 75 +--------- .../mongodb/mongodb-connectionoptions-test.js | 5 +- 8 files changed, 16 insertions(+), 338 deletions(-) delete mode 100644 test/unit/general/statistics-persistence_test.js diff --git a/CHANGES_NEXT_RELEASE b/CHANGES_NEXT_RELEASE index f42d6d674..1f4898923 100644 --- a/CHANGES_NEXT_RELEASE +++ b/CHANGES_NEXT_RELEASE @@ -1,5 +1,5 @@ - Add: openmetrics-compatible `/metrics` endpoint in nortbound API (#1627) -- Deprecate: push-based stats +- Remove: push-based stats - Fix: service header to use uppercase in case of update and delete (#1528) - Fix: Allow to send to CB batch update for multimeasures for NGSI-LD (#1623) - Add: new JEXL transformations for including into an array keys that have a certain value: valuePicker and valuePickerMulti diff --git a/doc/deprecated.md b/doc/deprecated.md index d3160bba6..754bf9914 100644 --- a/doc/deprecated.md +++ b/doc/deprecated.md @@ -58,3 +58,4 @@ The following table provides information about the last iotagent-node-lib versio | Support to Legacy Expressions | 3.1.0 | April 25th, 2023 | | bidirectional plugin | 3.3.0 | August 24th, 2023 | | appendMode configuration (`IOTA_APPEND_MODE` env var) | 3.3.0 | August 24th, 2023 | +| push-mode stats | 4.5.0 | June 11th, 2024 | diff --git a/lib/fiware-iotagent-lib.js b/lib/fiware-iotagent-lib.js index 8858c5b39..1c057e819 100644 --- a/lib/fiware-iotagent-lib.js +++ b/lib/fiware-iotagent-lib.js @@ -45,10 +45,9 @@ const context = { op: 'IoTAgentNGSI.Global' }; +/* eslint-disable-next-line no-unused-vars */ function activateStatLogs(newConfig, callback) { - // global stats are always initialized, since they - // will be collected regardless of `stats.interval` - logActions = [ + async.series([ apply(statsRegistry.globalLoad, { deviceCreationRequests: 0, deviceRemovalRequests: 0, @@ -58,13 +57,7 @@ function activateStatLogs(newConfig, callback) { updateEntityRequestsOk: 0, updateEntityRequestsError: 0 }) - ]; - // However, local stats will only be periodically - // logged / cleared if `stats.interval` is set - if (newConfig.stats && newConfig.stats.interval) { - logActions.push(apply(statsRegistry.addTimerAction, statsRegistry.logStats)); - } - async.series(logActions, callback); + ], callback); } /** diff --git a/lib/model/dbConn.js b/lib/model/dbConn.js index 310318b4c..b58198909 100644 --- a/lib/model/dbConn.js +++ b/lib/model/dbConn.js @@ -190,10 +190,7 @@ function configureDb(callback) { /*jshint camelcase:false, validthis:true */ const currentConfig = config.getConfig(); - if ( - (currentConfig.deviceRegistry && currentConfig.deviceRegistry.type === 'mongodb') || - (currentConfig.stats && currentConfig.stats.persistence === true) - ) { + if (currentConfig.deviceRegistry && currentConfig.deviceRegistry.type === 'mongodb') { if (!currentConfig.mongodb || !currentConfig.mongodb.host) { logger.fatal(context, 'MONGODB-003: No host found for MongoDB driver.'); callback(new errors.BadConfiguration('No host found for MongoDB driver')); diff --git a/lib/services/stats/statsRegistry.js b/lib/services/stats/statsRegistry.js index a594ae8bb..9bf1923d6 100644 --- a/lib/services/stats/statsRegistry.js +++ b/lib/services/stats/statsRegistry.js @@ -23,16 +23,9 @@ /* eslint-disable no-prototype-builtins */ -const async = require('async'); const _ = require('underscore'); -const apply = async.apply; const logger = require('logops'); -const config = require('../../commonConfig'); -const dbService = require('../../model/dbConn'); let globalStats = {}; -let currentStats = {}; -let timerActions = []; -let timerHandler; const statsContext = { op: 'IoTAgentNGSI.TimedStats' }; @@ -45,30 +38,14 @@ const statsContext = { * @param {Number} value Value to be added to the total. */ function add(key, value, callback) { - if (currentStats[key]) { - currentStats[key] += value; - } else { - currentStats[key] = value; - } - if (globalStats[key]) { globalStats[key] += value; } else { globalStats[key] = value; } - callback(null); } -/** - * Get the current value of a particular stat. - * - * @param {String} key Name of the stat to retrive. - */ -function getCurrent(key, callback) { - callback(null, currentStats[key]); -} - /** * Get the global value of the selected attribute. * @@ -85,104 +62,16 @@ function getAllGlobal(callback) { callback(null, globalStats); } -/** - * Get all the current stats currently stored in the repository. - */ -function getAllCurrent(callback) { - callback(null, currentStats); -} - /** * Loads the values passed as parameters into the global statistics repository. * * @param {Object} values Key-value map with the values to be load. */ function globalLoad(values, callback) { - globalStats = values; - currentStats = {}; - - for (const i in values) { - if (values.hasOwnProperty(i)) { - currentStats[i] = 0; - } - } - + globalStats = _.clone(values); callback(null); } -/** - * Reset each of the current stats to value zero. - */ -function resetCurrent(callback) { - for (const i in currentStats) { - if (currentStats.hasOwnProperty(i)) { - currentStats[i] = 0; - } - } - - callback(); -} - -/** - * Executes all the stored timer actions when a timer click is received. - */ -function tickHandler() { - process.nextTick(apply(async.series, timerActions)); -} - -/** - * Adds a new timer action to the timerActions Array, activating the timer if it was not previously activated. - * - * @param {Function} handler Action to be executed. Should take two statistics objects and a callback. - */ -function addTimerAction(handler, callback) { - if (!timerHandler && config.getConfig().stats.interval) { - timerHandler = setInterval(tickHandler, config.getConfig().stats.interval); - } - - timerActions.push(apply(handler, currentStats, globalStats)); - callback(); -} - -/** - * Clear the actions array and stop the timers. - */ -function clearTimers(callback) { - if (timerHandler) { - clearInterval(timerHandler); - timerHandler = undefined; - } - - timerActions = []; - callback(); -} - -/** - * Predefined stats action that logs the stats to the standard log. - * - * @param {Object} currentValues Current stat values. - * @param {Object} globalValues Global stat values. - */ -function logStats(currentValues, globalValues, callback) { - logger.info(statsContext, 'Global stat values:\n%s\n', JSON.stringify(globalValues, null, 4)); - logger.info(statsContext, 'Current stat values:\n%s\n', JSON.stringify(currentValues, null, 4)); - - resetCurrent(callback); -} - -/** - * Predefined action that persists the current value of the stats in the MongoDb instance. - * - * @param {Object} currentValues Current stat values. - * @param {Object} globalValues Global stat values. - */ -function mongodbPersistence(currentValues, globalValues, callback) { - const statStamp = _.clone(globalValues); - - statStamp.timestamp = new Date().toISOString(); - dbService.db.collection('kpis').insertOne(statStamp, callback); -} - /** * Predefined http handler that returns current openmetrics data */ @@ -225,16 +114,16 @@ function openmetricsHandler(req, res) { } } // charset MUST BE utf-8 - if (charset && charset != 'utf-8') { - logger.error(context, 'Unsupported charset: %s', charset); + if (charset && charset !== 'utf-8') { + logger.error(statsContext, 'Unsupported charset: %s', charset); res.status(406).send('Unsupported charset'); return; } switch (req.accepts(['text/plain', 'application/openmetrics-text'])) { case 'application/openmetrics-text': // Version MUST BE 1.0.0 for openmetrics - if (version && version != '1.0.0') { - logger.error(context, 'Unsupported openmetrics version: %s', version); + if (version && version !== '1.0.0') { + logger.error(statsContext, 'Unsupported openmetrics version: %s', version); res.status(406).send('Unsupported openmetrics version'); return; } @@ -243,11 +132,11 @@ function openmetricsHandler(req, res) { contentType = 'text/plain; version=0.0.4; charset=utf-8'; break; default: - logger.error(context, 'Unsupported accept header: %s', req.headers.accept); + logger.error(statsContext, 'Unsupported accept header: %s', req.headers.accept); res.status(406).send('Unsupported accept header'); return; } - const metrics = new Array(); + const metrics = []; for (const key in globalStats) { if (globalStats.hasOwnProperty(key)) { metrics.push('# HELP ' + key + ' global metric for ' + key); @@ -286,15 +175,8 @@ function withStats(okCounter, errCounter, callback) { } exports.add = add; -exports.getCurrent = getCurrent; exports.getGlobal = getGlobal; exports.getAllGlobal = getAllGlobal; -exports.getAllCurrent = getAllCurrent; exports.globalLoad = globalLoad; -exports.resetCurrent = resetCurrent; -exports.clearTimers = clearTimers; -exports.addTimerAction = addTimerAction; -exports.logStats = logStats; -exports.mongodbPersistence = mongodbPersistence; exports.withStats = withStats; exports.openmetricsHandler = openmetricsHandler; diff --git a/test/unit/general/statistics-persistence_test.js b/test/unit/general/statistics-persistence_test.js deleted file mode 100644 index 9e47b8d97..000000000 --- a/test/unit/general/statistics-persistence_test.js +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2014 Telefonica Investigación y Desarrollo, S.A.U - * - * This file is part of fiware-iotagent-lib - * - * fiware-iotagent-lib is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the License, - * or (at your option) any later version. - * - * fiware-iotagent-lib is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public - * License along with fiware-iotagent-lib. - * If not, see http://www.gnu.org/licenses/. - * - * For those usages not covered by the GNU Affero General Public License - * please contact with::[contacto@tid.es] - */ - -/* eslint-disable no-unused-vars */ - -const statsService = require('../../../lib/services/stats/statsRegistry'); -const commonConfig = require('../../../lib/commonConfig'); -const iotAgentLib = require('../../../lib/fiware-iotagent-lib'); -const should = require('should'); -const async = require('async'); -const mongoUtils = require('../mongodb/mongoDBUtils'); -const iotAgentConfig = { - logLevel: 'FATAL', - contextBroker: { - host: '192.168.1.1', - port: '1026' - }, - server: { - port: 4041, - host: 'localhost', - baseRoot: '/' - }, - stats: { - interval: 50, - persistence: true - }, - mongodb: { - host: 'localhost', - port: '27017', - db: 'iotagent' - }, - types: {}, - service: 'smartgondor', - subservice: 'gardens', - providerUrl: 'http://smartgondor.com', - deviceRegistrationDuration: 'P1M' -}; -let iotAgentDb; -let oldConfig; - -describe('Statistics persistence service', function () { - function insertDummy(n, callback) { - iotAgentDb.collection('tests').insertOne({ test: 'test' }, function () { - callback(); - }); - } - - beforeEach(function (done) { - oldConfig = commonConfig.getConfig(); - - iotAgentLib.activate(iotAgentConfig, function (error) { - statsService.globalLoad({}, function () { - iotAgentDb = require('../../../lib/model/dbConn').db; - - async.times(10, insertDummy, function () { - done(); - }); - }); - }); - }); - - afterEach(function (done) { - iotAgentLib.deactivate(function (error) { - commonConfig.setConfig(oldConfig); - statsService.globalLoad({}, function () { - mongoUtils.cleanDbs(done); - }); - }); - }); - - describe('When a periodic persitence action is set', function () { - beforeEach(function (done) { - statsService.globalLoad( - { - stat1: 10 - }, - function () { - statsService.add('stat1', 5, done); - } - ); - }); - - it('should store all the records in the database', function (done) { - statsService.addTimerAction(statsService.mongodbPersistence, function () { - setTimeout(function () { - statsService.clearTimers(function () { - iotAgentDb - .collection('kpis') - .find({}) - .toArray(function (err, docs) { - should.not.exist(err); - should.exist(docs); - docs.length.should.be.above(2); - done(); - }); - }); - }, 200); - }); - }); - }); -}); diff --git a/test/unit/general/statistics-service_test.js b/test/unit/general/statistics-service_test.js index 8d594a6da..cf3ae351c 100644 --- a/test/unit/general/statistics-service_test.js +++ b/test/unit/general/statistics-service_test.js @@ -35,9 +35,6 @@ const iotAgentConfig = { host: 'localhost', baseRoot: '/' }, - stats: { - interval: 100 - }, types: {}, service: 'smartgondor', subservice: 'gardens', @@ -51,9 +48,7 @@ describe('Statistics service', function () { oldConfig = commonConfig.getConfig(); commonConfig.setConfig(iotAgentConfig); - statsService.globalLoad({}, function () { - statsService.clearTimers(done); - }); + statsService.globalLoad({}, done); }); afterEach(function (done) { @@ -74,16 +69,6 @@ describe('Statistics service', function () { ); }); - it('should appear the modified value in the getCurrent() statistics', function (done) { - statsService.add(statName, statValue, function () { - statsService.getCurrent(statName, function (error, value) { - should.not.exist(error); - should.exist(value); - value.should.equal(statValue); - done(); - }); - }); - }); it('should add the value to the global values', function (done) { statsService.add(statName, statValue, function () { statsService.getGlobal(statName, function (error, value) { @@ -119,62 +104,4 @@ describe('Statistics service', function () { }); }); }); - describe('When the current statistics are reset', function () { - beforeEach(function (done) { - statsService.add('statA', 42, function () { - statsService.add('statB', 52, done); - }); - }); - - it('should return a value of zero for any of the individual statistics', function (done) { - statsService.resetCurrent(function (error) { - should.not.exist(error); - - statsService.getAllCurrent(function (error, data) { - should.exist(data); - should.exist(data.statA); - should.exist(data.statB); - data.statA.should.equal(0); - data.statB.should.equal(0); - done(); - }); - }); - }); - }); - describe('When a new periodic stats action is set', function () { - let valueCurrent = 0; - let valueGlobal = 0; - let times = 0; - - beforeEach(function (done) { - statsService.globalLoad( - { - stat1: 10 - }, - function () { - statsService.add('stat1', 5, done); - } - ); - }); - - function mockedAction(current, global, callback) { - valueCurrent = current.stat1; - valueGlobal = global.stat1; - times++; - callback(); - } - - it('should be triggered with the periodicity stated in the config.stats.interval parameter', function (done) { - statsService.addTimerAction(mockedAction, function () { - setTimeout(function () { - statsService.clearTimers(function () { - valueCurrent.should.equal(5); - valueGlobal.should.equal(15); - times.should.equal(4); - done(); - }); - }, 480); - }); - }); - }); }); diff --git a/test/unit/mongodb/mongodb-connectionoptions-test.js b/test/unit/mongodb/mongodb-connectionoptions-test.js index 9fd056a29..439a8c9a9 100644 --- a/test/unit/mongodb/mongodb-connectionoptions-test.js +++ b/test/unit/mongodb/mongodb-connectionoptions-test.js @@ -41,9 +41,8 @@ const iotAgentConfig = { host: 'localhost', baseRoot: '/' }, - stats: { - interval: 50, - persistence: true + deviceRegistry: { + type: 'mongodb' }, types: {}, service: 'smartgondor', From 2c9772df6f50af61c20353314e62a6e8e4141bdb Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Mon, 5 Aug 2024 09:47:49 +0200 Subject: [PATCH 33/33] Update CHANGES_NEXT_RELEASE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fermín Galán Márquez --- CHANGES_NEXT_RELEASE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES_NEXT_RELEASE b/CHANGES_NEXT_RELEASE index 1f4898923..254eaab41 100644 --- a/CHANGES_NEXT_RELEASE +++ b/CHANGES_NEXT_RELEASE @@ -1,5 +1,5 @@ - Add: openmetrics-compatible `/metrics` endpoint in nortbound API (#1627) -- Remove: push-based stats +- Remove: push-based stats (including stats section in config file) - Fix: service header to use uppercase in case of update and delete (#1528) - Fix: Allow to send to CB batch update for multimeasures for NGSI-LD (#1623) - Add: new JEXL transformations for including into an array keys that have a certain value: valuePicker and valuePickerMulti