Skip to content

Commit a5864d2

Browse files
committed
feat: fix start dates in consecutive intervals bar
1 parent 9923c13 commit a5864d2

7 files changed

Lines changed: 132 additions & 40 deletions

File tree

web/src/components/Incidents/IncidentsDetailsRowTable.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ const IncidentsDetailsRowTable = ({ alerts }: IncidentsDetailsRowTableProps) =>
2525
if (alerts && alerts.length > 0) {
2626
return [...alerts]
2727
.sort((a: IncidentsDetailsAlert, b: IncidentsDetailsAlert) => {
28-
const aStart = a.firstTimestamp > 0 ? a.firstTimestamp : a.alertsStartFiring;
29-
const bStart = b.firstTimestamp > 0 ? b.firstTimestamp : b.alertsStartFiring;
28+
const aFirstTimestamp = a.firstTimestamps[0][1];
29+
const bFirstTimestamp = b.firstTimestamps[0][1];
30+
const aStart = aFirstTimestamp > 0 ? aFirstTimestamp : a.alertsStartFiring;
31+
const bStart = bFirstTimestamp > 0 ? bFirstTimestamp : b.alertsStartFiring;
3032
return aStart - bStart;
3133
})
3234
.map((alertDetails: IncidentsDetailsAlert, rowIndex) => {
@@ -48,8 +50,8 @@ const IncidentsDetailsRowTable = ({ alerts }: IncidentsDetailsRowTableProps) =>
4850
<Td dataLabel="expanded-details-firingstart">
4951
<Timestamp
5052
timestamp={
51-
(alertDetails.firstTimestamp > 0
52-
? alertDetails.firstTimestamp
53+
(alertDetails.firstTimestamps[0][1] > 0
54+
? alertDetails.firstTimestamps[0][1]
5355
: alertDetails.alertsStartFiring) * 1000
5456
}
5557
/>

web/src/components/Incidents/IncidentsPage.tsx

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
onDeleteIncidentFilterChip,
3838
onIncidentFiltersSelect,
3939
parseUrlParams,
40+
PROMETHEUS_QUERY_INTERVAL_SECONDS,
4041
roundTimestampToFiveMinutes,
4142
updateBrowserUrl,
4243
} from './utils';
@@ -242,11 +243,13 @@ const IncidentsPage = () => {
242243
}
243244

244245
const currentTime = incidentsLastRefreshTime;
246+
const ONE_DAY = 24 * 60 * 60 * 1000;
245247

246248
// Fetch timestamps and alerts in parallel, but wait for both before processing
247-
const timestampPromise = fetchInstantData(
249+
const timestampPromise = fetchDataForIncidentsAndAlerts(
248250
safeFetch,
249-
'min_over_time(timestamp(ALERTS{alertstate="firing"})[15d:5m])',
251+
{ endTime: currentTime, duration: 15 * ONE_DAY },
252+
'timestamp(ALERTS{alertstate="firing"})',
250253
).then((res) => res.data.result);
251254

252255
const alertsPromise = Promise.all(
@@ -262,14 +265,26 @@ const IncidentsPage = () => {
262265

263266
Promise.all([timestampPromise, alertsPromise])
264267
.then(([timestampsResults, alertsResults]) => {
268+
// Gaps detection here such that if the same timestamp has
269+
// gaps greater than 5 minutes, this will be added more than one time.
270+
// For example, if there is a metric for AlertH_Gapped
271+
// with values:[ "1770699000", "1770699300", "1770708300", "1770708600", "1770708900"]
272+
// there will be two gaps detected. With the following min values: 1770699300 and 1770699300
273+
// the interval will be [1770699300 - 1770699300] and [1770708300 - 1770699300]
274+
275+
const timestampsValues = timestampsResults?.map((result: any) => ({
276+
...result,
277+
value: detectMinForEachGap(result.values, PROMETHEUS_QUERY_INTERVAL_SECONDS),
278+
}));
279+
265280
// Round timestamp values before storing
266281
const roundedTimestamps =
267-
timestampsResults?.map((result: any) => ({
282+
timestampsValues?.map((result: any) => ({
268283
...result,
269-
value: [
270-
result.value[0],
271-
roundTimestampToFiveMinutes(parseInt(result.value[1])).toString(),
272-
],
284+
value: result.value.map((value: any) => [
285+
value[0],
286+
roundTimestampToFiveMinutes(parseInt(value[1])).toString(),
287+
]),
273288
})) || [];
274289

275290
const fetchedAlertsTimestamps = {
@@ -744,3 +759,30 @@ export const McpCmoAlertingPage = () => {
744759
</MonitoringProvider>
745760
);
746761
};
762+
763+
/**
764+
* @param {Array<Array>} dataValues - The matrix from out.json (data.result[0].values)
765+
* @param {number} gapThreshold - e.g., 300
766+
*/
767+
const detectMinForEachGap = (dataValues, gapThreshold) => {
768+
if (!dataValues || dataValues.length === 0) return [];
769+
770+
const mins = [];
771+
let currentMin = dataValues[0];
772+
773+
// Start from the second element to compare with the previous one
774+
for (let i = 1; i < dataValues.length; i++) {
775+
const delta = dataValues[i][1] - dataValues[i - 1][1];
776+
777+
if (delta > gapThreshold) {
778+
// Gap detected: save the min of the interval that just ended
779+
mins.push(currentMin);
780+
// The current timestamp is the min of the NEW interval
781+
currentMin = dataValues[i];
782+
}
783+
}
784+
785+
// Always push the min of the last interval
786+
mins.push(currentMin);
787+
return mins;
788+
};

web/src/components/Incidents/IncidentsTable.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@ export const IncidentsTable = () => {
9595
if (!alert.alertsExpandedRowData || alert.alertsExpandedRowData.length === 0) {
9696
return 0;
9797
}
98-
return Math.min(...alert.alertsExpandedRowData.map((alertData) => alertData.firstTimestamp));
98+
return Math.min(
99+
...alert.alertsExpandedRowData.map((alertData) => alertData.firstTimestamps[0][1]),
100+
);
99101
};
100102

101103
if (isEmpty(alertsTableData) || alertsAreLoading || isEmpty(incidentsActiveFilters.groupId)) {

web/src/components/Incidents/model.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export type Incident = {
2020
values: Array<Timestamps>;
2121
metric: Metric;
2222
firstTimestamp: number;
23+
firstTimestamps: Array<Array<any>>;
2324
lastTimestamp: number;
2425
};
2526

@@ -59,7 +60,7 @@ export type Alert = {
5960
severity: Severity;
6061
silenced: boolean;
6162
x: number;
62-
firstTimestamp: number;
63+
firstTimestamps: Array<Array<any>>;
6364
values: Array<Timestamps>;
6465
alertsExpandedRowData?: Array<Alert>;
6566
};
@@ -114,7 +115,7 @@ export type IncidentsDetailsAlert = {
114115
resolved: boolean;
115116
severity: Severity;
116117
x: number;
117-
firstTimestamp: number;
118+
firstTimestamps: Array<Array<any>>;
118119
lastTimestamp: number;
119120
values: Array<Timestamps>;
120121
silenced: boolean;

web/src/components/Incidents/processAlerts.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,20 +277,24 @@ export function convertToAlerts(
277277
resolved,
278278
x: 0, // Will be set after sorting
279279
silenced: matchingIncident.silenced ?? false,
280-
firstTimestamp: 0, // Will be set from matched timestamp
280+
firstTimestamps: [],
281281
};
282282

283283
let matchedMinTimestamp = matchTimestampMetric(labeledAlert, alertsTimestamps.minOverTime);
284-
if (matchedMinTimestamp && matchedMinTimestamp.value[1] < matchingIncident.firstTimestamp) {
284+
const lastTimestampIdx = matchedMinTimestamp?.value.length - 1;
285+
if (
286+
matchedMinTimestamp &&
287+
parseInt(matchedMinTimestamp.value[lastTimestampIdx][1]) < matchingIncident.firstTimestamp
288+
) {
285289
matchedMinTimestamp = {
286-
value: [matchingIncident.firstTimestamp, matchingIncident.firstTimestamp.toString()],
290+
value: [[matchingIncident.firstTimestamp, matchingIncident.firstTimestamp.toString()]],
287291
};
288292
}
289293

290294
if (matchedMinTimestamp) {
291295
labeledAlert = {
292296
...labeledAlert,
293-
firstTimestamp: parseInt(matchedMinTimestamp?.value?.[1] ?? '0'),
297+
firstTimestamps: matchedMinTimestamp?.value,
294298
} as Alert;
295299
}
296300
return labeledAlert;

web/src/components/Incidents/processIncidents.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable max-len */
22

33
import { PrometheusLabels, PrometheusResult } from '@openshift-console/dynamic-plugin-sdk';
4-
import { Incident, IncidentsTimestamps, Metric, ProcessedIncident } from './model';
4+
import { IncidentsTimestamps, Metric, ProcessedIncident } from './model';
55
import {
66
insertPaddingPointsForChart,
77
isResolved,
@@ -194,17 +194,18 @@ export const getIncidentsTimeRanges = (
194194
export const processIncidentsForAlerts = (
195195
incidents: Array<PrometheusResult>,
196196
incidentsTimestamps: IncidentsTimestamps,
197-
): Array<Partial<Incident>> => {
197+
) => {
198198
const matchedIncidents = incidents.map((incident) => {
199199
// expand matchTimestampMetricForIncident here
200200
const matchedMinTimestamp = matchTimestampMetricForIncident(
201201
incident.metric,
202202
incidentsTimestamps.minOverTime,
203203
);
204+
204205
return {
205206
...incident,
206207
firstTimestamp: parseInt(matchedMinTimestamp?.value?.[1] ?? '0'),
207-
} as Partial<Incident>;
208+
};
208209
});
209210

210211
return matchedIncidents.map((incident, index) => {
@@ -213,12 +214,13 @@ export const processIncidentsForAlerts = (
213214
const silenced = incident.metric.silenced === 'true';
214215

215216
// Return the processed incident
216-
return {
217+
const retval = {
217218
...incident.metric,
218219
values: incident.values,
219220
x: incidents.length - index,
220221
silenced,
221222
firstTimestamp: incident.firstTimestamp,
222223
};
224+
return retval;
223225
});
224226
};

web/src/components/Incidents/utils.ts

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -335,20 +335,45 @@ export const createIncidentsChartBars = (incident: Incident, dateArray: SpanDate
335335
warning: t_global_color_status_warning_default.var,
336336
};
337337

338+
let prev = null;
338339
for (let i = 0; i < groupedData.length; i++) {
339340
const severity = getSeverityName(groupedData[i][2]);
340341
const isLastElement = i === groupedData.length - 1;
342+
const isNodata = groupedData[i][2] === 'nodata';
341343

342-
// - To avoid certain edge cases the startDate should
343-
// be the minimum between alert.firstTimestamp and groupedData[i][0]
344344
// - Round the result since groupedData comes from raw time series values.
345-
// - We are considering only the first element of the groupedData (i === 0)
346-
// because in the case of consecutive intervals (i.e. the incident changes priority)
347-
// we want that the end of a bar is equal to the start of the next one
348-
const startDate =
349-
i === 0
350-
? roundTimestampToFiveMinutes(Math.min(incident.firstTimestamp, groupedData[i][0]))
351-
: groupedData[i][0];
345+
let startDate = 0;
346+
if (i === 0) {
347+
// - For the first bar, use the incident's first timestamp (rounded)
348+
// This represents when the incident actually started
349+
// If the first interval is 'nodata', we still use incident.firstTimestamp
350+
// as the absolute start, since 'nodata' at the beginning is just visualization
351+
// - Math.min is needed to handle edge cases when the incident is quite new and the firstTimestamp may be greater
352+
// than the aggregated data from the query_range
353+
startDate =
354+
roundTimestampToFiveMinutes(Math.min(incident.firstTimestamp, groupedData[i][0])) * 1000;
355+
} else {
356+
// For subsequent bars, only calculate startDate for non-nodata intervals
357+
// (nodata intervals don't need accurate startDate for tooltip display)
358+
if (!isNodata) {
359+
if (prev) {
360+
// Calculate absolute start by:
361+
// Previous bar's absolute start + duration of previous bar + gap to current bar
362+
// This maintains continuity in the absolute timeline
363+
const prevBarDuration = prev.y.getTime() - prev.startDate.getTime();
364+
const gapToCurrent = groupedData[i][0] * 1000 - prev.y.getTime();
365+
startDate = prev.startDate.getTime() + prevBarDuration + gapToCurrent;
366+
} else {
367+
// If prev is null (e.g., first interval was 'nodata'), fall back to incident.firstTimestamp or first groupedData
368+
// This ensures we always have a valid absolute start time
369+
startDate = roundTimestampToFiveMinutes(Math.min(incident.firstTimestamp, groupedData[i][0])) * 1000;
370+
}
371+
} else {
372+
// For 'nodata' intervals, we can use the interval start timestamp
373+
// This is mainly for consistency, but won't be displayed in tooltips
374+
startDate = groupedData[i][0] * 1000;
375+
}
376+
}
352377

353378
data.push({
354379
y0: new Date(groupedData[i][0] * 1000),
@@ -359,14 +384,17 @@ export const createIncidentsChartBars = (incident: Incident, dateArray: SpanDate
359384
componentList: incident.componentList || [],
360385
group_id: incident.group_id,
361386
nodata: groupedData[i][2] === 'nodata' ? true : false,
362-
startDate: new Date(startDate * 1000),
387+
startDate: new Date(startDate),
363388
fill:
364389
severity === 'Critical'
365390
? barChartColorScheme.critical
366391
: severity === 'Warning'
367392
? barChartColorScheme.warning
368393
: barChartColorScheme.info,
369394
});
395+
if (!isNodata) {
396+
prev = data[i];
397+
}
370398
}
371399

372400
return data;
@@ -415,15 +443,26 @@ export const createAlertsChartBars = (alert: IncidentsDetailsAlert): AlertsChart
415443

416444
const data = [];
417445

418-
for (let i = 0; i < groupedData.length; i++) {
446+
let idx = alert.firstTimestamps.length - 1;
447+
for (let i = groupedData.length - 1; i >= 0; i--) {
419448
const isLastElement = i === groupedData.length - 1;
420-
421-
// to avoid certain edge cases the startDate should
422-
// be the minimum between alert.firstTimestamp and groupedData[i][0]
423-
// Round the result since groupedData comes from raw time series values
424-
const startDate = roundTimestampToFiveMinutes(
425-
Math.min(alert.firstTimestamp, groupedData[i][0]),
426-
);
449+
const isNodata = groupedData[i][2] === 'nodata';
450+
451+
let startDate = 0;
452+
if (i === 0) {
453+
// For the first bar, use the minimum of alert's first timestamp and the first interval start
454+
// This handles the case when the alert was created within 5 minutes
455+
startDate = roundTimestampToFiveMinutes(
456+
Math.min(parseInt(alert.firstTimestamps[idx][1]), groupedData[i][0]),
457+
);
458+
idx--;
459+
} else {
460+
if (!isNodata && idx >= 0) {
461+
startDate = roundTimestampToFiveMinutes(parseInt(alert.firstTimestamps[idx][1]));
462+
idx--;
463+
}
464+
// Note: If isNodata, startDate remains 0 (not used in tooltips for nodata intervals)
465+
}
427466

428467
data.push({
429468
y0: new Date(groupedData[i][0] * 1000),

0 commit comments

Comments
 (0)