mirror of
https://github.com/portainer/portainer.git
synced 2025-08-04 21:35:23 +02:00
feat(k8s/container): realtime metrics (#4416)
* feat(k8s/container): metrics layout * feat(k8s/container): memory graph * feat(k8s/container): cpu usage percent * feat(k8s/metrics): metrics api validation to enable metrics server * feat(k8s/pods): update error metrics view * feat(k8s/container): improve stopRepeater function * feat(k8s/pods): display empty view instead of empty graphs * feat(k8s/pods): fix CPU usage * feat(k8s/configure): fix the metrics server test * feat(k8s/pod): fix cpu issue * feat(k8s/pod): fix toaster for non register pods in metrics server * feat(k8s/service): remove options before 30 secondes for refresh rate * feat(k8s/pod): fix default value for the refresh rate * feat(k8s/pod): fix rebase
This commit is contained in:
parent
befccacc27
commit
d99358ea8e
13 changed files with 411 additions and 3 deletions
|
@ -587,6 +587,7 @@
|
|||
table-key="kubernetes.application.containers"
|
||||
is-pod="ctrl.application.ApplicationType === ctrl.KubernetesApplicationTypes.POD"
|
||||
order-by="{{ ctrl.application.ApplicationType === ctrl.KubernetesApplicationTypes.POD ? 'Name' : 'PodName' }}"
|
||||
use-server-metrics="ctrl.state.useServerMetrics"
|
||||
>
|
||||
</kubernetes-containers-datatable>
|
||||
</div>
|
||||
|
|
|
@ -106,7 +106,8 @@ class KubernetesApplicationController {
|
|||
KubernetesStackService,
|
||||
KubernetesPodService,
|
||||
KubernetesNodeService,
|
||||
KubernetesNamespaceHelper
|
||||
KubernetesNamespaceHelper,
|
||||
EndpointProvider
|
||||
) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
|
@ -125,6 +126,8 @@ class KubernetesApplicationController {
|
|||
|
||||
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
|
||||
this.KubernetesApplicationTypes = KubernetesApplicationTypes;
|
||||
this.EndpointProvider = EndpointProvider;
|
||||
|
||||
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
|
||||
this.KubernetesServiceTypes = KubernetesServiceTypes;
|
||||
this.KubernetesPodContainerTypes = KubernetesPodContainerTypes;
|
||||
|
@ -335,6 +338,7 @@ class KubernetesApplicationController {
|
|||
placementWarning: false,
|
||||
expandedNote: false,
|
||||
useIngress: false,
|
||||
useServerMetrics: this.EndpointProvider.currentEndpoint().Kubernetes.Configuration.UseServerMetrics,
|
||||
};
|
||||
|
||||
this.state.activeTab = this.LocalStorage.getActiveTab('application');
|
||||
|
|
82
app/kubernetes/views/applications/stats/stats.html
Normal file
82
app/kubernetes/views/applications/stats/stats.html
Normal file
|
@ -0,0 +1,82 @@
|
|||
<kubernetes-view-header title="Application stats" state="kubernetes.applications.application.stats" view-ready="ctrl.state.viewReady">
|
||||
<a ui-sref="kubernetes.resourcePools">Resource pools</a> >
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.state.transition.namespace })">{{ ctrl.state.transition.namespace }}</a> >
|
||||
<a ui-sref="kubernetes.applications">Applications</a> >
|
||||
<a ui-sref="kubernetes.applications.application({ name: ctrl.state.transition.applicationName, namespace: ctrl.state.transition.namespace })">{{
|
||||
ctrl.state.transition.applicationName
|
||||
}}</a>
|
||||
> Pods > {{ ctrl.state.transition.podName }} > Containers > {{ ctrl.state.transition.containerName }} > Stats
|
||||
</kubernetes-view-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<information-panel ng-if="!ctrl.state.getMetrics" title-text="Unable to retrieve container metrics">
|
||||
<span class="small text-muted">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Portainer was unable to retrieve any metrics associated to that container. Please contact your administrator to ensure that the Kubernetes metrics feature is properly
|
||||
configured.
|
||||
</span>
|
||||
</information-panel>
|
||||
<div class="row" ng-if="ctrl.state.getMetrics">
|
||||
<div class="col-md-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-info-circle" title-text="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>{{ ctrl.state.transition.containerName | trimcontainername }}</b
|
||||
>.
|
||||
</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="ctrl.state.refreshRate" ng-change="ctrl.changeUpdateRepeater()" class="form-control">
|
||||
<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>
|
||||
<div class="form-group" ng-if="ctrl.state.networkStatsUnavailable">
|
||||
<div class="col-sm-12">
|
||||
<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>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="ctrl.state.getMetrics">
|
||||
<div class="col-lg-6 col-md-12 col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-chart-area" title-text="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-6 col-md-12 col-sm-12" ng-if="!ctrl.state.networkStatsUnavailable">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-chart-area" title-text="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>
|
||||
</div>
|
8
app/kubernetes/views/applications/stats/stats.js
Normal file
8
app/kubernetes/views/applications/stats/stats.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesApplicationStatsView', {
|
||||
templateUrl: './stats.html',
|
||||
controller: 'KubernetesApplicationStatsController',
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
},
|
||||
});
|
164
app/kubernetes/views/applications/stats/statsController.js
Normal file
164
app/kubernetes/views/applications/stats/statsController.js
Normal file
|
@ -0,0 +1,164 @@
|
|||
import angular from 'angular';
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash-es';
|
||||
import filesizeParser from 'filesize-parser';
|
||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||
import KubernetesPodConverter from 'Kubernetes/pod/converter';
|
||||
|
||||
class KubernetesApplicationStatsController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, $interval, $document, Notifications, KubernetesPodService, KubernetesNodeService, KubernetesMetricsService, ChartService) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.$interval = $interval;
|
||||
this.$document = $document;
|
||||
this.Notifications = Notifications;
|
||||
this.KubernetesPodService = KubernetesPodService;
|
||||
this.KubernetesNodeService = KubernetesNodeService;
|
||||
this.KubernetesMetricsService = KubernetesMetricsService;
|
||||
this.ChartService = ChartService;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
}
|
||||
|
||||
changeUpdateRepeater() {
|
||||
var cpuChart = this.cpuChart;
|
||||
var memoryChart = this.memoryChart;
|
||||
|
||||
this.stopRepeater();
|
||||
this.setUpdateRepeater(cpuChart, memoryChart);
|
||||
$('#refreshRateChange').show();
|
||||
$('#refreshRateChange').fadeOut(1500);
|
||||
}
|
||||
|
||||
updateCPUChart() {
|
||||
const label = moment(this.stats.read).format('HH:mm:ss');
|
||||
|
||||
this.ChartService.UpdateCPUChart(label, this.stats.CPUUsage, this.cpuChart);
|
||||
}
|
||||
|
||||
updateMemoryChart() {
|
||||
const label = moment(this.stats.read).format('HH:mm:ss');
|
||||
|
||||
this.ChartService.UpdateMemoryChart(label, this.stats.MemoryUsage, this.stats.MemoryCache, this.memoryChart);
|
||||
}
|
||||
|
||||
stopRepeater() {
|
||||
var repeater = this.repeater;
|
||||
if (angular.isDefined(repeater)) {
|
||||
this.$interval.cancel(repeater);
|
||||
repeater = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
setUpdateRepeater() {
|
||||
const refreshRate = this.state.refreshRate;
|
||||
|
||||
this.repeater = this.$interval(async () => {
|
||||
try {
|
||||
await this.getStats();
|
||||
|
||||
this.updateCPUChart();
|
||||
this.updateMemoryChart();
|
||||
} catch (error) {
|
||||
this.stopRepeater();
|
||||
this.Notifications.error('Failure', error);
|
||||
}
|
||||
}, refreshRate * 1000);
|
||||
}
|
||||
|
||||
initCharts() {
|
||||
const cpuChartCtx = $('#cpuChart');
|
||||
const cpuChart = this.ChartService.CreateCPUChart(cpuChartCtx);
|
||||
this.cpuChart = cpuChart;
|
||||
|
||||
const memoryChartCtx = $('#memoryChart');
|
||||
const memoryChart = this.ChartService.CreateMemoryChart(memoryChartCtx);
|
||||
this.memoryChart = memoryChart;
|
||||
|
||||
this.updateCPUChart();
|
||||
this.updateMemoryChart();
|
||||
this.setUpdateRepeater();
|
||||
}
|
||||
|
||||
getStats() {
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
const stats = await this.KubernetesMetricsService.getPod(this.state.transition.namespace, this.state.transition.podName);
|
||||
const container = _.find(stats.containers, { name: this.state.transition.containerName });
|
||||
if (container) {
|
||||
const memory = filesizeParser(container.usage.memory);
|
||||
const cpu = KubernetesResourceReservationHelper.parseCPU(container.usage.cpu);
|
||||
this.stats = {
|
||||
read: stats.timestamp,
|
||||
preread: '',
|
||||
MemoryCache: 0,
|
||||
MemoryUsage: memory,
|
||||
NumProcs: '',
|
||||
isWindows: false,
|
||||
PreviousCPUTotalUsage: 0,
|
||||
CPUUsage: (cpu / this.nodeCPU) * 100,
|
||||
CPUCores: 0,
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve application stats');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
this.stopRepeater();
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
this.state = {
|
||||
autoRefresh: false,
|
||||
refreshRate: '30',
|
||||
viewReady: false,
|
||||
transition: {
|
||||
podName: this.$transition$.params().pod,
|
||||
containerName: this.$transition$.params().container,
|
||||
namespace: this.$transition$.params().namespace,
|
||||
applicationName: this.$transition$.params().name,
|
||||
},
|
||||
getMetrics: false,
|
||||
};
|
||||
|
||||
try {
|
||||
await this.KubernetesMetricsService.getPod(this.state.transition.namespace, this.state.transition.podName);
|
||||
} catch (error) {
|
||||
this.state.getMetrics = false;
|
||||
this.state.viewReady = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const podRaw = await this.KubernetesPodService.get(this.state.transition.namespace, this.state.transition.podName);
|
||||
const pod = KubernetesPodConverter.apiToModel(podRaw.Raw);
|
||||
if (pod) {
|
||||
const node = await this.KubernetesNodeService.get(pod.Node);
|
||||
this.nodeCPU = node.CPU;
|
||||
} else {
|
||||
throw new Error('Unable to find pod');
|
||||
}
|
||||
await this.getStats();
|
||||
this.state.getMetrics = true;
|
||||
|
||||
this.$document.ready(() => {
|
||||
this.initCharts();
|
||||
});
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve application stats');
|
||||
} finally {
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesApplicationStatsController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesApplicationStatsController', KubernetesApplicationStatsController);
|
Loading…
Add table
Add a link
Reference in a new issue