1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-04 21:35:23 +02:00

feat(container-stats): overhaul (#1183)

This commit is contained in:
Anthony Lapenna 2017-09-09 18:49:21 +02:00 committed by GitHub
parent b9b32f0526
commit c0d282e85b
13 changed files with 607 additions and 361 deletions

View file

@ -0,0 +1,123 @@
<rd-header>
<rd-header-title title="Container statistics">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="containers">Containers</a> &gt; <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> &gt; Stats
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-md-12">
<rd-widget>
<rd-widget-header icon="fa-info-circle" title="About statistics">
</rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">
This view displays real-time statistics about the container <b>{{ container.Name|trimcontainername }}</b> as well as a list of the running processes
inside this container.
</span>
</div>
</div>
<div class="form-group">
<label for="refreshRate" class="col-sm-3 col-md-2 col-lg-2 margin-sm-top control-label text-left">
Refresh rate
</label>
<div class="col-sm-3 col-md-2">
<select id="refreshRate" ng-model="state.refreshRate" ng-change="changeUpdateRepeater()" class="form-control">
<option value="5">5s</option>
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">60s</option>
</select>
</div>
<span>
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
</span>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-4 col-md-6 col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-area-chart" title="Memory usage"></rd-widget-header>
<rd-widget-body>
<div class="chart-container" style="position: relative;">
<canvas id="memoryChart" width="770" height="300"></canvas>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-4 col-md-6 col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-area-chart" title="CPU usage"></rd-widget-header>
<rd-widget-body>
<div class="chart-container" style="position: relative;">
<canvas id="cpuChart" width="770" height="300"></canvas>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-4 col-md-12 col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-area-chart" title="Network usage"></rd-widget-header>
<rd-widget-body>
<div class="chart-container" style="position: relative;">
<canvas id="networkChart" width="770" height="300"></canvas>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-sm-12" ng-if="applicationState.endpoint.mode.provider !== 'VMWARE_VIC'">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Processes">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table table-striped">
<thead>
<tr>
<th ng-repeat="title in processInfo.Titles">
<a ng-click="order(title)">
{{ title }}
<span ng-show="sortType == title && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == title && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="processDetails in state.filteredProcesses = (processInfo.Processes | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count)">
<td ng-repeat="procInfo in processDetails track by $index">{{ procInfo }}</td>
</tr>
<tr ng-if="!processInfo.Processes">
<td colspan="processInfo.Titles.length" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="state.filteredProcesses.length === 0">
<td colspan="processInfo.Titles.length" class="text-center text-muted">No processes available.</td>
</tr>
</tbody>
</table>
<div ng-if="processInfo.Processes" class="pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -0,0 +1,159 @@
angular.module('containerStats', [])
.controller('ContainerStatsController', ['$q', '$scope', '$stateParams', '$document', '$interval', 'ContainerService', 'ChartService', 'Notifications', 'Pagination',
function ($q, $scope, $stateParams, $document, $interval, ContainerService, ChartService, Notifications, Pagination) {
$scope.state = {
refreshRate: '5'
};
$scope.state.pagination_count = Pagination.getPaginationCount('stats_processes');
$scope.sortType = 'CMD';
$scope.sortReverse = false;
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('stats_processes', $scope.state.pagination_count);
};
$scope.$on('$destroy', function() {
stopRepeater();
});
function stopRepeater() {
var repeater = $scope.repeater;
if (angular.isDefined(repeater)) {
$interval.cancel(repeater);
repeater = null;
}
}
function updateNetworkChart(stats, chart) {
var rx = stats.Networks[0].rx_bytes;
var tx = stats.Networks[0].tx_bytes;
var label = moment(stats.Date).format('HH:mm:ss');
ChartService.UpdateNetworkChart(label, rx, tx, chart);
}
function updateMemoryChart(stats, chart) {
var label = moment(stats.Date).format('HH:mm:ss');
var value = stats.MemoryUsage;
ChartService.UpdateMemoryChart(label, value, chart);
}
function updateCPUChart(stats, chart) {
var label = moment(stats.Date).format('HH:mm:ss');
var value = calculateCPUPercentUnix(stats);
ChartService.UpdateCPUChart(label, value, chart);
}
function calculateCPUPercentUnix(stats) {
var cpuPercent = 0.0;
var cpuDelta = stats.CurrentCPUTotalUsage - stats.PreviousCPUTotalUsage;
var systemDelta = stats.CurrentCPUSystemUsage - stats.PreviousCPUSystemUsage;
if (systemDelta > 0.0 && cpuDelta > 0.0) {
cpuPercent = (cpuDelta / systemDelta) * stats.CPUCores * 100.0;
}
return cpuPercent;
}
$scope.changeUpdateRepeater = function() {
var networkChart = $scope.networkChart;
var cpuChart = $scope.cpuChart;
var memoryChart = $scope.memoryChart;
stopRepeater();
setUpdateRepeater(networkChart, cpuChart, memoryChart);
$('#refreshRateChange').show();
$('#refreshRateChange').fadeOut(1500);
};
function startChartUpdate(networkChart, cpuChart, memoryChart) {
$('#loadingViewSpinner').show();
$q.all({
stats: ContainerService.containerStats($stateParams.id),
top: ContainerService.containerTop($stateParams.id)
})
.then(function success(data) {
var stats = data.stats;
$scope.processInfo = data.top;
updateNetworkChart(stats, networkChart);
updateMemoryChart(stats, memoryChart);
updateCPUChart(stats, cpuChart);
setUpdateRepeater(networkChart, cpuChart, memoryChart);
})
.catch(function error(err) {
stopRepeater();
Notifications.error('Failure', err, 'Unable to retrieve container statistics');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
}
function setUpdateRepeater(networkChart, cpuChart, memoryChart) {
var refreshRate = $scope.state.refreshRate;
$scope.repeater = $interval(function() {
$q.all({
stats: ContainerService.containerStats($stateParams.id),
top: ContainerService.containerTop($stateParams.id)
})
.then(function success(data) {
var stats = data.stats;
$scope.processInfo = data.top;
updateNetworkChart(stats, networkChart);
updateMemoryChart(stats, memoryChart);
updateCPUChart(stats, cpuChart);
})
.catch(function error(err) {
stopRepeater();
Notifications.error('Failure', err, 'Unable to retrieve container statistics');
});
}, refreshRate * 1000);
}
function initCharts() {
var networkChartCtx = $('#networkChart');
var networkChart = ChartService.CreateNetworkChart(networkChartCtx);
$scope.networkChart = networkChart;
var cpuChartCtx = $('#cpuChart');
var cpuChart = ChartService.CreateCPUChart(cpuChartCtx);
$scope.cpuChart = cpuChart;
var memoryChartCtx = $('#memoryChart');
var memoryChart = ChartService.CreateMemoryChart(memoryChartCtx);
$scope.memoryChart = memoryChart;
startChartUpdate(networkChart, cpuChart, memoryChart);
}
function initView() {
$('#loadingViewSpinner').show();
ContainerService.container($stateParams.id)
.then(function success(data) {
$scope.container = data;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve container information');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
$document.ready(function() {
initCharts();
});
}
initView();
}]);

View file

@ -1,94 +0,0 @@
<rd-header>
<rd-header-title title="Container stats"></rd-header-title>
<rd-header-content>
<a ui-sref="containers">Containers</a> &gt; <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> &gt; Stats
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-server"></i>
</div>
<div class="title">{{ container.Name|trimcontainername }}</div>
<div class="comment">
Name
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<rd-widget>
<rd-widget-header icon="fa-area-chart" title="CPU usage"></rd-widget-header>
<rd-widget-body>
<canvas id="cpu-stats-chart" width="770" height="230"></canvas>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-6">
<rd-widget>
<rd-widget-header icon="fa-area-chart" title="Memory usage"></rd-widget-header>
<rd-widget-body>
<canvas id="memory-stats-chart" width="770" height="230"></canvas>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<rd-widget>
<rd-widget-header icon="fa-area-chart" title="Network usage"></rd-widget-header>
<rd-widget-body>
<canvas id="network-stats-chart" width="770" height="230"></canvas>
<div class="comment">
<div id="network-legend" style="margin-bottom: 20px;"></div>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-6" ng-if="applicationState.endpoint.mode.provider !== 'VMWARE_VIC'">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Processes">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table table-striped">
<thead>
<tr>
<th ng-repeat="title in containerTop.Titles">
<a ui-sref="stats({id: container.Id})" ng-click="order(title)">
{{title}}
<span ng-show="sortType == title && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == title && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="processInfos in state.filteredProcesses = (containerTop.Processes | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count)">
<td ng-repeat="processInfo in processInfos track by $index">{{processInfo}}</td>
</tr>
</tbody>
</table>
<div ng-if="containerTop.Processes" class="pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -1,220 +0,0 @@
angular.module('stats', [])
.controller('StatsController', ['Pagination', '$scope', 'Notifications', '$timeout', 'Container', 'ContainerTop', '$stateParams', 'humansizeFilter', '$sce', '$document',
function (Pagination, $scope, Notifications, $timeout, Container, ContainerTop, $stateParams, humansizeFilter, $sce, $document) {
// TODO: Force scale to 0-100 for cpu, fix charts on dashboard,
// TODO: Force memory scale to 0 - max memory
$scope.ps_args = '';
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('stats_processes');
$scope.sortType = 'CMD';
$scope.sortReverse = false;
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('stats_processes', $scope.state.pagination_count);
};
$scope.getTop = function () {
ContainerTop.get($stateParams.id, {
ps_args: $scope.ps_args
}, function (data) {
$scope.containerTop = data;
});
};
var destroyed = false;
var timeout;
$document.ready(function(){
var cpuLabels = [];
var cpuData = [];
var memoryLabels = [];
var memoryData = [];
var networkLabels = [];
var networkTxData = [];
var networkRxData = [];
for (var i = 0; i < 20; i++) {
cpuLabels.push('');
cpuData.push(0);
memoryLabels.push('');
memoryData.push(0);
networkLabels.push('');
networkTxData.push(0);
networkRxData.push(0);
}
var cpuDataset = { // CPU Usage
fillColor: 'rgba(151,187,205,0.5)',
strokeColor: 'rgba(151,187,205,1)',
pointColor: 'rgba(151,187,205,1)',
pointStrokeColor: '#fff',
data: cpuData
};
var memoryDataset = {
fillColor: 'rgba(151,187,205,0.5)',
strokeColor: 'rgba(151,187,205,1)',
pointColor: 'rgba(151,187,205,1)',
pointStrokeColor: '#fff',
data: memoryData
};
var networkRxDataset = {
label: 'Rx Bytes',
fillColor: 'rgba(151,187,205,0.5)',
strokeColor: 'rgba(151,187,205,1)',
pointColor: 'rgba(151,187,205,1)',
pointStrokeColor: '#fff',
data: networkRxData
};
var networkTxDataset = {
label: 'Tx Bytes',
fillColor: 'rgba(255,180,174,0.5)',
strokeColor: 'rgba(255,180,174,1)',
pointColor: 'rgba(255,180,174,1)',
pointStrokeColor: '#fff',
data: networkTxData
};
var networkLegendData = [
{
//value: '',
color: 'rgba(151,187,205,0.5)',
title: 'Rx Data'
},
{
//value: '',
color: 'rgba(255,180,174,0.5)',
title: 'Tx Data'
}
];
legend($('#network-legend').get(0), networkLegendData);
Chart.defaults.global.animationSteps = 30; // Lower from 60 to ease CPU load.
var cpuChart = new Chart($('#cpu-stats-chart').get(0).getContext('2d')).Line({
labels: cpuLabels,
datasets: [cpuDataset]
}, {
responsive: true
});
var memoryChart = new Chart($('#memory-stats-chart').get(0).getContext('2d')).Line({
labels: memoryLabels,
datasets: [memoryDataset]
},
{
scaleLabel: function (valueObj) {
return humansizeFilter(parseInt(valueObj.value, 10), 2);
},
responsive: true
//scaleOverride: true,
//scaleSteps: 10,
//scaleStepWidth: Math.ceil(initialStats.memory_stats.limit / 10),
//scaleStartValue: 0
});
var networkChart = new Chart($('#network-stats-chart').get(0).getContext('2d')).Line({
labels: networkLabels,
datasets: [networkRxDataset, networkTxDataset]
}, {
scaleLabel: function (valueObj) {
return humansizeFilter(parseInt(valueObj.value, 10), 2);
},
responsive: true
});
$scope.networkLegend = $sce.trustAsHtml(networkChart.generateLegend());
function updateStats() {
Container.stats({id: $stateParams.id}, function (d) {
var arr = Object.keys(d).map(function (key) {
return d[key];
});
if (arr.join('').indexOf('no such id') !== -1) {
Notifications.error('Unable to retrieve stats', {}, 'Is this container running?');
return;
}
// Update graph with latest data
$scope.data = d;
updateCpuChart(d);
updateMemoryChart(d);
updateNetworkChart(d);
setUpdateStatsTimeout();
}, function () {
Notifications.error('Unable to retrieve stats', {}, 'Is this container running?');
setUpdateStatsTimeout();
});
}
$scope.$on('$destroy', function () {
destroyed = true;
$timeout.cancel(timeout);
});
updateStats();
function updateCpuChart(data) {
cpuChart.addData([calculateCPUPercent(data)], new Date(data.read).toLocaleTimeString());
cpuChart.removeData();
}
function updateMemoryChart(data) {
memoryChart.addData([data.memory_stats.usage], new Date(data.read).toLocaleTimeString());
memoryChart.removeData();
}
var lastRxBytes = 0, lastTxBytes = 0;
function updateNetworkChart(data) {
// 1.9+ contains an object of networks, for now we'll just show stats for the first network
// TODO: Show graphs for all networks
if (data.networks) {
$scope.networkName = Object.keys(data.networks)[0];
data.network = data.networks[$scope.networkName];
}
if(data.network) {
var rxBytes = 0, txBytes = 0;
if (lastRxBytes !== 0 || lastTxBytes !== 0) {
// These will be zero on first call, ignore to prevent large graph spike
rxBytes = data.network.rx_bytes - lastRxBytes;
txBytes = data.network.tx_bytes - lastTxBytes;
}
lastRxBytes = data.network.rx_bytes;
lastTxBytes = data.network.tx_bytes;
networkChart.addData([rxBytes, txBytes], new Date(data.read).toLocaleTimeString());
networkChart.removeData();
}
}
function calculateCPUPercent(stats) {
// Same algorithm the official client uses: https://github.com/docker/docker/blob/master/api/client/stats.go#L195-L208
var prevCpu = stats.precpu_stats;
var curCpu = stats.cpu_stats;
var cpuPercent = 0.0;
// calculate the change for the cpu usage of the container in between readings
var cpuDelta = curCpu.cpu_usage.total_usage - prevCpu.cpu_usage.total_usage;
// calculate the change for the entire system between readings
var systemDelta = curCpu.system_cpu_usage - prevCpu.system_cpu_usage;
if (systemDelta > 0.0 && cpuDelta > 0.0) {
cpuPercent = (cpuDelta / systemDelta) * curCpu.cpu_usage.percpu_usage.length * 100.0;
}
return cpuPercent;
}
function setUpdateStatsTimeout() {
if(!destroyed) {
timeout = $timeout(updateStats, 5000);
}
}
});
Container.get({id: $stateParams.id}, function (d) {
$scope.container = d;
}, function (e) {
Notifications.error('Failure', e, 'Unable to retrieve container info');
});
var endpointProvider = $scope.applicationState.endpoint.mode.provider;
if (endpointProvider !== 'VMWARE_VIC') {
$scope.getTop();
}
}]);