Skip to content

Commit

Permalink
[v10.0.x] Alerting: Display correct results when using different filt…
Browse files Browse the repository at this point in the history
…ers on alerting panels (#70639)

Alerting: Display correct results when using different filters on alerting panels (#70482)

* Trigger separate rules request for each alerting panel in a dashboard

* Add RTK method to fetch prom rules

* Use RTKQuery to get prom rules in UnifiedAlertList

* Fix lint

* Mock promRules call

* Address PR comments

* Fix tests

(cherry picked from commit f17c49e)
  • Loading branch information
VikaCep committed Jun 23, 2023
1 parent 77f06f2 commit 5e42db4
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 56 deletions.
58 changes: 58 additions & 0 deletions public/app/features/alerting/unified/api/alertRuleApi.ts
@@ -0,0 +1,58 @@
import { Matcher } from 'app/plugins/datasource/alertmanager/types';
import { RuleIdentifier, RuleNamespace } from 'app/types/unified-alerting';
import { PromRulesResponse } from 'app/types/unified-alerting-dto';

import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
import { isCloudRuleIdentifier, isPrometheusRuleIdentifier } from '../utils/rules';

import { alertingApi } from './alertingApi';
import {
FetchPromRulesFilter,
groupRulesByFileName,
paramsWithMatcherAndState,
prepareRulesFilterQueryParams,
} from './prometheus';
export interface Datasource {
type: string;
uid: string;
}

export const PREVIEW_URL = '/api/v1/rule/test/grafana';
export const PROM_RULES_URL = 'api/prometheus/grafana/api/v1/rules';

export const alertRuleApi = alertingApi.injectEndpoints({
endpoints: (build) => ({
prometheusRulesByNamespace: build.query<
RuleNamespace[],
{
limitAlerts?: number;
identifier?: RuleIdentifier;
filter?: FetchPromRulesFilter;
state?: string[];
matcher?: Matcher[];
}
>({
query: ({ limitAlerts, identifier, filter, state, matcher }) => {
const searchParams = new URLSearchParams();

// if we're fetching for Grafana managed rules, we should add a limit to the number of alert instances
// we do this because the response is large otherwise and we don't show all of them in the UI anyway.
if (limitAlerts) {
searchParams.set('limit_alerts', String(limitAlerts));
}

if (identifier && (isPrometheusRuleIdentifier(identifier) || isCloudRuleIdentifier(identifier))) {
searchParams.set('file', identifier.namespace);
searchParams.set('rule_group', identifier.groupName);
}

const params = prepareRulesFilterQueryParams(searchParams, filter);

return { url: PROM_RULES_URL, params: paramsWithMatcherAndState(params, state, matcher) };
},
transformResponse: (response: PromRulesResponse): RuleNamespace[] => {
return groupRulesByFileName(response.data.groups, GRAFANA_RULES_SOURCE_NAME);
},
}),
}),
});
40 changes: 22 additions & 18 deletions public/app/features/alerting/unified/api/prometheus.ts
Expand Up @@ -3,7 +3,7 @@ import { lastValueFrom } from 'rxjs';
import { getBackendSrv } from '@grafana/runtime';
import { Matcher } from 'app/plugins/datasource/alertmanager/types';
import { RuleIdentifier, RuleNamespace } from 'app/types/unified-alerting';
import { PromRulesResponse } from 'app/types/unified-alerting-dto';
import { PromRuleGroupDTO, PromRulesResponse } from 'app/types/unified-alerting-dto';

import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
import { isCloudRuleIdentifier, isPrometheusRuleIdentifier } from '../utils/rules';
Expand Down Expand Up @@ -83,6 +83,26 @@ export function paramsWithMatcherAndState(
return paramsResult;
}

export const groupRulesByFileName = (groups: PromRuleGroupDTO[], dataSourceName: string) => {
const nsMap: { [key: string]: RuleNamespace } = {};
groups.forEach((group) => {
group.rules.forEach((rule) => {
rule.query = rule.query || '';
});
if (!nsMap[group.file]) {
nsMap[group.file] = {
dataSourceName,
name: group.file,
groups: [group],
};
} else {
nsMap[group.file].groups.push(group);
}
});

return Object.values(nsMap);
};

export async function fetchRules(
dataSourceName: string,
filter?: FetchPromRulesFilter,
Expand Down Expand Up @@ -116,21 +136,5 @@ export async function fetchRules(
throw e;
});

const nsMap: { [key: string]: RuleNamespace } = {};
response.data.data.groups.forEach((group) => {
group.rules.forEach((rule) => {
rule.query = rule.query || '';
});
if (!nsMap[group.file]) {
nsMap[group.file] = {
dataSourceName,
name: group.file,
groups: [group],
};
} else {
nsMap[group.file].groups.push(group);
}
});

return Object.values(nsMap);
return groupRulesByFileName(response.data.data.groups, dataSourceName);
}
Expand Up @@ -24,6 +24,7 @@ import {
import {
getAllRulesSources,
getRulesSourceByName,
GRAFANA_RULES_SOURCE_NAME,
isCloudRulesSource,
isGrafanaRulesSource,
} from '../utils/datasource';
Expand All @@ -45,7 +46,10 @@ interface CacheValue {

// this little monster combines prometheus rules and ruler rules to produce a unified data structure
// can limit to a single rules source
export function useCombinedRuleNamespaces(rulesSourceName?: string): CombinedRuleNamespace[] {
export function useCombinedRuleNamespaces(
rulesSourceName?: string,
grafanaPromRuleNamespaces?: RuleNamespace[]
): CombinedRuleNamespace[] {
const promRulesResponses = useUnifiedAlertingSelector((state) => state.promRules);
const rulerRulesResponses = useUnifiedAlertingSelector((state) => state.rulerRules);

Expand All @@ -67,9 +71,13 @@ export function useCombinedRuleNamespaces(rulesSourceName?: string): CombinedRul
return rulesSources
.map((rulesSource): CombinedRuleNamespace[] => {
const rulesSourceName = isCloudRulesSource(rulesSource) ? rulesSource.name : rulesSource;
const promRules = promRulesResponses[rulesSourceName]?.result;
const rulerRules = rulerRulesResponses[rulesSourceName]?.result;

let promRules = promRulesResponses[rulesSourceName]?.result;
if (rulesSourceName === GRAFANA_RULES_SOURCE_NAME && grafanaPromRuleNamespaces) {
promRules = grafanaPromRuleNamespaces;
}

const cached = cache.current[rulesSourceName];
if (cached && cached.promRules === promRules && cached.rulerRules === rulerRules) {
return cached.result;
Expand Down Expand Up @@ -104,7 +112,7 @@ export function useCombinedRuleNamespaces(rulesSourceName?: string): CombinedRul
return result;
})
.flat();
}, [promRulesResponses, rulerRulesResponses, rulesSources]);
}, [promRulesResponses, rulerRulesResponses, rulesSources, grafanaPromRuleNamespaces]);
}

// merge all groups in case of grafana managed, essentially treating namespaces (folders) as groups
Expand Down
141 changes: 141 additions & 0 deletions public/app/features/alerting/unified/mockApi.ts
@@ -0,0 +1,141 @@
import { rest } from 'msw';
import { setupServer, SetupServer } from 'msw/node';
import 'whatwg-fetch';

import { setBackendSrv } from '@grafana/runtime';

import { backendSrv } from '../../../core/services/backend_srv';
import {
AlertmanagerConfig,
AlertManagerCortexConfig,
EmailConfig,
MatcherOperator,
Receiver,
Route,
} from '../../../plugins/datasource/alertmanager/types';

class AlertmanagerConfigBuilder {
private alertmanagerConfig: AlertmanagerConfig = { receivers: [] };

addReceivers(configure: (builder: AlertmanagerReceiverBuilder) => void): AlertmanagerConfigBuilder {
const receiverBuilder = new AlertmanagerReceiverBuilder();
configure(receiverBuilder);
this.alertmanagerConfig.receivers?.push(receiverBuilder.build());
return this;
}

withRoute(configure: (routeBuilder: AlertmanagerRouteBuilder) => void): AlertmanagerConfigBuilder {
const routeBuilder = new AlertmanagerRouteBuilder();
configure(routeBuilder);

this.alertmanagerConfig.route = routeBuilder.build();

return this;
}

build() {
return this.alertmanagerConfig;
}
}

class AlertmanagerRouteBuilder {
private route: Route = { routes: [], object_matchers: [] };

withReceiver(receiver: string): AlertmanagerRouteBuilder {
this.route.receiver = receiver;
return this;
}
withoutReceiver(): AlertmanagerRouteBuilder {
return this;
}
withEmptyReceiver(): AlertmanagerRouteBuilder {
this.route.receiver = '';
return this;
}

addRoute(configure: (builder: AlertmanagerRouteBuilder) => void): AlertmanagerRouteBuilder {
const routeBuilder = new AlertmanagerRouteBuilder();
configure(routeBuilder);
this.route.routes?.push(routeBuilder.build());
return this;
}

addMatcher(key: string, operator: MatcherOperator, value: string): AlertmanagerRouteBuilder {
this.route.object_matchers?.push([key, operator, value]);
return this;
}

build() {
return this.route;
}
}

class EmailConfigBuilder {
private emailConfig: EmailConfig = { to: '' };

withTo(to: string): EmailConfigBuilder {
this.emailConfig.to = to;
return this;
}

build() {
return this.emailConfig;
}
}

class AlertmanagerReceiverBuilder {
private receiver: Receiver = { name: '', email_configs: [] };

withName(name: string): AlertmanagerReceiverBuilder {
this.receiver.name = name;
return this;
}

addEmailConfig(configure: (builder: EmailConfigBuilder) => void): AlertmanagerReceiverBuilder {
const builder = new EmailConfigBuilder();
configure(builder);
this.receiver.email_configs?.push(builder.build());
return this;
}

build() {
return this.receiver;
}
}

export function mockApi(server: SetupServer) {
return {
getAlertmanagerConfig: (amName: string, configure: (builder: AlertmanagerConfigBuilder) => void) => {
const builder = new AlertmanagerConfigBuilder();
configure(builder);

server.use(
rest.get(`api/alertmanager/${amName}/config/api/v1/alerts`, (req, res, ctx) =>
res(
ctx.status(200),
ctx.json<AlertManagerCortexConfig>({
alertmanager_config: builder.build(),
template_files: {},
})
)
)
);
},
};
}

// Creates a MSW server and sets up beforeAll and afterAll handlers for it
export function setupMswServer() {
const server = setupServer();

beforeAll(() => {
setBackendSrv(backendSrv);
server.listen({ onUnhandledRequest: 'error' });
});

afterAll(() => {
server.close();
});

return server;
}
10 changes: 10 additions & 0 deletions public/app/features/alerting/unified/mocks/alertRuleApi.ts
@@ -0,0 +1,10 @@
import { rest } from 'msw';
import { SetupServer } from 'msw/node';

import { PromRulesResponse } from 'app/types/unified-alerting-dto';

import { PROM_RULES_URL } from '../api/alertRuleApi';

export function mockPromRulesApiResponse(server: SetupServer, result: PromRulesResponse) {
server.use(rest.get(PROM_RULES_URL, (req, res, ctx) => res(ctx.json<PromRulesResponse>(result))));
}
17 changes: 15 additions & 2 deletions public/app/plugins/panel/alertlist/UnifiedAlertList.tsx
Expand Up @@ -18,6 +18,7 @@ import {
import { config } from 'app/core/config';
import { contextSrv } from 'app/core/services/context_srv';
import alertDef from 'app/features/alerting/state/alertDef';
import { alertRuleApi } from 'app/features/alerting/unified/api/alertRuleApi';
import { INSTANCES_DISPLAY_LIMIT } from 'app/features/alerting/unified/components/rules/RuleDetails';
import { useCombinedRuleNamespaces } from 'app/features/alerting/unified/hooks/useCombinedRuleNamespaces';
import { useUnifiedAlertingSelector } from 'app/features/alerting/unified/hooks/useUnifiedAlertingSelector';
Expand Down Expand Up @@ -60,6 +61,8 @@ export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
const rulesDataSourceNames = useMemo(getAllRulesSourceNames, []);
const [limitInstances, toggleLimit] = useToggle(true);

const { usePrometheusRulesByNamespaceQuery } = alertRuleApi;

// backwards compat for "Inactive" state filter
useEffect(() => {
if (props.options.stateFilter.inactive === true) {
Expand Down Expand Up @@ -153,9 +156,19 @@ export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {

const promRulesRequests = useUnifiedAlertingSelector((state) => state.promRules);
const rulerRulesRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
const combinedRules = useCombinedRuleNamespaces();

const somePromRulesDispatched = rulesDataSourceNames.some((name) => promRulesRequests[name]?.dispatched);

//For grafana managed rules, get the result using RTK Query to avoid the need of using the redux store
//See https://github.com/grafana/grafana/pull/70482
const { currentData: promRules = [], isLoading: grafanaRulesLoading } = usePrometheusRulesByNamespaceQuery({
limitAlerts: limitInstances ? INSTANCES_DISPLAY_LIMIT : undefined,
matcher: matcherList,
state: stateList,
});

const combinedRules = useCombinedRuleNamespaces(undefined, promRules);

const someRulerRulesDispatched = rulesDataSourceNames.some((name) => rulerRulesRequests[name]?.dispatched);
const dispatched = somePromRulesDispatched || someRulerRulesDispatched;

Expand Down Expand Up @@ -183,7 +196,7 @@ export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
return (
<CustomScrollbar autoHeightMin="100%" autoHeightMax="100%">
<div className={styles.container}>
{dispatched && loading && !haveResults && <LoadingPlaceholder text="Loading..." />}
{(grafanaRulesLoading || (dispatched && loading && !haveResults)) && <LoadingPlaceholder text="Loading..." />}
{noAlertsMessage && <div className={styles.noAlertsMessage}>{noAlertsMessage}</div>}
<section>
{props.options.viewMode === ViewMode.Stat && haveResults && (
Expand Down

0 comments on commit 5e42db4

Please sign in to comment.