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')
<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 <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 <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"
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', () => {
@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)
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
},
{
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`,
cellTemplate: this.externalLinkTpl
}
];
- this.prometheusAlertService.getAlerts(true);
+ this.prometheusAlertService.getGroupedAlerts(true);
+ }
+
+ setExpandedInnerRow(row: any) {
+ this.expandedInnerRow = row;
}
updateSelection(selection: CdTableSelection) {
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)]);
});
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',
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';
}
});
} else {
- this.prometheusService.getAlerts().subscribe((alerts) => {
+ this.prometheusService.getGroupedAlerts().subscribe((alerts) => {
const alert = _.find(alerts, ['fingerprint', params.id]);
if (!_.isUndefined(alert)) {
this.fillFormByAlert(alert);
this.form.updateValueAndValidity();
}
- private fillFormByAlert(alert: AlertmanagerAlert) {
+ private fillFormByAlert(alert: GroupAlertmanagerAlert) {
const labels = alert.labels;
this.setMatcher({
name: 'alertname',
<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>
@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 {
inhibitedBy: null
},
receivers: ['ceph2'],
- fingerprint: 'fingerprint'
+ fingerprint: 'fingerprint',
+ alert_count: 1
},
{
labels: {
inhibitedBy: null
},
receivers: ['default'],
- fingerprint: 'fingerprint'
+ fingerprint: 'fingerprint',
+ alert_count: 1
},
{
labels: {
inhibitedBy: null
},
receivers: ['ceph'],
- fingerprint: 'fingerprint'
+ fingerprint: 'fingerprint',
+ alert_count: 1
}
];
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;
});
this.getDetailsCardData();
this.getTelemetryReport();
this.getCapacityCardData();
- this.prometheusAlertService.getAlerts(true);
+ this.prometheusAlertService.getGroupedAlerts(true);
}
getTelemetryText(): string {
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();
});
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');
});
import {
AlertmanagerAlert,
AlertmanagerNotification,
+ GroupAlertmanagerAlert,
PrometheusRuleGroup
} from '../models/prometheus-alerts';
import moment from 'moment';
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 });
}
}
private triggerPrometheusAlerts() {
- this.prometheusAlertService.refresh(true);
+ this.prometheusAlertService.refresh();
this.prometheusNotificationService.refresh();
}
};
receivers: string[];
fingerprint: string;
+ alert_count: number;
+ subalerts?: AlertmanagerAlert[];
+}
+
+export class GroupAlertmanagerAlert {
+ alerts: AlertmanagerAlert[];
+ labels?: PrometheusAlertLabels;
}
export class AlertmanagerNotificationAlert extends CommonAlertmanagerAlert {
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';
describe('PrometheusAlertService', () => {
let service: PrometheusAlertService;
let notificationService: NotificationService;
- let alerts: AlertmanagerAlert[];
+ let alerts: GroupAlertmanagerAlert[];
let prometheusService: PrometheusService;
let prometheus: PrometheusHelper;
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(() => {
done();
}
- service.getAlerts();
+ service.getGroupedAlerts();
};
it('disables on 504 error which is thrown if the mgr failed', (done) => {
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();
});
});
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(
});
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(
});
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(
});
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);
});
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();
});
import {
AlertmanagerAlert,
PrometheusCustomAlert,
- PrometheusRule
+ PrometheusRule,
+ GroupAlertmanagerAlert
} from '../models/prometheus-alerts';
import { PrometheusAlertFormatter } from './prometheus-alert-formatter';
import { BehaviorSubject } from 'rxjs';
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)) {
});
}
- 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,
- Prometheus
/api/prometheus/alertgroup:
get:
- parameters: []
+ parameters:
+ - default: false
+ in: query
+ name: cluster_filter
+ schema:
+ type: boolean
responses:
'200':
content: