diff --git a/docs/references/api.md b/docs/references/api.md index 0dcd948e1..512e21fde 100644 --- a/docs/references/api.md +++ b/docs/references/api.md @@ -827,6 +827,32 @@ Response (200): } ``` +### Get logs + +GET `/api/v1/cloudron/logs` admin + +Get the system logs. + +The `lines` query parameter can be used to specify the number of log lines to download. + +The `units` query parameters can be set to `box` or `mail` to get logs of specific units. + +The response has `Content-Type` set to 'application/x-logs' and `Content-Disposition` set to +`attachment; filename="log.txt`. + +Response(200): + +``` +Line delimited JSON. + + { + realtimeTimestamp: , // wallclock timestamp + monotonicTimestamp: , // time passed since boot + message: [ ,... ], // utf8 buffer + source: // source of this message + } +``` + ### List events GET `/api/v1/cloudron/eventlog` admin diff --git a/src/cloudron.js b/src/cloudron.js index 03b521653..6082c1f33 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -10,6 +10,7 @@ exports = module.exports = { getStatus: getStatus, getDisks: getDisks, dnsSetup: dnsSetup, + getLogs: getLogs, sendHeartbeat: sendHeartbeat, @@ -57,6 +58,8 @@ var appdb = require('./appdb.js'), settings = require('./settings.js'), SettingsError = settings.SettingsError, shell = require('./shell.js'), + spawn = require('child_process').spawn, + split = require('split'), subdomains = require('./subdomains.js'), superagent = require('superagent'), sysinfo = require('./sysinfo.js'), @@ -945,3 +948,39 @@ function refreshDNS(callback) { }); }); } + +function getLogs(units, lines, follow, callback) { + assert(Array.isArray(units)); + assert.strictEqual(typeof lines, 'number'); + assert.strictEqual(typeof follow, 'boolean'); + assert.strictEqual(typeof callback, 'function'); + + debug('Getting logs for %j', units); + + var args = [ '--output=json', '--no-pager', '--lines=' + lines ]; + units.forEach(function (u) { + if (u === 'box') args.push('--unit=box'); + else if (u === 'mail') args.push('CONTAINER_ID=mail'); + }); + if (follow) args.push('--follow'); + + var cp = spawn('/bin/journalctl', args); + + var transformStream = split(function mapper(line) { + var obj = safe.JSON.parse(line); + if (!obj) return undefined; + + return JSON.stringify({ + realtimeTimestamp: obj.__REALTIME_TIMESTAMP, + monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP, + message: obj.MESSAGE, + source: obj.SYSLOG_IDENTIFIER || '' + }) + '\n'; + }); + + transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process + + cp.stdout.pipe(transformStream); + + return callback(null, transformStream); +} diff --git a/src/routes/cloudron.js b/src/routes/cloudron.js index d3580ed07..872ac3790 100644 --- a/src/routes/cloudron.js +++ b/src/routes/cloudron.js @@ -13,7 +13,8 @@ exports = module.exports = { getDisks: getDisks, update: update, feedback: feedback, - checkForUpdates: checkForUpdates + checkForUpdates: checkForUpdates, + getLogs: getLogs }; var assert = require('assert'), @@ -223,3 +224,24 @@ function feedback(req, res, next) { next(new HttpSuccess(201, {})); } + +function getLogs(req, res, next) { + var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100; + if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number')); + + var units = req.query.units || 'all'; + debug('Getting logs of unit:%s', units); + + cloudron.getLogs(units.split(','), lines, false /* follow */, function (error, logStream) { + if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, 'Invalid type')); + if (error) return next(new HttpError(500, error)); + + res.writeHead(200, { + 'Content-Type': 'application/x-logs', + 'Content-Disposition': 'attachment; filename="log.txt"', + 'Cache-Control': 'no-cache', + 'X-Accel-Buffering': 'no' // disable nginx buffering + }); + logStream.pipe(res); + }); +} diff --git a/src/server.js b/src/server.js index 88476d3eb..fabcae907 100644 --- a/src/server.js +++ b/src/server.js @@ -115,6 +115,7 @@ function initializeExpressSync() { router.post('/api/v1/cloudron/migrate', cloudronScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.migrate); router.get ('/api/v1/cloudron/graphs', cloudronScope, routes.user.requireAdmin, routes.graphs.getGraphs); router.get ('/api/v1/cloudron/disks', cloudronScope, routes.user.requireAdmin, routes.cloudron.getDisks); + router.get ('/api/v1/cloudron/logs', cloudronScope, routes.user.requireAdmin, routes.cloudron.getLogs); router.get ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.user.requireAdmin, routes.ssh.getAuthorizedKeys); router.put ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.user.requireAdmin, routes.ssh.addAuthorizedKey); router.get ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, routes.user.requireAdmin, routes.ssh.getAuthorizedKey);