]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: list configured Prometheus alerts
authorPatrick Seidensal <pseidensal@suse.com>
Mon, 25 Nov 2019 12:37:50 +0000 (13:37 +0100)
committerPatrick Seidensal <pseidensal@suse.com>
Tue, 3 Dec 2019 15:54:24 +0000 (16:54 +0100)
Fixes: https://tracker.ceph.com/issues/42877
Signed-off-by: Patrick Seidensal <pseidensal@suse.com>
14 files changed:
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/alert-list/alert-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/alert-list/alert-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/prometheus-alerts.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.ts

index 25b22a7157c07ce635c6c5688129d663bcae98c5..666b216698a01867035dade965fd0f41f435f705 100644 (file)
@@ -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 {}
index 59c409d12416f84cf39ed67815b1c6e8faae1d93..45c5a7ba62fdf3816f790ccd4077c12f1d3e805f 100644 (file)
@@ -1,5 +1,11 @@
 <cd-prometheus-tabs></cd-prometheus-tabs>
 
+<h3 class="cd-header"
+    i18n>All Alerts</h3>
+<cd-rules-list [data]="prometheusAlertService.rules"></cd-rules-list>
+
+<h3 class="cd-header"
+    i18n>Active Alerts</h3>
 <cd-table [data]="prometheusAlertService.alerts"
           [columns]="columns"
           identifier="fingerprint"
index e0ec0568da1fb6fe705a6905b1ac3c9ce2ccb6cf..247c20c12847e323c5ff035864c6713eade28f36 100644 (file)
@@ -10,9 +10,12 @@ import {
   i18nProviders,
   PermissionHelper
 } from '../../../../../testing/unit-test-helper';
+import { CoreModule } from '../../../../core/core.module';
 import { TableActionsComponent } from '../../../../shared/datatable/table-actions/table-actions.component';
 import { SharedModule } from '../../../../shared/shared.module';
-import { PrometheusTabsComponent } from '../prometheus-tabs/prometheus-tabs.component';
+import { CephModule } from '../../../ceph.module';
+import { DashboardModule } from '../../../dashboard/dashboard.module';
+import { ClusterModule } from '../../cluster.module';
 import { AlertListComponent } from './alert-list.component';
 
 describe('AlertListComponent', () => {
@@ -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 (file)
index 0000000..1c2aeb9
--- /dev/null
@@ -0,0 +1,8 @@
+<cd-table [data]="data"
+          [columns]="columns"
+          (updateSelection)="selectionUpdated($event)"
+          [selectionType]="'single'"></cd-table>
+
+<cd-table-key-value [data]="selectedRule"
+                    [renderObjects]="true"
+                    [hideKeys]="hideKeys"></cd-table-key-value>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..f09bd49
--- /dev/null
@@ -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<RulesListComponent>;
+
+  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 (file)
index 0000000..41577da
--- /dev/null
@@ -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();
+  }
+}
index db14f4679c1128cc7bf862625c73397092e76005..93e3c43c0c896dccfa0d063fcc97ad8a4c3ad459 100644 (file)
@@ -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', () => {
index 81488bbf0453e5660356f63d0c158d65442395ce..0c8f2ff06530788af45fa0015151f45ca508a7a9 100644 (file)
@@ -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<AlertmanagerSilence[]>(`${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) {
index 965418ddbf3b1371b5dd8b7e26a4692ac54dc3c9..4b6e109a03608654cf560b3418f5d903f91ac91f 100644 (file)
@@ -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();
index 7b832640f3972e7909a7e9068cfcc48792b1d5d4..8006bf721843f8e421fe99e52552bd796149377a 100644 (file)
@@ -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[] {
index 222581b1cda686415520783a991540d7dce00b3f..f5e8f850b15bb1011e52100d4843c7f7ebf3f166 100644 (file)
@@ -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 {
index f9d1fb2a7717f8ba826e79ea86d2d85394abf67f..de3ecf10298cd098b3ab022792340ac0e4fa584e 100644 (file)
@@ -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);
index 24d26a2cd8290c0bb5a3dd6058486950d95604c4..dc1731b92665796168dc31f6acba908040295e8e 100644 (file)
@@ -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);