]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/blob
958039a31dc716fd7d48fdbff9100b7becd49fb7
[ceph-ci.git] /
1 import { Component } from '@angular/core';
2 import { Validators } from '@angular/forms';
3 import { ActivatedRoute, Router } from '@angular/router';
4
5 import _ from 'lodash';
6 import moment from 'moment';
7
8 import { DashboardNotFoundError } from '~/app/core/error/error';
9 import { PrometheusService } from '~/app/shared/api/prometheus.service';
10 import { ActionLabelsI18n, SucceededActionLabelsI18n } from '~/app/shared/constants/app.constants';
11 import { Icons } from '~/app/shared/enum/icons.enum';
12 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
13 import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
14 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
15 import { CdValidators } from '~/app/shared/forms/cd-validators';
16 import {
17   AlertmanagerSilence,
18   AlertmanagerSilenceMatcher,
19   AlertmanagerSilenceMatcherMatch
20 } from '~/app/shared/models/alertmanager-silence';
21 import { Permission } from '~/app/shared/models/permissions';
22 import { AlertmanagerAlert, PrometheusRule } from '~/app/shared/models/prometheus-alerts';
23 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
24 import { ModalService } from '~/app/shared/services/modal.service';
25 import { NotificationService } from '~/app/shared/services/notification.service';
26 import { PrometheusSilenceMatcherService } from '~/app/shared/services/prometheus-silence-matcher.service';
27 import { TimeDiffService } from '~/app/shared/services/time-diff.service';
28 import { SilenceMatcherModalComponent } from '../silence-matcher-modal/silence-matcher-modal.component';
29
30 @Component({
31   selector: 'cd-prometheus-form',
32   templateUrl: './silence-form.component.html',
33   styleUrls: ['./silence-form.component.scss']
34 })
35 export class SilenceFormComponent {
36   icons = Icons;
37   permission: Permission;
38   form: CdFormGroup;
39   rules: PrometheusRule[];
40   matchName = '';
41   matchValue = '';
42
43   recreate = false;
44   edit = false;
45   id: string;
46
47   action: string;
48   resource = $localize`silence`;
49
50   matchers: AlertmanagerSilenceMatcher[] = [];
51   matcherMatch: AlertmanagerSilenceMatcherMatch = undefined;
52   matcherConfig = [
53     {
54       tooltip: $localize`Attribute name`,
55       attribute: 'name'
56     },
57     {
58       tooltip: $localize`Regular expression`,
59       attribute: 'isRegex'
60     },
61     {
62       tooltip: $localize`Value`,
63       attribute: 'value'
64     }
65   ];
66
67   datetimeFormat = 'YYYY-MM-DD HH:mm';
68   isNavigate = true;
69
70   constructor(
71     private router: Router,
72     private authStorageService: AuthStorageService,
73     private formBuilder: CdFormBuilder,
74     private prometheusService: PrometheusService,
75     private notificationService: NotificationService,
76     private route: ActivatedRoute,
77     private timeDiff: TimeDiffService,
78     private modalService: ModalService,
79     private silenceMatcher: PrometheusSilenceMatcherService,
80     private actionLabels: ActionLabelsI18n,
81     private succeededLabels: SucceededActionLabelsI18n
82   ) {
83     this.init();
84   }
85
86   private init() {
87     this.chooseMode();
88     this.authenticate();
89     this.createForm();
90     this.setupDates();
91     this.getData();
92   }
93
94   private chooseMode() {
95     this.edit = this.router.url.startsWith('/monitoring/silences/edit');
96     this.recreate = this.router.url.startsWith('/monitoring/silences/recreate');
97     if (this.edit) {
98       this.action = this.actionLabels.EDIT;
99     } else if (this.recreate) {
100       this.action = this.actionLabels.RECREATE;
101     } else {
102       this.action = this.actionLabels.CREATE;
103     }
104   }
105
106   private authenticate() {
107     this.permission = this.authStorageService.getPermissions().prometheus;
108     const allowed =
109       this.permission.read && (this.edit ? this.permission.update : this.permission.create);
110     if (!allowed) {
111       throw new DashboardNotFoundError();
112     }
113   }
114
115   private createForm() {
116     const formatValidator = CdValidators.custom('format', (expiresAt: string) => {
117       const result = expiresAt === '' || moment(expiresAt, this.datetimeFormat).isValid();
118       return !result;
119     });
120     this.form = this.formBuilder.group(
121       {
122         startsAt: ['', [Validators.required, formatValidator]],
123         duration: ['2h', [Validators.min(1)]],
124         endsAt: ['', [Validators.required, formatValidator]],
125         createdBy: [this.authStorageService.getUsername(), [Validators.required]],
126         comment: [null, [Validators.required]]
127       },
128       {
129         validators: CdValidators.custom('matcherRequired', () => this.matchers.length === 0)
130       }
131     );
132   }
133
134   private setupDates() {
135     const now = moment().format(this.datetimeFormat);
136     this.form.silentSet('startsAt', now);
137     this.updateDate();
138     this.subscribeDateChanges();
139   }
140
141   private updateDate(updateStartDate?: boolean) {
142     const date = moment(
143       this.form.getValue(updateStartDate ? 'endsAt' : 'startsAt'),
144       this.datetimeFormat
145     ).toDate();
146     const next = this.timeDiff.calculateDate(date, this.form.getValue('duration'), updateStartDate);
147     if (next) {
148       const nextDate = moment(next).format(this.datetimeFormat);
149       this.form.silentSet(updateStartDate ? 'startsAt' : 'endsAt', nextDate);
150     }
151   }
152
153   private subscribeDateChanges() {
154     this.form.get('startsAt').valueChanges.subscribe(() => {
155       this.onDateChange();
156     });
157     this.form.get('duration').valueChanges.subscribe(() => {
158       this.updateDate();
159     });
160     this.form.get('endsAt').valueChanges.subscribe(() => {
161       this.onDateChange(true);
162     });
163   }
164
165   private onDateChange(updateStartDate?: boolean) {
166     const startsAt = moment(this.form.getValue('startsAt'), this.datetimeFormat);
167     const endsAt = moment(this.form.getValue('endsAt'), this.datetimeFormat);
168     if (startsAt.isBefore(endsAt)) {
169       this.updateDuration();
170     } else {
171       this.updateDate(updateStartDate);
172     }
173   }
174
175   private updateDuration() {
176     const startsAt = moment(this.form.getValue('startsAt'), this.datetimeFormat).toDate();
177     const endsAt = moment(this.form.getValue('endsAt'), this.datetimeFormat).toDate();
178     this.form.silentSet('duration', this.timeDiff.calculateDuration(startsAt, endsAt));
179   }
180
181   private getData() {
182     this.getRules();
183     this.getModeSpecificData();
184   }
185
186   getRules() {
187     this.prometheusService.ifPrometheusConfigured(
188       () =>
189         this.prometheusService.getRules().subscribe(
190           (groups) => {
191             this.rules = groups['groups'].reduce(
192               (acc, group) => _.concat<PrometheusRule>(acc, group.rules),
193               []
194             );
195           },
196           () => {
197             this.prometheusService.disablePrometheusConfig();
198             this.rules = [];
199           }
200         ),
201       () => {
202         this.rules = [];
203         this.notificationService.show(
204           NotificationType.info,
205           $localize`Please add your Prometheus host to the dashboard configuration and refresh the page`,
206           undefined,
207           undefined,
208           'Prometheus'
209         );
210       }
211     );
212     return this.rules;
213   }
214
215   private getModeSpecificData() {
216     this.route.params.subscribe((params: { id: string }) => {
217       if (!params.id) {
218         return;
219       }
220       if (this.edit || this.recreate) {
221         this.prometheusService.getSilences().subscribe((silences) => {
222           const silence = _.find(silences, ['id', params.id]);
223           if (!_.isUndefined(silence)) {
224             this.fillFormWithSilence(silence);
225           }
226         });
227       } else {
228         this.prometheusService.getAlerts().subscribe((alerts) => {
229           const alert = _.find(alerts, ['fingerprint', params.id]);
230           if (!_.isUndefined(alert)) {
231             this.fillFormByAlert(alert);
232           }
233         });
234       }
235     });
236   }
237
238   private fillFormWithSilence(silence: AlertmanagerSilence) {
239     this.id = silence.id;
240     if (this.edit) {
241       ['startsAt', 'endsAt'].forEach((attr) =>
242         this.form.silentSet(attr, moment(silence[attr]).format(this.datetimeFormat))
243       );
244       this.updateDuration();
245     }
246     ['createdBy', 'comment'].forEach((attr) => this.form.silentSet(attr, silence[attr]));
247     this.matchers = silence.matchers;
248     this.validateMatchers();
249   }
250
251   private validateMatchers() {
252     if (!this.rules) {
253       window.setTimeout(() => this.validateMatchers(), 100);
254       return;
255     }
256     this.matcherMatch = this.silenceMatcher.multiMatch(this.matchers, this.rules);
257     this.form.markAsDirty();
258     this.form.updateValueAndValidity();
259   }
260
261   private fillFormByAlert(alert: AlertmanagerAlert) {
262     const labels = alert.labels;
263     this.setMatcher({
264       name: 'alertname',
265       value: labels.alertname,
266       isRegex: false
267     });
268   }
269
270   private setMatcher(matcher: AlertmanagerSilenceMatcher, index?: number) {
271     if (_.isNumber(index)) {
272       this.matchers[index] = matcher;
273     } else {
274       this.matchers.push(matcher);
275     }
276     this.validateMatchers();
277   }
278
279   showMatcherModal(index?: number) {
280     const modalRef = this.modalService.show(SilenceMatcherModalComponent);
281     const modalComponent = modalRef.componentInstance as SilenceMatcherModalComponent;
282     modalComponent.rules = this.rules;
283     if (_.isNumber(index)) {
284       modalComponent.editMode = true;
285       modalComponent.preFillControls(this.matchers[index]);
286     }
287     modalComponent.submitAction.subscribe((matcher: AlertmanagerSilenceMatcher) => {
288       this.setMatcher(matcher, index);
289     });
290   }
291
292   deleteMatcher(index: number) {
293     this.matchers.splice(index, 1);
294     this.validateMatchers();
295   }
296
297   submit(data?: any) {
298     if (this.form.invalid) {
299       return;
300     }
301     this.prometheusService.setSilence(this.getSubmitData()).subscribe(
302       (resp) => {
303         if (data) {
304           data.silenceId = resp.body['silenceId'];
305         }
306         if (this.isNavigate) {
307           this.router.navigate(['/monitoring/silences']);
308         }
309         this.notificationService.show(
310           NotificationType.success,
311           this.getNotificationTile(this.matchers),
312           undefined,
313           undefined,
314           'Prometheus'
315         );
316         this.matchers = [];
317       },
318       () => this.form.setErrors({ cdSubmitButton: true })
319     );
320   }
321
322   private getSubmitData(): AlertmanagerSilence {
323     const payload = this.form.value;
324     delete payload.duration;
325     payload.startsAt = moment(payload.startsAt, this.datetimeFormat).toISOString();
326     payload.endsAt = moment(payload.endsAt, this.datetimeFormat).toISOString();
327     payload.matchers = this.matchers;
328     if (this.edit) {
329       payload.id = this.id;
330     }
331     return payload;
332   }
333
334   private getNotificationTile(matchers: AlertmanagerSilenceMatcher[]) {
335     let action;
336     if (this.edit) {
337       action = this.succeededLabels.EDITED;
338     } else if (this.recreate) {
339       action = this.succeededLabels.RECREATED;
340     } else {
341       action = this.succeededLabels.CREATED;
342     }
343     let msg = '';
344     for (const matcher of matchers) {
345       msg = msg.concat(` ${matcher.name} - ${matcher.value},`);
346     }
347     return `${action} ${this.resource} for ${msg.slice(0, -1)}`;
348   }
349 }