Files
cloudron-box/src/graphs.js
T

175 lines
8.1 KiB
JavaScript
Raw Normal View History

2022-09-14 13:03:14 +02:00
'use strict';
exports = module.exports = {
getSystem,
getByApp
2022-09-14 13:03:14 +02:00
};
const apps = require('./apps.js'),
assert = require('assert'),
2022-09-14 13:03:14 +02:00
BoxError = require('./boxerror.js'),
docker = require('./docker.js'),
2022-09-14 13:03:14 +02:00
safe = require('safetydance'),
2022-09-15 14:43:04 +02:00
superagent = require('superagent'),
system = require('./system.js');
2022-09-14 13:03:14 +02:00
// for testing locally: curl 'http://${graphite-ip}:8000/graphite-web/render?format=json&from=-1min&target=absolute(collectd.localhost.du-docker.capacity-usage)'
// the datapoint is (value, timestamp) https://graphite.readthedocs.io/en/latest/
async function getGraphiteUrl() {
const [error, result] = await safe(docker.inspect('graphite'));
if (error && error.reason === BoxError.NOT_FOUND) return { status: exports.SERVICE_STATUS_STOPPED };
if (error) throw error;
const ip = safe.query(result, 'NetworkSettings.Networks.cloudron.IPAddress', null);
if (!ip) throw new BoxError(BoxError.INACTIVE, 'Error getting IP of graphite service');
return `http://${ip}:8000/graphite-web/render`;
}
2022-09-14 13:03:14 +02:00
async function getByApp(app, fromMinutes, noNullPoints) {
assert.strictEqual(typeof app, 'object');
2022-09-14 13:03:14 +02:00
assert.strictEqual(typeof fromMinutes, 'number');
assert.strictEqual(typeof noNullPoints, 'boolean');
const timeBucketSize = fromMinutes > (24 * 60) ? (6*60) : 5;
const graphiteUrl = await getGraphiteUrl();
2022-09-14 13:03:14 +02:00
2022-10-10 19:52:29 +02:00
const targets = [
`summarize(collectd.localhost.docker-stats-${app.id}.gauge-cpu-perc, "${timeBucketSize}min", "avg")`,
`summarize(collectd.localhost.docker-stats-${app.id}.gauge-mem-used, "${timeBucketSize}min", "avg")`,
// `summarize(collectd.localhost.docker-stats-${app.id}.gauge-mem-max, "${timeBucketSize}min", "avg")`,
`summarize(collectd.localhost.docker-stats-${app.id}.counter-blockio-read, "${timeBucketSize}min", "sum")`,
`summarize(collectd.localhost.docker-stats-${app.id}.counter-blockio-write, "${timeBucketSize}min", "sum")`,
`summarize(collectd.localhost.docker-stats-${app.id}.counter-network-read, "${timeBucketSize}min", "sum")`,
`summarize(collectd.localhost.docker-stats-${app.id}.counter-network-write, "${timeBucketSize}min", "sum")`,
2022-10-10 19:52:29 +02:00
];
2022-09-14 13:03:14 +02:00
2022-10-10 19:52:29 +02:00
const results = [];
2022-09-14 13:03:14 +02:00
2022-10-10 19:52:29 +02:00
for (const target of targets) {
const query = {
target: target,
format: 'json',
from: `-${fromMinutes}min`,
2022-10-10 19:52:29 +02:00
until: 'now',
noNullPoints: !!noNullPoints
};
2022-09-14 13:03:14 +02:00
const [error, response] = await safe(superagent.get(graphiteUrl).query(query).timeout(30 * 1000).ok(() => true));
2022-10-10 19:52:29 +02:00
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Unknown error with ${target}: ${response.status} ${response.text}`);
2022-09-14 13:03:14 +02:00
2022-10-10 19:52:29 +02:00
results.push(response.body[0]);
}
2022-09-14 13:03:14 +02:00
2022-10-10 19:52:29 +02:00
return {
cpu: results[0],
memory: results[1],
blockRead: results[2],
blockWrite: results[3],
networkRead: results[4],
networkWrite: results[5]
};
2022-09-14 13:03:14 +02:00
}
async function getSystem(fromMinutes, noNullPoints) {
assert.strictEqual(typeof fromMinutes, 'number');
assert.strictEqual(typeof noNullPoints, 'boolean');
const timeBucketSize = fromMinutes > (24 * 60) ? (6*60) : 5;
const graphiteUrl = await getGraphiteUrl();
const cpuQuery = `summarize(sum(collectd.localhost.aggregation-cpu-average.cpu-system, collectd.localhost.aggregation-cpu-average.cpu-user), "${timeBucketSize}min", "avg")`;
2022-10-11 11:54:02 +02:00
const memoryQuery = `summarize(collectd.localhost.memory.memory-used, "${timeBucketSize}min", "avg")`;
const query = {
target: [ cpuQuery, memoryQuery ],
format: 'json',
from: `-${fromMinutes}min`,
until: 'now'
};
const [memCpuError, memCpuResponse] = await safe(superagent.get(graphiteUrl).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 getByApp(app, fromMinutes, noNullPoints);
}
2022-09-15 14:43:04 +02:00
const diskInfo = await system.getDisks();
// segregate locations into the correct disks based on 'filesystem'
diskInfo.disks.forEach(function (disk, index) {
disk.id = index;
disk.contains = [];
if (disk.filesystem === diskInfo.platformDataDisk) disk.contains.push({ type: 'standard', label: 'Platform data', id: 'platformdata', usage: 0 });
if (disk.filesystem === diskInfo.boxDataDisk) disk.contains.push({ type: 'standard', label: 'Box data', id: 'boxdata', usage: 0 });
if (disk.filesystem === diskInfo.dockerDataDisk) disk.contains.push({ type: 'standard', label: 'Docker images', id: 'docker', usage: 0 });
if (disk.filesystem === diskInfo.mailDataDisk) disk.contains.push({ type: 'standard', label: 'Email data', id: 'maildata', usage: 0 });
if (disk.filesystem === diskInfo.backupsDisk) disk.contains.push({ type: 'standard', label: 'Backup data', id: 'cloudron-backup', usage: 0 });
// attach appIds which reside on this disk
const apps = Object.keys(diskInfo.apps).filter(function (appId) { return diskInfo.apps[appId] === disk.filesystem; });
apps.forEach(function (appId) {
disk.contains.push({ type: 'app', id: appId, label: '', usage: 0 });
2022-09-15 14:43:04 +02:00
});
// attach volumeIds which reside on this disk
const volumes = Object.keys(diskInfo.volumes).filter(function (volumeId) { return diskInfo.volumes[volumeId] === disk.filesystem; });
volumes.forEach(function (volumeId) {
disk.contains.push({ type: 'volume', id: volumeId, label: '', usage: 0 });
2022-09-15 14:43:04 +02:00
});
});
for (const disk of diskInfo.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: '-1day',
2022-09-15 14:43:04 +02:00
until: 'now'
};
const [diskError, diskResponse] = await safe(superagent.get(graphiteUrl).query(diskQuery).timeout(30 * 1000).ok(() => true));
2022-09-15 14:43:04 +02:00
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}`);
disk.size = diskResponse.body[2].datapoints[0][0] + diskResponse.body[1].datapoints[0][0] + diskResponse.body[0].datapoints[0][0];
disk.free = diskResponse.body[0].datapoints[0][0];
disk.occupied = diskResponse.body[2].datapoints[0][0];
for (const content of disk.contains) {
const query = {
target: `absolute(collectd.localhost.du-${content.id}.capacity-usage)`,
format: 'json',
from: '-1day',
2022-09-15 14:43:04 +02:00
until: 'now'
};
const [error, response] = await safe(superagent.get(graphiteUrl).query(query).timeout(30 * 1000).ok(() => true));
2022-09-15 14:43:04 +02:00
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Unknown error: ${response.status} ${response.text}`);
// we may not have any datapoints
if (response.body.length === 0) content.usage = null;
else content.usage = response.body[0].datapoints[0][0];
2022-09-15 14:43:04 +02:00
}
}
return { cpu: memCpuResponse.body[0], memory: memCpuResponse.body[1], apps: appResponses, disks: diskInfo.disks };
}