'use strict'; /* global Chart:false */ /* global asyncForEach:false */ /* global angular:false */ /* global $:false */ angular.module('Application').controller('GraphsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) { Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); }); $scope.memoryUsageSystem = []; $scope.memoryUsageApps = []; $scope.activeApp = null; $scope.disks = []; $scope.errorMessage = ''; $scope.installedApps = Client.getInstalledApps(); function bytesToMegaBytes(value) { return (value/1024/1024).toFixed(2); } // http://stackoverflow.com/questions/1484506/random-color-generator-in-javascript function getRandomColor() { var letters = '0123456789ABCDEF'.split(''); var color = '#'; for (var i = 0; i < 6; i++ ) { color += letters[Math.floor(Math.random() * 16)]; } return color; } var colorIndex = 0; var colors = [ '#2196F3', '#3995b1', '#f0ad4e', '#ff4c4c' ]; function getNextColor() { if (colors[colorIndex+1]) return colors[colorIndex++]; return getRandomColor(); } $scope.setError = function (context, error) { $scope.errorMessage = 'Error loading ' + context + ' stats : ' + error.message + '. Try restarting the graphite service.'; }; $scope.setMemoryApp = function (app, color) { $scope.activeApp = app; var timePeriod = 12 * 60; // in minutes var timeBucketSize = 60; // in minutes var target; if (app === 'system') target = 'summarize(collectd.localhost.memory.memory-used, "' + timeBucketSize + 'min", "avg")'; else target = 'summarize(collectd.localhost.table-' + app.id + '-memory.gauge-rss, "' + timeBucketSize + 'min", "avg")'; Client.graphs([target], '-' + timePeriod + 'min', {}, function (error, result) { if (error) return $scope.setError('memory', error); // translate the data from bytes to MB var datapoints = result[0].datapoints.map(function (d) { return parseInt((d[0] / 1024 / 1024).toFixed(2)); }); var labels = datapoints.map(function (d, index) { var dateTime = new Date(Date.now() - ((timePeriod - (index * timeBucketSize)) * 60 *1000)); return ('0' + dateTime.getHours()).slice(-2) + ':00'; }); var data = { labels: labels, datasets: [{ label: 'Memory', backgroundColor: color || '#82C4F8', borderColor: color || '#2196F3', borderWidth: 2, pointBackgroundColor: color || 'rgba(151,187,205,1)', pointBorderColor: color || '#2196F3', pointHoverBackgroundColor: color || '#82C4F8', pointHoverBorderColor: color || '#82C4F8', data: datapoints }] }; var scaleMax = 0; if ($scope.activeApp === 'system') { scaleMax = Client.getConfig().memory; } else { scaleMax = $scope.activeApp.memoryLimit || $scope.activeApp.manifest.memoryLimit || (256 * 1024 * 1024); } var stepSize; if (scaleMax >= (8 * 1024 * 1024 * 1024)) stepSize = 1024; else if (scaleMax >= (4 * 1024 * 1024 * 1024)) stepSize = 512; else if (scaleMax >= (2 * 1024 * 1024 * 1024)) stepSize = 256; else stepSize = 128; var options = { legend: { display: false }, scales: { yAxes: [{ ticks: { min: 0, max: Math.round(scaleMax / (1024 * 1024)), stepSize: stepSize, beginAtZero: true } }] } }; var ctx = $('#memoryAppChart').get(0).getContext('2d'); new Chart(ctx, { type: 'line', data: data, options: options }); }); }; $scope.updateDiskGraphs = function () { // https://graphite.readthedocs.io/en/latest/render_api.html#paths-and-wildcards // on scaleway, for some reason docker devices are collected as part of collectd // until we figure why just hardcode popular disk devices - https://www.mjmwired.net/kernel/Documentation/devices.txt Client.disks(function (error, result) { if (error) return $scope.setError('disk', error); result.disks.forEach(function (disk, index) { disk.id = index; disk.contains = []; if (disk.filesystem === result.platformDataDisk) disk.contains.push({ label: 'Platform data', id: 'platformdata', usage: 0 }); if (disk.filesystem === result.boxDataDisk) disk.contains.push({ label: 'Box data', id: 'boxdata', usage: 0 }); if (disk.filesystem === result.dockerDataDisk) disk.contains.push({ label: 'Docker images', id: 'docker', usage: 0 }); if (disk.filesystem === result.mailDataDisk) disk.contains.push({ label: 'Email data', id: 'maildata', usage: 0 }); const apps = Object.keys(result.apps).filter(function (appId) { return result.apps[appId] === disk.filesystem; }); apps.forEach(function (appId) { var app = Client.getCachedAppSync(appId); disk.contains.push({ app: app, label: app.label || app.fqdn, id: appId, usage: 0 }); }); }); $scope.disks = result.disks; // lazy fetch graphite data $scope.disks.forEach(function (disk) { // /dev/sda1 -> sda1 // /dev/mapper/foo -> mapper_foo (see #348) var diskName = disk.filesystem.slice(disk.filesystem.indexOf('/', 1) + 1); diskName = diskName.replace(/\//g, '_'); Client.graphs([ 'absolute(collectd.localhost.df-' + diskName + '.df_complex-free)', 'absolute(collectd.localhost.df-' + diskName + '.df_complex-reserved)', 'absolute(collectd.localhost.df-' + diskName + '.df_complex-used)' ], '-1min', {}, function (error, data) { if (error) return $scope.setError('disk', error); disk.size = data[2].datapoints[0][0] + data[1].datapoints[0][0] + data[0].datapoints[0][0]; disk.free = data[0].datapoints[0][0]; disk.occupied = data[2].datapoints[0][0] + data[1].datapoints[0][0]; colorIndex = 0; disk.contains.forEach(function (content) { content.color = getNextColor(); }); // get disk usage data var graphiteQueries = disk.contains.map(function (content) { return 'absolute(collectd.localhost.du-' + content.id + '.capacity-usage)'; }); Client.graphs(graphiteQueries, '-1day', { noNullPoints: true }, function (error, data) { if (error) return $scope.setError('disk', error); var usageOther = disk.occupied; data.forEach(function (d) { var content = disk.contains.find(function (content) { return d.target.indexOf(content.id) !== -1; }); if (!content) return; // didn't match any content var tmp = d.datapoints[d.datapoints.length-1][0]; content.usage = tmp; // deduct from overal disk usage to track other usageOther -= tmp; }); // add content container for other non tracked data disk.contains.push({ label: 'Other', id: 'other', color: '#27CE65', usage: usageOther }); }); }); }); }); }; $scope.updateMemorySystemChart = function () { var targets = []; var targetsInfo = []; targets.push('summarize(collectd.localhost.memory.memory-used, "1min", "avg")'); targetsInfo.push({ label: 'System', color: '#2196F3' }); targets.push('summarize(sum(collectd.localhost.memory.memory-buffered, collectd.localhost.memory.memory-cached), "1min", "avg")'); targetsInfo.push({ label: 'Cached', color: '#f0ad4e' }); targets.push('summarize(collectd.localhost.memory.memory-free, "1min", "avg")'); targetsInfo.push({ label: 'Free', color: '#27CE65' }); Client.graphs(targets, '-1min', {}, function (error, result) { if (error) return $scope.setError('memory', error); $scope.memoryUsageSystem = result.map(function (data, index) { return { value: bytesToMegaBytes(data.datapoints[0][0]), color: targetsInfo[index].color, highlight: targetsInfo[index].color, label: targetsInfo[index].label }; }); var tmp = { datasets: [{ data: result.map(function (data) { return bytesToMegaBytes(data.datapoints[0][0]); }), backgroundColor: result.map(function (data, index) { return targetsInfo[index].color; }) }], labels: result.map(function (data, index) { return targetsInfo[index].label; }) }; var ctx = $('#memoryUsageSystemChart').get(0).getContext('2d'); new Chart(ctx, { type: 'doughnut', data: tmp, options: { legend: { display: false }}}); $('#memoryUsageSystemChart').get(0).onclick = function () { $scope.setMemoryApp('system'); }; }); }; $scope.updateMemoryAppsChart = function () { var targets = []; var targetsInfo = []; colorIndex = 0; $scope.installedApps.forEach(function (app) { targets.push('summarize(collectd.localhost.table-' + app.id + '-memory.gauge-rss, "1min", "avg")'); targetsInfo.push({ label: app.fqdn, color: getNextColor(), app: app }); }); // we split up the request, to avoid too large query strings into graphite var tmp = []; var aggregatedResult= []; while (targets.length > 0) tmp.push(targets.splice(0, 10)); asyncForEach(tmp, function (targets, callback) { Client.graphs(targets, '-1min', {}, function (error, result) { if (error) return callback(error); aggregatedResult = aggregatedResult.concat(result); callback(null); }); }, function (error) { if (error) return $scope.setError('memory', error); $scope.memoryUsageApps = aggregatedResult.map(function (data, index) { return { value: bytesToMegaBytes(data.datapoints[0][0]), color: targetsInfo[index].color, highlight: targetsInfo[index].color, label: targetsInfo[index].label }; }); var tmp = { datasets: [{ data: aggregatedResult.map(function (data) { return bytesToMegaBytes(data.datapoints[0][0]); }), backgroundColor: aggregatedResult.map(function (data, index) { return targetsInfo[index].color; }) }], labels: aggregatedResult.map(function (data, index) { return targetsInfo[index].label; }) }; var options = { onClick: function (event, dataset) { var selectedDataInfo = targetsInfo.find(function (info) { return info.label === dataset[0]._model.label; }); if (selectedDataInfo) $scope.setMemoryApp(selectedDataInfo.app, selectedDataInfo.color); }, legend: { display: false } }; var ctx = $('#memoryUsageAppsChart').get(0).getContext('2d'); new Chart(ctx, { type: 'doughnut', data: tmp, options: options }); }); }; Client.onReady($scope.updateDiskGraphs); Client.onReady($scope.updateMemorySystemChart); Client.onReady($scope.updateMemoryAppsChart); Client.onReady($scope.setMemoryApp.bind(null, 'system')); $('.modal-backdrop').remove(); }]);