Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v10.0.x] Alerting: Display correct results when using different filters on alerting panels #70639

Merged
merged 2 commits into from Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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