mirror of https://github.com/portainer/portainer
feat(container-stats): introduce container block I/O stats (#5017)
* feat(container-stats):introduce container block io stats * Change charts to 2x2 view * fix(container-stats): handle missing io stats by detecting stats based on op codes Co-authored-by: DarkAEther <30438425+DarkAEther@users.noreply.github.com>pull/5143/head
parent
49bd139466
commit
6e9f472723
|
@ -91,6 +91,20 @@ export function ContainerStatsViewModel(data) {
|
||||||
this.CPUCores = data.cpu_stats.cpu_usage.percpu_usage.length;
|
this.CPUCores = data.cpu_stats.cpu_usage.percpu_usage.length;
|
||||||
}
|
}
|
||||||
this.Networks = _.values(data.networks);
|
this.Networks = _.values(data.networks);
|
||||||
|
if (data.blkio_stats !== undefined) {
|
||||||
|
//TODO: take care of multiple block devices
|
||||||
|
var readData = data.blkio_stats.io_service_bytes_recursive.find((d) => d.op === 'Read');
|
||||||
|
if (readData !== undefined) {
|
||||||
|
this.BytesRead = readData.value;
|
||||||
|
}
|
||||||
|
var writeData = data.blkio_stats.io_service_bytes_recursive.find((d) => d.op === 'Write');
|
||||||
|
if (writeData !== undefined) {
|
||||||
|
this.BytesWrite = writeData.value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//no IO related data is available
|
||||||
|
this.noIOdata = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ContainerDetailsViewModel(data) {
|
export function ContainerDetailsViewModel(data) {
|
||||||
|
|
|
@ -14,6 +14,7 @@ angular.module('portainer.docker').controller('ContainerStatsController', [
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
refreshRate: '5',
|
refreshRate: '5',
|
||||||
networkStatsUnavailable: false,
|
networkStatsUnavailable: false,
|
||||||
|
ioStatsUnavailable: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.$on('$destroy', function () {
|
$scope.$on('$destroy', function () {
|
||||||
|
@ -44,6 +45,13 @@ angular.module('portainer.docker').controller('ContainerStatsController', [
|
||||||
ChartService.UpdateMemoryChart(label, stats.MemoryUsage, stats.MemoryCache, chart);
|
ChartService.UpdateMemoryChart(label, stats.MemoryUsage, stats.MemoryCache, chart);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateIOChart(stats, chart) {
|
||||||
|
var label = moment(stats.read).format('HH:mm:ss');
|
||||||
|
if (stats.noIOData !== true) {
|
||||||
|
ChartService.UpdateIOChart(label, stats.BytesRead, stats.BytesWrite, chart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateCPUChart(stats, chart) {
|
function updateCPUChart(stats, chart) {
|
||||||
var label = moment(stats.read).format('HH:mm:ss');
|
var label = moment(stats.read).format('HH:mm:ss');
|
||||||
var value = stats.isWindows ? calculateCPUPercentWindows(stats) : calculateCPUPercentUnix(stats);
|
var value = stats.isWindows ? calculateCPUPercentWindows(stats) : calculateCPUPercentUnix(stats);
|
||||||
|
@ -77,14 +85,15 @@ angular.module('portainer.docker').controller('ContainerStatsController', [
|
||||||
var networkChart = $scope.networkChart;
|
var networkChart = $scope.networkChart;
|
||||||
var cpuChart = $scope.cpuChart;
|
var cpuChart = $scope.cpuChart;
|
||||||
var memoryChart = $scope.memoryChart;
|
var memoryChart = $scope.memoryChart;
|
||||||
|
var ioChart = $scope.ioChart;
|
||||||
|
|
||||||
stopRepeater();
|
stopRepeater();
|
||||||
setUpdateRepeater(networkChart, cpuChart, memoryChart);
|
setUpdateRepeater(networkChart, cpuChart, memoryChart, ioChart);
|
||||||
$('#refreshRateChange').show();
|
$('#refreshRateChange').show();
|
||||||
$('#refreshRateChange').fadeOut(1500);
|
$('#refreshRateChange').fadeOut(1500);
|
||||||
};
|
};
|
||||||
|
|
||||||
function startChartUpdate(networkChart, cpuChart, memoryChart) {
|
function startChartUpdate(networkChart, cpuChart, memoryChart, ioChart) {
|
||||||
$q.all({
|
$q.all({
|
||||||
stats: ContainerService.containerStats($transition$.params().id),
|
stats: ContainerService.containerStats($transition$.params().id),
|
||||||
top: ContainerService.containerTop($transition$.params().id),
|
top: ContainerService.containerTop($transition$.params().id),
|
||||||
|
@ -95,10 +104,14 @@ angular.module('portainer.docker').controller('ContainerStatsController', [
|
||||||
if (stats.Networks.length === 0) {
|
if (stats.Networks.length === 0) {
|
||||||
$scope.state.networkStatsUnavailable = true;
|
$scope.state.networkStatsUnavailable = true;
|
||||||
}
|
}
|
||||||
|
if (stats.noIOData === true) {
|
||||||
|
$scope.state.ioStatsUnavailable = true;
|
||||||
|
}
|
||||||
updateNetworkChart(stats, networkChart);
|
updateNetworkChart(stats, networkChart);
|
||||||
updateMemoryChart(stats, memoryChart);
|
updateMemoryChart(stats, memoryChart);
|
||||||
updateCPUChart(stats, cpuChart);
|
updateCPUChart(stats, cpuChart);
|
||||||
setUpdateRepeater(networkChart, cpuChart, memoryChart);
|
updateIOChart(stats, ioChart);
|
||||||
|
setUpdateRepeater(networkChart, cpuChart, memoryChart, ioChart);
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
stopRepeater();
|
stopRepeater();
|
||||||
|
@ -106,7 +119,7 @@ angular.module('portainer.docker').controller('ContainerStatsController', [
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setUpdateRepeater(networkChart, cpuChart, memoryChart) {
|
function setUpdateRepeater(networkChart, cpuChart, memoryChart, ioChart) {
|
||||||
var refreshRate = $scope.state.refreshRate;
|
var refreshRate = $scope.state.refreshRate;
|
||||||
$scope.repeater = $interval(function () {
|
$scope.repeater = $interval(function () {
|
||||||
$q.all({
|
$q.all({
|
||||||
|
@ -119,6 +132,7 @@ angular.module('portainer.docker').controller('ContainerStatsController', [
|
||||||
updateNetworkChart(stats, networkChart);
|
updateNetworkChart(stats, networkChart);
|
||||||
updateMemoryChart(stats, memoryChart);
|
updateMemoryChart(stats, memoryChart);
|
||||||
updateCPUChart(stats, cpuChart);
|
updateCPUChart(stats, cpuChart);
|
||||||
|
updateIOChart(stats, ioChart);
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
stopRepeater();
|
stopRepeater();
|
||||||
|
@ -140,7 +154,11 @@ angular.module('portainer.docker').controller('ContainerStatsController', [
|
||||||
var memoryChart = ChartService.CreateMemoryChart(memoryChartCtx);
|
var memoryChart = ChartService.CreateMemoryChart(memoryChartCtx);
|
||||||
$scope.memoryChart = memoryChart;
|
$scope.memoryChart = memoryChart;
|
||||||
|
|
||||||
startChartUpdate(networkChart, cpuChart, memoryChart);
|
var ioChartCtx = $('#ioChart');
|
||||||
|
var ioChart = ChartService.CreateIOChart(ioChartCtx);
|
||||||
|
$scope.ioChart = ioChart;
|
||||||
|
|
||||||
|
startChartUpdate(networkChart, cpuChart, memoryChart, ioChart);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
|
|
|
@ -42,6 +42,11 @@
|
||||||
<span class="small text-muted"> <i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true"></i> Network stats are unavailable for this container. </span>
|
<span class="small text-muted"> <i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true"></i> Network stats are unavailable for this container. </span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group" ng-if="state.ioStatsUnavailable">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<span class="small text-muted"> <i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true"></i> I/O stats are unavailable for this container. </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
</rd-widget>
|
</rd-widget>
|
||||||
|
@ -49,7 +54,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div ng-class="{ true: 'col-md-6 col-sm-12', false: 'col-lg-4 col-md-6 col-sm-12' }[state.networkStatsUnavailable]">
|
<div class="col-lg-6 col-md-6 col-sm-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-header icon="fa-chart-area" title-text="Memory usage"></rd-widget-header>
|
<rd-widget-header icon="fa-chart-area" title-text="Memory usage"></rd-widget-header>
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
|
@ -59,7 +64,8 @@
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
</rd-widget>
|
</rd-widget>
|
||||||
</div>
|
</div>
|
||||||
<div ng-class="{ true: 'col-md-6 col-sm-12', false: 'col-lg-4 col-md-6 col-sm-12' }[state.networkStatsUnavailable]">
|
|
||||||
|
<div class="col-lg-6 col-md-6 col-sm-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-header icon="fa-chart-area" title-text="CPU usage"></rd-widget-header>
|
<rd-widget-header icon="fa-chart-area" title-text="CPU usage"></rd-widget-header>
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
|
@ -69,7 +75,8 @@
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
</rd-widget>
|
</rd-widget>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-4 col-md-12 col-sm-12" ng-if="!state.networkStatsUnavailable">
|
|
||||||
|
<div class="col-lg-6 col-md-6 col-sm-12" ng-if="!state.networkStatsUnavailable">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-header icon="fa-chart-area" title-text="Network usage (aggregate)"></rd-widget-header>
|
<rd-widget-header icon="fa-chart-area" title-text="Network usage (aggregate)"></rd-widget-header>
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
|
@ -80,6 +87,19 @@
|
||||||
</rd-widget>
|
</rd-widget>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-6 col-md-6 col-sm-12" ng-if="!state.ioStatsUnavailable">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-header icon="fa-chart-area" title-text="I/O usage (aggregate)"></rd-widget-header>
|
||||||
|
<rd-widget-body>
|
||||||
|
<div class="chart-container" style="position: relative;">
|
||||||
|
<canvas id="ioChart" width="770" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<container-processes-datatable
|
<container-processes-datatable
|
||||||
title-text="Processes"
|
title-text="Processes"
|
||||||
|
|
|
@ -98,10 +98,48 @@ angular.module('portainer.app').factory('ChartService', [
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CreateIOChart(context, tooltipCallback, scalesCallback) {
|
||||||
|
return new Chart(context, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Read (Aggregate)',
|
||||||
|
data: [],
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: 'rgba(151,187,205,0.4)',
|
||||||
|
borderColor: 'rgba(151,187,205,0.6)',
|
||||||
|
pointBackgroundColor: 'rgba(151,187,205,1)',
|
||||||
|
pointBorderColor: 'rgba(151,187,205,1)',
|
||||||
|
pointRadius: 2,
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Write (Aggregate)',
|
||||||
|
data: [],
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: 'rgba(255,180,174,0.4)',
|
||||||
|
borderColor: 'rgba(255,180,174,0.6)',
|
||||||
|
pointBackgroundColor: 'rgba(255,180,174,1)',
|
||||||
|
pointBorderColor: 'rgba(255,180,174,1)',
|
||||||
|
pointRadius: 2,
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: defaultChartOptions('nearest', tooltipCallback, scalesCallback, true),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
service.CreateCPUChart = function (context) {
|
service.CreateCPUChart = function (context) {
|
||||||
return CreateChart(context, 'CPU', percentageBasedTooltipLabel, percentageBasedAxisLabel);
|
return CreateChart(context, 'CPU', percentageBasedTooltipLabel, percentageBasedAxisLabel);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.CreateIOChart = function (context) {
|
||||||
|
return CreateIOChart(context, byteBasedTooltipLabel, byteBasedAxisLabel);
|
||||||
|
};
|
||||||
|
|
||||||
service.CreateMemoryChart = function (context) {
|
service.CreateMemoryChart = function (context) {
|
||||||
return CreateMemoryChart(context, byteBasedTooltipLabel, byteBasedAxisLabel);
|
return CreateMemoryChart(context, byteBasedTooltipLabel, byteBasedAxisLabel);
|
||||||
};
|
};
|
||||||
|
@ -176,6 +214,13 @@ angular.module('portainer.app').factory('ChartService', [
|
||||||
chart.update(0);
|
chart.update(0);
|
||||||
};
|
};
|
||||||
service.UpdateCPUChart = UpdateChart;
|
service.UpdateCPUChart = UpdateChart;
|
||||||
|
service.UpdateIOChart = function (label, read, write, chart) {
|
||||||
|
chart.data.labels.push(label);
|
||||||
|
chart.data.datasets[0].data.push(read);
|
||||||
|
chart.data.datasets[1].data.push(write);
|
||||||
|
LimitChartItems(chart);
|
||||||
|
chart.update(0);
|
||||||
|
};
|
||||||
|
|
||||||
service.UpdateNetworkChart = function (label, rx, tx, chart) {
|
service.UpdateNetworkChart = function (label, rx, tx, chart) {
|
||||||
chart.data.labels.push(label);
|
chart.data.labels.push(label);
|
||||||
|
|
Loading…
Reference in New Issue