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