diff --git a/src/graphs.js b/src/graphs.js index 70295a082..5d0fc0866 100644 --- a/src/graphs.js +++ b/src/graphs.js @@ -1,15 +1,19 @@ 'use strict'; exports = module.exports = { + getSystem, getByAppId }; -const assert = require('assert'), +const apps = require('./apps.js'), + assert = require('assert'), BoxError = require('./boxerror.js'), fs = require('fs'), safe = require('safetydance'), superagent = require('superagent'); +// 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 const GRAPHITE_RENDER_URL = 'http://127.0.0.1:8417/graphite-web/render'; // https://rootlesscontaine.rs/getting-started/common/cgroup2/#checking-whether-cgroup-v2-is-already-enabled @@ -41,7 +45,7 @@ async function getByAppId(appId, fromMinutes, noNullPoints) { .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}`); + if (memoryResponse.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Unknown error: ${memoryResponse.status} ${memoryResponse.text}`); const diskQuery = { target: `summarize(collectd.localhost.du-${appId}.capacity-usage, "${timeBucketSize}min", "avg")`, @@ -57,7 +61,67 @@ async function getByAppId(appId, fromMinutes, noNullPoints) { .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}`); + if (diskResponse.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Unknown error: ${diskResponse.status} ${diskResponse.text}`); return { memory: memoryResponse.body[0], disk: diskResponse.body[0] }; } + +async function getSystem(fromMinutes, noNullPoints) { + assert.strictEqual(typeof fromMinutes, 'number'); + assert.strictEqual(typeof noNullPoints, 'boolean'); + + const timeBucketSize = fromMinutes > (24 * 60) ? (6*60) : 5; + + const cpuQuery = `summarize(sum(collectd.localhost.aggregation-cpu-average.cpu-system, collectd.localhost.aggregation-cpu-average.cpu-user), "${timeBucketSize}min", "avg")`; + const memoryQuery = `summarize(sum(collectd.localhost.memory.memory-used, collectd.localhost.swap.swap-used), "${timeBucketSize}min", "avg")`; + + const query = { + target: [ cpuQuery, memoryQuery ], + format: 'json', + from: `-${fromMinutes}min`, + until: 'now' + }; + + const [memCpuError, memCpuResponse] = await safe(superagent.get(GRAPHITE_RENDER_URL) + .query(query) + .timeout(30 * 1000) + .ok(() => true)); + + if (memCpuError) throw new BoxError(BoxError.NETWORK_ERROR, memCpuError.message); + if (memCpuResponse.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Unknown error: ${memCpuResponse.status} ${memCpuResponse.text}`); + + const allApps = await apps.list(); + const appResponses = {}; + for (const app of allApps) { + appResponses[app.id] = await getByAppId(app.id, fromMinutes, noNullPoints); + } + + // const disks = await system.getDisks(); + // const diskResponses = {}; // indexed by filesystem + // for (const disk of disks) { + // // /dev/sda1 -> sda1 + // // /dev/mapper/foo.com -> mapper_foo_com (see #348) + // let diskName = disk.filesystem.slice(disk.filesystem.indexOf('/', 1) + 1); + // diskName = diskName.replace(/\/|\./g, '_'); + + // const target = [ + // `absolute(collectd.localhost.df-${diskName}.df_complex-free)`, + // `absolute(collectd.localhost.df-${diskName}.df_complex-reserved)`, // reserved for root (default: 5%) tune2fs -l/m + // `absolute(collectd.localhost.df-${diskName}.df_complex-used)` + // ]; + + // const diskQuery = { + // target: target, + // format: 'json', + // from: '-1min', + // until: 'now' + // }; + + // 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: ${diskResponse.status} ${diskResponse.text}`); + // diskResponses[disk.filesystem] = diskResponse.body[0]; + // } + + return { cpu: memCpuResponse.body[0], memory: memCpuResponse.body[1], apps: appResponses }; +} diff --git a/src/routes/graphs.js b/src/routes/graphs.js index add00602a..2b24d1403 100644 --- a/src/routes/graphs.js +++ b/src/routes/graphs.js @@ -1,7 +1,7 @@ 'use strict'; exports = module.exports = { - getGraphs, + getSystemGraphs, getAppGraphs }; @@ -9,36 +9,17 @@ const assert = require('assert'), graphs = require('../graphs.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, - middleware = require('../middleware/index.js'), - safe = require('safetydance'), - url = require('url'); + safe = require('safetydance'); -// 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 -const graphiteProxy = middleware.proxy(url.parse('http://127.0.0.1:8417')); +async function getSystemGraphs(req, res, next) { + if (!req.query.fromMinutes || !parseInt(req.query.fromMinutes)) return next(new HttpError(400, 'fromMinutes must be a number')); -function getGraphs(req, res, next) { - const parsedUrl = url.parse(req.url, true /* parseQueryString */); - delete parsedUrl.query['access_token']; - delete req.headers['authorization']; - delete req.headers['cookies']; + const fromMinutes = parseInt(req.query.fromMinutes); + const noNullPoints = !!req.query.noNullPoints; + const [error, result] = await safe(graphs.getSystem(fromMinutes, noNullPoints)); + if (error) return next(new HttpError(500, error)); - // 'graphite-web' is the URL_PREFIX in docker-graphite - req.url = url.format({ pathname: 'graphite-web/render', query: parsedUrl.query }); - - // graphs may take very long to respond so we run into headers already sent issues quite often - // nginx still has a request timeout which can deal with this then. - req.clearTimeout(); - - graphiteProxy(req, res, function (error) { - if (!error) return next(); - - if (error.code === 'ECONNREFUSED') return next(new HttpError(424, 'Unable to connect to graphite')); - // ECONNRESET here is most likely because of a bug in the query or the uwsgi buffer size is too small - if (error.code === 'ECONNRESET') return next(new HttpError(424, 'Unable to query graphite')); - - next(new HttpError(500, error)); - }); + next(new HttpSuccess(200, result)); } async function getAppGraphs(req, res, next) { diff --git a/src/server.js b/src/server.js index df02399e7..b80ef87b8 100644 --- a/src/server.js +++ b/src/server.js @@ -113,7 +113,7 @@ function initializeExpressSync() { router.post('/api/v1/cloudron/check_for_updates', json, token, authorizeAdmin, routes.cloudron.checkForUpdates); router.get ('/api/v1/cloudron/reboot', token, authorizeAdmin, routes.cloudron.isRebootRequired); router.post('/api/v1/cloudron/reboot', json, token, authorizeAdmin, routes.cloudron.reboot); - router.get ('/api/v1/cloudron/graphs', token, authorizeAdmin, routes.graphs.getGraphs); + router.get ('/api/v1/cloudron/graphs', token, authorizeAdmin, routes.graphs.getSystemGraphs); router.get ('/api/v1/cloudron/disks', token, authorizeAdmin, routes.cloudron.getDisks); router.get ('/api/v1/cloudron/memory', token, authorizeAdmin, routes.cloudron.getMemory); router.get ('/api/v1/cloudron/logs/:unit', token, authorizeAdmin, routes.cloudron.getLogs);