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