openvibers/openvibe.host Commits
Metrics collection (Track O): scripts/prometheus-config.js renders a Prometheus scrape config from the host inventory (one loopback job per service port, declared extra processes such as the Tools satellites, node-exporter; metrics:false opts out); inventory example declares Tools' seven satellites and Sites' opt-out
Changes
@@@ -171,6 +171,7 @@],"envFile": "/etc/openvibe/tools.env","env": { "required": ["OV_NETWORK_URL", "OV_OAUTH_CLIENT_ID", "OV_OAUTH_CLIENT_SECRET", "MEDIA_URL"] },++ "metrics": { "extra": [{ "name": "maps", "port": 4010 }, { "name": "food", "port": 4011 }, { "name": "img", "port": 4012 }, { "name": "yt", "port": 4013 }, { "name": "audio", "port": 4014 }, { "name": "text", "port": 4015 }, { "name": "docs", "port": 4016 }] },"port": 4001,"ready": { "url": "http://127.0.0.1:4001/api/ready", "headers": { "Host": "openvibe.tools" }, "timeoutSeconds": 60 },"noRestartPaths": ["**/*.md"],@@@ -308,6 +309,7 @@}},"sites": {++ "metrics": false,"repo": "/opt/openvibe.sites","units": [],"install": { "command": ["npm", "ci", "--omit=dev", "--no-audit", "--no-fund"], "always": true },nnew file mode 100644@@@ -0,0 +1,60 @@++#!/usr/bin/env node++'use strict';++/**++ * Prometheus scrape configuration from the host inventory (roadmap Track O): one job per service++ * with a port, scraping its loopback /metrics, plus the extra targets a service declares:++ *++ * "tools": { …, "metrics": { "extra": [{ "name": "img", "port": 4012 }, …] } } more processes++ * "sites": { …, "metrics": false } nothing to scrape++ *++ * Every target is 127.0.0.1 (the services answer /metrics only to direct loopback callers), and++ * node-exporter (host CPU, memory, disk) is scraped on 127.0.0.1:9100.++ *++ * node scripts/prometheus-config.js [--inventory /etc/openvibe/host.json] [--interval 30s] > prometheus.yml++ */++const fs = require('fs');++++function arg(name, fallback) {++ const i = process.argv.indexOf(`--${name}`);++ return i > 0 && process.argv[i + 1] ? process.argv[i + 1] : fallback;++}++++function render(inventory, { interval = '30s' } = {}) {++ const jobs = [];++ const services = inventory.services || {};++ for (const id of Object.keys(services).sort()) {++ const s = services[id];++ if (s.metrics === false) continue;++ const m = s.metrics && typeof s.metrics === 'object' ? s.metrics : {};++ const path = m.path || '/metrics';++ const targets = [];++ if (Number.isInteger(s.port)) targets.push({ port: s.port, process: id });++ for (const x of Array.isArray(m.extra) ? m.extra : []) {++ if (Number.isInteger(x.port) && /^[a-z][a-z0-9-]*$/.test(String(x.name || ''))) targets.push({ port: x.port, process: `${id}-${x.name}` });++ }++ if (!targets.length) continue;++ jobs.push({ id, path, targets });++ }++ const lines = [++ '# Generated by OpenVibe.Host scripts/prometheus-config.js from the host inventory. Do not edit by hand.',++ 'global:',++ ` scrape_interval: ${interval}`,++ ' scrape_timeout: 10s',++ 'scrape_configs:',++ ' - job_name: node',++ ' static_configs:',++ " - targets: ['127.0.0.1:9100']",++ ];++ for (const j of jobs) {++ lines.push(` - job_name: ${j.id}`, ` metrics_path: ${j.path}`, ' static_configs:');++ for (const t of j.targets) lines.push(` - targets: ['127.0.0.1:${t.port}']`, ' labels:', ` service: ${j.id}`, ` process: ${t.process}`);++ }++ return `${lines.join('\n')}\n`;++}++++if (require.main === module) {++ const inv = JSON.parse(fs.readFileSync(arg('inventory', process.env.OVHOST_INVENTORY || '/etc/openvibe/host.json'), 'utf8'));++ process.stdout.write(render(inv, { interval: arg('interval', '30s') }));++}++++module.exports = { render };nnew file mode 100644@@@ -0,0 +1,20 @@++'use strict';++// scripts/prometheus-config.js: one loopback job per service port, declared extra processes, opt-out.++const assert = require('assert');++const { render } = require('../scripts/prometheus-config');++++const out = render({ services: {++ live: { port: 3000 },++ tools: { port: 4001, metrics: { extra: [{ name: 'img', port: 4012 }, { name: 'bad name', port: 1 }] } },++ sites: { metrics: false },++ games: { port: 8000 },++ noport: {},++} }, { interval: '15s' });++assert.ok(out.includes('scrape_interval: 15s'));++assert.ok(out.includes("- targets: ['127.0.0.1:9100']"), 'node-exporter');++assert.ok(out.includes(" - job_name: live\n metrics_path: /metrics\n static_configs:\n - targets: ['127.0.0.1:3000']\n labels:\n service: live\n process: live\n"));++assert.ok(out.includes(" - targets: ['127.0.0.1:4012']\n labels:\n service: tools\n process: tools-img\n"), 'declared extra process');++assert.ok(!out.includes('bad name') && !out.includes(':1\''), 'malformed extras are skipped');++assert.ok(!out.includes('job_name: sites') && !out.includes('job_name: noport'), 'opted out or nothing to scrape');++assert.ok(!/0\.0\.0\.0|localhost/.test(out), 'loopback addresses only');++console.log('prometheus config: all checks passed');
@@ -171,6 +171,7 @@
@@ -171,6 +171,7 @@
],
],
"envFile": "/etc/openvibe/tools.env",
"envFile": "/etc/openvibe/tools.env",
"env": { "required": ["OV_NETWORK_URL", "OV_OAUTH_CLIENT_ID", "OV_OAUTH_CLIENT_SECRET", "MEDIA_URL"] },
"env": { "required": ["OV_NETWORK_URL", "OV_OAUTH_CLIENT_ID", "OV_OAUTH_CLIENT_SECRET", "MEDIA_URL"] },
+ "metrics": { "extra": [{ "name": "maps", "port": 4010 }, { "name": "food", "port": 4011 }, { "name": "img", "port": 4012 }, { "name": "yt", "port": 4013 }, { "name": "audio", "port": 4014 }, { "name": "text", "port": 4015 }, { "name": "docs", "port": 4016 }] },
"port": 4001,
"port": 4001,
"ready": { "url": "http://127.0.0.1:4001/api/ready", "headers": { "Host": "openvibe.tools" }, "timeoutSeconds": 60 },
"ready": { "url": "http://127.0.0.1:4001/api/ready", "headers": { "Host": "openvibe.tools" }, "timeoutSeconds": 60 },
"noRestartPaths": ["**/*.md"],
"noRestartPaths": ["**/*.md"],
@@ -308,6 +309,7 @@
@@ -308,6 +309,7 @@
}
}
},
},
"sites": {
"sites": {
+ "metrics": false,
"repo": "/opt/openvibe.sites",
"repo": "/opt/openvibe.sites",
"units": [],
"units": [],
"install": { "command": ["npm", "ci", "--omit=dev", "--no-audit", "--no-fund"], "always": true },
"install": { "command": ["npm", "ci", "--omit=dev", "--no-audit", "--no-fund"], "always": true },
new file mode 100644
new file mode 100644
@@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
+#!/usr/bin/env node
+'use strict';
+/**
+ * Prometheus scrape configuration from the host inventory (roadmap Track O): one job per service
+ * with a port, scraping its loopback /metrics, plus the extra targets a service declares:
+ *
+ * "tools": { …, "metrics": { "extra": [{ "name": "img", "port": 4012 }, …] } } more processes
+ * "sites": { …, "metrics": false } nothing to scrape
+ *
+ * Every target is 127.0.0.1 (the services answer /metrics only to direct loopback callers), and
+ * node-exporter (host CPU, memory, disk) is scraped on 127.0.0.1:9100.
+ *
+ * node scripts/prometheus-config.js [--inventory /etc/openvibe/host.json] [--interval 30s] > prometheus.yml
+ */
+const fs = require('fs');
+
+function arg(name, fallback) {
+ const i = process.argv.indexOf(`--${name}`);
+ return i > 0 && process.argv[i + 1] ? process.argv[i + 1] : fallback;
+}
+
+function render(inventory, { interval = '30s' } = {}) {
+ const jobs = [];
+ const services = inventory.services || {};
+ for (const id of Object.keys(services).sort()) {
+ const s = services[id];
+ if (s.metrics === false) continue;
+ const m = s.metrics && typeof s.metrics === 'object' ? s.metrics : {};
+ const path = m.path || '/metrics';
+ const targets = [];
+ if (Number.isInteger(s.port)) targets.push({ port: s.port, process: id });
+ for (const x of Array.isArray(m.extra) ? m.extra : []) {
+ if (Number.isInteger(x.port) && /^[a-z][a-z0-9-]*$/.test(String(x.name || ''))) targets.push({ port: x.port, process: `${id}-${x.name}` });
+ }
+ if (!targets.length) continue;
+ jobs.push({ id, path, targets });
+ }
+ const lines = [
+ '# Generated by OpenVibe.Host scripts/prometheus-config.js from the host inventory. Do not edit by hand.',
+ 'global:',
+ ` scrape_interval: ${interval}`,
+ ' scrape_timeout: 10s',
+ 'scrape_configs:',
+ ' - job_name: node',
+ ' static_configs:',
+ " - targets: ['127.0.0.1:9100']",
+ ];
+ for (const j of jobs) {
+ lines.push(` - job_name: ${j.id}`, ` metrics_path: ${j.path}`, ' static_configs:');
+ for (const t of j.targets) lines.push(` - targets: ['127.0.0.1:${t.port}']`, ' labels:', ` service: ${j.id}`, ` process: ${t.process}`);
+ }
+ return `${lines.join('\n')}\n`;
+}
+
+if (require.main === module) {
+ const inv = JSON.parse(fs.readFileSync(arg('inventory', process.env.OVHOST_INVENTORY || '/etc/openvibe/host.json'), 'utf8'));
+ process.stdout.write(render(inv, { interval: arg('interval', '30s') }));
+}
+
+module.exports = { render };
new file mode 100644
new file mode 100644
@@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
+'use strict';
+// scripts/prometheus-config.js: one loopback job per service port, declared extra processes, opt-out.
+const assert = require('assert');
+const { render } = require('../scripts/prometheus-config');
+
+const out = render({ services: {
+ live: { port: 3000 },
+ tools: { port: 4001, metrics: { extra: [{ name: 'img', port: 4012 }, { name: 'bad name', port: 1 }] } },
+ sites: { metrics: false },
+ games: { port: 8000 },
+ noport: {},
+} }, { interval: '15s' });
+assert.ok(out.includes('scrape_interval: 15s'));
+assert.ok(out.includes("- targets: ['127.0.0.1:9100']"), 'node-exporter');
+assert.ok(out.includes(" - job_name: live\n metrics_path: /metrics\n static_configs:\n - targets: ['127.0.0.1:3000']\n labels:\n service: live\n process: live\n"));
+assert.ok(out.includes(" - targets: ['127.0.0.1:4012']\n labels:\n service: tools\n process: tools-img\n"), 'declared extra process');
+assert.ok(!out.includes('bad name') && !out.includes(':1\''), 'malformed extras are skipped');
+assert.ok(!out.includes('job_name: sites') && !out.includes('job_name: noport'), 'opted out or nothing to scrape');
+assert.ok(!/0\.0\.0\.0|localhost/.test(out), 'loopback addresses only');
+console.log('prometheus config: all checks passed');