From 4701b11e1bc65591dcdbf60b5ab5d3be9b99dc1a Mon Sep 17 00:00:00 2001 From: Abhishek Desai Date: Fri, 29 Aug 2025 19:59:09 +0530 Subject: [PATCH] mgr/dashboard: Group similar alerts fixes : https://tracker.ceph.com/issues/72788 Signed-off-by: Abhishek Desai (cherry picked from commit cdd74a35103ecea7f8031aed494868fbd618d45b) Conflicts: src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.scss Accept the incoming changes Conflicts: src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.html src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.ts --- .../mgr/dashboard/controllers/prometheus.py | 8 +- .../active-alert-list.component.html | 99 +++++++++++++------ .../active-alert-list.component.spec.ts | 2 +- .../active-alert-list.component.ts | 39 ++++++-- .../silence-form.component.spec.ts | 4 +- .../silence-form/silence-form.component.ts | 6 +- .../dashboard/dashboard-v3.component.html | 7 +- .../dashboard/dashboard-v3.component.scss | 12 +++ .../dashboard/dashboard-v3.component.spec.ts | 11 ++- .../dashboard/dashboard-v3.component.ts | 2 +- .../navigation/navigation.component.spec.ts | 2 +- .../app/shared/api/prometheus.service.spec.ts | 4 +- .../src/app/shared/api/prometheus.service.ts | 6 ++ .../notifications-sidebar.component.ts | 2 +- .../app/shared/models/prometheus-alerts.ts | 7 ++ .../services/prometheus-alert.service.spec.ts | 38 ++++--- .../services/prometheus-alert.service.ts | 25 +++-- src/pybind/mgr/dashboard/openapi.yaml | 7 +- 18 files changed, 197 insertions(+), 84 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/prometheus.py b/src/pybind/mgr/dashboard/controllers/prometheus.py index c00d8c70e63..20cf9fdc3a4 100644 --- a/src/pybind/mgr/dashboard/controllers/prometheus.py +++ b/src/pybind/mgr/dashboard/controllers/prometheus.py @@ -173,7 +173,13 @@ class Prometheus(PrometheusRESTController): return self.alert_proxy('DELETE', '/silence/' + s_id) if s_id else None @RESTController.Collection(method='GET', path='/alertgroup') - def get_alertgroup(self, **params): + def get_alertgroup(self, cluster_filter=False, **params): + if cluster_filter: + try: + fsid = mgr.get('config')['fsid'] + except KeyError: + raise DashboardException("Cluster fsid not found", component='prometheus') + return self.alert_proxy('GET', f'/alerts/groups?filter=cluster={fsid}', params) return self.alert_proxy('GET', '/alerts/groups', params) @RESTController.Collection(method='GET', path='/prometheus_query_data') diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.html index 700f747a90e..dac63401338 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.html @@ -1,40 +1,75 @@ -To see all active Prometheus alerts, please provide - the URL to the API of Prometheus' Alertmanager as described - in the . +@if (!isAlertmanagerConfigured) { + To see all active Prometheus alerts, please provide + the URL to the API of Prometheus' Alertmanager as described + in the . +} - - - +@if (isAlertmanagerConfigured) { + + + - - + @if (expandedRow?.alert_count == 1) { + - - + } @else if (expandedRow?.alert_count > 1) { + + @if (expandedInnerRow) { + + } + + } + +} { fixture = TestBed.createComponent(ActiveAlertListComponent); component = fixture.componentInstance; let prometheusAlertService = TestBed.inject(PrometheusAlertService); - spyOn(prometheusAlertService, 'getAlerts').and.callFake(() => of([])); + spyOn(prometheusAlertService, 'getGroupedAlerts').and.callFake(() => of([])); }); it('should create', () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.ts index 95071f88c50..51a4b6fc8ce 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.ts @@ -24,11 +24,13 @@ export class ActiveAlertListComponent extends PrometheusListHelper implements On @ViewChild('externalLinkTpl', { static: true }) externalLinkTpl: TemplateRef; columns: CdTableColumn[]; + innerColumns: CdTableColumn[]; tableActions: CdTableAction[]; permission: Permission; selection = new CdTableSelection(); icons = Icons; multilineTextKeys = ['description', 'impact', 'fix']; + expandedInnerRow: any; constructor( // NotificationsComponent will refresh all alerts every 5s (No need to do it here as well) @@ -55,16 +57,10 @@ export class ActiveAlertListComponent extends PrometheusListHelper implements On ngOnInit() { super.ngOnInit(); - this.columns = [ - { - name: $localize`Name`, - prop: 'labels.alertname', - cellClass: 'fw-bold', - flexGrow: 2 - }, + this.innerColumns = [ { - name: $localize`Summary`, - prop: 'annotations.summary', + name: $localize`Description`, + prop: 'annotations.description', flexGrow: 3 }, { @@ -97,6 +93,25 @@ export class ActiveAlertListComponent extends PrometheusListHelper implements On prop: 'startsAt', cellTransformation: CellTemplate.timeAgo, flexGrow: 1 + } + ]; + this.columns = [ + { + name: $localize`Name`, + prop: 'labels.alertname', + cellClass: 'fw-bold', + flexGrow: 2 + }, + { + name: $localize`Summary`, + prop: 'annotations.summary', + flexGrow: 3 + }, + ...this.innerColumns.slice(1), + { + name: $localize`Occurrence`, + prop: 'alert_count', + flexGrow: 1 }, { name: $localize`URL`, @@ -106,7 +121,11 @@ export class ActiveAlertListComponent extends PrometheusListHelper implements On cellTemplate: this.externalLinkTpl } ]; - this.prometheusAlertService.getAlerts(true); + this.prometheusAlertService.getGroupedAlerts(true); + } + + setExpandedInnerRow(row: any) { + this.expandedInnerRow = row; } updateSelection(selection: CdTableSelection) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts index acff1a473d2..1613c9650b8 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts @@ -102,7 +102,7 @@ describe('SilenceFormComponent', () => { prometheus = new PrometheusHelper(); prometheusService = TestBed.inject(PrometheusService); - spyOn(prometheusService, 'getAlerts').and.callFake(() => { + spyOn(prometheusService, 'getGroupedAlerts').and.callFake(() => { const name = _.split(router.url, '/').pop(); return of([prometheus.createAlert(name)]); }); @@ -285,7 +285,7 @@ describe('SilenceFormComponent', () => { params = { id: 'alert0' }; expectMode('alertAdd', false, false, 'Create'); expect(prometheusService.getSilences).not.toHaveBeenCalled(); - expect(prometheusService.getAlerts).toHaveBeenCalled(); + expect(prometheusService.getGroupedAlerts).toHaveBeenCalled(); expect(component.matchers).toEqual([createMatcher('alertname', 'alert0', false)]); expect(component.matcherMatch).toEqual({ cssClass: 'has-success', 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 index 958039a31dc..a9002040f07 100644 --- 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 @@ -19,7 +19,7 @@ import { AlertmanagerSilenceMatcherMatch } from '~/app/shared/models/alertmanager-silence'; import { Permission } from '~/app/shared/models/permissions'; -import { AlertmanagerAlert, PrometheusRule } from '~/app/shared/models/prometheus-alerts'; +import { GroupAlertmanagerAlert, 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'; @@ -225,7 +225,7 @@ export class SilenceFormComponent { } }); } else { - this.prometheusService.getAlerts().subscribe((alerts) => { + this.prometheusService.getGroupedAlerts().subscribe((alerts) => { const alert = _.find(alerts, ['fingerprint', params.id]); if (!_.isUndefined(alert)) { this.fillFormByAlert(alert); @@ -258,7 +258,7 @@ export class SilenceFormComponent { this.form.updateValueAndValidity(); } - private fillFormByAlert(alert: AlertmanagerAlert) { + private fillFormByAlert(alert: GroupAlertmanagerAlert) { const labels = alert.labels; this.setMatcher({ name: 'alertname', diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.html index 81885b1133a..ddb5d68e905 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.html @@ -300,12 +300,15 @@
{{ alert.labels.alertname }}

