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';
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.'
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) => {
it('should create a silence', () => {
fillAndSubmit();
expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
- expectSuccessNotification('Created');
+ expectSuccessNotification('Created', silence.matchers);
});
it('should recreate a silence', () => {
component.id = 'recreateId';
fillAndSubmit();
expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
- expectSuccessNotification('Recreated');
+ expectSuccessNotification('Recreated', silence.matchers);
});
it('should edit a silence', () => {
silence.id = component.id;
fillAndSubmit();
expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
- expectSuccessNotification('Edited');
+ expectSuccessNotification('Edited', silence.matchers);
});
});
});
permission: Permission;
form: CdFormGroup;
rules: PrometheusRule[];
+ matchName = '';
+ matchValue = '';
recreate = false;
edit = false;
];
datetimeFormat = 'YYYY-MM-DD HH:mm';
+ isNavigate = true;
constructor(
private router: Router,
this.getModeSpecificData();
}
- private getRules() {
+ getRules() {
this.prometheusService.ifPrometheusConfigured(
() =>
this.prometheusService.getRules().subscribe(
);
}
);
+ return this.rules;
}
private getModeSpecificData() {
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) {
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 })
);
return payload;
}
- private getNotificationTile(id: string) {
+ private getNotificationTile(matchers: AlertmanagerSilenceMatcher[]) {
let action;
if (this.edit) {
action = this.succeededLabels.EDITED;
} 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);
}
}
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';
let component: SilenceListComponent;
let fixture: ComponentFixture<SilenceListComponent>;
let prometheusService: PrometheusService;
+ let authStorageService: AuthStorageService;
+ let prometheusPermissions: Permission;
configureTestBed({
imports: [
});
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);
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';
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']
'badge badge-default': 'expired'
};
sorts: SortPropDir[] = [{ prop: 'endsAt', dir: SortDirection.desc }];
+ rules: PrometheusRule[];
+ visited: boolean;
constructor(
private authStorageService: AuthStorageService,
private urlBuilder: URLBuilderService,
private actionLabels: ActionLabelsI18n,
private succeededLabels: SucceededActionLabelsI18n,
+ private silenceFormComponent: SilenceFormComponent,
+ private silenceMatcher: PrometheusSilenceMatcherService,
@Inject(PrometheusService) prometheusService: PrometheusService
) {
super(prometheusService);
prop: 'id',
flexGrow: 3
},
+ {
+ name: $localize`Alerts Silenced`,
+ prop: 'silencedAlerts',
+ flexGrow: 3,
+ cellTransformation: CellTemplate.badge
+ },
{
name: $localize`Created by`,
prop: 'createdBy',
this.prometheusService.getSilences().subscribe(
(silences) => {
this.silences = silences;
+ const activeSilences = silences.filter(
+ (silence: AlertmanagerSilence) => silence.status.state !== 'expired'
+ );
+ this.getAlerts(activeSilences);
},
() => {
this.prometheusService.disableAlertmanagerConfig();
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`;
(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"
.card-text {
margin-right: 15px;
}
+
+.mute {
+ margin-right: -17px;
+ margin-top: -4px;
+}
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';
describe('NotificationsSidebarComponent', () => {
let component: NotificationsSidebarComponent;
let fixture: ComponentFixture<NotificationsSidebarComponent>;
+ let prometheusUpdatePermission: string;
+ let prometheusReadPermission: string;
+ let prometheusCreatePermission: string;
+ let configOptReadPermission: string;
configureTestBed({
imports: [
});
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;
});
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);
};
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()
);
tick(6000);
expect(component.notifications.length).toBe(1);
expect(component.notifications[0].title).toBe('Sample title');
+ discardPeriodicTasks();
}));
});
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';
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'],
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
) {
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';
+ }
+ );
+ }
}
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
+import { PrometheusRule } from './prometheus-alerts';
+
export class AlertmanagerSilenceMatcher {
name: string;
value: any;
status?: {
state: 'expired' | 'active' | 'pending';
};
+ silencedAlerts?: PrometheusRule[];
}
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];
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);
}