]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: Group similar alerts
authorAbhishek Desai <abhishek.desai1@ibm.com>
Fri, 29 Aug 2025 14:29:09 +0000 (19:59 +0530)
committerAbhishek Desai <abhishek.desai1@ibm.com>
Tue, 9 Sep 2025 11:58:59 +0000 (17:28 +0530)
fixes : https://tracker.ceph.com/issues/72788

Signed-off-by: Abhishek Desai <abhishek.desai1@ibm.com>
18 files changed:
src/pybind/mgr/dashboard/controllers/prometheus.py
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.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/prometheus-alerts.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.ts
src/pybind/mgr/dashboard/openapi.yaml

index c00d8c70e638cfd772daa2a92f6085ef0501695e..20cf9fdc3a4b0c97258bfa6e355d44b31729b997 100644 (file)
@@ -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')
index e105cae7c9037c75b3af3306f85093a2e6d1a988..b3ba03caed84014ec8f915e3d40b340c7b6b37da 100644 (file)
@@ -1,38 +1,71 @@
 <cd-prometheus-tabs></cd-prometheus-tabs>
 
-<cd-alert-panel *ngIf="!isAlertmanagerConfigured"
-                type="info"
-                i18n>To see all active Prometheus alerts, please provide
-  the URL to the API of Prometheus' Alertmanager as described
-  in the&nbsp;<cd-doc section="prometheus"></cd-doc>.</cd-alert-panel>
+@if (!isAlertmanagerConfigured) {
+  <cd-alert-panel type="info"
+                  i18n>To see all active Prometheus alerts, please provide
+    the URL to the API of Prometheus' Alertmanager as described
+    in the&nbsp;<cd-doc section="prometheus"></cd-doc>.</cd-alert-panel>
+}
 
-<cd-table *ngIf="isAlertmanagerConfigured"
-          [data]="prometheusAlertService.alerts"
-          [columns]="columns"
-          identifier="fingerprint"
-          [forceIdentifier]="true"
-          [customCss]="customCss"
-          selectionType="single"
-          [hasDetails]="true"
-          (setExpandedRow)="setExpandedRow($event)"
-          (updateSelection)="updateSelection($event)">
-  <cd-table-actions class="table-actions"
-                    [permission]="permission"
-                    [selection]="selection"
-                    [tableActions]="tableActions">
-  </cd-table-actions>
+@if (isAlertmanagerConfigured) {
+  <cd-table
+    [data]="prometheusAlertService.alerts"
+    [columns]="columns"
+    identifier="fingerprint"
+    [forceIdentifier]="true"
+    [customCss]="customCss"
+    selectionType="single"
+    [hasDetails]="true"
+    (setExpandedRow)="setExpandedRow($event)"
+    (updateSelection)="updateSelection($event)"
+  >
+    <cd-table-actions
+      class="table-actions"
+      [permission]="permission"
+      [selection]="selection"
+      [tableActions]="tableActions"
+    >
+    </cd-table-actions>
 
-  <ng-container *ngIf="expandedRow">
-    <cd-table-key-value *cdTableDetail
-                        [renderObjects]="true"
-                        [hideEmpty]="true"
-                        [appendParentKey]="false"
-                        [data]="expandedRow"
-                        [customCss]="customCss"
-                        [autoReload]="false">
+    @if (expandedRow?.alert_count == 1) {
+    <cd-table-key-value
+      *cdTableDetail
+      [renderObjects]="true"
+      [hideEmpty]="true"
+      [appendParentKey]="false"
+      [data]="expandedRow"
+      [customCss]="customCss"
+      [autoReload]="false"
+    >
     </cd-table-key-value>
-  </ng-container>
-</cd-table>
+    } @else if (expandedRow?.alert_count > 1) {
+    <cd-table
+      *cdTableDetail
+      [data]="expandedRow?.subalerts"
+      [columns]="innerColumns"
+      identifier="fingerprint"
+      [forceIdentifier]="true"
+      [customCss]="customCss"
+      selectionType="single"
+      [hasDetails]="true"
+      (setExpandedRow)="setExpandedInnerRow($event)"
+      [scrollable]="false"
+    >
+    @if (expandedInnerRow) {
+      <cd-table-key-value
+        *cdTableDetail
+        [renderObjects]="true"
+        [hideEmpty]="true"
+        [appendParentKey]="false"
+        [data]="expandedInnerRow"
+        [customCss]="customCss"
+        [autoReload]="false"
+      >
+      </cd-table-key-value> }
+    </cd-table>
+    }
+  </cd-table>
+}
 
 <ng-template #externalLinkTpl
              let-row="data.row"