+ [innerHtml]="alert.annotations.summary" + [ngbTooltip]="alert.annotations.summary">

Active since: {{ alert.startsAt | relativeDate }} + Total occurrences: {{ alert.alert_count }}

diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.scss index 49ab49bc81b..3d957ba68ec 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.scss @@ -1,3 +1,6 @@ +@use './src/styles/vendor/variables' as vv; +@use '@carbon/layout'; + .details { font-size: larger; @@ -30,4 +33,13 @@ -webkit-line-clamp: 2; white-space: normal; } + + .card-text .date { + display: inline-block; + min-width: layout.rem(220px); + } + + .card-text .alert_count { + display: inline-block; + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts index 2b28492bc1a..1f168561cd4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts @@ -119,7 +119,8 @@ describe('Dashbord Component', () => { inhibitedBy: null }, receivers: ['ceph2'], - fingerprint: 'fingerprint' + fingerprint: 'fingerprint', + alert_count: 1 }, { labels: { @@ -141,7 +142,8 @@ describe('Dashbord Component', () => { inhibitedBy: null }, receivers: ['default'], - fingerprint: 'fingerprint' + fingerprint: 'fingerprint', + alert_count: 1 }, { labels: { @@ -163,7 +165,8 @@ describe('Dashbord Component', () => { inhibitedBy: null }, receivers: ['ceph'], - fingerprint: 'fingerprint' + fingerprint: 'fingerprint', + alert_count: 1 } ]; @@ -201,7 +204,7 @@ describe('Dashbord Component', () => { component.prometheusAlertService.alerts = alertsPayload; component.isAlertmanagerConfigured = true; let prometheusAlertService = TestBed.inject(PrometheusAlertService); - spyOn(prometheusAlertService, 'getAlerts').and.callFake(() => of([])); + spyOn(prometheusAlertService, 'getGroupedAlerts').and.callFake(() => of([])); prometheusAlertService.activeCriticalAlerts = 2; prometheusAlertService.activeWarningAlerts = 1; }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.ts index ec77518371e..b11b748f7f2 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.ts @@ -147,7 +147,7 @@ export class DashboardV3Component extends PrometheusListHelper implements OnInit this.getDetailsCardData(); this.getTelemetryReport(); this.getCapacityCardData(); - this.prometheusAlertService.getAlerts(true); + this.prometheusAlertService.getGroupedAlerts(true); } getTelemetryText(): string { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts index a9d512d9b31..8979db7233f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts @@ -92,7 +92,7 @@ describe('NavigationComponent', () => { spyOn(TestBed.inject(SummaryService), 'subscribe').and.callFake(() => of({ health: { status: 'HEALTH_OK' } }) ); - spyOn(TestBed.inject(PrometheusAlertService), 'getAlerts').and.callFake(() => of([])); + spyOn(TestBed.inject(PrometheusAlertService), 'getGroupedAlerts').and.callFake(() => of([])); fixture = TestBed.createComponent(NavigationComponent); component = fixture.componentInstance; fixture.detectChanges(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.spec.ts index 96940845595..0b949c46c62 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.spec.ts @@ -29,8 +29,8 @@ describe('PrometheusService', () => { }); it('should get alerts', () => { - service.getAlerts().subscribe(); - const req = httpTesting.expectOne('api/prometheus?cluster_filter=false'); + service.getGroupedAlerts().subscribe(); + const req = httpTesting.expectOne('api/prometheus/alertgroup?cluster_filter=false'); expect(req.request.method).toBe('GET'); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts index cefcedca5c9..bf340df1d03 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts @@ -8,6 +8,7 @@ import { AlertmanagerSilence } from '../models/alertmanager-silence'; import { AlertmanagerAlert, AlertmanagerNotification, + GroupAlertmanagerAlert, PrometheusRuleGroup } from '../models/prometheus-alerts'; import moment from 'moment'; @@ -79,6 +80,11 @@ export class PrometheusService { return this.http.get(this.baseURL, { params }); } + getGroupedAlerts(clusterFilteredAlerts = false, params: Record = {}) { + params['cluster_filter'] = clusterFilteredAlerts; + return this.http.get(`${this.baseURL}/alertgroup`, { params }); + } + getSilences(params = {}): Observable { return this.http.get(`${this.baseURL}/silences`, { params }); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.ts index a662a898b16..3868f55ba53 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.ts @@ -154,7 +154,7 @@ export class NotificationsSidebarComponent implements OnInit, OnDestroy { } private triggerPrometheusAlerts() { - this.prometheusAlertService.refresh(true); + this.prometheusAlertService.refresh(); this.prometheusNotificationService.refresh(); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/prometheus-alerts.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/prometheus-alerts.ts index 9deaa537895..9a454a27c51 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/prometheus-alerts.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/prometheus-alerts.ts @@ -54,6 +54,13 @@ export class AlertmanagerAlert extends CommonAlertmanagerAlert { }; receivers: string[]; fingerprint: string; + alert_count: number; + subalerts?: AlertmanagerAlert[]; +} + +export class GroupAlertmanagerAlert { + alerts: AlertmanagerAlert[]; + labels?: PrometheusAlertLabels; } export class AlertmanagerNotificationAlert extends CommonAlertmanagerAlert { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.spec.ts index 115802a7d21..c734442cf17 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.spec.ts @@ -8,7 +8,7 @@ import { configureTestBed, PrometheusHelper } from '~/testing/unit-test-helper'; import { PrometheusService } from '../api/prometheus.service'; import { NotificationType } from '../enum/notification-type.enum'; import { CdNotificationConfig } from '../models/cd-notification'; -import { AlertmanagerAlert } from '../models/prometheus-alerts'; +import { GroupAlertmanagerAlert } from '../models/prometheus-alerts'; import { SharedModule } from '../shared.module'; import { NotificationService } from './notification.service'; import { PrometheusAlertFormatter } from './prometheus-alert-formatter'; @@ -17,7 +17,7 @@ import { PrometheusAlertService } from './prometheus-alert.service'; describe('PrometheusAlertService', () => { let service: PrometheusAlertService; let notificationService: NotificationService; - let alerts: AlertmanagerAlert[]; + let alerts: GroupAlertmanagerAlert[]; let prometheusService: PrometheusService; let prometheus: PrometheusHelper; @@ -39,7 +39,7 @@ describe('PrometheusAlertService', () => { service = TestBed.inject(PrometheusAlertService); prometheusService = TestBed.inject(PrometheusService); spyOn(prometheusService, 'ifAlertmanagerConfigured').and.callFake((fn) => fn()); - spyOn(prometheusService, 'getAlerts').and.returnValue( + spyOn(prometheusService, 'getGroupedAlerts').and.returnValue( new Observable((observer: any) => observer.error({ status: statusCode, error: {} })) ); const disableFn = spyOn(prometheusService, 'disableAlertmanagerConfig').and.callFake(() => { @@ -52,7 +52,7 @@ describe('PrometheusAlertService', () => { done(); } - service.getAlerts(); + service.getGroupedAlerts(); }; it('disables on 504 error which is thrown if the mgr failed', (done) => { @@ -116,9 +116,9 @@ describe('PrometheusAlertService', () => { prometheusService = TestBed.inject(PrometheusService); spyOn(prometheusService, 'ifAlertmanagerConfigured').and.callFake((fn) => fn()); - spyOn(prometheusService, 'getAlerts').and.callFake(() => of(alerts)); + spyOn(prometheusService, 'getGroupedAlerts').and.callFake(() => of(alerts)); - alerts = [prometheus.createAlert('alert0')]; + alerts = [{ alerts: [prometheus.createAlert('alert0')] }]; service.refresh(); }); @@ -132,7 +132,7 @@ describe('PrometheusAlertService', () => { }); it('should notify on alert change', () => { - alerts = [prometheus.createAlert('alert0', 'resolved')]; + alerts = [{ alerts: [prometheus.createAlert('alert0', 'resolved')] }]; service.refresh(); expect(notificationService.show).toHaveBeenCalledWith( new CdNotificationConfig( @@ -146,13 +146,16 @@ describe('PrometheusAlertService', () => { }); it('should not notify on change to suppressed', () => { - alerts = [prometheus.createAlert('alert0', 'suppressed')]; + alerts = [{ alerts: [prometheus.createAlert('alert0', 'suppressed')] }]; service.refresh(); expect(notificationService.show).not.toHaveBeenCalled(); }); it('should notify on a new alert', () => { - alerts = [prometheus.createAlert('alert1'), prometheus.createAlert('alert0')]; + alerts = [ + { alerts: [prometheus.createAlert('alert0')] }, + { alerts: [prometheus.createAlert('alert1')] } + ]; service.refresh(); expect(notificationService.show).toHaveBeenCalledTimes(1); expect(notificationService.show).toHaveBeenCalledWith( @@ -167,7 +170,7 @@ describe('PrometheusAlertService', () => { }); it('should notify a resolved alert if it is not there anymore', () => { - alerts = []; + alerts = [{ alerts: [] }]; service.refresh(); expect(notificationService.show).toHaveBeenCalledTimes(1); expect(notificationService.show).toHaveBeenCalledWith( @@ -182,10 +185,13 @@ describe('PrometheusAlertService', () => { }); it('should call multiple times for multiple changes', () => { + service['alerts'] = []; const alert1 = prometheus.createAlert('alert1'); - alerts.push(alert1); + alerts = [{ alerts: [] }, { alerts: [] }]; + alerts[0].alerts.push(alert1); service.refresh(); - alerts = [alert1, prometheus.createAlert('alert2')]; + const alert2 = prometheus.createAlert('alert2'); + alerts[1].alerts.push(alert2); service.refresh(); expect(notificationService.show).toHaveBeenCalledTimes(2); }); @@ -197,12 +203,12 @@ describe('PrometheusAlertService', () => { prometheusService = TestBed.inject(PrometheusService); spyOn(prometheusService, 'ifAlertmanagerConfigured').and.callFake((fn) => fn()); - spyOn(prometheusService, 'getAlerts').and.callFake(() => of(alerts)); + spyOn(prometheusService, 'getGroupedAlerts').and.callFake(() => of(alerts)); alerts = [ - prometheus.createAlert('alert0', 'active'), - prometheus.createAlert('alert1', 'suppressed'), - prometheus.createAlert('alert2', 'suppressed') + { alerts: [prometheus.createAlert('alert0', 'active')] }, + { alerts: [prometheus.createAlert('alert1', 'suppressed')] }, + { alerts: [prometheus.createAlert('alert2', 'suppressed')] } ]; service.refresh(); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.ts index f39a53048b9..cba2aeedeb3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.ts @@ -6,7 +6,8 @@ import { PrometheusService } from '../api/prometheus.service'; import { AlertmanagerAlert, PrometheusCustomAlert, - PrometheusRule + PrometheusRule, + GroupAlertmanagerAlert } from '../models/prometheus-alerts'; import { PrometheusAlertFormatter } from './prometheus-alert-formatter'; import { BehaviorSubject } from 'rxjs'; @@ -28,9 +29,9 @@ export class PrometheusAlertService { private prometheusService: PrometheusService ) {} - getAlerts(clusterFilteredAlerts?: boolean) { + getGroupedAlerts(clusterFilteredAlerts = false) { this.prometheusService.ifAlertmanagerConfigured(() => { - this.prometheusService.getAlerts(clusterFilteredAlerts).subscribe( + this.prometheusService.getGroupedAlerts(clusterFilteredAlerts).subscribe( (alerts) => this.handleAlerts(alerts), (resp) => { if ([404, 504].includes(resp.status)) { @@ -57,13 +58,23 @@ export class PrometheusAlertService { }); } - refresh(clusterFilteredAlerts?: boolean) { - this.getAlerts(clusterFilteredAlerts); + refresh() { + this.getGroupedAlerts(true); } - private handleAlerts(alerts: AlertmanagerAlert[]) { + private handleAlerts(alertGroups: GroupAlertmanagerAlert[]) { + const alerts: AlertmanagerAlert[] = alertGroups + .map((g) => { + if (!g.alerts.length) return null; + if (g.alerts.length === 1) return { ...g.alerts[0], alert_count: 1 }; + return { ...g.alerts[0], alert_count: g.alerts.length, subalerts: g.alerts }; + }) + .filter(Boolean) as AlertmanagerAlert[]; + if (this.canAlertsBeNotified) { - this.notifyOnAlertChanges(alerts, this.alerts); + const allSubalerts = alertGroups.flatMap((g) => g.alerts); + const oldAlerts = this.alerts.flatMap((a) => (a.subalerts ? a.subalerts : a)); + this.notifyOnAlertChanges(allSubalerts, oldAlerts); } this.activeAlerts = _.reduce( alerts, diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index f68a86ff003..b4cc3b9f09d 100755 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -11742,7 +11742,12 @@ paths: - Prometheus /api/prometheus/alertgroup: get: - parameters: [] + parameters: + - default: false + in: query + name: cluster_filter + schema: + type: boolean responses: '200': content: -- 2.39.5