diff --git a/src/docker.js b/src/docker.js index 7443b3574..1551ae772 100644 --- a/src/docker.js +++ b/src/docker.js @@ -324,7 +324,8 @@ async function createSubcontainer(app, name, cmd, options) { ExposedPorts: isAppContainer ? exposedPorts : { }, Volumes: { // see also ReadonlyRootfs '/tmp': {}, - '/run': {} + '/run': {}, + '/app/code/node_modules': {} }, Labels: { 'fqdn': app.fqdn, diff --git a/src/graphs.js b/src/graphs.js new file mode 100644 index 000000000..632933a65 --- /dev/null +++ b/src/graphs.js @@ -0,0 +1,57 @@ +'use strict'; + +exports = module.exports = { + getByAppId +}; + +const assert = require('assert'), + BoxError = require('./boxerror.js'), + safe = require('safetydance'), + superagent = require('superagent'), + debug = require('debug')('box:graphs'); + +const GRAPHITE_RENDER_URL = 'http://127.0.0.1:8417/graphite-web/render'; + +async function getByAppId(appId, fromMinutes, noNullPoints) { + assert.strictEqual(typeof appId, 'string'); + assert.strictEqual(typeof fromMinutes, 'number'); + assert.strictEqual(typeof noNullPoints, 'boolean'); + + const timeBucketSize = fromMinutes > (24 * 60) ? (6*60) : 5; + + debug(`getByAppId: ${appId} from ${fromMinutes}`); + + const memoryQuery = { + target: `summarize(sum(collectd.localhost.table-${appId}-memory.gauge-rss, collectd.localhost.table-${appId}-memory.gauge-swap), "${timeBucketSize}min", "avg")`, + format: 'json', + from: `-${fromMinutes}min`, + until: 'now' + }; + if (noNullPoints) memoryQuery.noNullPoints = true; + + const [memoryError, memoryResponse] = await safe(superagent.get(GRAPHITE_RENDER_URL) + .query(memoryQuery) + .timeout(30 * 1000) + .ok(() => true)); + + if (memoryError) throw new BoxError(BoxError.NETWORK_ERROR, memoryError.message); + if (memoryResponse.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Unknown error: ${memoryError.message}`); + + const diskQuery = { + target: `summarize(collectd.localhost.du-${appId}.capacity-usage, "${timeBucketSize}min", "avg")`, + format: 'json', + from: `-${fromMinutes}min`, + until: 'now' + }; + if (noNullPoints) diskQuery.noNullPoints = true; + + const [diskError, diskResponse] = await safe(superagent.get(GRAPHITE_RENDER_URL) + .query(diskQuery) + .timeout(30 * 1000) + .ok(() => true)); + + if (diskError) throw new BoxError(BoxError.NETWORK_ERROR, diskError.message); + if (diskResponse.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Unknown error: ${diskError.message}`); + + return { memory: memoryResponse.body[0], disk: diskResponse.body[0] }; +} diff --git a/src/routes/graphs.js b/src/routes/graphs.js index e474f4429..59446c5e9 100644 --- a/src/routes/graphs.js +++ b/src/routes/graphs.js @@ -1,12 +1,17 @@ 'use strict'; exports = module.exports = { - getGraphs + getGraphs, + getAppGraphs }; -const middleware = require('../middleware/index.js'), +const assert = require('assert'), + graphs = require('../graphs.js'), + safe = require('safetydance'), + middleware = require('../middleware/index.js'), + url = require('url'), HttpError = require('connect-lastmile').HttpError, - url = require('url'); + HttpSuccess = require('connect-lastmile').HttpSuccess; // for testing locally: curl 'http://127.0.0.1:8417/graphite-web/render?format=json&from=-1min&target=absolute(collectd.localhost.du-docker.capacity-usage)' // the datapoint is (value, timestamp) https://buildmedia.readthedocs.org/media/pdf/graphite/0.9.16/graphite.pdf @@ -36,3 +41,15 @@ function getGraphs(req, res, next) { }); } +async function getAppGraphs(req, res, next) { + assert.strictEqual(typeof req.app, 'object'); + + if (!req.query.fromMinutes || !parseInt(req.query.fromMinutes)) return next(new HttpError(400, 'fromMinutes must be a number')); + + const fromMinutes = parseInt(req.query.fromMinutes) || 60; + const noNullPoints = !!req.query.noNullPoints; + const [error, result] = await safe(graphs.getByAppId(req.app.id, fromMinutes, noNullPoints)); + if (error) return next(new HttpError(500, error)); + + next(new HttpSuccess(200, result)); +} diff --git a/src/server.js b/src/server.js index bfe6c771a..df02399e7 100644 --- a/src/server.js +++ b/src/server.js @@ -245,7 +245,7 @@ function initializeExpressSync() { router.get ('/api/v1/apps/:id/eventlog', token, routes.apps.load, authorizeOperator, routes.apps.listEventlog); router.get ('/api/v1/apps/:id/limits', token, routes.apps.load, authorizeOperator, routes.apps.getLimits); router.get ('/api/v1/apps/:id/task', token, routes.apps.load, authorizeOperator, routes.apps.getTask); - router.get ('/api/v1/apps/:id/graphs', token, routes.apps.load, authorizeOperator, routes.graphs.getGraphs); // TODO: restrict to app graphs + router.get ('/api/v1/apps/:id/graphs', token, routes.apps.load, authorizeOperator, routes.graphs.getAppGraphs); router.post('/api/v1/apps/:id/clone', json, token, routes.apps.load, authorizeAdmin, routes.apps.clone); router.get ('/api/v1/apps/:id/download', token, routes.apps.load, authorizeOperator, routes.apps.downloadFile); router.post('/api/v1/apps/:id/upload', json, token, multipart, routes.apps.load, authorizeOperator, routes.apps.uploadFile);