Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 55 additions & 45 deletions web/src/components/Incidents/processAlerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

import { PrometheusResult, PrometheusRule } from '@openshift-console/dynamic-plugin-sdk';
import { Alert, Incident, Severity } from './model';
import { sortObjectsByEarliestTimestamp } from './processIncidents';
import { sortByEarliestTimestamp } from './utils';

/**
* Groups alert objects by their `alertname`, `namespace`, and `component` fields and merges their values
* Deduplicates alert objects by their `alertname`, `namespace`, and `component` fields and merges their values
* while removing duplicates. Alerts with the same combination of `alertname`, `namespace`, and `component`
* are combined, with values being deduplicated.
*
* @param {Array<Object>} objects - Array of alert objects to be grouped. Each object contains a `metric` field
* @param {Array<Object>} objects - Array of alert objects to be deduplicated. Each object contains a `metric` field
* with properties such as `alertname`, `namespace`, `component`, and an array of `values`.
* @param {Object} objects[].metric - The metric information of the alert.
* @param {string} objects[].metric.alertname - The name of the alert.
Expand All @@ -18,20 +18,20 @@ import { sortObjectsByEarliestTimestamp } from './processIncidents';
* @param {Array<Array<Number | string>>} objects[].values - The array of values corresponding to the alert, where
* each value is a tuple containing a timestamp and a value (e.g., [timestamp, value]).
*
* @returns {Array<Object>} - An array of grouped alert objects. Each object contains a unique combination of
* @returns {Array<Object>} - An array of deduplicated alert objects. Each object contains a unique combination of
* `alertname`, `namespace`, and `component`, with deduplicated values.
* @returns {Object} return[].metric - The metric information of the grouped alert.
* @returns {Array<Array<Number | string>>} return[].values - The deduplicated array of values for the grouped alert.
* @returns {Object} return[].metric - The metric information of the deduplicated alert.
* @returns {Array<Array<Number | string>>} return[].values - The deduplicated array of values for the alert.
*
* @example
* const alerts = [
* { metric: { alertname: "Alert1", namespace: "ns1", component: "comp1" }, values: [[12345, "2"], [12346, "2"]] },
* { metric: { alertname: "Alert1", namespace: "ns1", component: "comp1" }, values: [[12346, "2"], [12347, "2"]] }
* ];
* const groupedAlerts = groupAlerts(alerts);
* // Returns an array where the two alerts are grouped together with deduplicated values.
* const deduplicated = deduplicateAlerts(alerts);
* // Returns an array where the two alerts are combined with deduplicated values.
*/
export function groupAlerts(objects: Array<PrometheusResult>): Array<PrometheusResult> {
export function deduplicateAlerts(objects: Array<PrometheusResult>): Array<PrometheusResult> {
// Step 1: Filter out all non firing alerts
const filteredObjects = objects.filter((obj) => obj.metric.alertstate === 'firing');
const groupedObjects = new Map();
Expand Down Expand Up @@ -220,7 +220,7 @@ export function processAlerts(
data: Array<PrometheusResult>,
selectedIncidents: Array<Partial<Incident>>,
): Array<Alert> {
const firing = groupAlerts(data).filter((alert) => alert.metric.alertname !== 'Watchdog');
const alerts = deduplicateAlerts(data).filter((alert) => alert.metric.alertname !== 'Watchdog');

// Merge incidents that have the same composite key to handle duplicates from multiple queries
// or silence status changes over time
Expand All @@ -233,11 +233,10 @@ export function processAlerts(
const firstTimestamp = Math.min(...timestamps);
const lastTimestamp = Math.max(...timestamps);

return sortObjectsByEarliestTimestamp(firing)
.map((alert, index) => {
// Filter values based on firstTimestamp and lastTimestamp keep only values within range
// Add 30 seconds padding before and after
const paddingSeconds = 30;
// Filter values based on firstTimestamp and lastTimestamp
const paddingSeconds = 30;
const filteredAlerts = alerts
.map((alert) => {
const processedValues: Array<[number, string]> = alert.values.filter(
([date]) =>
date >= firstTimestamp - paddingSeconds && date <= lastTimestamp + paddingSeconds,
Expand All @@ -249,41 +248,52 @@ export function processAlerts(
return null;
}

const alertsStartFiring = sortedValues[0][0] * 1000;
const alertsEndFiring = sortedValues[sortedValues.length - 1][0] * 1000;
const resolved = Date.now() - alertsEndFiring > 10 * 60 * 1000;

// Find matching incident from incidents to get silenced value
// Since incidents are already merged by (group_id, src_alertname, src_namespace, src_severity),
// there should be at most one matching incident with the latest silenced value
const matchingIncident = incidents.find(
(incident) =>
incident.src_alertname === alert.metric.alertname &&
incident.src_namespace === alert.metric.namespace &&
incident.src_severity === alert.metric.severity,
);

// Use silenced value from incident data (cluster_health_components_map)
// Default to false if no matching incident found
const silenced = matchingIncident?.silenced ?? false;

return {
alertname: alert.metric.alertname,
namespace: alert.metric.namespace,
severity: alert.metric.severity as Severity,
component: alert.metric.component,
layer: alert.metric.layer,
name: alert.metric.name,
alertstate: resolved ? 'resolved' : 'firing',
...alert,
values: sortedValues,
alertsStartFiring,
alertsEndFiring,
resolved,
x: firing.length - index,
silenced,
};
})
.filter((alert) => alert !== null);

const sortedAlerts = sortByEarliestTimestamp(filteredAlerts);

const result = sortedAlerts.map((alert, index) => {
const alertsStartFiring = alert.values[0][0] * 1000;
const alertsEndFiring = alert.values[alert.values.length - 1][0] * 1000;
const resolved = Date.now() - alertsEndFiring > 10 * 60 * 1000;

// Find matching incident from incidents to get silenced value
// Since incidents are already merged by (group_id, src_alertname, src_namespace, src_severity),
// there should be at most one matching incident with the latest silenced value
const matchingIncident = incidents.find(
(incident) =>
incident.src_alertname === alert.metric.alertname &&
incident.src_namespace === alert.metric.namespace &&
incident.src_severity === alert.metric.severity,
);

// Use silenced value from incident data (cluster_health_components_map)
// Default to false if no matching incident found
const silenced = matchingIncident?.silenced ?? false;

return {
alertname: alert.metric.alertname,
namespace: alert.metric.namespace,
severity: alert.metric.severity as Severity,
component: alert.metric.component,
layer: alert.metric.layer,
name: alert.metric.name,
alertstate: resolved ? 'resolved' : 'firing',
values: alert.values,
alertsStartFiring,
alertsEndFiring,
resolved,
x: filteredAlerts.length - index,
silenced,
};
});

return result;
}

export const groupAlertsForTable = (
Expand Down
12 changes: 2 additions & 10 deletions web/src/components/Incidents/processIncidents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,13 @@

import { PrometheusLabels, PrometheusResult } from '@openshift-console/dynamic-plugin-sdk';
import { Incident, Metric, ProcessedIncident } from './model';

//this will be moved to the utils.js file when I convert them to the Typescript
export function sortObjectsByEarliestTimestamp(incidents: PrometheusResult[]): PrometheusResult[] {
return incidents.sort((a, b) => {
const earliestA = Math.min(...a.values.map((value) => value[0]));
const earliestB = Math.min(...b.values.map((value) => value[0]));
return earliestA - earliestB;
});
}
import { sortByEarliestTimestamp } from './utils';

export function processIncidents(data: PrometheusResult[]): ProcessedIncident[] {
const incidents = groupById(data).filter(
(incident) => incident.metric.src_alertname !== 'Watchdog',
);
const sortedIncidents = sortObjectsByEarliestTimestamp(incidents);
const sortedIncidents = sortByEarliestTimestamp(incidents);

return sortedIncidents.map((incident, index) => {
// Determine severity flags based on values array
Expand Down
14 changes: 14 additions & 0 deletions web/src/components/Incidents/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
t_global_color_status_info_default,
t_global_color_status_warning_default,
} from '@patternfly/react-tokens';
import { PrometheusResult } from '@openshift-console/dynamic-plugin-sdk';
import { Dispatch } from 'redux';
import { setIncidentsActiveFilters } from '../../store/actions';
import {
Expand All @@ -18,6 +19,19 @@ import {
Timestamps,
} from './model';

/**
* Sorts items by their earliest timestamp in ascending order.
* @param items - Array of Prometheus results to sort
* @returns Sorted array with earliest items first
*/
export function sortByEarliestTimestamp(items: PrometheusResult[]): PrometheusResult[] {
return items.sort((a, b) => {
const earliestA = Math.min(...a.values.map((value) => value[0]));
const earliestB = Math.min(...b.values.map((value) => value[0]));
return earliestA - earliestB;
});
}

function consolidateAndMergeIntervals(data: Incident, dateArray: SpanDates) {
const severityRank = { 2: 2, 1: 1, 0: 0 };
const filteredValues = filterAndSortValues(data, severityRank);
Expand Down