def rules(self, **params):
return self.prometheus_proxy('GET', '/rules', params)
+ @RESTController.Collection(method='GET', path='/data')
+ def get_prometeus_data(self, **params):
+ params['query'] = params.pop('params')
+ return self.prometheus_proxy('GET', '/query_range', params)
+
@RESTController.Collection(method='GET', path='/silences')
def get_silences(self, **params):
return self.alert_proxy('GET', '/silences', params)
--- /dev/null
+<div class="row">
+ <div class="col-3">
+ <br>
+ <b class="chartTitle"
+ i18n>{{ chartTitle }}</b>
+ <br>
+ <span [ngbTooltip]="label"
+ i18n>{{currentData}} {{ currentDataUnits }}</span>
+ <br>
+ <span [ngbTooltip]="label2"
+ i18n>{{currentData2}} {{ currentDataUnits2 }}</span>
+ </div>
+ <div class="col-9">
+ <div class="chart">
+ <canvas baseChart
+ [datasets]="chartData.dataset"
+ [options]="options"
+ [chartType]="'line'"
+ [plugins]="chartAreaBorderPlugin">
+ </canvas>
+ </div>
+ </div>
+</div>
--- /dev/null
+.chartTitle {
+ margin-top: 5vw;
+}
+
+.chart {
+ height: 7vh;
+ margin-top: 15px;
+}
--- /dev/null
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CssHelper } from '~/app/shared/classes/css-helper';
+import { DimlessBinaryPerSecondPipe } from '~/app/shared/pipes/dimless-binary-per-second.pipe';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { DashboardAreaChartComponent } from './dashboard-area-chart.component';
+
+describe('DashboardAreaChartComponent', () => {
+ let component: DashboardAreaChartComponent;
+ let fixture: ComponentFixture<DashboardAreaChartComponent>;
+
+ configureTestBed({
+ schemas: [NO_ERRORS_SCHEMA],
+ declarations: [DashboardAreaChartComponent],
+ providers: [
+ CssHelper,
+ DimlessBinaryPipe,
+ DimlessBinaryPerSecondPipe,
+ DimlessPipe,
+ FormatterService
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DashboardAreaChartComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { AfterViewInit, Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
+
+import { CssHelper } from '~/app/shared/classes/css-helper';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { DimlessBinaryPerSecondPipe } from '~/app/shared/pipes/dimless-binary-per-second.pipe';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { BaseChartDirective, PluginServiceGlobalRegistrationAndOptions } from 'ng2-charts';
+import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe';
+
+@Component({
+ selector: 'cd-dashboard-area-chart',
+ templateUrl: './dashboard-area-chart.component.html',
+ styleUrls: ['./dashboard-area-chart.component.scss']
+})
+export class DashboardAreaChartComponent implements OnInit, OnChanges, AfterViewInit {
+ @ViewChild(BaseChartDirective) chart: BaseChartDirective;
+
+ @Input()
+ chartTitle: string;
+ @Input()
+ maxValue?: any;
+ @Input()
+ dataUnits: string;
+ @Input()
+ data: any;
+ @Input()
+ data2?: any;
+ @Input()
+ label: any;
+ @Input()
+ label2?: any;
+
+ currentDataUnits: string;
+ currentData: number;
+ currentDataUnits2?: string;
+ currentData2?: number;
+
+ chartData: any = {
+ dataset: [
+ {
+ label: '',
+ data: [{ x: 0, y: 0 }],
+ tension: 0,
+ pointBackgroundColor: this.cssHelper.propertyValue('chart-color-strong-blue'),
+ backgroundColor: this.cssHelper.propertyValue('chart-color-translucent-blue'),
+ borderColor: this.cssHelper.propertyValue('chart-color-strong-blue')
+ },
+ {
+ label: '',
+ data: [],
+ tension: 0,
+ pointBackgroundColor: this.cssHelper.propertyValue('chart-color-orange'),
+ backgroundColor: this.cssHelper.propertyValue('chart-color-yellow'),
+ borderColor: this.cssHelper.propertyValue('chart-color-orange')
+ }
+ ]
+ };
+
+ options: any = {
+ responsive: true,
+ maintainAspectRatio: false,
+ elements: {
+ point: {
+ radius: 0
+ }
+ },
+ legend: {
+ display: false
+ },
+ tooltips: {
+ intersect: false,
+ displayColors: true,
+ backgroundColor: this.cssHelper.propertyValue('chart-color-tooltip-background'),
+ callbacks: {
+ title: function (tooltipItem: any): any {
+ return tooltipItem[0].xLabel;
+ }
+ }
+ },
+ hover: {
+ intersect: false
+ },
+ scales: {
+ xAxes: [
+ {
+ display: false,
+ type: 'time',
+ gridLines: {
+ display: false
+ },
+ time: {
+ tooltipFormat: 'YYYY/MM/DD hh:mm:ss'
+ }
+ }
+ ],
+ yAxes: [
+ {
+ gridLines: {
+ display: false
+ },
+ ticks: {
+ beginAtZero: true,
+ maxTicksLimit: 3,
+ callback: (value: any) => {
+ if (value === 0) {
+ return null;
+ }
+ return this.fillString(this.convertUnits(value));
+ }
+ }
+ }
+ ]
+ },
+ plugins: {
+ borderArea: true,
+ chartAreaBorder: {
+ borderColor: this.cssHelper.propertyValue('chart-color-slight-dark-gray'),
+ borderWidth: 2
+ }
+ }
+ };
+
+ public chartAreaBorderPlugin: PluginServiceGlobalRegistrationAndOptions[] = [
+ {
+ beforeDraw(chart: Chart) {
+ if (!chart.options.plugins.borderArea) {
+ return;
+ }
+ const {
+ ctx,
+ chartArea: { left, top, right, bottom }
+ } = chart;
+ ctx.save();
+ ctx.strokeStyle = chart.options.plugins.chartAreaBorder.borderColor;
+ ctx.lineWidth = chart.options.plugins.chartAreaBorder.borderWidth;
+ ctx.setLineDash(chart.options.plugins.chartAreaBorder.borderDash || []);
+ ctx.lineDashOffset = chart.options.plugins.chartAreaBorder.borderDashOffset;
+ ctx.strokeRect(left, top, right - left - 1, bottom);
+ ctx.restore();
+ }
+ }
+ ];
+
+ constructor(
+ private cssHelper: CssHelper,
+ private dimlessBinary: DimlessBinaryPipe,
+ private dimlessBinaryPerSecond: DimlessBinaryPerSecondPipe,
+ private dimlessPipe: DimlessPipe,
+ private formatter: FormatterService
+ ) {}
+
+ ngOnInit(): void {
+ this.currentData = Number(
+ this.chartData.dataset[0].data[this.chartData.dataset[0].data.length - 1].y
+ );
+ if (this.data2) {
+ this.currentData2 = Number(
+ this.chartData.dataset[1].data[this.chartData.dataset[1].data.length - 1].y
+ );
+ }
+ }
+
+ ngOnChanges(): void {
+ if (this.data) {
+ this.setChartTicks();
+ this.chartData.dataset[0].data = this.formatData(this.data);
+ this.chartData.dataset[0].label = this.label;
+ [this.currentData, this.currentDataUnits] = this.convertUnits(
+ this.data[this.data.length - 1][1]
+ ).split(' ');
+ }
+ if (this.data2) {
+ this.chartData.dataset[1].data = this.formatData(this.data2);
+ this.chartData.dataset[1].label = this.label2;
+ [this.currentData2, this.currentDataUnits2] = this.convertUnits(
+ this.data2[this.data2.length - 1][1]
+ ).split(' ');
+ }
+ }
+
+ ngAfterViewInit(): void {
+ if (this.data) {
+ this.setChartTicks();
+ }
+ }
+
+ private formatData(array: Array<any>): any {
+ let formattedData = {};
+ formattedData = array.map((data: any) => ({
+ x: data[0] * 1000,
+ y: Number(this.convertUnits(data[1]).replace(/[^\d,.]+/g, ''))
+ }));
+ return formattedData;
+ }
+
+ private convertUnits(data: any): any {
+ let dataWithUnits: string;
+ if (this.dataUnits === 'bytes') {
+ dataWithUnits = this.dimlessBinary.transform(data);
+ } else if (this.dataUnits === 'bytesPerSecond') {
+ dataWithUnits = this.dimlessBinaryPerSecond.transform(data);
+ } else if (this.dataUnits === 'ms') {
+ dataWithUnits = this.formatter.format_number(data, 1000, ['ms', 's']);
+ } else {
+ dataWithUnits = this.dimlessPipe.transform(data);
+ }
+ return dataWithUnits;
+ }
+
+ private fillString(str: string): string {
+ let maxNumberOfChar: number = 8;
+ let numberOfChars: number = str.length;
+ if (str.length < 4) {
+ maxNumberOfChar = 11;
+ }
+ for (; numberOfChars < maxNumberOfChar; numberOfChars++) {
+ str = '\u00A0' + str;
+ }
+ return str + '\u00A0\u00A0';
+ }
+
+ private setChartTicks() {
+ if (this.chart && this.maxValue) {
+ let [maxValue, maxValueDataUnits] = this.convertUnits(this.maxValue).split(' ');
+ this.chart.chart.options.scales.yAxes[0].ticks.suggestedMax = maxValue;
+ this.chart.chart.options.scales.yAxes[0].ticks.suggestedMin = 0;
+ this.chart.chart.options.scales.yAxes[0].ticks.stepSize = Number((maxValue / 2).toFixed(0));
+ this.chart.chart.options.scales.yAxes[0].ticks.callback = (value: any) => {
+ if (value === 0) {
+ return null;
+ }
+ return this.fillString(`${value} ${maxValueDataUnits}`);
+ };
+ this.chart.chart.update();
+ } else if (this.chart && this.data) {
+ let maxValueData = Math.max(...this.data.map((values: any) => values[1]));
+ if (this.data2) {
+ var maxValueData2 = Math.max(...this.data2.map((values: any) => values[1]));
+ }
+ let [maxValue, maxValueDataUnits] = this.convertUnits(
+ Math.max(maxValueData, maxValueData2)
+ ).split(' ');
+
+ this.chart.chart.options.scales.yAxes[0].ticks.suggestedMax = maxValue * 1.2;
+ this.chart.chart.options.scales.yAxes[0].ticks.suggestedMin = 0;
+ this.chart.chart.options.scales.yAxes[0].ticks.stepSize = Number(
+ ((maxValue * 1.2) / 2).toFixed(0)
+ );
+ this.chart.chart.options.scales.yAxes[0].ticks.callback = (value: any) => {
+ if (value === 0) {
+ return null;
+ }
+ if (!maxValueDataUnits) {
+ return this.fillString(`${value}`);
+ }
+ return this.fillString(`${value} ${maxValueDataUnits}`);
+ };
+ this.chart.chart.update();
+ }
+ }
+}
--- /dev/null
+<div class="timeSelector">
+ <select id="timepicker"
+ name="timepicker"
+ [(ngModel)]="time"
+ (ngModelChange)="emitTime()"
+ class="form-select">
+ <option *ngFor="let key of times"
+ [ngValue]="key.value">{{ key.name }}
+ </option>
+ </select>
+</div>
--- /dev/null
+select#timepicker {
+ border: 0;
+}
+
+.timeSelector {
+ position: absolute;
+ right: 18px;
+ top: 20px;
+ width: 12rem;
+}
--- /dev/null
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { DashboardTimeSelectorComponent } from './dashboard-time-selector.component';
+
+describe('DashboardTimeSelectorComponent', () => {
+ let component: DashboardTimeSelectorComponent;
+ let fixture: ComponentFixture<DashboardTimeSelectorComponent>;
+
+ configureTestBed({
+ schemas: [NO_ERRORS_SCHEMA],
+ declarations: [DashboardTimeSelectorComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DashboardTimeSelectorComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, EventEmitter, Output } from '@angular/core';
+
+import moment from 'moment';
+
+@Component({
+ selector: 'cd-dashboard-time-selector',
+ templateUrl: './dashboard-time-selector.component.html',
+ styleUrls: ['./dashboard-time-selector.component.scss']
+})
+export class DashboardTimeSelectorComponent {
+ @Output()
+ selectedTime = new EventEmitter<any>();
+
+ times: any;
+ time: any;
+
+ constructor() {
+ this.times = [
+ {
+ name: $localize`Last 5 minutes`,
+ value: this.timeToDate(5 * 60)
+ },
+ {
+ name: $localize`Last 15 minutes`,
+ value: this.timeToDate(15 * 60)
+ },
+ {
+ name: $localize`Last 30 minutes`,
+ value: this.timeToDate(30 * 60)
+ },
+ {
+ name: $localize`Last 1 hour`,
+ value: this.timeToDate(3600)
+ },
+ {
+ name: $localize`Last 3 hours`,
+ value: this.timeToDate(3 * 3600)
+ },
+ {
+ name: $localize`Last 6 hours`,
+ value: this.timeToDate(6 * 3600, 30)
+ },
+ {
+ name: $localize`Last 12 hours`,
+ value: this.timeToDate(12 * 3600, 60)
+ },
+ {
+ name: $localize`Last 24 hours`,
+ value: this.timeToDate(24 * 3600, 120)
+ },
+ {
+ name: $localize`Last 2 days`,
+ value: this.timeToDate(48 * 3600, 300)
+ },
+ {
+ name: $localize`Last 7 days`,
+ value: this.timeToDate(168 * 3600, 900)
+ }
+ ];
+ this.time = this.times[3].value;
+ }
+
+ emitTime() {
+ this.selectedTime.emit(this.time);
+ }
+
+ private timeToDate(secondsAgo: number, step: number = 30): any {
+ const date: number = moment().unix() - secondsAgo;
+ const dateNow: number = moment().unix();
+ const formattedDate: any = {
+ start: date,
+ end: dateNow,
+ step: step
+ };
+ return formattedDate;
+ }
+}
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
-import { NgbNavModule, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap';
+import { NgbNavModule, NgbPopoverModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
import { ChartsModule } from 'ng2-charts';
import { SimplebarAngularModule } from 'simplebar-angular';
import { SharedModule } from '~/app/shared/shared.module';
import { CephSharedModule } from '../shared/ceph-shared.module';
import { CardComponent } from './card/card.component';
+import { DashboardAreaChartComponent } from './dashboard-area-chart/dashboard-area-chart.component';
import { DashboardPieComponent } from './dashboard-pie/dashboard-pie.component';
+import { DashboardTimeSelectorComponent } from './dashboard-time-selector/dashboard-time-selector.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { CardRowComponent } from './card-row/card-row.component';
import { PgSummaryPipe } from './pg-summary.pipe';
ChartsModule,
RouterModule,
NgbPopoverModule,
+ NgbTooltipModule,
FormsModule,
ReactiveFormsModule,
SimplebarAngularModule
DashboardComponent,
CardComponent,
DashboardPieComponent,
- DashboardPieComponent,
CardRowComponent,
- PgSummaryPipe
+ PgSummaryPipe,
+ DashboardAreaChartComponent,
+ DashboardTimeSelectorComponent
]
})
export class NewDashboardModule {}
</li>
</cd-card>
- <cd-card title="Capacity utilization"
+ <cd-card title="Cluster utilization"
i18n-title
class="col-sm-6 px-3">
<div class="ms-4 me-4">
- Text
+ <cd-dashboard-time-selector (selectedTime)="getPrometheusData($event)">
+ </cd-dashboard-time-selector>
+ <ng-container *ngIf="capacity">
+ <cd-dashboard-area-chart chartTitle="Used Capacity"
+ [maxValue]="capacity.total_bytes"
+ dataUnits="bytes"
+ label="Used Capacity"
+ [data]="queriesResults.USEDCAPACITY">
+ </cd-dashboard-area-chart>
+ </ng-container>
+ <cd-dashboard-area-chart chartTitle="IOPS"
+ dataUnits="none"
+ label="IPS"
+ label2="OPS"
+ [data]="queriesResults.IPS"
+ [data2]="queriesResults.OPS">
+ </cd-dashboard-area-chart>
+ <cd-dashboard-area-chart chartTitle="Latency"
+ dataUnits="ms"
+ label="Read"
+ label2="Write"
+ [data]="queriesResults.READLATENCY"
+ [data2]="queriesResults.WRITELATENCY">
+ </cd-dashboard-area-chart>
+ <cd-dashboard-area-chart chartTitle="Client Throughput"
+ dataUnits="bytesPerSecond"
+ label="Read"
+ label2="Write"
+ [data]="queriesResults.READCLIENTTHROUGHPUT"
+ [data2]="queriesResults.WRITECLIENTTHROUGHPUT">
+ </cd-dashboard-area-chart>
</div>
</cd-card>
import { Component, OnDestroy, OnInit } from '@angular/core';
import _ from 'lodash';
-import { Observable, Subscription } from 'rxjs';
+import { Observable, Subscription, timer } from 'rxjs';
import { take } from 'rxjs/operators';
+import moment from 'moment';
import { ClusterService } from '~/app/shared/api/cluster.service';
import { ConfigurationService } from '~/app/shared/api/configuration.service';
import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
import { OsdService } from '~/app/shared/api/osd.service';
import { PrometheusService } from '~/app/shared/api/prometheus.service';
+import { Promqls as queries } from '~/app/shared/enum/dashboard-promqls.enum';
import { Icons } from '~/app/shared/enum/icons.enum';
import { DashboardDetails } from '~/app/shared/models/cd-details';
import { Permissions } from '~/app/shared/models/permissions';
healthData: any;
categoryPgAmount: Record<string, number> = {};
totalPgs = 0;
+ queriesResults: any = {
+ USEDCAPACITY: '',
+ IPS: '',
+ OPS: '',
+ READLATENCY: '',
+ WRITELATENCY: '',
+ READCLIENTTHROUGHPUT: '',
+ WRITECLIENTTHROUGHPUT: ''
+ };
+ timerGetPrometheusDataSub: Subscription;
+ timerTime = 30000;
+ readonly lastHourDateObject = {
+ start: moment().unix() - 3600,
+ end: moment().unix(),
+ step: 30
+ };
constructor(
private summaryService: SummaryService,
this.triggerPrometheusAlerts();
this.getCapacityCardData();
});
+ this.getPrometheusData(this.lastHourDateObject);
this.getDetailsCardData();
}
});
});
}
+
+ getPrometheusData(selectedTime: any) {
+ if (this.timerGetPrometheusDataSub) {
+ this.timerGetPrometheusDataSub.unsubscribe();
+ }
+ this.timerGetPrometheusDataSub = timer(0, this.timerTime).subscribe(() => {
+ selectedTime = this.updateTimeStamp(selectedTime);
+
+ for (const queryName in queries) {
+ if (queries.hasOwnProperty(queryName)) {
+ this.prometheusService
+ .getPrometheusData({
+ params: queries[queryName],
+ start: selectedTime['start'],
+ end: selectedTime['end'],
+ step: selectedTime['step']
+ })
+ .subscribe((data: any) => {
+ if (data.result.length) {
+ this.queriesResults[queryName] = data.result[0].values;
+ }
+ });
+ }
+ }
+ });
+ }
+
+ private updateTimeStamp(selectedTime: any): any {
+ let formattedDate = {};
+ const date: number = selectedTime['start'] + this.timerTime / 1000;
+ const dateNow: number = selectedTime['end'] + this.timerTime / 1000;
+ formattedDate = {
+ start: date,
+ end: dateNow,
+ step: selectedTime['step']
+ };
+ return formattedDate;
+ }
}
constructor(private http: HttpClient, private settingsService: SettingsService) {}
+ getPrometheusData(params: any): any {
+ return this.http.get<any>(`${this.baseURL}/data`, { params });
+ }
+
ifAlertmanagerConfigured(fn: (value?: string) => void, elseFn?: () => void): void {
this.settingsService.ifSettingConfigured(this.settingsKey.alertmanager, fn, elseFn);
}
--- /dev/null
+export enum Promqls {
+ USEDCAPACITY = 'ceph_cluster_total_used_bytes',
+ IPS = 'sum(irate(ceph_osd_op_w_in_bytes[1m]))',
+ OPS = 'sum(irate(ceph_osd_op_r_out_bytes[1m]))',
+ READLATENCY = 'avg(ceph_osd_apply_latency_ms)',
+ WRITELATENCY = 'avg(ceph_osd_commit_latency_ms)',
+ READCLIENTTHROUGHPUT = 'sum(irate(ceph_pool_rd_bytes[1m]))',
+ WRITECLIENTTHROUGHPUT = 'sum(irate(ceph_pool_wr_bytes[1m]))'
+}
$chart-color-center-text-description: #72767b !default;
$chart-color-tooltip-background: $black !default;
$chart-danger: #c9190b !default;
+$chart-color-strong-blue: #0078c8 !default;
+$chart-color-translucent-blue: #0096dc80 !default;
// Typography
- jwt: []
tags:
- Prometheus
+ /api/prometheus/data:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Prometheus
/api/prometheus/notifications:
get:
parameters: []