Skip to content

Commit 1b594e8

Browse files
committed
feat: split ALERTS query_range into several requests
Assisted-By: Claude Code
1 parent dbf1a3d commit 1b594e8

File tree

2 files changed

+96
-52
lines changed

2 files changed

+96
-52
lines changed

web/src/components/Incidents/api.ts

Lines changed: 94 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,42 @@
33
import { PrometheusEndpoint, PrometheusResponse } from '@openshift-console/dynamic-plugin-sdk';
44
import { getPrometheusBasePath, buildPrometheusUrl } from '../utils';
55
import { PROMETHEUS_QUERY_INTERVAL_SECONDS } from './utils';
6+
7+
const MAX_URL_LENGTH = 5000;
8+
9+
/**
10+
* Creates a single Prometheus alert query string from a grouped alert value.
11+
* @param {Object} query - Single grouped alert object with src_ prefixed properties and layer/component.
12+
* @returns {string} - A string representing a single Prometheus alert query.
13+
*/
14+
const createSingleAlertQuery = (query) => {
15+
// Dynamically get all keys starting with "src_"
16+
const srcKeys = Object.keys(query).filter((key) => key.startsWith('src_'));
17+
18+
// Create the alertParts array using the dynamically discovered src_ keys,
19+
// but remove the "src_" prefix from the keys in the final query string.
20+
const alertParts = srcKeys
21+
.filter((key) => query[key]) // Only include keys that are present in the query object
22+
.map((key) => `${key.replace('src_', '')}="${query[key]}"`) // Remove "src_" prefix from keys
23+
.join(', ');
24+
25+
// Construct the query string for each grouped alert
26+
return `ALERTS{${alertParts}}`;
27+
}
28+
29+
630
/**
731
* Creates a Prometheus alerts query string from grouped alert values.
832
* The function dynamically includes any properties in the input objects that have the "src_" prefix,
933
* but the prefix is removed from the keys in the final query string.
1034
*
1135
* @param {Object[]} groupedAlertsValues - Array of grouped alert objects.
12-
* Each alert object should contain various properties, including "src_" prefixed properties,
13-
* as well as "layer" and "component" for constructing the meta fields in the query.
36+
* Each alert object should contain various properties, including "src_" prefixed properties
1437
*
1538
* @param {string} groupedAlertsValues[].layer - The layer of the alert, used in the absent condition.
1639
* @param {string} groupedAlertsValues[].component - The component of the alert, used in the absent condition.
17-
* @returns {string} - A string representing the combined Prometheus alerts query.
18-
* Each alert query is formatted as `(ALERTS{key="value", ...} + on () group_left (component, layer) (absent(meta{layer="value", component="value"})))`
19-
* and multiple queries are joined by "or".
40+
* @returns {string[]} - An array of strings representing the combined Prometheus alerts query.
41+
* Each alert query is formatted as `(ALERTS{key="value", ...} and multiple queries are joined by "or".
2042
*
2143
* @example
2244
* const alerts = [
@@ -38,63 +60,85 @@ import { PROMETHEUS_QUERY_INTERVAL_SECONDS } from './utils';
3860
*
3961
* const query = createAlertsQuery(alerts);
4062
* // Returns:
41-
* // '(ALERTS{alertname="AlertmanagerReceiversNotConfigured", namespace="openshift-monitoring", severity="warning"} + on () group_left (component, layer) (absent(meta{layer="core", component="monitoring"}))) or
42-
* // (ALERTS{alertname="AnotherAlert", namespace="default", severity="critical"} + on () group_left (component, layer) (absent(meta{layer="app", component="frontend"})))'
63+
* // ['ALERTS{alertname="AlertmanagerReceiversNotConfigured", namespace="openshift-monitoring", severity="warning"} or
64+
* // ALERTS{alertname="AnotherAlert", namespace="default", severity="critical"}']
4365
*/
4466
export const createAlertsQuery = (groupedAlertsValues) => {
45-
const alertsQuery = groupedAlertsValues
46-
.map((query) => {
47-
// Dynamically get all keys starting with "src_"
48-
const srcKeys = Object.keys(query).filter((key) => key.startsWith('src_'));
49-
50-
// Create the alertParts array using the dynamically discovered src_ keys,
51-
// but remove the "src_" prefix from the keys in the final query string.
52-
const alertParts = srcKeys
53-
.filter((key) => query[key]) // Only include keys that are present in the query object
54-
.map((key) => `${key.replace('src_', '')}="${query[key]}"`) // Remove "src_" prefix from keys
55-
.join(', ');
56-
57-
// Construct the query string for each grouped alert
58-
return `(ALERTS{${alertParts}} + on () group_left (component, layer) (absent(meta{layer="${query.layer}", component="${query.component}"})))`;
59-
})
60-
.join(' or '); // Join all individual alert queries with "or"
61-
62-
// TODO: remove duplicated conditions, optimize query
63-
64-
return alertsQuery;
67+
const queries = [];
68+
let currentQueryParts = [];
69+
let currentQueryLength = 0;
70+
71+
for (const alertValue of groupedAlertsValues) {
72+
const singleAlertQuery = createSingleAlertQuery(alertValue);
73+
const newQueryLength = currentQueryLength + singleAlertQuery.length + 4; // 4 for ' or '
74+
75+
if (newQueryLength <= MAX_URL_LENGTH) {
76+
currentQueryParts.push(singleAlertQuery);
77+
currentQueryLength = newQueryLength;
78+
continue;
79+
}
80+
queries.push(currentQueryParts.join(' or '));
81+
currentQueryParts = [singleAlertQuery];
82+
currentQueryLength = singleAlertQuery.length;
83+
}
84+
85+
if (currentQueryParts.length > 0) {
86+
queries.push(currentQueryParts.join(' or '));
87+
}
88+
89+
return queries;
6590
};
6691

