openvibers/openvibe.host Commits
Add a logs subcommand to the ovhost CLI (#3)
Changes
@@@ -110,6 +110,7 @@ Production deploys, rollbacks, backups and drills now run through `ovhost`, and```ovhost status [<service>...] unit state, sha, readiness, protected sessions++ovhost logs <service> [--tail <n>] the service's units' journal, newest last (default 200, max 2000)ovhost validate <service> [--manifest <file>] env NAMES, unit files, port, vhost + nginx -t, deps, lifecycleovhost env-names <service> names declared in the checkout's .env.exampleovhost show [<service>...] the inventory as ovhost reads it (repo, owner, units, socket, workers, probes)@@@ -18,8 +18,8 @@ const USAGE = `ovhost — OpenVibe.Host operator plane (Stage A)Usage: ovhost <command> [args] [--inventory <file>] [--json]status [<service>...] unit state, sha, readiness, protected sessions (all services)-- logs <service> [--tail <n>] the journal of the service's units, newest last-- (journalctl -u <unit>… -n <n> --no-pager; default 100)++ logs <service> [--tail <n>] the journal of the service's units, newest last++ (journalctl -u <unit>… -n <n> --no-pager -o short-iso; default 200, max 2000)validate <service> [--manifest <file>] env names, unit files, port, nginx vhost + nginx -t, deps, and thelifecycle declaration (inventory, --manifest or the installedopenvibe-contracts manifest): every field, the shutdown deadline@@@ -334,12 +334,12 @@ async function main(argv, { exec = lib.createSystemExecutor(), out = (s) => proc// service→unit mapping). journalctl runs with an argv array, never a// shell string, so a service id or tail can inject nothing.const svc = lib.inventory.service(inv, needService());-- const tail = opts.tail === undefined ? 100 : Number(opts.tail);-- if (!(Number.isInteger(tail) && tail >= 1 && tail <= 100000)) throw new UsageError('--tail must be a whole number from 1 to 100000');++ const tail = opts.tail === undefined ? 200 : Number(opts.tail);++ if (!(Number.isInteger(tail) && tail >= 1 && tail <= 2000)) throw new UsageError('--tail must be a whole number from 1 to 2000');if (!svc.units.length) throw new UsageError(`${svc.id} declares no units — nothing to read from the journal`);const args = [];for (const u of svc.units) args.push('-u', u);-- args.push('-n', String(tail), '--no-pager');++ args.push('-n', String(tail), '--no-pager', '-o', 'short-iso');const r = await exec.run('journalctl', args, { privileged: true });if (json) { emit({ service: svc.id, units: svc.units, tail, code: r.code, lines: String(r.stdout || '').split('\n').filter((l) => l.length) }); return r.code === 0 ? 0 : 1; }if (r.code !== 0) { out(`[ovhost] journalctl failed (exit ${r.code}): ${String(r.stderr || '').trim()}`); return 1; }@@@ -9,13 +9,13 @@ const assert = require('assert');const { scenario, test, runTests } = require('./helpers');runTests([-- test('logs runs journalctl for every unit of the service, default tail 100', async () => {++ test('logs runs journalctl for every unit of the service, default tail 200', async () => {const host = scenario();host.journalctl = (args) => ({ stdout: `journal of ${args.filter((a) => a.endsWith('.service')).join(' ')}\n` });const r = await host.cli('logs', 'tools');assert.strictEqual(r.code, 0, r.out);assert.deepStrictEqual(host.journalctlCalls, [{-- args: ['-u', 'openvibe-tools.service', '-u', 'openvibe-tools-maps.service', '-n', '100', '--no-pager'],++ args: ['-u', 'openvibe-tools.service', '-u', 'openvibe-tools-maps.service', '-n', '200', '--no-pager', '-o', 'short-iso'],as: null,privileged: true,}]);@@@ -26,11 +26,15 @@ runTests([const host = scenario();let r = await host.cli('logs', 'live', '--tail', '5');assert.strictEqual(r.code, 0, r.out);-- assert.deepStrictEqual(host.journalctlCalls[0].args, ['-u', 'openvibe-live.service', '-n', '5', '--no-pager']);++ assert.deepStrictEqual(host.journalctlCalls[0].args, ['-u', 'openvibe-live.service', '-n', '5', '--no-pager', '-o', 'short-iso']);host.journalctlCalls.length = 0;r = await host.cli('logs', 'live', '--tail=42');assert.strictEqual(r.code, 0, r.out);-- assert.deepStrictEqual(host.journalctlCalls[0].args, ['-u', 'openvibe-live.service', '-n', '42', '--no-pager']);++ assert.deepStrictEqual(host.journalctlCalls[0].args, ['-u', 'openvibe-live.service', '-n', '42', '--no-pager', '-o', 'short-iso']);++ host.journalctlCalls.length = 0;++ r = await host.cli('logs', 'live', '--tail', '2000'); // the maximum++ assert.strictEqual(r.code, 0, r.out);++ assert.deepStrictEqual(host.journalctlCalls[0].args, ['-u', 'openvibe-live.service', '-n', '2000', '--no-pager', '-o', 'short-iso']);}),test('logs prints journal output verbatim, newest last', async () => {@@@ -51,16 +55,13 @@ runTests([assert.deepStrictEqual(j, { service: 'live', units: ['openvibe-live.service'], tail: 3, code: 0, lines: ['one', 'two'] });}),-- test('logs validates --tail: whole number from 1, journalctl never runs', async () => {++ test('logs validates --tail: whole number from 1 to 2000, journalctl never runs', async () => {const host = scenario();-- for (const bad of ['0', '-1', 'abc', '1.5', '']) {++ for (const bad of ['0', '-1', 'abc', '1.5', '', '2001']) {const r = await host.cli('logs', 'live', '--tail', bad);assert.strictEqual(r.code, 1, `${bad}: ${r.out}`);-- assert.match(r.out, /--tail must be a whole number from 1 to 100000/);++ assert.match(r.out, /--tail must be a whole number from 1 to 2000/);}-- const r = await host.cli('logs', 'live', '--tail', '100001');-- assert.strictEqual(r.code, 1, r.out);-- assert.match(r.out, /--tail must be a whole number from 1 to 100000/);assert.deepStrictEqual(host.journalctlCalls, []);}),@@@ -12,4 +12,6 @@* ○ and not counted as passed (openvibe-shared/test-runner).*/'use strict';--require('openvibe-shared/test-runner').main({ dir: __dirname, timeoutMs: 60000, pad: 32, parallel: 1 });++// 180 s per file: d41-proof.test.js runs scripts/d41-proof.sh with a node stub per systemctl/ovhost call++// (hundreds of process starts, ~13 s of CPU), which takes 60–120 s of wall time on a loaded machine.++require('openvibe-shared/test-runner').main({ dir: __dirname, timeoutMs: 180000, pad: 32, parallel: 1 });
@@ -110,6 +110,7 @@ Production deploys, rollbacks, backups and drills now run through `ovhost`, and
@@ -110,6 +110,7 @@ Production deploys, rollbacks, backups and drills now run through `ovhost`, and
```
```
ovhost status [<service>...] unit state, sha, readiness, protected sessions
ovhost status [<service>...] unit state, sha, readiness, protected sessions
+ovhost logs <service> [--tail <n>] the service's units' journal, newest last (default 200, max 2000)
ovhost validate <service> [--manifest <file>] env NAMES, unit files, port, vhost + nginx -t, deps, lifecycle
ovhost validate <service> [--manifest <file>] env NAMES, unit files, port, vhost + nginx -t, deps, lifecycle
ovhost env-names <service> names declared in the checkout's .env.example
ovhost env-names <service> names declared in the checkout's .env.example
ovhost show [<service>...] the inventory as ovhost reads it (repo, owner, units, socket, workers, probes)
ovhost show [<service>...] the inventory as ovhost reads it (repo, owner, units, socket, workers, probes)
@@ -18,8 +18,8 @@ const USAGE = `ovhost — OpenVibe.Host operator plane (Stage A)
@@ -18,8 +18,8 @@ const USAGE = `ovhost — OpenVibe.Host operator plane (Stage A)
Usage: ovhost <command> [args] [--inventory <file>] [--json]
Usage: ovhost <command> [args] [--inventory <file>] [--json]
status [<service>...] unit state, sha, readiness, protected sessions (all services)
status [<service>...] unit state, sha, readiness, protected sessions (all services)
- logs <service> [--tail <n>] the journal of the service's units, newest last
- (journalctl -u <unit>… -n <n> --no-pager; default 100)
+ logs <service> [--tail <n>] the journal of the service's units, newest last
+ (journalctl -u <unit>… -n <n> --no-pager -o short-iso; default 200, max 2000)
validate <service> [--manifest <file>] env names, unit files, port, nginx vhost + nginx -t, deps, and the
validate <service> [--manifest <file>] env names, unit files, port, nginx vhost + nginx -t, deps, and the
lifecycle declaration (inventory, --manifest or the installed
lifecycle declaration (inventory, --manifest or the installed
openvibe-contracts manifest): every field, the shutdown deadline
openvibe-contracts manifest): every field, the shutdown deadline
@@ -334,12 +334,12 @@ async function main(argv, { exec = lib.createSystemExecutor(), out = (s) => proc
@@ -334,12 +334,12 @@ async function main(argv, { exec = lib.createSystemExecutor(), out = (s) => proc
// service→unit mapping). journalctl runs with an argv array, never a
// service→unit mapping). journalctl runs with an argv array, never a
// shell string, so a service id or tail can inject nothing.
// shell string, so a service id or tail can inject nothing.
const svc = lib.inventory.service(inv, needService());
const svc = lib.inventory.service(inv, needService());
- const tail = opts.tail === undefined ? 100 : Number(opts.tail);
- if (!(Number.isInteger(tail) && tail >= 1 && tail <= 100000)) throw new UsageError('--tail must be a whole number from 1 to 100000');
+ const tail = opts.tail === undefined ? 200 : Number(opts.tail);
+ if (!(Number.isInteger(tail) && tail >= 1 && tail <= 2000)) throw new UsageError('--tail must be a whole number from 1 to 2000');
if (!svc.units.length) throw new UsageError(`${svc.id} declares no units — nothing to read from the journal`);
if (!svc.units.length) throw new UsageError(`${svc.id} declares no units — nothing to read from the journal`);
const args = [];
const args = [];
for (const u of svc.units) args.push('-u', u);
for (const u of svc.units) args.push('-u', u);
- args.push('-n', String(tail), '--no-pager');
+ args.push('-n', String(tail), '--no-pager', '-o', 'short-iso');
const r = await exec.run('journalctl', args, { privileged: true });
const r = await exec.run('journalctl', args, { privileged: true });
if (json) { emit({ service: svc.id, units: svc.units, tail, code: r.code, lines: String(r.stdout || '').split('\n').filter((l) => l.length) }); return r.code === 0 ? 0 : 1; }
if (json) { emit({ service: svc.id, units: svc.units, tail, code: r.code, lines: String(r.stdout || '').split('\n').filter((l) => l.length) }); return r.code === 0 ? 0 : 1; }
if (r.code !== 0) { out(`[ovhost] journalctl failed (exit ${r.code}): ${String(r.stderr || '').trim()}`); return 1; }
if (r.code !== 0) { out(`[ovhost] journalctl failed (exit ${r.code}): ${String(r.stderr || '').trim()}`); return 1; }
@@ -9,13 +9,13 @@ const assert = require('assert');
@@ -9,13 +9,13 @@ const assert = require('assert');
const { scenario, test, runTests } = require('./helpers');
const { scenario, test, runTests } = require('./helpers');
runTests([
runTests([
- test('logs runs journalctl for every unit of the service, default tail 100', async () => {
+ test('logs runs journalctl for every unit of the service, default tail 200', async () => {
const host = scenario();
const host = scenario();
host.journalctl = (args) => ({ stdout: `journal of ${args.filter((a) => a.endsWith('.service')).join(' ')}\n` });
host.journalctl = (args) => ({ stdout: `journal of ${args.filter((a) => a.endsWith('.service')).join(' ')}\n` });
const r = await host.cli('logs', 'tools');
const r = await host.cli('logs', 'tools');
assert.strictEqual(r.code, 0, r.out);
assert.strictEqual(r.code, 0, r.out);
assert.deepStrictEqual(host.journalctlCalls, [{
assert.deepStrictEqual(host.journalctlCalls, [{
- args: ['-u', 'openvibe-tools.service', '-u', 'openvibe-tools-maps.service', '-n', '100', '--no-pager'],
+ args: ['-u', 'openvibe-tools.service', '-u', 'openvibe-tools-maps.service', '-n', '200', '--no-pager', '-o', 'short-iso'],
as: null,
as: null,
privileged: true,
privileged: true,
}]);
}]);
@@ -26,11 +26,15 @@ runTests([
@@ -26,11 +26,15 @@ runTests([
const host = scenario();
const host = scenario();
let r = await host.cli('logs', 'live', '--tail', '5');
let r = await host.cli('logs', 'live', '--tail', '5');
assert.strictEqual(r.code, 0, r.out);
assert.strictEqual(r.code, 0, r.out);
- assert.deepStrictEqual(host.journalctlCalls[0].args, ['-u', 'openvibe-live.service', '-n', '5', '--no-pager']);
+ assert.deepStrictEqual(host.journalctlCalls[0].args, ['-u', 'openvibe-live.service', '-n', '5', '--no-pager', '-o', 'short-iso']);
host.journalctlCalls.length = 0;
host.journalctlCalls.length = 0;
r = await host.cli('logs', 'live', '--tail=42');
r = await host.cli('logs', 'live', '--tail=42');
assert.strictEqual(r.code, 0, r.out);
assert.strictEqual(r.code, 0, r.out);
- assert.deepStrictEqual(host.journalctlCalls[0].args, ['-u', 'openvibe-live.service', '-n', '42', '--no-pager']);
+ assert.deepStrictEqual(host.journalctlCalls[0].args, ['-u', 'openvibe-live.service', '-n', '42', '--no-pager', '-o', 'short-iso']);
+ host.journalctlCalls.length = 0;
+ r = await host.cli('logs', 'live', '--tail', '2000'); // the maximum
+ assert.strictEqual(r.code, 0, r.out);
+ assert.deepStrictEqual(host.journalctlCalls[0].args, ['-u', 'openvibe-live.service', '-n', '2000', '--no-pager', '-o', 'short-iso']);
}),
}),
test('logs prints journal output verbatim, newest last', async () => {
test('logs prints journal output verbatim, newest last', async () => {
@@ -51,16 +55,13 @@ runTests([
@@ -51,16 +55,13 @@ runTests([
assert.deepStrictEqual(j, { service: 'live', units: ['openvibe-live.service'], tail: 3, code: 0, lines: ['one', 'two'] });
assert.deepStrictEqual(j, { service: 'live', units: ['openvibe-live.service'], tail: 3, code: 0, lines: ['one', 'two'] });
}),
}),
- test('logs validates --tail: whole number from 1, journalctl never runs', async () => {
+ test('logs validates --tail: whole number from 1 to 2000, journalctl never runs', async () => {
const host = scenario();
const host = scenario();
- for (const bad of ['0', '-1', 'abc', '1.5', '']) {
+ for (const bad of ['0', '-1', 'abc', '1.5', '', '2001']) {
const r = await host.cli('logs', 'live', '--tail', bad);
const r = await host.cli('logs', 'live', '--tail', bad);
assert.strictEqual(r.code, 1, `${bad}: ${r.out}`);
assert.strictEqual(r.code, 1, `${bad}: ${r.out}`);
- assert.match(r.out, /--tail must be a whole number from 1 to 100000/);
+ assert.match(r.out, /--tail must be a whole number from 1 to 2000/);
}
}
- const r = await host.cli('logs', 'live', '--tail', '100001');
- assert.strictEqual(r.code, 1, r.out);
- assert.match(r.out, /--tail must be a whole number from 1 to 100000/);
assert.deepStrictEqual(host.journalctlCalls, []);
assert.deepStrictEqual(host.journalctlCalls, []);
}),
}),
@@ -12,4 +12,6 @@
@@ -12,4 +12,6 @@
* ○ and not counted as passed (openvibe-shared/test-runner).
* ○ and not counted as passed (openvibe-shared/test-runner).
*/
*/
'use strict';
'use strict';
-require('openvibe-shared/test-runner').main({ dir: __dirname, timeoutMs: 60000, pad: 32, parallel: 1 });
+// 180 s per file: d41-proof.test.js runs scripts/d41-proof.sh with a node stub per systemctl/ovhost call
+// (hundreds of process starts, ~13 s of CPU), which takes 60–120 s of wall time on a loaded machine.
+require('openvibe-shared/test-runner').main({ dir: __dirname, timeoutMs: 180000, pad: 32, parallel: 1 });