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);