diff --git a/src/addons.js b/src/addons.js index 23353ea14..26df7a991 100644 --- a/src/addons.js +++ b/src/addons.js @@ -9,6 +9,7 @@ exports = module.exports = { getEnvironment: getEnvironment, getLinksSync: getLinksSync, getBindsSync: getBindsSync, + getContainerNamesSync: getContainerNamesSync, // exported for testing _setupOauth: setupOauth, @@ -239,6 +240,27 @@ function getBindsSync(app, addons) { return binds; } +function getContainerNamesSync(app, addons) { + assert.strictEqual(typeof app, 'object'); + assert(!addons || typeof addons === 'object'); + + var names = [ ]; + + if (!addons) return names; + + for (var addon in addons) { + switch (addon) { + case 'scheduler': + // names here depend on how scheduler.js creates containers + names = names.concat(Object.keys(addons.scheduler).map(function (taskName) { return app.id + '-' + taskName; })); + break; + default: break; + } + } + + return names; +} + function setupOauth(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); diff --git a/src/apps.js b/src/apps.js index 9a8d04ec8..c02bc795a 100644 --- a/src/apps.js +++ b/src/apps.js @@ -23,7 +23,6 @@ exports = module.exports = { backup: backup, backupApp: backupApp, - getLogStream: getLogStream, getLogs: getLogs, start: start, @@ -62,6 +61,7 @@ var addons = require('./addons.js'), settings = require('./settings.js'), semver = require('semver'), shell = require('./shell.js'), + spawn = require('child_process').spawn, split = require('split'), superagent = require('superagent'), taskmanager = require('./taskmanager.js'), @@ -475,58 +475,45 @@ function update(appId, force, manifest, portBindings, icon, callback) { }); } -function getLogStream(appId, fromLine, callback) { - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof fromLine, 'number'); // behaves like tail -n - assert.strictEqual(typeof callback, 'function'); +function appLogFilter(app) { + var names = [ app.id ].concat(addons.getContainerNamesSync(app, app.addons)); - debug('Getting logs for %s', appId); - appdb.get(appId, function (error, app) { - if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND)); - if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); - - if (app.installationState !== appdb.ISTATE_INSTALLED) return callback(new AppsError(AppsError.BAD_STATE, util.format('App is in %s state.', app.installationState))); - - var container = docker.getContainer(app.containerId); - var tail = fromLine < 0 ? -fromLine : 'all'; - - // note: cannot access docker file directly because it needs root access - container.logs({ stdout: true, stderr: true, follow: true, timestamps: true, tail: tail }, function (error, logStream) { - if (error && error.statusCode === 404) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app')); - if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); - - var lineCount = 0; - var skipLinesStream = split(function mapper(line) { - if (++lineCount < fromLine) return undefined; - var timestamp = line.substr(0, line.indexOf(' ')); // sometimes this has square brackets around it - return JSON.stringify({ lineNumber: lineCount, timestamp: timestamp.replace(/[[\]]/g,''), log: line.substr(timestamp.length + 1) }); - }); - skipLinesStream.close = logStream.req.abort; - logStream.pipe(skipLinesStream); - return callback(null, skipLinesStream); - }); - }); + return names.map(function (name) { return 'CONTAINER_NAME=' + name; }); } -function getLogs(appId, callback) { +function getLogs(appId, lines, follow, callback) { assert.strictEqual(typeof appId, 'string'); + assert.strictEqual(typeof lines, 'number'); + assert.strictEqual(typeof follow, 'boolean'); assert.strictEqual(typeof callback, 'function'); debug('Getting logs for %s', appId); + appdb.get(appId, function (error, app) { if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); if (app.installationState !== appdb.ISTATE_INSTALLED) return callback(new AppsError(AppsError.BAD_STATE, util.format('App is in %s state.', app.installationState))); - var container = docker.getContainer(app.containerId); - // note: cannot access docker file directly because it needs root access - container.logs({ stdout: true, stderr: true, follow: false, timestamps: true, tail: 'all' }, function (error, logStream) { - if (error && error.statusCode === 404) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app')); - if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); + var args = [ '--output=json', '--no-pager', '--lines=' + lines ]; + if (follow) args.push('--follow'); + args = args.concat(appLogFilter(app)); - return callback(null, logStream); + var cp = spawn('/bin/journalctl', args); + + var transformStream = split(function mapper(line) { + var obj = safe.JSON.parse(line); + if (!obj) return undefined; + + var source = obj.CONTAINER_NAME.slice(app.id.length + 1); + return JSON.stringify({ timestamp: obj.__REALTIME_TIMESTAMP, message: obj.MESSAGE, source: source || 'main' }); }); + + 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/apps.js b/src/routes/apps.js index 22b344f6a..ca8e465a2 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -286,14 +286,14 @@ function getLogStream(req, res, next) { debug('Getting logstream of app id:%s', req.params.id); - var fromLine = req.query.fromLine ? parseInt(req.query.fromLine, 10) : -10; // we ignore last-event-id - if (isNaN(fromLine)) return next(new HttpError(400, 'fromLine must be a valid number')); + var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id + if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number')); function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; } if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream')); - apps.getLogStream(req.params.id, fromLine, function (error, logStream) { + apps.getLogs(req.params.id, lines, true /* follow */, function (error, logStream) { if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app')); if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(412, error.message)); if (error) return next(new HttpError(500, error)); @@ -309,7 +309,7 @@ function getLogStream(req, res, next) { res.on('close', logStream.close); logStream.on('data', function (data) { var obj = JSON.parse(data); - res.write(sse(obj.lineNumber, JSON.stringify(obj))); + res.write(sse(obj.timestamp, JSON.stringify(obj))); // send timestamp as id }); logStream.on('end', res.end.bind(res)); logStream.on('error', res.end.bind(res, null)); @@ -319,9 +319,12 @@ function getLogStream(req, res, next) { function getLogs(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); + var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100; + if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number')); + debug('Getting logs of app id:%s', req.params.id); - apps.getLogs(req.params.id, function (error, logStream) { + apps.getLogs(req.params.id, lines, false /* follow */, function (error, logStream) { if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app')); if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(412, error.message)); if (error) return next(new HttpError(500, error)); diff --git a/src/scheduler.js b/src/scheduler.js index 882d90284..c9eb3147d 100644 --- a/src/scheduler.js +++ b/src/scheduler.js @@ -180,6 +180,7 @@ function doTask(appId, taskName, callback) { debug('Creating createSubcontainer for %s/%s : %s', app.id, taskName, gState[appId].schedulerConfig[taskName].command); + // NOTE: if you change container name here, fix addons.js to return correct container names docker.createSubcontainer(app, app.id + '-' + taskName, [ '/bin/sh', '-c', gState[appId].schedulerConfig[taskName].command ], function (error, container) { appState.containerIds[taskName] = container.id;