From: Aashish Sharma Date: Mon, 5 Sep 2022 05:51:40 +0000 (+0530) Subject: mgr/dashboard: Add a Silence button shortcut to alert notifications X-Git-Tag: v18.0.0~52^2 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=94fe31061500696937bc33a3a0e6fb9cba651c01;p=ceph-ci.git mgr/dashboard: Add a Silence button shortcut to alert notifications Fixes: https://tracker.ceph.com/issues/57457 Signed-off-by: Aashish Sharma --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts index e82bd2d274a..b4d8a86526d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts @@ -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); }); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts index d573a68e148..ca9efef0765 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts @@ -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); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.spec.ts index cc4b76c3271..a136b2bac11 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.spec.ts @@ -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; 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); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.ts index c351a64e5ac..29af2bd2ae8 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.ts @@ -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`; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.html index bba23747b01..37fc7f6bb94 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.html @@ -71,6 +71,21 @@ (click)="remove(i); $event.stopPropagation()"> + + +
{{ notification.title }}

{ let component: NotificationsSidebarComponent; let fixture: ComponentFixture; + 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(); })); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.ts index 8c5caf7ff6b..2062d537168 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.ts @@ -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'; + } + ); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts index 6b65f04e8cb..a08bfcecc36 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts @@ -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 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/alertmanager-silence.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/alertmanager-silence.ts index b7b8862954b..5f69f1e1e81 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/alertmanager-silence.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/alertmanager-silence.ts @@ -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[]; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-notification.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-notification.ts index c283c5d801d..ddc737c2dde 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-notification.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-notification.ts @@ -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]; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-silence-matcher.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-silence-matcher.service.ts index 7aec6d1d37c..d3dc1ea5020 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-silence-matcher.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-silence-matcher.service.ts @@ -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); }