]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: Add a Silence button shortcut to alert notifications
authorAashish Sharma <aasharma@redhat.com>
Mon, 5 Sep 2022 05:51:40 +0000 (11:21 +0530)
committerAashish Sharma <aasharma@redhat.com>
Mon, 12 Sep 2022 09:16:16 +0000 (14:46 +0530)
Fixes: https://tracker.ceph.com/issues/57457
Signed-off-by: Aashish Sharma <aasharma@redhat.com>
12 files changed:
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/alertmanager-silence.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-notification.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-silence-matcher.service.ts

index e82bd2d274a15036957616a77ae05bb3a79be1e4..b4d8a86526dc59946b81f9f5c15100f1ae9a9d9a 100644 (file)
@@ -15,7 +15,10 @@ import { ErrorComponent } from '~/app/core/error/error.component';
 import { PrometheusService } from '~/app/shared/api/prometheus.service';
 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
-import { AlertmanagerSilence } from '~/app/shared/models/alertmanager-silence';
+import {
+  AlertmanagerSilence,
+  AlertmanagerSilenceMatcher
+} from '~/app/shared/models/alertmanager-silence';
 import { Permission } from '~/app/shared/models/permissions';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { ModalService } from '~/app/shared/services/modal.service';
@@ -283,12 +286,7 @@ describe('SilenceFormComponent', () => {
       expectMode('alertAdd', false, false, 'Create');
       expect(prometheusService.getSilences).not.toHaveBeenCalled();
       expect(prometheusService.getAlerts).toHaveBeenCalled();
-      expect(component.matchers).toEqual([
-        createMatcher('alertname', 'alert0', false),
-        createMatcher('instance', 'someInstance', false),
-        createMatcher('job', 'someJob', false),
-        createMatcher('severity', 'someSeverity', false)
-      ]);
+      expect(component.matchers).toEqual([createMatcher('alertname', 'alert0', false)]);
       expect(component.matcherMatch).toEqual({
         cssClass: 'has-success',
         status: 'Matches 1 rule with 1 active alert.'
@@ -495,14 +493,22 @@ describe('SilenceFormComponent', () => {
     let silence: AlertmanagerSilence;
     const silenceId = '50M3-10N6-1D';
 
-    const expectSuccessNotification = (titleStartsWith: string) =>
+    const expectSuccessNotification = (
+      titleStartsWith: string,
+      matchers: AlertmanagerSilenceMatcher[]
+    ) => {
+      let msg = '';
+      for (const matcher of matchers) {
+        msg = msg.concat(` ${matcher.name} - ${matcher.value},`);
+      }
       expect(notificationService.show).toHaveBeenCalledWith(
         NotificationType.success,
-        `${titleStartsWith} silence ${silenceId}`,
+        `${titleStartsWith} silence for ${msg.slice(0, -1)}`,
         undefined,
         undefined,
         'Prometheus'
       );
+    };
 
     const fillAndSubmit = () => {
       ['createdBy', 'comment'].forEach((attr) => {
@@ -564,7 +570,7 @@ describe('SilenceFormComponent', () => {
     it('should create a silence', () => {
       fillAndSubmit();
       expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
-      expectSuccessNotification('Created');
+      expectSuccessNotification('Created', silence.matchers);
     });
 
     it('should recreate a silence', () => {
@@ -572,7 +578,7 @@ describe('SilenceFormComponent', () => {
       component.id = 'recreateId';
       fillAndSubmit();
       expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
-      expectSuccessNotification('Recreated');
+      expectSuccessNotification('Recreated', silence.matchers);
     });
 
     it('should edit a silence', () => {
@@ -581,7 +587,7 @@ describe('SilenceFormComponent', () => {
       silence.id = component.id;
       fillAndSubmit();
       expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
-      expectSuccessNotification('Edited');
+      expectSuccessNotification('Edited', silence.matchers);
     });
   });
 });
index d573a68e148587943f4f87385fd88093d1dca4d9..ca9efef0765c36c3f925acf8c0382a5d82e96daa 100644 (file)
@@ -37,6 +37,8 @@ export class SilenceFormComponent {
   permission: Permission;
   form: CdFormGroup;
   rules: PrometheusRule[];
+  matchName = '';
+  matchValue = '';
 
   recreate = false;
   edit = false;
@@ -63,6 +65,7 @@ export class SilenceFormComponent {
   ];
 
   datetimeFormat = 'YYYY-MM-DD HH:mm';
+  isNavigate = true;
 
   constructor(
     private router: Router,
@@ -180,7 +183,7 @@ export class SilenceFormComponent {
     this.getModeSpecificData();
   }
 
-  private getRules() {
+  getRules() {
     this.prometheusService.ifPrometheusConfigured(
       () =>
         this.prometheusService.getRules().subscribe(
@@ -206,6 +209,7 @@ export class SilenceFormComponent {
         );
       }
     );
+    return this.rules;
   }
 
   private getModeSpecificData() {
@@ -256,13 +260,11 @@ export class SilenceFormComponent {
 
   private fillFormByAlert(alert: AlertmanagerAlert) {
     const labels = alert.labels;
-    Object.keys(labels).forEach((key) =>
-      this.setMatcher({
-        name: key,
-        value: labels[key],
-        isRegex: false
-      })
-    );
+    this.setMatcher({
+      name: 'alertname',
+      value: labels.alertname,
+      isRegex: false
+    });
   }
 
   private setMatcher(matcher: AlertmanagerSilenceMatcher, index?: number) {
@@ -292,20 +294,26 @@ export class SilenceFormComponent {
     this.validateMatchers();
   }
 
-  submit() {
+  submit(data?: any) {
     if (this.form.invalid) {
       return;
     }
     this.prometheusService.setSilence(this.getSubmitData()).subscribe(
       (resp) => {
-        this.router.navigate(['/monitoring/silences']);
+        if (data) {
+          data.silenceId = resp.body['silenceId'];
+        }
+        if (this.isNavigate) {
+          this.router.navigate(['/monitoring/silences']);
+        }
         this.notificationService.show(
           NotificationType.success,
-          this.getNotificationTile(resp.body['silenceId']),
+          this.getNotificationTile(this.matchers),
           undefined,
           undefined,
           'Prometheus'
         );
+        this.matchers = [];
       },
       () => this.form.setErrors({ cdSubmitButton: true })
     );
@@ -323,7 +331,7 @@ export class SilenceFormComponent {
     return payload;
   }
 
-  private getNotificationTile(id: string) {
+  private getNotificationTile(matchers: AlertmanagerSilenceMatcher[]) {
     let action;
     if (this.edit) {
       action = this.succeededLabels.EDITED;
@@ -332,6 +340,23 @@ export class SilenceFormComponent {
     } else {
       action = this.succeededLabels.CREATED;
     }
-    return `${action} ${this.resource} ${id}`;
+    let msg = '';
+    for (const matcher of matchers) {
+      msg = msg.concat(` ${matcher.name} - ${matcher.value},`);
+    }
+    return `${action} ${this.resource} for ${msg.slice(0, -1)}`;
+  }
+
+  createSilenceFromNotification(data: any) {
+    this.isNavigate = false;
+    this.setMatcher({
+      name: 'alertname',
+      value: data['title'].split(' ')[0],
+      isRegex: false
+    });
+    this.createForm();
+    this.form.get('comment').setValue('Silence created from the alert notification');
+    this.setupDates();
+    this.submit(data);
   }
 }
index cc4b76c327152c6a2ead9cb75ec27ce0720d3e79..a136b2bac1119b6c328e0d909bc5255df1402766 100644 (file)
@@ -11,6 +11,8 @@ import { PrometheusService } from '~/app/shared/api/prometheus.service';
 import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
 import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { ModalService } from '~/app/shared/services/modal.service';
 import { NotificationService } from '~/app/shared/services/notification.service';
 import { SharedModule } from '~/app/shared/shared.module';
@@ -22,6 +24,8 @@ describe('SilenceListComponent', () => {
   let component: SilenceListComponent;
   let fixture: ComponentFixture<SilenceListComponent>;
   let prometheusService: PrometheusService;
+  let authStorageService: AuthStorageService;
+  let prometheusPermissions: Permission;
 
   configureTestBed({
     imports: [
@@ -36,6 +40,11 @@ describe('SilenceListComponent', () => {
   });
 
   beforeEach(() => {
+    authStorageService = TestBed.inject(AuthStorageService);
+    prometheusPermissions = new Permission(['update', 'delete', 'read', 'create']);
+    spyOn(authStorageService, 'getPermissions').and.callFake(() => ({
+      prometheus: prometheusPermissions
+    }));
     fixture = TestBed.createComponent(SilenceListComponent);
     component = fixture.componentInstance;
     prometheusService = TestBed.inject(PrometheusService);
index c351a64e5ac7ad148a959b6157b8d87ac053a855..29af2bd2ae891c33865fb9750cb12c302a5b0f27 100644 (file)
@@ -4,6 +4,8 @@ import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
 import { SortDirection, SortPropDir } from '@swimlane/ngx-datatable';
 import { Observable, Subscriber } from 'rxjs';
 
+import { PrometheusListHelper } from '~/app/ceph/cluster/prometheus/prometheus-list-helper';
+import { SilenceFormComponent } from '~/app/ceph/cluster/prometheus/silence-form/silence-form.component';
 import { PrometheusService } from '~/app/shared/api/prometheus.service';
 import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
 import { ActionLabelsI18n, SucceededActionLabelsI18n } from '~/app/shared/constants/app.constants';
@@ -15,17 +17,21 @@ import { CdTableAction } from '~/app/shared/models/cd-table-action';
 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
 import { Permission } from '~/app/shared/models/permissions';
+import { PrometheusRule } from '~/app/shared/models/prometheus-alerts';
 import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { ModalService } from '~/app/shared/services/modal.service';
 import { NotificationService } from '~/app/shared/services/notification.service';
+import { PrometheusSilenceMatcherService } from '~/app/shared/services/prometheus-silence-matcher.service';
 import { URLBuilderService } from '~/app/shared/services/url-builder.service';
-import { PrometheusListHelper } from '../prometheus-list-helper';
 
 const BASE_URL = 'monitoring/silences';
 
 @Component({
-  providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }],
+  providers: [
+    { provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) },
+    SilenceFormComponent
+  ],
   selector: 'cd-silences-list',
   templateUrl: './silence-list.component.html',
   styleUrls: ['./silence-list.component.scss']
@@ -43,6 +49,8 @@ export class SilenceListComponent extends PrometheusListHelper {
     'badge badge-default': 'expired'
   };
   sorts: SortPropDir[] = [{ prop: 'endsAt', dir: SortDirection.desc }];
+  rules: PrometheusRule[];
+  visited: boolean;
 
   constructor(
     private authStorageService: AuthStorageService,
@@ -52,6 +60,8 @@ export class SilenceListComponent extends PrometheusListHelper {
     private urlBuilder: URLBuilderService,
     private actionLabels: ActionLabelsI18n,
     private succeededLabels: SucceededActionLabelsI18n,
+    private silenceFormComponent: SilenceFormComponent,
+    private silenceMatcher: PrometheusSilenceMatcherService,
     @Inject(PrometheusService) prometheusService: PrometheusService
   ) {
     super(prometheusService);
@@ -111,6 +121,12 @@ export class SilenceListComponent extends PrometheusListHelper {
         prop: 'id',
         flexGrow: 3
       },
+      {
+        name: $localize`Alerts Silenced`,
+        prop: 'silencedAlerts',
+        flexGrow: 3,
+        cellTransformation: CellTemplate.badge
+      },
       {
         name: $localize`Created by`,
         prop: 'createdBy',
@@ -144,6 +160,10 @@ export class SilenceListComponent extends PrometheusListHelper {
       this.prometheusService.getSilences().subscribe(
         (silences) => {
           this.silences = silences;
+          const activeSilences = silences.filter(
+            (silence: AlertmanagerSilence) => silence.status.state !== 'expired'
+          );
+          this.getAlerts(activeSilences);
         },
         () => {
           this.prometheusService.disableAlertmanagerConfig();
@@ -156,6 +176,20 @@ export class SilenceListComponent extends PrometheusListHelper {
     this.selection = selection;
   }
 
+  getAlerts(silences: any) {
+    const rules = this.silenceFormComponent.getRules();
+    silences.forEach((silence: any) => {
+      silence.matchers.forEach((matcher: any) => {
+        this.rules = this.silenceMatcher.getMatchedRules(matcher, rules);
+        const alertNames: string[] = [];
+        for (const rule of this.rules) {
+          alertNames.push(rule.name);
+        }
+        silence.silencedAlerts = alertNames;
+      });
+    });
+  }
+
   expireSilence() {
     const id = this.selection.first().id;
     const i18nSilence = $localize`Silence`;
index bba23747b01d4be0aeceebd3f300887f4e0bd8a9..37fc7f6bb941080ca4ba8c9edcfa5824bf5ef3fb 100644 (file)
                       (click)="remove(i); $event.stopPropagation()">
                 <i [ngClass]="[icons.trash]"></i>
               </button>
+              <button *ngIf="notification.application == 'Prometheus' && notification.type != 2 && !notification.alertSilenced"
+                      class="btn btn-link float-right text-muted mute"
+                      title="Silence Alert"
+                      i18n-title
+                      (click)="silence(notification)">
+                <i [ngClass]="[icons.mute]"></i>
+              </button>
+              <button *ngIf="notification.application == 'Prometheus' && notification.type != 2 && notification.alertSilenced"
+                      class="btn btn-link float-right text-muted mute"
+                      title="Expire Silence"
+                      i18n-title
+                      (click)="expire(notification)">
+                <i [ngClass]="[icons.bell]"></i>
+              </button>
+
 
               <h6 class="card-title bold">{{ notification.title }}</h6>
               <p class="card-text"
index 596f3c358bf4d8b9f44de077ef225364645068f1..f3fe9cea3cff41040f772b761ea409f14c7a35e7 100644 (file)
@@ -1,5 +1,11 @@
 import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import {
+  ComponentFixture,
+  discardPeriodicTasks,
+  fakeAsync,
+  TestBed,
+  tick
+} from '@angular/core/testing';
 import { NoopAnimationsModule } from '@angular/platform-browser/animations';
 import { RouterTestingModule } from '@angular/router/testing';
 
@@ -26,6 +32,10 @@ import { NotificationsSidebarComponent } from './notifications-sidebar.component
 describe('NotificationsSidebarComponent', () => {
   let component: NotificationsSidebarComponent;
   let fixture: ComponentFixture<NotificationsSidebarComponent>;
+  let prometheusUpdatePermission: string;
+  let prometheusReadPermission: string;
+  let prometheusCreatePermission: string;
+  let configOptReadPermission: string;
 
   configureTestBed({
     imports: [
@@ -43,6 +53,21 @@ describe('NotificationsSidebarComponent', () => {
   });
 
   beforeEach(() => {
+    prometheusReadPermission = 'read';
+    prometheusUpdatePermission = 'update';
+    prometheusCreatePermission = 'create';
+    configOptReadPermission = 'read';
+    spyOn(TestBed.inject(AuthStorageService), 'getPermissions').and.callFake(
+      () =>
+        new Permissions({
+          prometheus: [
+            prometheusReadPermission,
+            prometheusUpdatePermission,
+            prometheusCreatePermission
+          ],
+          'config-opt': [configOptReadPermission]
+        })
+    );
     fixture = TestBed.createComponent(NotificationsSidebarComponent);
     component = fixture.componentInstance;
   });
@@ -55,8 +80,6 @@ describe('NotificationsSidebarComponent', () => {
   describe('prometheus alert handling', () => {
     let prometheusAlertService: PrometheusAlertService;
     let prometheusNotificationService: PrometheusNotificationService;
-    let prometheusReadPermission: string;
-    let configOptReadPermission: string;
 
     const expectPrometheusServicesToBeCalledTimes = (n: number) => {
       expect(prometheusNotificationService.refresh).toHaveBeenCalledTimes(n);
@@ -64,16 +87,6 @@ describe('NotificationsSidebarComponent', () => {
     };
 
     beforeEach(() => {
-      prometheusReadPermission = 'read';
-      configOptReadPermission = 'read';
-      spyOn(TestBed.inject(AuthStorageService), 'getPermissions').and.callFake(
-        () =>
-          new Permissions({
-            prometheus: [prometheusReadPermission],
-            'config-opt': [configOptReadPermission]
-          })
-      );
-
       spyOn(TestBed.inject(PrometheusService), 'ifAlertmanagerConfigured').and.callFake((fn) =>
         fn()
       );
@@ -152,6 +165,7 @@ describe('NotificationsSidebarComponent', () => {
       tick(6000);
       expect(component.notifications.length).toBe(1);
       expect(component.notifications[0].title).toBe('Sample title');
+      discardPeriodicTasks();
     }));
   });
 
index 8c5caf7ff6bd69c8eb0e51ff573dc95bed139b28..2062d53716871c25a89ce66e30aaeedf97e06057 100644 (file)
@@ -13,7 +13,11 @@ import _ from 'lodash';
 import moment from 'moment';
 import { Subscription } from 'rxjs';
 
+import { SilenceFormComponent } from '~/app/ceph/cluster/prometheus/silence-form/silence-form.component';
+import { PrometheusService } from '~/app/shared/api/prometheus.service';
+import { SucceededActionLabelsI18n } from '~/app/shared/constants/app.constants';
 import { Icons } from '~/app/shared/enum/icons.enum';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
 import { CdNotification } from '~/app/shared/models/cd-notification';
 import { ExecutingTask } from '~/app/shared/models/executing-task';
 import { FinishedTask } from '~/app/shared/models/finished-task';
@@ -25,6 +29,7 @@ import { SummaryService } from '~/app/shared/services/summary.service';
 import { TaskMessageService } from '~/app/shared/services/task-message.service';
 
 @Component({
+  providers: [SilenceFormComponent],
   selector: 'cd-notifications-sidebar',
   templateUrl: './notifications-sidebar.component.html',
   styleUrls: ['./notifications-sidebar.component.scss'],
@@ -56,8 +61,11 @@ export class NotificationsSidebarComponent implements OnInit, OnDestroy {
     private summaryService: SummaryService,
     private taskMessageService: TaskMessageService,
     private prometheusNotificationService: PrometheusNotificationService,
+    private succeededLabels: SucceededActionLabelsI18n,
     private authStorageService: AuthStorageService,
     private prometheusAlertService: PrometheusAlertService,
+    private prometheusService: PrometheusService,
+    private silenceFormComponent: SilenceFormComponent,
     private ngZone: NgZone,
     private cdRef: ChangeDetectorRef
   ) {
@@ -164,4 +172,27 @@ export class NotificationsSidebarComponent implements OnInit, OnDestroy {
   trackByFn(index: number) {
     return index;
   }
+
+  silence(data: CdNotification) {
+    data.alertSilenced = true;
+    this.silenceFormComponent.createSilenceFromNotification(data);
+  }
+
+  expire(data: CdNotification) {
+    data.alertSilenced = false;
+    this.prometheusService.expireSilence(data.silenceId).subscribe(
+      () => {
+        this.notificationService.show(
+          NotificationType.success,
+          `${this.succeededLabels.EXPIRED} ${data.silenceId}`,
+          undefined,
+          undefined,
+          'Prometheus'
+        );
+      },
+      (resp) => {
+        resp['application'] = 'Prometheus';
+      }
+    );
+  }
 }
index 6b65f04e8cb2f47af1489ba22dc7d5177fa4d966..a08bfcecc3603f31e6f05b6245fe4fdb2f78e65f 100644 (file)
@@ -53,6 +53,7 @@ export enum Icons {
   health = 'fa fa-heartbeat', // Health
   circle = 'fa fa-circle', // Circle
   bell = 'fa fa-bell', // Notification
+  mute = 'fa fa-bell-slash', // Mute or silence
   tag = 'fa fa-tag', // Tag, Badge
   leftArrow = 'fa fa-angle-left', // Left facing angle
   rightArrow = 'fa fa-angle-right', // Right facing angle
index b7b8862954baeb61433e8255a5725e3d5b836379..5f69f1e1e8164e51efbf9923dbb60e966334a35e 100644 (file)
@@ -1,3 +1,5 @@
+import { PrometheusRule } from './prometheus-alerts';
+
 export class AlertmanagerSilenceMatcher {
   name: string;
   value: any;
@@ -20,4 +22,5 @@ export class AlertmanagerSilence {
   status?: {
     state: 'expired' | 'active' | 'pending';
   };
+  silencedAlerts?: PrometheusRule[];
 }
index c283c5d801d23f86272aa83dd7cd8abb4b38147b..ddc737c2ddeea215a2afc5df2a2d84de8174286c 100644 (file)
@@ -29,6 +29,8 @@ export class CdNotification extends CdNotificationConfig {
   iconClass: string;
   duration: number;
   borderClass: string;
+  alertSilenced = false;
+  silenceId?: string;
 
   private textClasses = ['text-danger', 'text-info', 'text-success'];
   private iconClasses = [Icons.warning, Icons.info, Icons.check];
index 7aec6d1d37ccc362e99fed2d28f8e7106b3f7ec2..d3dc1ea502074ba50d2db9c472d10617d30b8d64 100644 (file)
@@ -39,10 +39,7 @@ export class PrometheusSilenceMatcherService {
     return this.describeMatch(rules);
   }
 
-  private getMatchedRules(
-    matcher: AlertmanagerSilenceMatcher,
-    rules: PrometheusRule[]
-  ): PrometheusRule[] {
+  getMatchedRules(matcher: AlertmanagerSilenceMatcher, rules: PrometheusRule[]): PrometheusRule[] {
     const attributePath = this.getAttributePath(matcher.name);
     return rules.filter((r) => _.get(r, attributePath) === matcher.value);
   }