Skip to content

Commit 7ca9eb0

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

File tree

2 files changed

+95
-52
lines changed

2 files changed

+95
-52
lines changed

web/src/components/Incidents/api.ts

Lines changed: 93 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,41 @@
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+
629
/**
730
* Creates a Prometheus alerts query string from grouped alert values.
831
* The function dynamically includes any properties in the input objects that have the "src_" prefix,
932
* but the prefix is removed from the keys in the final query string.
1033
*
1134
* @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.
35+
* Each alert object should contain various properties, including "src_" prefixed properties
1436
*
1537
* @param {string} groupedAlertsValues[].layer - The layer of the alert, used in the absent condition.
1638
* @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".
39+
* @returns {string[]} - An array of strings representing the combined Prometheus alerts query.
40+
* Each alert query is formatted as `(ALERTS{key="value", ...} and multiple queries are joined by "or".
2041
*
2142
* @example
2243
* const alerts = [
@@ -38,63 +59,85 @@ import { PROMETHEUS_QUERY_INTERVAL_SECONDS } from './utils';
3859
*
3960
* const query = createAlertsQuery(alerts);
4061
* // 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"})))'
62+
* // ['ALERTS{alertname="AlertmanagerReceiversNotConfigured", namespace="openshift-monitoring", severity="warning"} or
63+
* // ALERTS{alertname="AnotherAlert", namespace="default", severity="critical"}']
4364
*/
4465
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;
66+
const queries = [];
67+
let currentQueryParts = [];
68+
let currentQueryLength = 0;
69+
70+
for (const alertValue of groupedAlertsValues) {
71+
const singleAlertQuery = createSingleAlertQuery(alertValue);
72+
const newQueryLength = currentQueryLength + singleAlertQuery.length + 4; // 4 for ' or '
73+
74+
if (newQueryLength <= MAX_URL_LENGTH) {
75+
currentQueryParts.push(singleAlertQuery);
76+
currentQueryLength = newQueryLength;
77+
continue;
78+
}
79+
queries.push(currentQueryParts.join(' or '));
80+
currentQueryParts = [singleAlertQuery];
81+
currentQueryLength = singleAlertQuery.length;
82+
}
83+
84+
if (currentQueryParts.length > 0) {
85+
queries.push(currentQueryParts.join(' or '));
86+
}
87+
88+
return queries;
6589
};
6690

67-
export const fetchDataForIncidentsAndAlerts = (
91+
export const fetchDataForIncidentsAndAlerts = async (
6892
fetch: (url: string) => Promise<PrometheusResponse>,
6993
range: { endTime: number; duration: number },
70-
customQuery: string,
94+
customQuery: string | string[],
7195
) => {
7296
// Calculate samples to ensure step=PROMETHEUS_QUERY_INTERVAL_SECONDS (300s / 5 minutes)
7397
// For 24h duration: Math.ceil(86400000 / 288 / 1000) = 300 seconds
7498
const samples = Math.floor(range.duration / (PROMETHEUS_QUERY_INTERVAL_SECONDS * 1000));
99+
const queries = Array.isArray(customQuery) ? customQuery : [customQuery];
75100

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: [],
101+
const promises = queries.map((query) => {
102+
const url = buildPrometheusUrl({
103+
prometheusUrlProps: {
104+
endpoint: PrometheusEndpoint.QUERY_RANGE,
105+
endTime: range.endTime,
106+
query,
107+
samples,
108+
timespan: range.duration,
95109
},
110+
basePath: getPrometheusBasePath({
111+
prometheus: 'cmo',
112+
useTenancyPath: false,
113+
}),
96114
});
97-
}
98115

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

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)