Alerting: Apply backend label filtering to chart visualization in state history UI (#109459)

This commit is contained in:
Alexander Akhmetov
2025-08-11 15:20:35 +02:00
committed by GitHub
parent ce1afa626d
commit 0e59bcdbb8
4 changed files with 49 additions and 16 deletions
@@ -12,7 +12,7 @@ import { stateHistoryApi } from '../../../api/stateHistoryApi';
import { DataSourceInformation } from '../../../home/Insights';
import { LIMIT_EVENTS } from './EventListSceneObject';
import { historyResultToDataFrame } from './utils';
import { historyResultToDataFrame, parseBackendLabelFilters } from './utils';
const historyDataSourceUid = '__history_api_ds_uid__';
const historyDataSourcePluginId = '__history_api_ds_pluginId__';
@@ -65,7 +65,9 @@ class HistoryAPIDatasource extends RuntimeDataSource<HistoryAPIQuery> {
const stateTo = templateSrv.replace(query.stateTo ?? '', request.scopedVars);
const stateFrom = templateSrv.replace(query.stateFrom ?? '', request.scopedVars);
const historyResult = await getHistory(from, to);
const labelFilters = parseBackendLabelFilters(labels);
const historyResult = await getHistory(from, to, labelFilters);
return {
data: historyResultToDataFrame(historyResult, { stateTo, stateFrom, labels }),
@@ -85,15 +87,17 @@ class HistoryAPIDatasource extends RuntimeDataSource<HistoryAPIQuery> {
* Fetch the history events from the history api.
* @param from the start time
* @param to the end time
* @returns the history events only filtered by time
* @param labels optional label filters for backend filtering
* @returns the history events filtered by time and labels
*/
export const getHistory = (from: number, to: number) => {
export const getHistory = (from: number, to: number, labels?: Record<string, string>) => {
return dispatch(
stateHistoryApi.endpoints.getRuleHistory.initiate(
{
from: from,
to: to,
limit: LIMIT_EVENTS,
labels: labels,
},
{
forceRefetch: Boolean(getTimeSrv().getAutoRefreshInteval().interval), // force refetch in case we are using the refresh option
@@ -29,7 +29,6 @@ import { AITriageButtonComponent } from '../../../enterprise-components/AI/AIGen
import { usePagination } from '../../../hooks/usePagination';
import { combineMatcherStrings } from '../../../utils/alertmanager';
import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
import { parsePromQLStyleMatcherLooseSafe } from '../../../utils/matchers';
import { createRelativeUrl } from '../../../utils/url';
import { AlertLabels } from '../../AlertLabels';
import { CollapseToggle } from '../../CollapseToggle';
@@ -39,6 +38,7 @@ import { LABELS_FILTER, STATE_FILTER_FROM, STATE_FILTER_TO } from './CentralAler
import { EventDetails } from './EventDetails';
import { HistoryErrorMessage } from './HistoryErrorMessage';
import { useRuleHistoryRecords } from './useRuleHistoryRecords';
import { parseBackendLabelFilters } from './utils';
export const LIMIT_EVENTS = 5000; // limit is hard-capped at 5000 at the BE level.
const PAGE_SIZE = 100;
@@ -66,16 +66,7 @@ export const HistoryEventsList = ({
const from = timeRange?.from.unix();
const to = timeRange?.to.unix();
const labelMatchers = parsePromQLStyleMatcherLooseSafe(valueInLabelFilter.toString());
// Prepare labels for filtering on the backend side.
// Backend supports only exact matchers.
const labelFilters: Record<string, string> = {};
labelMatchers.forEach((matcher) => {
if (!matcher.isRegex && matcher.isEqual) {
labelFilters[matcher.name] = matcher.value;
}
});
const labelFilters = parseBackendLabelFilters(valueInLabelFilter.toString());
const {
data: stateHistory,
@@ -86,7 +77,7 @@ export const HistoryEventsList = ({
from: from,
to: to,
limit: LIMIT_EVENTS,
labels: Object.keys(labelFilters).length > 0 ? labelFilters : undefined,
labels: labelFilters,
});
const { historyRecords: historyRecordsNotSorted } = useRuleHistoryRecords(stateHistory, {
@@ -7,6 +7,7 @@ import { setupMswServer } from '../../../mockApi';
import { captureRequests } from '../../../mocks/server/events';
import { StateFilterValues } from './CentralAlertHistoryScene';
import { getHistory } from './CentralHistoryRuntimeDataSource';
import { HistoryEventsList } from './EventListSceneObject';
setupMswServer();
@@ -178,5 +179,25 @@ describe('HistoryEventsList', () => {
expect(url.searchParams.get('labels_grafana_folder')).toBeNull();
expect(url.searchParams.get('labels_severity')).toBeNull();
});
it('should apply same backend filtering to chart data via getHistory function', async () => {
const capture = captureRequests((req) => req.url.includes('/api/v1/rules/history'));
const from = 123;
const to = 456;
const labels = { alertname: 'alert_1', team: 'alerting' };
await getHistory(from, to, labels);
const requests = await capture;
expect(requests).toHaveLength(1);
const url = new URL(requests[0].url);
expect(url.searchParams.get('labels_alertname')).toBe(labels.alertname);
expect(url.searchParams.get('labels_team')).toBe(labels.team);
expect(url.searchParams.get('from')).toBe(from.toString());
expect(url.searchParams.get('to')).toBe(to.toString());
});
});
});
@@ -24,6 +24,23 @@ import { LABELS_FILTER, STATE_FILTER_FROM, STATE_FILTER_TO, StateFilterValues }
const GROUPING_INTERVAL = 10 * 1000; // 10 seconds
const QUERY_PARAM_PREFIX = 'var-'; // Prefix used by Grafana to sync variables in the URL
/**
* Parse label filters and prepare backend filters.
* Backend supports only exact matchers.
*/
export function parseBackendLabelFilters(labelFilter: string): Record<string, string> {
const labelMatchers = parsePromQLStyleMatcherLooseSafe(labelFilter);
const labelFilters: Record<string, string> = {};
labelMatchers.forEach((matcher) => {
if (!matcher.isRegex && matcher.isEqual) {
labelFilters[matcher.name] = matcher.value;
}
});
return labelFilters;
}
interface HistoryFilters {
stateTo: string;
stateFrom: string;