[Metric threshold] Stop reporting no data alert for missing groups that are untracked (#179374)

Fixes #161621

## Summary

This PR removes untracked alerts from the missing group array in the
rule state and as a result, those no data alerts will not be reported
again.

### 🧪 How to test
- Create some metric threshold alerts for different groups
- Stop sending data for `group-1` (You should see a no data alert for
it)
- Untrack no data alert related to `group-1`, after the next rule
execution, you should not see any no data alert for this group.
- Again send data for `group-1` to meet the threshold, you should see an
alert for it as before.
This commit is contained in:
Maryam Saeidi 2024-03-27 14:29:13 +01:00 committed by GitHub
parent fc17526a1d
commit 39379a2e9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 150 additions and 2 deletions

View file

@ -35,6 +35,7 @@ const createPublicAlertsClientMock = () => {
return {
create: jest.fn(),
report: jest.fn(),
isTrackedAlert: jest.fn(),
getAlertLimitValue: jest.fn().mockReturnValue(1000),
setAlertLimitReached: jest.fn(),
getRecoveredAlerts: jest.fn().mockReturnValue([]),

View file

@ -87,7 +87,7 @@ const mockOptions = {
};
const setEvaluationResults = (response: Array<Record<string, Evaluation>>) => {
jest.requireMock('./lib/evaluate_rule').evaluateRule.mockImplementation(() => response);
return jest.requireMock('./lib/evaluate_rule').evaluateRule.mockImplementation(() => response);
};
describe('The metric threshold rule type', () => {
@ -733,6 +733,148 @@ describe('The metric threshold rule type', () => {
);
expect(stateResult3.missingGroups).toEqual(expect.arrayContaining([]));
});
test('should remove a group from previous missing groups if the related alert is untracked', async () => {
setEvaluationResults([
{
a: {
...baseNonCountCriterion,
comparator: Comparator.GT,
threshold: [0.75],
metric: 'test.metric.2',
currentValue: 1.0,
timestamp: new Date().toISOString(),
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseNonCountCriterion,
comparator: Comparator.GT,
threshold: [0.75],
metric: 'test.metric.2',
currentValue: 3,
timestamp: new Date().toISOString(),
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'b' },
},
c: {
...baseNonCountCriterion,
comparator: Comparator.GT,
threshold: [0.75],
metric: 'test.metric.2',
currentValue: 3,
timestamp: new Date().toISOString(),
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'c' },
},
},
]);
const { state: stateResult1 } = await executeWithFilter(
Comparator.GT,
[0.75],
JSON.stringify({ query: 'q' }),
'test.metric.2'
);
expect(stateResult1.missingGroups).toEqual(expect.arrayContaining([]));
setEvaluationResults([
{
a: {
...baseNonCountCriterion,
comparator: Comparator.GT,
threshold: [0.75],
metric: 'test.metric.1',
currentValue: 1.0,
timestamp: new Date().toISOString(),
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseNonCountCriterion,
comparator: Comparator.GT,
threshold: [0.75],
metric: 'test.metric.1',
currentValue: null,
timestamp: new Date().toISOString(),
shouldFire: true,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: 'b' },
},
c: {
...baseNonCountCriterion,
comparator: Comparator.GT,
threshold: [0.75],
metric: 'test.metric.1',
currentValue: null,
timestamp: new Date().toISOString(),
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: 'c' },
},
},
]);
const { state: stateResult2 } = await executeWithFilter(
Comparator.GT,
[0.75],
JSON.stringify({ query: 'q' }),
'test.metric.1',
stateResult1
);
expect(stateResult2.missingGroups).toEqual([
{ key: 'b', bucketKey: { groupBy0: 'b' } },
{ key: 'c', bucketKey: { groupBy0: 'c' } },
]);
const mockedEvaluateRule = setEvaluationResults([
{
a: {
...baseNonCountCriterion,
comparator: Comparator.GT,
threshold: [0.75],
metric: 'test.metric.1',
currentValue: 1.0,
timestamp: new Date().toISOString(),
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseNonCountCriterion,
comparator: Comparator.GT,
threshold: [0.75],
metric: 'test.metric.1',
currentValue: null,
timestamp: new Date().toISOString(),
shouldFire: true,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: 'b' },
},
},
]);
// Consider c as untracked
services.alertsClient.isTrackedAlert.mockImplementation((id: string) => id !== 'c');
const { state: stateResult3 } = await executeWithFilter(
Comparator.GT,
[0.75],
JSON.stringify({ query: 'q' }),
'test.metric.1',
stateResult2
);
expect(stateResult3.missingGroups).toEqual([{ key: 'b', bucketKey: { groupBy0: 'b' } }]);
expect(mockedEvaluateRule.mock.calls[2][8]).toEqual([
{ bucketKey: { groupBy0: 'b' }, key: 'b' },
]);
});
});
describe('querying with a groupBy parameter host.name and rule tags', () => {

View file

@ -224,7 +224,12 @@ export const createMetricThresholdExecutor =
const groupByIsSame = isEqual(state.groupBy, params.groupBy);
const previousMissingGroups =
alertOnGroupDisappear && filterQueryIsSame && groupByIsSame && state.missingGroups
? state.missingGroups
? state.missingGroups.filter((missingGroup) =>
// We use isTrackedAlert to remove missing groups that are untracked by the user
typeof missingGroup === 'string'
? alertsClient.isTrackedAlert(missingGroup)
: alertsClient.isTrackedAlert(missingGroup.key)
)
: [];
const alertResults = await evaluateRule(