openvibers/openvibe.live Commits
openvibe-shared v1.5.1 (Track R release manifest), openvibe-contracts v0.33.0, openvibe-sdk v0.5.0
Changes
@@@ -53,6 +53,9 @@ restart. A rollback resets the checkout but cannot restore previous `node_module- `GET /metrics` (Prometheus text: requests by route template, latency, in-flight, process,`release_info`, live streams, WebSocket connections per server, outbox) answers only`curl http://127.0.0.1:3000/metrics` on the host; through nginx it is a 404.++ `release_client_updates_total{outcome,reason}` counts what open tabs did with a new release++ (applied, reloaded, deferred, failed); they report it to `POST /release-metrics`, which++ `/release.json` names in `metrics_url` (openvibe-shared `release.mount`).- `--rollback` selects the previous release with **its own** `node_modules`.- The last 5 releases are kept.- Content-hashed assets from the previous release are still served under their old hashes, so a page@@@ -25,9 +25,9 @@"multer": "^1.4.5-lts.1","node-media-server": "^2.7.1","onnxruntime-node": "^1.20.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.32.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1","sanitize-html": "^2.17.7","sdp-transform": "^3.0.0","sharp": "^0.33.0",@@@ -2814,9 +2814,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.32.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.32.0",-- "integrity": "sha512-ntN58SIG03YDxbhsiIzOM5GRH19jRMHZUYWI3xU2VwovExagF5DdwApLVfezsu84UTJ+V2Jfn+LWDc+Oz8ab+g==",++ "version": "0.33.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",++ "integrity": "sha512-7q8NUhcPxgb8lxrVdC3pFbiPdjzliJM1mMP/1G5a2PmOr2tJVAXwYa8+a8YgCJvYeMSeMHWijLkIEiZ9IhllOg==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -2830,18 +2830,18 @@}},"node_modules/openvibe-sdk": {-- "version": "0.4.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",-- "integrity": "sha512-Hyz55YOMIs0xwCQ1NXsN0sUYhlbKXAX3uWY2fPCdEEL18FXUYVhK8wlG/5JMMypFHnjDiSd9VHzBNG12hWsmkQ==",++ "version": "0.5.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",++ "integrity": "sha512-QVJkR5iazQX0ZwIrDp2A88ZeYYBSXEmSzhpgURPUSnzezIM3jz0HBhVKowyhkvg2aBO61XNf77eb4zJGkMjZiw==","license": "MIT","engines": {"node": ">=20"}},"node_modules/openvibe-shared": {-- "version": "1.4.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",-- "integrity": "sha512-H7jAsUklS1p57d9y6ozME++HmsX18iPYZ/U3P6bLoL1E2oSl6DbSPzLsVUkA7RYoYmAtcI3AwJ4hTNlAr66Vdw==",++ "version": "1.5.1",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",++ "integrity": "sha512-1XWRFwN8br3iiy2Z8fmnXQMgR76VlfMpHjP1xr5uKLCJmtvtNSWO1BgA1kQmVtyBY7g+1n6YFIjahaD4qWbD/Q==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0"@@@ -43,9 +43,9 @@"multer": "^1.4.5-lts.1","node-media-server": "^2.7.1","onnxruntime-node": "^1.20.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.32.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1","sanitize-html": "^2.17.7","sdp-transform": "^3.0.0","sharp": "^0.33.0",@@@ -772,8 +772,9 @@ app.get('/api/health', (req, res) => {*/let _bootComplete = false;// The shared navbar loads /shared/release-watch.js, which polls this and prompts open tabs after a--// deploy; it never reloads a tab that is watching, broadcasting or typing.--app.get('/release.json', release.handler);++// deploy; it never reloads a tab that is watching, broadcasting or typing. GET /release.json, and++// POST /release-metrics: open tabs' update reports into /metrics (release_client_updates_total).++release.mount(app, { registry: metricsRegistry });// Required: boot finished and the database answers. Optional (a failure is "degraded", still 200):// the WebRTC SFU, OpenVibe.Media and the Network signing key — Live serves channels, chat and RTMP@@@ -221,6 +221,7 @@ const safe = { DB_PATH, DATA_DIR, HOST: '127.0.0.1', PORT: '13000' };const rel = await request('GET', '/release.json');assert.strictEqual(rel.status, 200);assert.strictEqual(rel.json.release, head);++ assert.deepStrictEqual(require('openvibe-contracts').validate('registry.release-manifest@1', rel.json).errors, []);const streams = await request('GET', '/api/streams');assert.strictEqual(streams.status, 200);assert.deepStrictEqual(streams.json.streams.map((s) => [s.title, s.user_id, s.username]), [['Restored stream', 1, 'drillstar']]);@@@ -269,7 +270,7 @@ const safe = { DB_PATH, DATA_DIR, HOST: '127.0.0.1', PORT: '13000' };['POST', '/api/streams', { title: 'x' }], ['PUT', '/api/streams/1', { title: 'y' }], ['PATCH', '/api/streams/1', { title: 'y' }],['DELETE', '/api/streams/1', null], ['POST', '/api/chat/send', { message: 'hi' }], ['POST', '/api/auth/logout', null],['POST', '/internal/media-events', { type: 'media.vod.ready' }], ['POST', '/internal/media-webhook', {}], ['POST', '/whip/1', null],-- ['POST', '/banned/continue', null], ['POST', '/api/csp-report', {}],++ ['POST', '/banned/continue', null], ['POST', '/api/csp-report', {}], ['POST', '/release-metrics', { counts: { applied: { style: 1 } } }],];for (const [m, p, body] of writes) {const r = await request(m, p, { body });@@@ -6,6 +6,8 @@* - release_info and the domain gauges (live streams, WebSocket connections, outbox)* - /api/ready: boot + db required (503 when either fails); SFU, Media and the Network key optional* (200 "degraded", naming what is missing)++ * - /release.json validates as registry.release-manifest@1 and names /release-metrics, whose beacons++ * (text/plain or JSON, behind the global JSON parser) land in release_client_updates_total* A temp SQLite database stands in for Live's; no Media, Network or mediasoup is needed.** Run: node test/observability.test.js@@@ -41,6 +43,10 @@ const tmpDb = path.join(os.tmpdir(), `openvibe-observability-test-${process.pid}const app = express();const { registry } = observability.mountMetrics(app, { release });++ // As server/index.js: the global JSON parser, then GET /release.json + POST /release-metrics.++ app.use(express.json({ limit: '1mb' }));++ const liveRelease = require('openvibe-shared/release').createRelease({ service: 'live', root: path.join(__dirname, '..') });++ liveRelease.mount(app, { registry });const readiness = observability.createLiveReadiness({release, bootComplete: () => booted, dbQuery, sfuReady: () => sfu, mediaUrl, networkKey: () => key,});@@@ -154,6 +160,27 @@ const tmpDb = path.join(os.tmpdir(), `openvibe-observability-test-${process.pid}outbox = { enabled: false };assert.ok(!(await get('/metrics')).body.includes('live_events_outbox{'), 'a disabled outbox reports nothing, not zero');++ // ── release manifest (ADR-016, Track R) ──++ // registry.release-manifest 1.1.0 under openvibe-contracts >= 0.31: it names where open tabs report.++ const rel = await get('/release.json');++ assert.strictEqual(rel.status, 200);++ const manifest = JSON.parse(rel.body);++ assert.strictEqual(manifest.service, 'live');++ assert.deepStrictEqual(require('openvibe-contracts').validate('registry.release-manifest@1', manifest).errors, []);++ assert.strictEqual(manifest.metrics_url, '/release-metrics');++ // release-watch beacons its counts as text/plain (sendBeacon); express.json() must not swallow them.++ const beacon = (payload, type = 'text/plain;charset=UTF-8') => new Promise((resolve, reject) => {++ const req = http.request(base + '/release-metrics', { method: 'POST', headers: { 'Content-Type': type } }, (res) => { res.resume(); res.on('end', () => resolve(res.statusCode)); });++ req.on('error', reject); req.end(payload);++ });++ assert.strictEqual(await beacon(JSON.stringify({ counts: { applied: { style: 2 }, reloaded: { user: 1 } } })), 204);++ assert.strictEqual(await beacon(JSON.stringify({ counts: { deferred: { typing: 1 } } }), 'application/json'), 204);++ assert.strictEqual(await beacon('not json'), 400);++ const mt = (await get('/metrics')).body;++ assert.ok(mt.includes('release_client_updates_total{outcome="applied",reason="style"} 2\n'), mt);++ assert.ok(mt.includes('release_client_updates_total{outcome="reloaded",reason="user"} 1\n'));++ assert.ok(mt.includes('release_client_updates_total{outcome="deferred",reason="typing"} 1\n'));++// Through nginx (X-Forwarded-For on a loopback connection) /metrics does not exist.assert.strictEqual((await get('/metrics', { 'X-Forwarded-For': '203.0.113.7' })).status, 404);assert.strictEqual((await get('/metrics', { 'X-Real-IP': '203.0.113.7' })).status, 404);@@@ -163,6 +190,8 @@ const tmpDb = path.join(os.tmpdir(), `openvibe-observability-test-${process.pid}const mountAt = src.indexOf('observability.mountMetrics(app');assert.ok(mountAt > 0 && mountAt < src.indexOf('app.use(helmet('), 'metrics middleware is mounted before the rest of the middleware');assert.ok(/app\.get\('\/api\/ready', readiness\.handler\)/.test(src));++ assert.ok(/release\.mount\(app, \{ registry: metricsRegistry \}\)/.test(src), '/release.json and /release-metrics are mounted with the metrics registry');++ assert.ok(src.indexOf('release.mount(app') > src.indexOf('app.use(express.json('), 'after the JSON parser, as tested above');server.close(); media.close(); sqlite.close();for (const f of [tmpDb, `${tmpDb}-wal`, `${tmpDb}-shm`]) { try { fs.unlinkSync(f); } catch { /* */ } }
@@ -53,6 +53,9 @@ restart. A rollback resets the checkout but cannot restore previous `node_module
@@ -53,6 +53,9 @@ restart. A rollback resets the checkout but cannot restore previous `node_module
- `GET /metrics` (Prometheus text: requests by route template, latency, in-flight, process,
- `GET /metrics` (Prometheus text: requests by route template, latency, in-flight, process,
`release_info`, live streams, WebSocket connections per server, outbox) answers only
`release_info`, live streams, WebSocket connections per server, outbox) answers only
`curl http://127.0.0.1:3000/metrics` on the host; through nginx it is a 404.
`curl http://127.0.0.1:3000/metrics` on the host; through nginx it is a 404.
+ `release_client_updates_total{outcome,reason}` counts what open tabs did with a new release
+ (applied, reloaded, deferred, failed); they report it to `POST /release-metrics`, which
+ `/release.json` names in `metrics_url` (openvibe-shared `release.mount`).
- `--rollback` selects the previous release with **its own** `node_modules`.
- `--rollback` selects the previous release with **its own** `node_modules`.
- The last 5 releases are kept.
- The last 5 releases are kept.
- Content-hashed assets from the previous release are still served under their old hashes, so a page
- Content-hashed assets from the previous release are still served under their old hashes, so a page
@@ -25,9 +25,9 @@
@@ -25,9 +25,9 @@
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
"node-media-server": "^2.7.1",
"node-media-server": "^2.7.1",
"onnxruntime-node": "^1.20.0",
"onnxruntime-node": "^1.20.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.32.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
"sanitize-html": "^2.17.7",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
"sdp-transform": "^3.0.0",
"sharp": "^0.33.0",
"sharp": "^0.33.0",
@@ -2814,9 +2814,9 @@
@@ -2814,9 +2814,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.32.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.32.0",
- "integrity": "sha512-ntN58SIG03YDxbhsiIzOM5GRH19jRMHZUYWI3xU2VwovExagF5DdwApLVfezsu84UTJ+V2Jfn+LWDc+Oz8ab+g==",
+ "version": "0.33.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
+ "integrity": "sha512-7q8NUhcPxgb8lxrVdC3pFbiPdjzliJM1mMP/1G5a2PmOr2tJVAXwYa8+a8YgCJvYeMSeMHWijLkIEiZ9IhllOg==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -2830,18 +2830,18 @@
@@ -2830,18 +2830,18 @@
}
}
},
},
"node_modules/openvibe-sdk": {
"node_modules/openvibe-sdk": {
- "version": "0.4.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
- "integrity": "sha512-Hyz55YOMIs0xwCQ1NXsN0sUYhlbKXAX3uWY2fPCdEEL18FXUYVhK8wlG/5JMMypFHnjDiSd9VHzBNG12hWsmkQ==",
+ "version": "0.5.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",
+ "integrity": "sha512-QVJkR5iazQX0ZwIrDp2A88ZeYYBSXEmSzhpgURPUSnzezIM3jz0HBhVKowyhkvg2aBO61XNf77eb4zJGkMjZiw==",
"license": "MIT",
"license": "MIT",
"engines": {
"engines": {
"node": ">=20"
"node": ">=20"
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "1.4.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
- "integrity": "sha512-H7jAsUklS1p57d9y6ozME++HmsX18iPYZ/U3P6bLoL1E2oSl6DbSPzLsVUkA7RYoYmAtcI3AwJ4hTNlAr66Vdw==",
+ "version": "1.5.1",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
+ "integrity": "sha512-1XWRFwN8br3iiy2Z8fmnXQMgR76VlfMpHjP1xr5uKLCJmtvtNSWO1BgA1kQmVtyBY7g+1n6YFIjahaD4qWbD/Q==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -43,9 +43,9 @@
@@ -43,9 +43,9 @@
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
"node-media-server": "^2.7.1",
"node-media-server": "^2.7.1",
"onnxruntime-node": "^1.20.0",
"onnxruntime-node": "^1.20.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.32.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
"sanitize-html": "^2.17.7",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
"sdp-transform": "^3.0.0",
"sharp": "^0.33.0",
"sharp": "^0.33.0",
@@ -772,8 +772,9 @@ app.get('/api/health', (req, res) => {
@@ -772,8 +772,9 @@ app.get('/api/health', (req, res) => {
*/
*/
let _bootComplete = false;
let _bootComplete = false;
// The shared navbar loads /shared/release-watch.js, which polls this and prompts open tabs after a
// The shared navbar loads /shared/release-watch.js, which polls this and prompts open tabs after a
-// deploy; it never reloads a tab that is watching, broadcasting or typing.
-app.get('/release.json', release.handler);
+// deploy; it never reloads a tab that is watching, broadcasting or typing. GET /release.json, and
+// POST /release-metrics: open tabs' update reports into /metrics (release_client_updates_total).
+release.mount(app, { registry: metricsRegistry });
// Required: boot finished and the database answers. Optional (a failure is "degraded", still 200):
// Required: boot finished and the database answers. Optional (a failure is "degraded", still 200):
// the WebRTC SFU, OpenVibe.Media and the Network signing key — Live serves channels, chat and RTMP
// the WebRTC SFU, OpenVibe.Media and the Network signing key — Live serves channels, chat and RTMP
@@ -221,6 +221,7 @@ const safe = { DB_PATH, DATA_DIR, HOST: '127.0.0.1', PORT: '13000' };
@@ -221,6 +221,7 @@ const safe = { DB_PATH, DATA_DIR, HOST: '127.0.0.1', PORT: '13000' };
const rel = await request('GET', '/release.json');
const rel = await request('GET', '/release.json');
assert.strictEqual(rel.status, 200);
assert.strictEqual(rel.status, 200);
assert.strictEqual(rel.json.release, head);
assert.strictEqual(rel.json.release, head);
+ assert.deepStrictEqual(require('openvibe-contracts').validate('registry.release-manifest@1', rel.json).errors, []);
const streams = await request('GET', '/api/streams');
const streams = await request('GET', '/api/streams');
assert.strictEqual(streams.status, 200);
assert.strictEqual(streams.status, 200);
assert.deepStrictEqual(streams.json.streams.map((s) => [s.title, s.user_id, s.username]), [['Restored stream', 1, 'drillstar']]);
assert.deepStrictEqual(streams.json.streams.map((s) => [s.title, s.user_id, s.username]), [['Restored stream', 1, 'drillstar']]);
@@ -269,7 +270,7 @@ const safe = { DB_PATH, DATA_DIR, HOST: '127.0.0.1', PORT: '13000' };
@@ -269,7 +270,7 @@ const safe = { DB_PATH, DATA_DIR, HOST: '127.0.0.1', PORT: '13000' };
['POST', '/api/streams', { title: 'x' }], ['PUT', '/api/streams/1', { title: 'y' }], ['PATCH', '/api/streams/1', { title: 'y' }],
['POST', '/api/streams', { title: 'x' }], ['PUT', '/api/streams/1', { title: 'y' }], ['PATCH', '/api/streams/1', { title: 'y' }],
['DELETE', '/api/streams/1', null], ['POST', '/api/chat/send', { message: 'hi' }], ['POST', '/api/auth/logout', null],
['DELETE', '/api/streams/1', null], ['POST', '/api/chat/send', { message: 'hi' }], ['POST', '/api/auth/logout', null],
['POST', '/internal/media-events', { type: 'media.vod.ready' }], ['POST', '/internal/media-webhook', {}], ['POST', '/whip/1', null],
['POST', '/internal/media-events', { type: 'media.vod.ready' }], ['POST', '/internal/media-webhook', {}], ['POST', '/whip/1', null],
- ['POST', '/banned/continue', null], ['POST', '/api/csp-report', {}],
+ ['POST', '/banned/continue', null], ['POST', '/api/csp-report', {}], ['POST', '/release-metrics', { counts: { applied: { style: 1 } } }],
];
];
for (const [m, p, body] of writes) {
for (const [m, p, body] of writes) {
const r = await request(m, p, { body });
const r = await request(m, p, { body });
@@ -6,6 +6,8 @@
@@ -6,6 +6,8 @@
* - release_info and the domain gauges (live streams, WebSocket connections, outbox)
* - release_info and the domain gauges (live streams, WebSocket connections, outbox)
* - /api/ready: boot + db required (503 when either fails); SFU, Media and the Network key optional
* - /api/ready: boot + db required (503 when either fails); SFU, Media and the Network key optional
* (200 "degraded", naming what is missing)
* (200 "degraded", naming what is missing)
+ * - /release.json validates as registry.release-manifest@1 and names /release-metrics, whose beacons
+ * (text/plain or JSON, behind the global JSON parser) land in release_client_updates_total
* A temp SQLite database stands in for Live's; no Media, Network or mediasoup is needed.
* A temp SQLite database stands in for Live's; no Media, Network or mediasoup is needed.
*
*
* Run: node test/observability.test.js
* Run: node test/observability.test.js
@@ -41,6 +43,10 @@ const tmpDb = path.join(os.tmpdir(), `openvibe-observability-test-${process.pid}
@@ -41,6 +43,10 @@ const tmpDb = path.join(os.tmpdir(), `openvibe-observability-test-${process.pid}
const app = express();
const app = express();
const { registry } = observability.mountMetrics(app, { release });
const { registry } = observability.mountMetrics(app, { release });
+ // As server/index.js: the global JSON parser, then GET /release.json + POST /release-metrics.
+ app.use(express.json({ limit: '1mb' }));
+ const liveRelease = require('openvibe-shared/release').createRelease({ service: 'live', root: path.join(__dirname, '..') });
+ liveRelease.mount(app, { registry });
const readiness = observability.createLiveReadiness({
const readiness = observability.createLiveReadiness({
release, bootComplete: () => booted, dbQuery, sfuReady: () => sfu, mediaUrl, networkKey: () => key,
release, bootComplete: () => booted, dbQuery, sfuReady: () => sfu, mediaUrl, networkKey: () => key,
});
});
@@ -154,6 +160,27 @@ const tmpDb = path.join(os.tmpdir(), `openvibe-observability-test-${process.pid}
@@ -154,6 +160,27 @@ const tmpDb = path.join(os.tmpdir(), `openvibe-observability-test-${process.pid}
outbox = { enabled: false };
outbox = { enabled: false };
assert.ok(!(await get('/metrics')).body.includes('live_events_outbox{'), 'a disabled outbox reports nothing, not zero');
assert.ok(!(await get('/metrics')).body.includes('live_events_outbox{'), 'a disabled outbox reports nothing, not zero');
+ // ── release manifest (ADR-016, Track R) ──
+ // registry.release-manifest 1.1.0 under openvibe-contracts >= 0.31: it names where open tabs report.
+ const rel = await get('/release.json');
+ assert.strictEqual(rel.status, 200);
+ const manifest = JSON.parse(rel.body);
+ assert.strictEqual(manifest.service, 'live');
+ assert.deepStrictEqual(require('openvibe-contracts').validate('registry.release-manifest@1', manifest).errors, []);
+ assert.strictEqual(manifest.metrics_url, '/release-metrics');
+ // release-watch beacons its counts as text/plain (sendBeacon); express.json() must not swallow them.
+ const beacon = (payload, type = 'text/plain;charset=UTF-8') => new Promise((resolve, reject) => {
+ const req = http.request(base + '/release-metrics', { method: 'POST', headers: { 'Content-Type': type } }, (res) => { res.resume(); res.on('end', () => resolve(res.statusCode)); });
+ req.on('error', reject); req.end(payload);
+ });
+ assert.strictEqual(await beacon(JSON.stringify({ counts: { applied: { style: 2 }, reloaded: { user: 1 } } })), 204);
+ assert.strictEqual(await beacon(JSON.stringify({ counts: { deferred: { typing: 1 } } }), 'application/json'), 204);
+ assert.strictEqual(await beacon('not json'), 400);
+ const mt = (await get('/metrics')).body;
+ assert.ok(mt.includes('release_client_updates_total{outcome="applied",reason="style"} 2\n'), mt);
+ assert.ok(mt.includes('release_client_updates_total{outcome="reloaded",reason="user"} 1\n'));
+ assert.ok(mt.includes('release_client_updates_total{outcome="deferred",reason="typing"} 1\n'));
+
// Through nginx (X-Forwarded-For on a loopback connection) /metrics does not exist.
// Through nginx (X-Forwarded-For on a loopback connection) /metrics does not exist.
assert.strictEqual((await get('/metrics', { 'X-Forwarded-For': '203.0.113.7' })).status, 404);
assert.strictEqual((await get('/metrics', { 'X-Forwarded-For': '203.0.113.7' })).status, 404);
assert.strictEqual((await get('/metrics', { 'X-Real-IP': '203.0.113.7' })).status, 404);
assert.strictEqual((await get('/metrics', { 'X-Real-IP': '203.0.113.7' })).status, 404);
@@ -163,6 +190,8 @@ const tmpDb = path.join(os.tmpdir(), `openvibe-observability-test-${process.pid}
@@ -163,6 +190,8 @@ const tmpDb = path.join(os.tmpdir(), `openvibe-observability-test-${process.pid}
const mountAt = src.indexOf('observability.mountMetrics(app');
const mountAt = src.indexOf('observability.mountMetrics(app');
assert.ok(mountAt > 0 && mountAt < src.indexOf('app.use(helmet('), 'metrics middleware is mounted before the rest of the middleware');
assert.ok(mountAt > 0 && mountAt < src.indexOf('app.use(helmet('), 'metrics middleware is mounted before the rest of the middleware');
assert.ok(/app\.get\('\/api\/ready', readiness\.handler\)/.test(src));
assert.ok(/app\.get\('\/api\/ready', readiness\.handler\)/.test(src));
+ assert.ok(/release\.mount\(app, \{ registry: metricsRegistry \}\)/.test(src), '/release.json and /release-metrics are mounted with the metrics registry');
+ assert.ok(src.indexOf('release.mount(app') > src.indexOf('app.use(express.json('), 'after the JSON parser, as tested above');
server.close(); media.close(); sqlite.close();
server.close(); media.close(); sqlite.close();
for (const f of [tmpDb, `${tmpDb}-wal`, `${tmpDb}-shm`]) { try { fs.unlinkSync(f); } catch { /* */ } }
for (const f of [tmpDb, `${tmpDb}-wal`, `${tmpDb}-shm`]) { try { fs.unlinkSync(f); } catch { /* */ } }