openvibers/openvibe.host Commits

Add a logs subcommand to the ovhost CLI (#3)

OP OpenVibers committed 2 Oct 2026 · 203642ca80 · 4 files, +20 −16

local checks ✓ 4/4 · CI ✓ 3/3 · review SHIP by space-bunny-alpha · 1 fix round

https://github.com/OpenVibers/OpenVibe.Host/pull/3

Changes

ddiff --git a/README.md b/README.md
iindex 88d8136..5fb48c1 100644
---- a/README.md
++++ b/README.md
@@@ -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, lifecycle
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)
ddiff --git a/lib/cli.js b/lib/cli.js
iindex 1731451..913193d 100644
---- a/lib/cli.js
++++ b/lib/cli.js
@@@ -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 the
lifecycle declaration (inventory, --manifest or the installed
openvibe-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; }
ddiff --git a/test/cli-logs.test.js b/test/cli-logs.test.js
iindex cc7a38a..abe2754 100644
---- a/test/cli-logs.test.js
++++ b/test/cli-logs.test.js
@@@ -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, []);
}),
ddiff --git a/test/run.js b/test/run.js
iindex 6341fa9..2a15384 100644
---- a/test/run.js
++++ b/test/run.js
@@@ -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 });