67-
export const fetchDataForIncidentsAndAlerts = (
92+
export const fetchDataForIncidentsAndAlerts = async (
6893
fetch: (url: string) => Promise<PrometheusResponse>,
6994
range: { endTime: number; duration: number },
70-
customQuery: string,
95+
customQuery: string | string[],
7196
) => {
7297
// Calculate samples to ensure step=PROMETHEUS_QUERY_INTERVAL_SECONDS (300s / 5 minutes)
7398
// For 24h duration: Math.ceil(86400000 / 288 / 1000) = 300 seconds
7499
const samples = Math.floor(range.duration / (PROMETHEUS_QUERY_INTERVAL_SECONDS * 1000));
100+
const queries = Array.isArray(customQuery) ? customQuery : [customQuery];
75101

76-
const url = buildPrometheusUrl({
77-
prometheusUrlProps: {
78-
endpoint: PrometheusEndpoint.QUERY_RANGE,
79-
endTime: range.endTime,
80-
query: customQuery,
81-
samples,
82-
timespan: range.duration,
83-
},
84-
basePath: getPrometheusBasePath({
85-
prometheus: 'cmo',
86-
useTenancyPath: false,
87-
}),
88-
});
89-
90-
if (!url) {
91-
// Return empty result when query is empty to avoid making invalid API calls
92-
return Promise.resolve({
93-
data: {
94-
result: [],
102+
const promises = queries.map((query) => {
103+
const url = buildPrometheusUrl({
104+
prometheusUrlProps: {
105+
endpoint: PrometheusEndpoint.QUERY_RANGE,
106+
endTime: range.endTime,
107+
query,
108+
samples,
109+
timespan: range.duration,
95110
},
111+
basePath: getPrometheusBasePath({
112+
prometheus: 'cmo',
113+
useTenancyPath: false,
114+
}),
96115
});
97-
}
98116

99-
return fetch(url);
117+
if (!url) {
118+
// Return empty result when query is empty to avoid making invalid API calls
119+
return Promise.resolve({
120+
status: 'success',
121+
data: {
122+
resultType: 'matrix',
123+
result: [],
124+
},
125+
} as PrometheusResponse);
126+
}
127+
128+
return fetch(url);
129+
});
130+
131+
const responses = await Promise.all(promises);
132+
133+
// Merge responses
134+
const combinedResult = responses.flatMap((r) => r.data?.result || []);
135+
136+
// Construct a synthetic response
137+
return {
138+
status: 'success',
139+
data: {
140+
resultType: responses[0]?.data?.resultType || 'matrix',
141+
result: combinedResult,
142+
},
143+
} as PrometheusResponse;
100144
};

web/src/components/Incidents/processAlerts.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,8 @@ export function convertToAlerts(
265265
alertname: alert.metric.alertname,
266266
namespace: alert.metric.namespace,
267267
severity: alert.metric.severity as Severity,
268-
component: alert.metric.component,
269-
layer: alert.metric.layer,
268+
component: matchingIncident.component,
269+
layer: matchingIncident.layer,
270270
name: alert.metric.name,
271271
alertstate: resolved ? 'resolved' : 'firing',
272272
values: paddedValues,

0 commit comments

Comments
 (0)