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
pull/5027/head
Alice Groux 2021-04-29 03:10:14 +02:00 committed by GitHub
parent befccacc27
commit d99358ea8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 411 additions and 3 deletions

View File

@ -100,6 +100,16 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([
},
};
const applicationStats = {
name: 'kubernetes.applications.application.stats',
url: '/:pod/:container/stats',
views: {
'content@': {
component: 'kubernetesApplicationStatsView',
},
},
};
const stacks = {
name: 'kubernetes.stacks',
url: '/stacks',
@ -259,6 +269,7 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([
$stateRegistryProvider.register(applicationEdit);
$stateRegistryProvider.register(applicationConsole);
$stateRegistryProvider.register(applicationLogs);
$stateRegistryProvider.register(applicationStats);
$stateRegistryProvider.register(stacks);
$stateRegistryProvider.register(stack);
$stateRegistryProvider.register(stackLogs);

View File

@ -131,6 +131,13 @@
</td>
<td>{{ item.CreationDate | getisodate }}</td>
<td>
<a
ng-if="item.Status === 'Running' && $ctrl.useServerMetrics"
ui-sref="kubernetes.applications.application.stats({ pod: item.PodName, container: item.Name })"
style="margin-right: 10px;"
>
<i class="fa fa-chart-area" aria-hidden="true"></i> Stats
</a>
<a ui-sref="kubernetes.applications.application.logs({ pod: item.PodName, container: item.Name })"> <i class="fa fa-file-alt" aria-hidden="true"></i> Logs </a>
<a ng-if="item.Status === 'Running'" ui-sref="kubernetes.applications.application.console({ pod: item.PodName, container: item.Name })" style="margin-left: 10px;">
<i class="fa fa-terminal" aria-hidden="true"></i> Console

View File

@ -9,5 +9,6 @@ angular.module('portainer.kubernetes').component('kubernetesContainersDatatable'
orderBy: '@',
refreshCallback: '<',
isPod: '<',
useServerMetrics: '<',
},
});

View File

@ -29,6 +29,8 @@ class KubernetesResourceReservationHelper {
let res = parseInt(cpu, 10);
if (_.endsWith(cpu, 'm')) {
res /= 1000;
} else if (_.endsWith(cpu, 'n')) {
res /= 1000000000;
}
return res;
}

View File

@ -0,0 +1,53 @@
import angular from 'angular';
import PortainerError from 'Portainer/error';
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
class KubernetesMetricsService {
/* @ngInject */
constructor($async, KubernetesMetrics) {
this.$async = $async;
this.KubernetesMetrics = KubernetesMetrics;
this.capabilitiesAsync = this.capabilitiesAsync.bind(this);
this.getPodAsync = this.getPodAsync.bind(this);
}
/**
* GET
*/
async capabilitiesAsync(endpointID) {
try {
await this.KubernetesMetrics().capabilities({ endpointId: endpointID }).$promise;
} catch (err) {
throw new PortainerError('Unable to retrieve metrics', err);
}
}
capabilities(endpointID) {
return this.$async(this.capabilitiesAsync, endpointID);
}
/**
* Stats
*
* @param {string} namespace
* @param {string} podName
*/
async getPodAsync(namespace, podName) {
try {
const params = new KubernetesCommonParams();
params.id = podName;
const data = await this.KubernetesMetrics(namespace).getPod(params).$promise;
return data;
} catch (err) {
throw new PortainerError('Unable to retrieve pod stats', err);
}
}
getPod(namespace, podName) {
return this.$async(this.getPodAsync, namespace, podName);
}
}
export default KubernetesMetricsService;
angular.module('portainer.kubernetes').service('KubernetesMetricsService', KubernetesMetricsService);

View File

@ -0,0 +1,27 @@
angular.module('portainer.kubernetes').factory('KubernetesMetrics', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function KubernetesMetrics($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return function (namespace) {
const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/apis/metrics.k8s.io/v1beta1';
const podUrl = `${url}${namespace ? '/namespaces/:namespace' : ''}/pods/:id`;
return $resource(
url,
{
endpointId: EndpointProvider.endpointID,
namespace: namespace,
},
{
capabilities: { method: 'GET' },
getPod: {
method: 'GET',
url: podUrl,
},
}
);
};
},
]);

