Skip to content

Commit

Permalink
Fix UnifiedAlertStatesWorker work
Browse files Browse the repository at this point in the history
  • Loading branch information
soniaAguilarPeiron committed Apr 29, 2024
1 parent b2b275c commit 9613576
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 101 deletions.
Expand Up @@ -15,12 +15,12 @@ interface AlertRulesToolbarButtonProps {
export default function AlertRulesToolbarButton({ dashboardUid }: AlertRulesToolbarButtonProps) {
const { showModal, hideModal } = useContext(ModalsContext);

const { currentData: promRuleNs } = alertRuleApi.endpoints.prometheusRuleNamespaces.useQuery({
const { data: namespaces = [] } = alertRuleApi.endpoints.prometheusRuleNamespaces.useQuery({
ruleSourceName: GRAFANA_RULES_SOURCE_NAME,
dashboardUid: dashboardUid,
});

if (promRuleNs?.length === 0) {
if (namespaces.length === 0) {
return null;
}

Expand Down
Expand Up @@ -3,11 +3,16 @@ import { lastValueFrom } from 'rxjs';
import { AlertState, getDefaultTimeRange, TimeRange } from '@grafana/data';
import { config } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv';
import { grantUserPermissions } from 'app/features/alerting/unified/mocks';
import {
grantUserPermissions,
mockPromAlertingRule,
mockPromRuleGroup,
mockPromRuleNamespace,
} from 'app/features/alerting/unified/mocks';
import { Annotation } from 'app/features/alerting/unified/utils/constants';
import { createDashboardModelFixture } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures';
import { AccessControlAction } from 'app/types/accessControl';
import { PromAlertingRuleState, PromRuleDTO, PromRulesResponse, PromRuleType } from 'app/types/unified-alerting-dto';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';

import { silenceConsoleOutput } from '../../../../../test/core/utils/silenceConsoleOutput';
import * as store from '../../../../store/store';
Expand Down Expand Up @@ -37,9 +42,8 @@ function getTestContext() {
jest.clearAllMocks();
const dispatchMock = jest.spyOn(store, 'dispatch');
const options = getDefaultOptions();
const getMock = jest.spyOn(backendSrv, 'get');

return { getMock, options, dispatchMock };
return { options, dispatchMock };
}

describe('UnifiedAlertStatesWorker', () => {
Expand Down Expand Up @@ -88,89 +92,79 @@ describe('UnifiedAlertStatesWorker', () => {

describe('when run is called with incorrect props', () => {
it('then it should return the correct results', async () => {
const { getMock, options } = getTestContext();
const { options } = getTestContext();
const dashboard = createDashboardModelFixture({});

await expect(worker.work({ ...options, dashboard })).toEmitValuesWith((received) => {
expect(received).toHaveLength(1);
const results = received[0];
expect(results).toEqual({ alertStates: [], annotations: [] });
expect(getMock).not.toHaveBeenCalled();
});
});
});

describe('when run repeatedly for the same dashboard and no alert rules are found', () => {
const nameSpaces = [mockPromRuleNamespace({ groups: [] })];
const { dispatchMock, options } = getTestContext();
//getMock.mockResolvedValue(getResults);
dispatchMock.mockResolvedValue(nameSpaces);
it('then canWork should start returning false', async () => {
const worker = new UnifiedAlertStatesWorker();

const getResults: PromRulesResponse = {
status: 'success',
data: {
groups: [],
},
};
const { getMock, options } = getTestContext();
getMock.mockResolvedValue(getResults);
expect(worker.canWork(options)).toBe(true);
await lastValueFrom(worker.work(options));
expect(worker.canWork(options)).toBe(false);
});
});

describe('when run is called with correct props and request is successful', () => {
function mockPromRuleDTO(overrides: Partial<PromRuleDTO>): PromRuleDTO {
return {
alerts: [],
health: 'ok',
name: 'foo',
query: 'foo',
type: PromRuleType.Alerting,
state: PromAlertingRuleState.Firing,
labels: {},
annotations: {},
...overrides,
};
}

it('then it should return the correct results', async () => {
const getResults: PromRulesResponse = {
status: 'success',
data: {
const nameSpaces = [
mockPromRuleNamespace({
groups: [
{
name: 'group',
file: '',
interval: 1,
mockPromRuleGroup({
name: 'group1',
rules: [
mockPromRuleDTO({
mockPromAlertingRule({
name: 'alert1',
state: PromAlertingRuleState.Firing,
annotations: {
[Annotation.dashboardUID]: 'a uid',
[Annotation.panelID]: '1',
},
}),
mockPromRuleDTO({
],
}),
mockPromRuleGroup({
name: 'group2',
rules: [
mockPromAlertingRule({
name: 'alert2',
state: PromAlertingRuleState.Inactive,
annotations: {
[Annotation.dashboardUID]: 'a uid',
[Annotation.panelID]: '2',
},
}),
mockPromRuleDTO({
],
}),
mockPromRuleGroup({
name: 'group3',
rules: [
mockPromAlertingRule({
name: 'alert3',
state: PromAlertingRuleState.Pending,
annotations: {
[Annotation.dashboardUID]: 'a uid',
[Annotation.panelID]: '2',
},
}),
],
},
}),
],
},
};
const { getMock, options } = getTestContext();
getMock.mockResolvedValue(getResults);
}),
];
const { dispatchMock, options } = getTestContext();
dispatchMock.mockResolvedValue({ data: nameSpaces });

await expect(worker.work(options)).toEmitValuesWith((received) => {
expect(received).toHaveLength(1);
Expand All @@ -184,46 +178,38 @@ describe('UnifiedAlertStatesWorker', () => {
});
});

expect(getMock).toHaveBeenCalledTimes(1);
expect(getMock).toHaveBeenCalledWith(
'/api/prometheus/grafana/api/v1/rules',
{ dashboard_uid: 'a uid' },
'dashboard-query-runner-unified-alert-states-12345'
);
expect(dispatchMock).toHaveBeenCalledTimes(1);
});
});

describe('when run is called with correct props and request fails', () => {
silenceConsoleOutput();
it('then it should return the correct results', async () => {
const { getMock, options, dispatchMock } = getTestContext();
getMock.mockRejectedValue({ message: 'An error' });
const { options, dispatchMock } = getTestContext();
dispatchMock.mockResolvedValue({ error: 'An error' });

await expect(worker.work(options)).toEmitValuesWith((received) => {
expect(received).toHaveLength(1);
const results = received[0];
expect(results).toEqual({ alertStates: [], annotations: [] });
expect(getMock).toHaveBeenCalledTimes(1);
expect(dispatchMock).toHaveBeenCalledTimes(1);
});
});
});

describe('when run is called with correct props and request is cancelled', () => {
silenceConsoleOutput();
it('then it should return the correct results', async () => {
const { getMock, options, dispatchMock } = getTestContext();
getMock.mockRejectedValue({ cancelled: true });

await expect(worker.work(options)).toEmitValuesWith((received) => {
expect(received).toHaveLength(1);
const results = received[0];
expect(results).toEqual({ alertStates: [], annotations: [] });
expect(getMock).toHaveBeenCalledTimes(1);
expect(dispatchMock).not.toHaveBeenCalled();
});
});
});
// not sure how to test this use case as now we are using dispatch instead of getBackendSrv
// describe('when run is called with correct props and request is cancelled', () => {
// silenceConsoleOutput();
// it('then it should return the correct results', async () => {
// const { options, dispatchMock } = getTestContext();

// await expect(worker.work(options)).toEmitValuesWith((received) => {
// expect(received).toHaveLength(1);
// const results = received[0];
// expect(results).toEqual({ alertStates: [], annotations: [] });
// expect(dispatchMock).not.toHaveBeenCalled();
// });
// });
// });
});

describe('UnifiedAlertStateWorker with RBAC', () => {
Expand Down
@@ -1,5 +1,5 @@
import { Observable, from } from 'rxjs';
import { catchError, concatMap, map } from 'rxjs/operators';
import { catchError, map } from 'rxjs/operators';

import { AlertState, AlertStateInfo } from '@grafana/data';
import { config } from '@grafana/runtime';
Expand Down Expand Up @@ -65,42 +65,46 @@ export class UnifiedAlertStatesWorker implements DashboardQueryRunnerWorker {
ruleSourceName: GRAFANA_RULES_SOURCE_NAME,
dashboardUid: dashboard.uid,
})
).then((response: { data: RuleNamespace[] }) => response);
);
return (await promRules).data;
};

return from(fetchData()).pipe(
concatMap((namespaces: RuleNamespace[]) => ungroupRulesByFileName(namespaces)),
map((group: PromRuleGroupDTO) => {
const res: Observable<PromRuleGroupDTO[]> = from<Promise<RuleNamespace[]>>(fetchData()).pipe(
map((namespaces: RuleNamespace[]) => ungroupRulesByFileName(namespaces))
);

return res.pipe(
map((groups: PromRuleGroupDTO[]) => {
this.hasAlertRules[dashboard.uid] = false;
const panelIdToAlertState: Record<number, AlertStateInfo> = {};
// result.data.groups.forEach((group) =>
group.rules.forEach((rule) => {
if (isAlertingRule(rule) && rule.annotations && rule.annotations[Annotation.panelID]) {
this.hasAlertRules[dashboard.uid] = true;
const panelId = Number(rule.annotations[Annotation.panelID]);
const state = promAlertStateToAlertState(rule.state);
groups.forEach((group) =>
group.rules.forEach((rule) => {
if (isAlertingRule(rule) && rule.annotations && rule.annotations[Annotation.panelID]) {
this.hasAlertRules[dashboard.uid] = true;
const panelId = Number(rule.annotations[Annotation.panelID]);
const state = promAlertStateToAlertState(rule.state);

// there can be multiple alerts per panel, so we make sure we get the most severe state:
// alerting > pending > ok
if (!panelIdToAlertState[panelId]) {
panelIdToAlertState[panelId] = {
state,
id: Object.keys(panelIdToAlertState).length,
panelId,
dashboardId: dashboard.id,
};
} else if (state === AlertState.Alerting && panelIdToAlertState[panelId].state !== AlertState.Alerting) {
panelIdToAlertState[panelId].state = AlertState.Alerting;
} else if (
state === AlertState.Pending &&
panelIdToAlertState[panelId].state !== AlertState.Alerting &&
panelIdToAlertState[panelId].state !== AlertState.Pending
) {
panelIdToAlertState[panelId].state = AlertState.Pending;
// there can be multiple alerts per panel, so we make sure we get the most severe state:
// alerting > pending > ok
if (!panelIdToAlertState[panelId]) {
panelIdToAlertState[panelId] = {
state,
id: Object.keys(panelIdToAlertState).length,
panelId,
dashboardId: dashboard.id,
};
} else if (state === AlertState.Alerting && panelIdToAlertState[panelId].state !== AlertState.Alerting) {
panelIdToAlertState[panelId].state = AlertState.Alerting;
} else if (
state === AlertState.Pending &&
panelIdToAlertState[panelId].state !== AlertState.Alerting &&
panelIdToAlertState[panelId].state !== AlertState.Pending
) {
panelIdToAlertState[panelId].state = AlertState.Pending;
}
}
}
});
})
);
return { alertStates: Object.values(panelIdToAlertState), annotations: [] };
}),
catchError(handleDashboardQueryRunnerWorkerError)
Expand Down

0 comments on commit 9613576

Please sign in to comment.