From: Patrick Seidensal Date: Mon, 25 Nov 2019 12:37:50 +0000 (+0100) Subject: mgr/dashboard: list configured Prometheus alerts X-Git-Tag: v15.1.0~650^2~4 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=2a50e277d4ae764aa50f9b726f79c84f335ef15a;p=ceph.git mgr/dashboard: list configured Prometheus alerts Fixes: https://tracker.ceph.com/issues/42877 Signed-off-by: Patrick Seidensal --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts index 25b22a7157c07..666b216698a01 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts @@ -44,6 +44,7 @@ import { OsdScrubModalComponent } from './osd/osd-scrub-modal/osd-scrub-modal.co import { OsdSmartListComponent } from './osd/osd-smart-list/osd-smart-list.component'; import { AlertListComponent } from './prometheus/alert-list/alert-list.component'; import { PrometheusTabsComponent } from './prometheus/prometheus-tabs/prometheus-tabs.component'; +import { RulesListComponent } from './prometheus/rules-list/rules-list.component'; import { SilenceFormComponent } from './prometheus/silence-form/silence-form.component'; import { SilenceListComponent } from './prometheus/silence-list/silence-list.component'; import { SilenceMatcherModalComponent } from './prometheus/silence-matcher-modal/silence-matcher-modal.component'; @@ -114,7 +115,9 @@ import { ServicesComponent } from './services/services.component'; OsdDevicesSelectionModalComponent, InventoryDevicesComponent, OsdDevicesSelectionGroupsComponent, - OsdCreationPreviewModalComponent + OsdCreationPreviewModalComponent, + RulesListComponent, + AlertListComponent ] }) export class ClusterModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/alert-list/alert-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/alert-list/alert-list.component.html index 59c409d12416f..45c5a7ba62fdf 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/alert-list/alert-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/alert-list/alert-list.component.html @@ -1,5 +1,11 @@ +

All Alerts

+ + +

Active Alerts

