diff options
Diffstat (limited to 'src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts')
-rw-r--r-- | src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts | 340 |
1 files changed, 340 insertions, 0 deletions
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts new file mode 100644 index 000000000..b698e4958 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts @@ -0,0 +1,340 @@ +import { Component } from '@angular/core'; +import { Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; + +import _ from 'lodash'; +import moment from 'moment'; + +import { DashboardNotFoundError } from '~/app/core/error/error'; +import { PrometheusService } from '~/app/shared/api/prometheus.service'; +import { ActionLabelsI18n, SucceededActionLabelsI18n } from '~/app/shared/constants/app.constants'; +import { Icons } from '~/app/shared/enum/icons.enum'; +import { NotificationType } from '~/app/shared/enum/notification-type.enum'; +import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder'; +import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { CdValidators } from '~/app/shared/forms/cd-validators'; +import { + AlertmanagerSilence, + AlertmanagerSilenceMatcher, + AlertmanagerSilenceMatcherMatch +} from '~/app/shared/models/alertmanager-silence'; +import { Permission } from '~/app/shared/models/permissions'; +import { AlertmanagerAlert, PrometheusRule } from '~/app/shared/models/prometheus-alerts'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { ModalService } from '~/app/shared/services/modal.service'; +import { NotificationService } from '~/app/shared/services/notification.service'; +import { PrometheusSilenceMatcherService } from '~/app/shared/services/prometheus-silence-matcher.service'; +import { TimeDiffService } from '~/app/shared/services/time-diff.service'; +import { SilenceMatcherModalComponent } from '../silence-matcher-modal/silence-matcher-modal.component'; + +@Component({ + selector: 'cd-prometheus-form', + templateUrl: './silence-form.component.html', + styleUrls: ['./silence-form.component.scss'] +}) +export class SilenceFormComponent { + icons = Icons; + permission: Permission; + form: CdFormGroup; + rules: PrometheusRule[]; + + recreate = false; + edit = false; + id: string; + + action: string; + resource = $localize`silence`; + + matchers: AlertmanagerSilenceMatcher[] = []; + matcherMatch: AlertmanagerSilenceMatcherMatch = undefined; + matcherConfig = [ + { + tooltip: $localize`Attribute name`, + icon: this.icons.paragraph, + attribute: 'name' + }, + { + tooltip: $localize`Value`, + icon: this.icons.terminal, + attribute: 'value' + }, + { + tooltip: $localize`Regular expression`, + icon: this.icons.magic, + attribute: 'isRegex' + } + ]; + + datetimeFormat = 'YYYY-MM-DD HH:mm'; + + constructor( + private router: Router, + private authStorageService: AuthStorageService, + private formBuilder: CdFormBuilder, + private prometheusService: PrometheusService, + private notificationService: NotificationService, + private route: ActivatedRoute, + private timeDiff: TimeDiffService, + private modalService: ModalService, + private silenceMatcher: PrometheusSilenceMatcherService, + private actionLabels: ActionLabelsI18n, + private succeededLabels: SucceededActionLabelsI18n + ) { + this.init(); + } + + private init() { + this.chooseMode(); + this.authenticate(); + this.createForm(); + this.setupDates(); + this.getData(); + } + + private chooseMode() { + this.edit = this.router.url.startsWith('/monitoring/silences/edit'); + this.recreate = this.router.url.startsWith('/monitoring/silences/recreate'); + if (this.edit) { + this.action = this.actionLabels.EDIT; + } else if (this.recreate) { + this.action = this.actionLabels.RECREATE; + } else { + this.action = this.actionLabels.CREATE; + } + } + + private authenticate() { + this.permission = this.authStorageService.getPermissions().prometheus; + const allowed = + this.permission.read && (this.edit ? this.permission.update : this.permission.create); + if (!allowed) { + throw new DashboardNotFoundError(); + } + } + + private createForm() { + const formatValidator = CdValidators.custom('format', (expiresAt: string) => { + const result = expiresAt === '' || moment(expiresAt, this.datetimeFormat).isValid(); + return !result; + }); + this.form = this.formBuilder.group( + { + startsAt: ['', [Validators.required, formatValidator]], + duration: ['2h', [Validators.min(1)]], + endsAt: ['', [Validators.required, formatValidator]], + createdBy: [this.authStorageService.getUsername(), [Validators.required]], + comment: [null, [Validators.required]] + }, + { + validators: CdValidators.custom('matcherRequired', () => this.matchers.length === 0) + } + ); + } + + private setupDates() { + const now = moment().format(this.datetimeFormat); + this.form.silentSet('startsAt', now); + this.updateDate(); + this.subscribeDateChanges(); + } + + private updateDate(updateStartDate?: boolean) { + const date = moment( + this.form.getValue(updateStartDate ? 'endsAt' : 'startsAt'), + this.datetimeFormat + ).toDate(); + const next = this.timeDiff.calculateDate(date, this.form.getValue('duration'), updateStartDate); + if (next) { + const nextDate = moment(next).format(this.datetimeFormat); + this.form.silentSet(updateStartDate ? 'startsAt' : 'endsAt', nextDate); + } + } + + private subscribeDateChanges() { + this.form.get('startsAt').valueChanges.subscribe(() => { + this.onDateChange(); + }); + this.form.get('duration').valueChanges.subscribe(() => { + this.updateDate(); + }); + this.form.get('endsAt').valueChanges.subscribe(() => { + this.onDateChange(true); + }); + } + + private onDateChange(updateStartDate?: boolean) { + const startsAt = moment(this.form.getValue('startsAt'), this.datetimeFormat); + const endsAt = moment(this.form.getValue('endsAt'), this.datetimeFormat); + if (startsAt.isBefore(endsAt)) { + this.updateDuration(); + } else { + this.updateDate(updateStartDate); + } + } + + private updateDuration() { + const startsAt = moment(this.form.getValue('startsAt'), this.datetimeFormat).toDate(); + const endsAt = moment(this.form.getValue('endsAt'), this.datetimeFormat).toDate(); + this.form.silentSet('duration', this.timeDiff.calculateDuration(startsAt, endsAt)); + } + + private getData() { + this.getRules(); + this.getModeSpecificData(); + } + + private getRules() { + this.prometheusService.ifPrometheusConfigured( + () => + this.prometheusService.getRules().subscribe( + (groups) => { + this.rules = groups['groups'].reduce( + (acc, group) => _.concat<PrometheusRule>(acc, group.rules), + [] + ); + }, + () => { + this.prometheusService.disablePrometheusConfig(); + this.rules = []; + } + ), + () => { + this.rules = []; + this.notificationService.show( + NotificationType.info, + $localize`Please add your Prometheus host to the dashboard configuration and refresh the page`, + undefined, + undefined, + 'Prometheus' + ); + } + ); + } + + private getModeSpecificData() { + this.route.params.subscribe((params: { id: string }) => { + if (!params.id) { + return; + } + if (this.edit || this.recreate) { + this.prometheusService.getSilences().subscribe((silences) => { + const silence = _.find(silences, ['id', params.id]); + if (!_.isUndefined(silence)) { + this.fillFormWithSilence(silence); + } + }); + } else { + this.prometheusService.getAlerts().subscribe((alerts) => { + const alert = _.find(alerts, ['fingerprint', params.id]); + if (!_.isUndefined(alert)) { + this.fillFormByAlert(alert); + } + }); + } + }); + } + + private fillFormWithSilence(silence: AlertmanagerSilence) { + this.id = silence.id; + if (this.edit) { + ['startsAt', 'endsAt'].forEach((attr) => + this.form.silentSet(attr, moment(silence[attr]).format(this.datetimeFormat)) + ); + this.updateDuration(); + } + ['createdBy', 'comment'].forEach((attr) => this.form.silentSet(attr, silence[attr])); + this.matchers = silence.matchers; + this.validateMatchers(); + } + + private validateMatchers() { + if (!this.rules) { + window.setTimeout(() => this.validateMatchers(), 100); + return; + } + this.matcherMatch = this.silenceMatcher.multiMatch(this.matchers, this.rules); + this.form.markAsDirty(); + this.form.updateValueAndValidity(); + } + + private fillFormByAlert(alert: AlertmanagerAlert) { + const labels = alert.labels; + Object.keys(labels).forEach((key) => + this.setMatcher({ + name: key, + value: labels[key], + isRegex: false + }) + ); + } + + private setMatcher(matcher: AlertmanagerSilenceMatcher, index?: number) { + if (_.isNumber(index)) { + this.matchers[index] = matcher; + } else { + this.matchers.push(matcher); + } + this.validateMatchers(); + } + + showMatcherModal(index?: number) { + const modalRef = this.modalService.show(SilenceMatcherModalComponent); + const modalComponent = modalRef.componentInstance as SilenceMatcherModalComponent; + modalComponent.rules = this.rules; + if (_.isNumber(index)) { + modalComponent.editMode = true; + modalComponent.preFillControls(this.matchers[index]); + } + modalComponent.submitAction.subscribe((matcher: AlertmanagerSilenceMatcher) => { + this.setMatcher(matcher, index); + }); + } + + deleteMatcher(index: number) { + this.matchers.splice(index, 1); + this.validateMatchers(); + } + + submit() { + if (this.form.invalid) { + return; + } + this.prometheusService.setSilence(this.getSubmitData()).subscribe( + (resp) => { + this.router.navigate(['/monitoring/silences']); + this.notificationService.show( + NotificationType.success, + this.getNotificationTile(resp.body['silenceId']), + undefined, + undefined, + 'Prometheus' + ); + }, + () => this.form.setErrors({ cdSubmitButton: true }) + ); + } + + private getSubmitData(): AlertmanagerSilence { + const payload = this.form.value; + delete payload.duration; + payload.startsAt = moment(payload.startsAt, this.datetimeFormat).toISOString(); + payload.endsAt = moment(payload.endsAt, this.datetimeFormat).toISOString(); + payload.matchers = this.matchers; + if (this.edit) { + payload.id = this.id; + } + return payload; + } + + private getNotificationTile(id: string) { + let action; + if (this.edit) { + action = this.succeededLabels.EDITED; + } else if (this.recreate) { + action = this.succeededLabels.RECREATED; + } else { + action = this.succeededLabels.CREATED; + } + return `${action} ${this.resource} ${id}`; + } +} |