diff --git a/docs/API.md b/docs/API.md index 46f05b64..ecb7f457 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1149,6 +1149,7 @@ Response: ] } ``` + ### V2 Utilities #### Cleanup volumes with no references @@ -1179,3 +1180,37 @@ Unsuccessful response: } ``` +#### Journald logs + +Added in supervisor version v10.1.0 + +Retrieve a stream to the journald logs on device. This is +equivalent to running `journalctl -o export`. Options +supported are: + +##### all: boolean +Show all fields in full, equivalent to `journalctl --all`. + +##### follow: boolean +Continuously stream logs as they are generated, equivalent +to `journalctl --follow`. + +##### count: integer +Show the most recent `count` events, equivalent to +`journalctl --line=`. + +##### unit +Show journal logs from `unit` only, equivalent to +`journalctl --unit=`. + +Fields should be provided via POST body in JSON format. + +From an application container (with systemd installed): +``` +$ curl -X POST --data '{"follow":true,"all":true}' "$BALENA_SUPERVISOR_ADDRESS/v2/journal-logs?apikey=$BALENA_SUPERVISOR_API_KEY" | systemd-journal-remote - -o log.journal +``` + +The `log.journal` file can then be viewed with +``` +journalctl --file log.journal -f +``` diff --git a/src/device-api/v2.ts b/src/device-api/v2.ts index 04dbb698..44337bfa 100644 --- a/src/device-api/v2.ts +++ b/src/device-api/v2.ts @@ -13,8 +13,11 @@ import { import { doPurge, doRestart, serviceAction } from './common'; import Volume from '../compose/volume'; +import { spawnJournalctl } from '../lib/journald'; + import log from '../lib/supervisor-console'; import supervisorVersion = require('../lib/supervisor-version'); +import { checkInt, checkTruthy } from '../lib/validation'; export function createV2Api(router: Router, applications: ApplicationManager) { const { _lockingIfNecessary, deviceState } = applications; @@ -533,4 +536,32 @@ export function createV2Api(router: Router, applications: ApplicationManager) { }); } }); + + router.post('/v2/journal-logs', (req, res) => { + try { + const all = checkTruthy(req.body.all) || false; + const follow = checkTruthy(req.body.follow) || false; + const count = checkInt(req.body.count, { positive: true }) || undefined; + const unit = req.body.unit; + + const journald = spawnJournalctl({ all, follow, count, unit }); + res.status(200); + journald.stdout.pipe(res); + res.on('close', () => { + journald.kill('SIGKILL'); + }); + journald.on('exit', () => { + res.end(); + }); + } catch (e) { + log.error('There was an error reading the journalctl process', e); + if (res.headersSent) { + return; + } + res.json({ + status: 'failed', + message: messageFromError(e), + }); + } + }); } diff --git a/src/lib/journald.ts b/src/lib/journald.ts new file mode 100644 index 00000000..d7fe9195 --- /dev/null +++ b/src/lib/journald.ts @@ -0,0 +1,41 @@ +import { ChildProcess, spawn } from 'child_process'; + +import constants = require('./constants'); +import log from './supervisor-console'; + +export function spawnJournalctl(opts: { + all: boolean; + follow: boolean; + count?: number; + unit?: string; +}): ChildProcess { + const args = [ + // The directory we want to run the chroot from + constants.rootMountPoint, + 'journalctl', + '-o', + 'export', + ]; + if (opts.all) { + args.push('-a'); + } + if (opts.follow) { + args.push('--follow'); + } + if (opts.unit != null) { + args.push('-u'); + args.push(opts.unit); + } + if (opts.count != null) { + args.push('-n'); + args.push(opts.count.toString()); + } + + log.debug('Spawning journald with: chroot ', args.join(' ')); + + const journald = spawn('chroot', args, { + stdio: 'pipe', + }); + + return journald; +}