{ @@ -25,9 +28,13 @@ describe('AlertListComponent', () => { TabsModule.forRoot(), RouterTestingModule, ToastrModule.forRoot(), - SharedModule + SharedModule, + ClusterModule, + DashboardModule, + CephModule, + CoreModule ], - declarations: [AlertListComponent, PrometheusTabsComponent], + declarations: [], providers: [i18nProviders] }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.html new file mode 100644 index 0000000000000..1c2aeb9ea740c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.html @@ -0,0 +1,8 @@ + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.spec.ts new file mode 100644 index 0000000000000..f09bd49d15264 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.spec.ts @@ -0,0 +1,29 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { configureTestBed, i18nProviders } from '../../../../../testing/unit-test-helper'; +import { PrometheusService } from '../../../../shared/api/prometheus.service'; +import { SettingsService } from '../../../../shared/api/settings.service'; +import { SharedModule } from '../../../../shared/shared.module'; +import { RulesListComponent } from './rules-list.component'; + +describe('RulesListComponent', () => { + let component: RulesListComponent; + let fixture: ComponentFixture; + + configureTestBed({ + declarations: [RulesListComponent], + imports: [HttpClientTestingModule, SharedModule], + providers: [PrometheusService, SettingsService, i18nProviders] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RulesListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.ts new file mode 100644 index 0000000000000..41577da9d9e4d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.ts @@ -0,0 +1,44 @@ +import { Component, Input, OnInit } from '@angular/core'; + +import { I18n } from '@ngx-translate/i18n-polyfill'; + +import { CdTableColumn } from '../../../../shared/models/cd-table-column'; +import { CdTableSelection } from '../../../../shared/models/cd-table-selection'; +import { PrometheusRule } from '../../../../shared/models/prometheus-alerts'; +import { DurationPipe } from '../../../../shared/pipes/duration.pipe'; + +@Component({ + selector: 'cd-rules-list', + templateUrl: './rules-list.component.html', + styleUrls: ['./rules-list.component.scss'] +}) +export class RulesListComponent implements OnInit { + @Input() + data: any; + columns: CdTableColumn[]; + selectedRule: PrometheusRule; + + /** + * Hide active alerts in details of alerting rules as they are already shown + * in the 'active alerts' table. Also hide the 'type' column as the type is + * always supposed to be 'alerting'. + */ + hideKeys = ['alerts', 'type']; + + constructor(private i18n: I18n) {} + + ngOnInit() { + this.columns = [ + { prop: 'name', name: this.i18n('Name') }, + { prop: 'labels.severity', name: this.i18n('Severity') }, + { prop: 'group', name: this.i18n('Group') }, + { prop: 'duration', name: this.i18n('Duration'), pipe: new DurationPipe() }, + { prop: 'query', name: this.i18n('Query'), isHidden: true }, + { prop: 'annotations.description', name: this.i18n('Description') } + ]; + } + + selectionUpdated(selection: CdTableSelection) { + this.selectedRule = selection.first(); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.spec.ts index db14f4679c112..93e3c43c0c896 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.spec.ts @@ -79,10 +79,85 @@ describe('PrometheusService', () => { expect(req.request.method).toBe('GET'); }); - it('should get prometheus rules', () => { - service.getRules({}).subscribe(); - const req = httpTesting.expectOne('api/prometheus/rules'); - expect(req.request.method).toBe('GET'); + describe('test getRules()', () => { + let data: {}; // Subset of PrometheusRuleGroup to keep the tests concise. + + beforeEach(() => { + data = { + groups: [ + { + name: 'test', + rules: [ + { + name: 'load_0', + type: 'alerting' + }, + { + name: 'load_1', + type: 'alerting' + }, + { + name: 'load_2', + type: 'alerting' + } + ] + }, + { + name: 'recording_rule', + rules: [ + { + name: 'node_memory_MemUsed_percent', + type: 'recording' + } + ] + } + ] + }; + }); + + it('should get rules without applying filters', () => { + service.getRules().subscribe((rules) => { + expect(rules).toEqual(data); + }); + + const req = httpTesting.expectOne('api/prometheus/rules'); + expect(req.request.method).toBe('GET'); + req.flush(data); + }); + + it('should get rewrite rules only', () => { + service.getRules('rewrites').subscribe((rules) => { + expect(rules).toEqual({ + groups: [{ name: 'test', rules: [] }, { name: 'recording_rule', rules: [] }] + }); + }); + + const req = httpTesting.expectOne('api/prometheus/rules'); + expect(req.request.method).toBe('GET'); + req.flush(data); + }); + + it('should get alerting rules only', () => { + service.getRules('alerting').subscribe((rules) => { + expect(rules).toEqual({ + groups: [ + { + name: 'test', + rules: [ + { name: 'load_0', type: 'alerting' }, + { name: 'load_1', type: 'alerting' }, + { name: 'load_2', type: 'alerting' } + ] + }, + { name: 'recording_rule', rules: [] } + ] + }); + }); + + const req = httpTesting.expectOne('api/prometheus/rules'); + expect(req.request.method).toBe('GET'); + req.flush(data); + }); }); describe('ifAlertmanagerConfigured', () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts index 81488bbf0453e..0c8f2ff065307 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts @@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; import { AlertmanagerSilence } from '../models/alertmanager-silence'; import { @@ -48,8 +49,28 @@ export class PrometheusService { return this.http.get(`${this.baseURL}/silences`, { params }); } - getRules(params = {}): Observable<{ groups: PrometheusRuleGroup[] }> { - return this.http.get<{ groups: PrometheusRuleGroup[] }>(`${this.baseURL}/rules`, { params }); + getRules( + type: 'all' | 'alerting' | 'rewrites' = 'all' + ): Observable<{ groups: PrometheusRuleGroup[] }> { + let rules = this.http.get<{ groups: PrometheusRuleGroup[] }>(`${this.baseURL}/rules`); + const filterByType = (_type: 'alerting' | 'rewrites') => { + return rules.pipe( + map((_rules) => { + _rules.groups = _rules.groups.map((group) => { + group.rules = group.rules.filter((rule) => rule.type === _type); + return group; + }); + return _rules; + }) + ); + }; + switch (type) { + case 'alerting': + case 'rewrites': + rules = filterByType(type); + break; + } + return rules; } setSilence(silence: AlertmanagerSilence) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.spec.ts index 965418ddbf3b1..4b6e109a03608 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.spec.ts @@ -50,6 +50,13 @@ describe('TableKeyValueComponent', () => { ]); }); + it('should not show data supposed to be have hidden by key', () => { + component.data = [['a', 1], ['b', 2]]; + component.hideKeys = ['a']; + component.ngOnInit(); + expect(component.tableData).toEqual([{ key: 'b', value: 2 }]); + }); + it('should remove items with objects as values', () => { component.data = [[3, 'something'], ['will be removed', { a: 3, b: 4, c: 5 }]]; component.ngOnInit(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts index 7b832640f3972..8006bf721843f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts @@ -48,6 +48,8 @@ export class TableKeyValueComponent implements OnInit, OnChanges { appendParentKey = true; @Input() hideEmpty = false; + @Input() + hideKeys = []; // Keys of pairs not to be displayed // If set, the classAddingTpl is used to enable different css for different values @Input() @@ -100,7 +102,11 @@ export class TableKeyValueComponent implements OnInit, OnChanges { if (!this.data) { return; // Wait for data } - this.tableData = this.makePairs(this.data); + let pairs = this.makePairs(this.data); + if (this.hideKeys) { + pairs = pairs.filter((pair) => !this.hideKeys.includes(pair.key)); + } + this.tableData = pairs; } private makePairs(data: any): KeyValueItem[] { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/prometheus-alerts.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/prometheus-alerts.ts index 222581b1cda68..f5e8f850b15bb 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/prometheus-alerts.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/prometheus-alerts.ts @@ -43,6 +43,7 @@ export class PrometheusRule { alerts: PrometheusAlert[]; // Shows only active alerts health: string; type: string; + group?: string; // Added field for flattened list } export class AlertmanagerAlert extends CommonAlertmanagerAlert { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.spec.ts index f9d1fb2a7717f..de3ecf10298cd 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.spec.ts @@ -47,7 +47,7 @@ describe('PrometheusAlertService', () => { getAlerts: () => ({ subscribe: (_fn, err) => err(resp) }), disableAlertmanagerConfig: () => (disabledSetting = true) } as object) as PrometheusService); - service.refresh(); + service.getAlerts(); expect(disabledSetting).toBe(expectation); }; @@ -64,6 +64,40 @@ describe('PrometheusAlertService', () => { }); }); + it('should flatten the response of getRules()', () => { + service = TestBed.get(PrometheusAlertService); + prometheusService = TestBed.get(PrometheusService); + + spyOn(service['prometheusService'], 'ifPrometheusConfigured').and.callFake((fn) => fn()); + spyOn(prometheusService, 'getRules').and.returnValue( + of({ + groups: [ + { + name: 'group1', + rules: [{ name: 'nearly_full', type: 'alerting' }] + }, + { + name: 'test', + rules: [ + { name: 'load_0', type: 'alerting' }, + { name: 'load_1', type: 'alerting' }, + { name: 'load_2', type: 'alerting' } + ] + } + ] + }) + ); + + service.getRules(); + + expect(service.rules as any).toEqual([ + { name: 'nearly_full', type: 'alerting', group: 'group1' }, + { name: 'load_0', type: 'alerting', group: 'test' }, + { name: 'load_1', type: 'alerting', group: 'test' }, + { name: 'load_2', type: 'alerting', group: 'test' } + ]); + }); + describe('refresh', () => { beforeEach(() => { service = TestBed.get(PrometheusAlertService); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.ts index 24d26a2cd8290..dc1731b926657 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.ts @@ -3,7 +3,11 @@ import { Injectable } from '@angular/core'; import * as _ from 'lodash'; import { PrometheusService } from '../api/prometheus.service'; -import { AlertmanagerAlert, PrometheusCustomAlert } from '../models/prometheus-alerts'; +import { + AlertmanagerAlert, + PrometheusCustomAlert, + PrometheusRule +} from '../models/prometheus-alerts'; import { PrometheusAlertFormatter } from './prometheus-alert-formatter'; @Injectable({ @@ -12,13 +16,14 @@ import { PrometheusAlertFormatter } from './prometheus-alert-formatter'; export class PrometheusAlertService { private canAlertsBeNotified = false; alerts: AlertmanagerAlert[] = []; + rules: PrometheusRule[] = []; constructor( private alertFormatter: PrometheusAlertFormatter, private prometheusService: PrometheusService ) {} - refresh() { + getAlerts() { this.prometheusService.ifAlertmanagerConfigured(() => { this.prometheusService.getAlerts().subscribe( (alerts) => this.handleAlerts(alerts), @@ -31,6 +36,26 @@ export class PrometheusAlertService { }); } + getRules() { + this.prometheusService.ifPrometheusConfigured(() => { + this.prometheusService.getRules('alerting').subscribe((groups) => { + this.rules = groups['groups'].reduce((acc, group) => { + return acc.concat( + group.rules.map((rule) => { + rule.group = group.name; + return rule; + }) + ); + }, []); + }); + }); + } + + refresh() { + this.getAlerts(); + this.getRules(); + } + private handleAlerts(alerts: AlertmanagerAlert[]) { if (this.canAlertsBeNotified) { this.notifyOnAlertChanges(alerts, this.alerts);