]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts
import ceph quincy 17.2.1
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / cluster / prometheus / silence-form / silence-form.component.ts
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 }