View File

@ -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>

View File

@ -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');

View 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> &gt;
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.state.transition.namespace })">{{ ctrl.state.transition.namespace }}</a> &gt;
<a ui-sref="kubernetes.applications">Applications</a> &gt;
<a ui-sref="kubernetes.applications.application({ name: ctrl.state.transition.applicationName, namespace: ctrl.state.transition.namespace })">{{
ctrl.state.transition.applicationName
}}</a>
&gt; Pods &gt; {{ ctrl.state.transition.podName }} &gt; Containers &gt; {{ ctrl.state.transition.containerName }} &gt; 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>

View File

@ -0,0 +1,8 @@
angular.module('portainer.kubernetes').component('kubernetesApplicationStatsView', {
templateUrl: './stats.html',
controller: 'KubernetesApplicationStatsController',
controllerAs: 'ctrl',
bindings: {
$transition$: '<',
},
});

View 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);

View File

@ -196,7 +196,27 @@
<label class="control-label text-left">
Enable features using metrics server
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.formValues.UseServerMetrics" /><i></i> </label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="ctrl.formValues.UseServerMetrics" ng-change="ctrl.enableMetricsServer()" /><i></i>
</label>
</div>
<div ng-if="ctrl.state.metrics.pending && ctrl.state.metrics.userClick" class="col-sm-12 small text-muted" style="margin-top: 5px;">
Checking metrics API... <i class="fa fa-spinner fa-spin" style="margin-left: 2px;"></i>
</div>
<div
ng-if="!ctrl.state.metrics.pending && ctrl.state.metrics.isServerRunning && ctrl.state.metrics.userClick"
class="col-sm-12 small text-muted"
style="margin-top: 5px;"
>
<i class="fa fa-check green-icon" aria-hidden="true" style="margin-right: 2px;"></i> Successfully reached metrics API
</div>
<div
ng-if="!ctrl.state.metrics.pending && !ctrl.state.metrics.isServerRunning && ctrl.state.metrics.userClick"
class="col-sm-12 small text-muted"
style="margin-top: 5px;"
>
<i class="fa fa-times red-icon" aria-hidden="true" style="margin-right: 2px;"></i> Unable to reach metrics API, make sure metrics server is properly deployed inside
that cluster.
</div>
</div>

View File

@ -28,7 +28,8 @@ class KubernetesConfigureController {
ModalService,
KubernetesNamespaceHelper,
KubernetesResourcePoolService,
KubernetesIngressService
KubernetesIngressService,
KubernetesMetricsService
) {
this.$async = $async;
this.$state = $state;
@ -41,6 +42,7 @@ class KubernetesConfigureController {
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
this.KubernetesIngressService = KubernetesIngressService;
this.KubernetesMetricsService = KubernetesMetricsService;
this.IngressClassTypes = KubernetesIngressClassTypes;
@ -161,6 +163,27 @@ class KubernetesConfigureController {
}
});
}
enableMetricsServer() {
if (this.formValues.UseServerMetrics) {
this.state.metrics.userClick = true;
this.state.metrics.pending = true;
this.KubernetesMetricsService.capabilities(this.endpoint.Id)
.then(() => {
this.state.metrics.isServerRunning = true;
this.state.metrics.pending = false;
this.formValues.UseServerMetrics = true;
})
.catch(() => {
this.state.metrics.isServerRunning = false;
this.state.metrics.pending = false;
this.formValues.UseServerMetrics = false;
});
} else {
this.state.metrics.userClick = false;
this.formValues.UseServerMetrics = false;
}
}
async configureAsync() {
try {
@ -222,6 +245,11 @@ class KubernetesConfigureController {
duplicates: {
ingressClasses: new KubernetesFormValidationReferences(),
},
metrics: {
pending: false,
isServerRunning: false,
userClick: false,
},
};
this.formValues = {