index 5bfd2898866af76796f6ee52369952ddc01b612d..c9a658ac00c7fda8e59ab41b90eb7fc69201c71e 100644 (file)
@@ -40,7 +40,7 @@ describe('ActiveAlertListComponent', () => {
     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', () => {
index e3892f0a67942b28eae4cc6307a94cac6e797a3e..cc12b29bfcab41261f3a9acde21dd12f708240c6 100644 (file)
@@ -24,10 +24,12 @@ export class ActiveAlertListComponent extends PrometheusListHelper implements On
   @ViewChild('externalLinkTpl', { static: true })
   externalLinkTpl: TemplateRef<any>;
   columns: CdTableColumn[];
+  innerColumns: CdTableColumn[];
   tableActions: CdTableAction[];
   permission: Permission;
   selection = new CdTableSelection();
   icons = Icons;
+  expandedInnerRow: any;
 
   constructor(
     // NotificationsComponent will refresh all alerts every 5s (No need to do it here as well)
@@ -54,16 +56,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
       },
       {
@@ -96,6 +92,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`,
@@ -105,7 +120,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) {
index acff1a473d28ac114895a3e099b3fafdad46829a..1613c9650b877deef7488ca1c50703ae86a40520 100644 (file)
@@ -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',
index 958039a31dc716fd7d48fdbff9100b7becd49fb7..a9002040f07db70ff87886a5be2fac9e8687de79 100644 (file)
@@ -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',
index 53768ab54b556ef2eaed93fdf794fbc0f065ac45..153a99770737c90cff451bd7cbeab1ae4a13137f 100644 (file)
             <div class="card-body ps-0 pe-1 pb-1 pt-0">
               <h6 class="card-title bold">{{ alert.labels.alertname }}</h6>
               <p class="card-text me-3 mb-0 text-truncate"
-                 [innerHtml]="alert.annotations.description"
-                 [ngbTooltip]="alert.annotations.description"></p>
+                 [innerHtml]="alert.annotations.summary"
+                 [ngbTooltip]="alert.annotations.summary"></p>
               <p class="card-text text-muted me-3">
                 <small class="date"
                        [title]="alert.startsAt | cdDate"
                        i18n>Active since: {{ alert.startsAt  | relativeDate }}</small>
+                <small class="alert_count"
+                       [title]="alert.alert_count"
+                       i18n>Total occurrences: {{ alert.alert_count }}</small>
               </p>
             </div>
           </div>
index 28fb5afdb25891bb36df8c2a72baf0627db5f51b..0f2702b5ccc869ebf2fa1cb430380c2713a1a9fe 100644 (file)
@@ -1,4 +1,5 @@
 @use './src/styles/vendor/variables' as vv;
+@use '@carbon/layout';
 
 .details {
   font-size: larger;
     -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;
+  }
 }
 
 .info-card-popover-cluster-status {
index 2b28492bc1a47f204d8df03d5c876ebcbf5b3269..1f168561cd4c8efd58061e400266ecec9fa8b42a 100644 (file)
@@ -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;
   });
index ec77518371edf18bcb443bc614c177e3a7366873..b11b748f7f2b6d748d919cee4d5b7a4246669489 100644 (file)
@@ -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 {
index a9d512d9b31dd7f1400228de761226ec37e1cf49..8979db7233fa9646f55d13d8665808181b889d1e 100644 (file)
@@ -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();
index 9694084559518ab56bfbe6115c5067ef90a6d103..0b949c46c62cac2f4c10dcdf3211651fbdde353b 100644 (file)
@@ -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');
   });
 
index cefcedca5c9a03327071dfb9c52a9558f8d92ca3..bf340df1d033418115077031fd11a055d1914c8f 100644 (file)
@@ -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<AlertmanagerAlert[]>(this.baseURL, { params });
   }
 
+  getGroupedAlerts(clusterFilteredAlerts = false, params: Record<string, any> = {}) {
+    params['cluster_filter'] = clusterFilteredAlerts;
+    return this.http.get<GroupAlertmanagerAlert[]>(`${this.baseURL}/alertgroup`, { params });
+  }
+
   getSilences(params = {}): Observable<AlertmanagerSilence[]> {
     return this.http.get<AlertmanagerSilence[]>(`${this.baseURL}/silences`, { params });
   }
index 8393eb889498008b52cff3c9a211db28421f6d68..ae836d406331b986fa97cf7792acde29f041cf70 100644 (file)
@@ -146,7 +146,7 @@ export class NotificationsSidebarComponent implements OnInit, OnDestroy {
   }
 
   private triggerPrometheusAlerts() {
-    this.prometheusAlertService.refresh(true);
+    this.prometheusAlertService.refresh();
     this.prometheusNotificationService.refresh();
   }
 
index 9deaa537895310e7a6bfcb9cc53436007091a36a..9a454a27c51da1f1db216057273bcf4b9e45a9a1 100644 (file)
@@ -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 {
index 115802a7d21619cbaf4177abc5a58eef6774bbba..c734442cf17affd0ef3b243641f8c49ea66ff5f0 100644 (file)
@@ -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();
     });
index f39a53048b9f593b2644025edbec62a3401130c1..cba2aeedeb39c39f2e32177c49a0dbe2167ea4ed 100644 (file)
@@ -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<AlertmanagerAlert, number>(
       alerts,
index 556cb053ccf7be36244aa6e038b27265ec57ed17..7612efa3104b5afe8ad643e31909aed19ba36cfd 100755 (executable)
@@ -11734,7 +11734,12 @@ paths:
       - Prometheus
   /api/prometheus/alertgroup:
     get:
-      parameters: []
+      parameters:
+      - default: false
+        in: query
+        name: cluster_filter
+        schema:
+          type: boolean
       responses:
         '200':
           content: