]> git.apps.os.sepia.ceph.com Git - ceph.git/blob
b698e4958eddacbdfb10716da42ad8c2b2b47be3
[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
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       icon: this.icons.paragraph,
54       attribute: 'name'
55     },
56     {
57       tooltip: $localize`Value`,
58       icon: this.icons.terminal,
59       attribute: 'value'
60     },
61     {
62       tooltip: $localize`Regular expression`,
63       icon: this.icons.magic,
64       attribute: 'isRegex'
65     }
66   ];
67
68   datetimeFormat = 'YYYY-MM-DD HH:mm';
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   private 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   }
213
214   private getModeSpecificData() {
215     this.route.params.subscribe((params: { id: string }) => {
216       if (!params.id) {
217         return;
218       }
219       if (this.edit || this.recreate) {
220         this.prometheusService.getSilences().subscribe((silences) => {
221           const silence = _.find(silences, ['id', params.id]);
222           if (!_.isUndefined(silence)) {
223             this.fillFormWithSilence(silence);
224           }
225         });
226       } else {
227         this.prometheusService.getAlerts().subscribe((alerts) => {
228           const alert = _.find(alerts, ['fingerprint', params.id]);
229           if (!_.isUndefined(alert)) {
230             this.fillFormByAlert(alert);
231           }
232         });
233       }
234     });
235   }
236
237   private fillFormWithSilence(silence: AlertmanagerSilence) {
238     this.id = silence.id;
239     if (this.edit) {
240       ['startsAt', 'endsAt'].forEach((attr) =>
241         this.form.silentSet(attr, moment(silence[attr]).format(this.datetimeFormat))
242       );
243       this.updateDuration();
244     }
245     ['createdBy', 'comment'].forEach((attr) => this.form.silentSet(attr, silence[attr]));
246     this.matchers = silence.matchers;
247     this.validateMatchers();
248   }
249
250   private validateMatchers() {
251     if (!this.rules) {
252       window.setTimeout(() => this.validateMatchers(), 100);
253       return;
254     }
255     this.matcherMatch = this.silenceMatcher.multiMatch(this.matchers, this.rules);
256     this.form.markAsDirty();
257     this.form.updateValueAndValidity();
258   }
259
260   private fillFormByAlert(alert: AlertmanagerAlert) {
261     const labels = alert.labels;
262     Object.keys(labels).forEach((key) =>
263       this.setMatcher({
264         name: key,
265         value: labels[key],
266         isRegex: false
267       })
268     );
269   }
270
271   private setMatcher(matcher: AlertmanagerSilenceMatcher, index?: number) {
272     if (_.isNumber(index)) {
273       this.matchers[index] = matcher;
274     } else {
275       this.matchers.push(matcher);
276     }
277     this.validateMatchers();
278   }
279
280   showMatcherModal(index?: number) {
281     const modalRef = this.modalService.show(SilenceMatcherModalComponent);
282     const modalComponent = modalRef.componentInstance as SilenceMatcherModalComponent;
283     modalComponent.rules = this.rules;
284     if (_.isNumber(index)) {
285       modalComponent.editMode = true;
286       modalComponent.preFillControls(this.matchers[index]);
287     }
288     modalComponent.submitAction.subscribe((matcher: AlertmanagerSilenceMatcher) => {
289       this.setMatcher(matcher, index);
290     });
291   }
292
293   deleteMatcher(index: number) {
294     this.matchers.splice(index, 1);
295     this.validateMatchers();
296   }
297
298   submit() {
299     if (this.form.invalid) {
300       return;
301     }
302     this.prometheusService.setSilence(this.getSubmitData()).subscribe(
303       (resp) => {
304         this.router.navigate(['/monitoring/silences']);
305         this.notificationService.show(
306           NotificationType.success,
307           this.getNotificationTile(resp.body['silenceId']),
308           undefined,
309           undefined,
310           'Prometheus'
311         );
312       },
313       () => this.form.setErrors({ cdSubmitButton: true })
314     );
315   }
316
317   private getSubmitData(): AlertmanagerSilence {
318     const payload = this.form.value;
319     delete payload.duration;
320     payload.startsAt = moment(payload.startsAt, this.datetimeFormat).toISOString();
321     payload.endsAt = moment(payload.endsAt, this.datetimeFormat).toISOString();
322     payload.matchers = this.matchers;
323     if (this.edit) {
324       payload.id = this.id;
325     }
326     return payload;
327   }
328
329   private getNotificationTile(id: string) {
330     let action;
331     if (this.edit) {
332       action = this.succeededLabels.EDITED;
333     } else if (this.recreate) {
334       action = this.succeededLabels.RECREATED;
335     } else {
336       action = this.succeededLabels.CREATED;
337     }
338     return `${action} ${this.resource} ${id}`;
339   }
340 }