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

[Alerts] Uses aggregations in RulesClient.aggregate() method #119852

Merged
merged 12 commits into from
Nov 30, 2021
2 changes: 2 additions & 0 deletions x-pack/plugins/alerting/common/alert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export interface AlertAction {

export interface AlertAggregations {
alertExecutionStatus: { [status: string]: number };
ruleEnabledStatus: { enabled: number; disabled: number };
ruleMutedStatus: { muted: number; unmuted: number };
}

export interface Alert<Params extends AlertTypeParams = never> {
Expand Down
24 changes: 24 additions & 0 deletions x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ describe('aggregateRulesRoute', () => {
pending: 1,
unknown: 0,
},
ruleEnabledStatus: {
disabled: 1,
enabled: 40,
},
ruleMutedStatus: {
muted: 2,
unmuted: 39,
},
};
rulesClient.aggregate.mockResolvedValueOnce(aggregateResult);

Expand All @@ -65,13 +73,21 @@ describe('aggregateRulesRoute', () => {
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
Object {
"body": Object {
"rule_enabled_status": Object {
"disabled": 1,
"enabled": 40,
},
"rule_execution_status": Object {
"active": 23,
"error": 2,
"ok": 15,
"pending": 1,
"unknown": 0,
},
"rule_muted_status": Object {
"muted": 2,
"unmuted": 39,
},
},
}
`);
Expand All @@ -89,13 +105,21 @@ describe('aggregateRulesRoute', () => {

expect(res.ok).toHaveBeenCalledWith({
body: {
rule_enabled_status: {
disabled: 1,
enabled: 40,
},
rule_execution_status: {
ok: 15,
error: 2,
active: 23,
pending: 1,
unknown: 0,
},
rule_muted_status: {
muted: 2,
unmuted: 39,
},
},
});
});
Expand Down
4 changes: 4 additions & 0 deletions x-pack/plugins/alerting/server/routes/aggregate_rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,14 @@ const rewriteQueryReq: RewriteRequestCase<AggregateOptions> = ({
});
const rewriteBodyRes: RewriteResponseCase<AggregateResult> = ({
alertExecutionStatus,
ruleEnabledStatus,
ruleMutedStatus,
...rest
}) => ({
...rest,
rule_execution_status: alertExecutionStatus,
rule_enabled_status: ruleEnabledStatus,
rule_muted_status: ruleMutedStatus,
});

export const aggregateRulesRoute = (
Expand Down
102 changes: 76 additions & 26 deletions x-pack/plugins/alerting/server/rules_client/rules_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,29 @@ export type InvalidateAPIKeyResult =
| { apiKeysEnabled: false }
| { apiKeysEnabled: true; result: SecurityPluginInvalidateAPIKeyResult };

export interface RuleAggregation {
status: {
buckets: Array<{
key: string;
doc_count: number;
}>;
};
muted: {
buckets: Array<{
key: number;
key_as_string: string;
doc_count: number;
}>;
};
enabled: {
buckets: Array<{
key: number;
key_as_string: string;
doc_count: number;
}>;
};
}

export interface ConstructorOptions {
logger: Logger;
taskManager: TaskManagerStartContract;
Expand Down Expand Up @@ -150,6 +173,8 @@ interface IndexType {

export interface AggregateResult {
alertExecutionStatus: { [status: string]: number };
ruleEnabledStatus?: { enabled: number; disabled: number };
ruleMutedStatus?: { muted: number; unmuted: number };
}

export interface FindResult<Params extends AlertTypeParams> {
Expand Down Expand Up @@ -644,42 +669,67 @@ export class RulesClient {
}

public async aggregate({
options: { fields, ...options } = {},
options: { fields, filter, ...options } = {},
}: { options?: AggregateOptions } = {}): Promise<AggregateResult> {
// Replace this when saved objects supports aggregations https://github.com/elastic/kibana/pull/64002
const alertExecutionStatus = await Promise.all(
AlertExecutionStatusValues.map(async (status: string) => {
const { filter: authorizationFilter } = await this.authorization.getFindAuthorizationFilter(
AlertingAuthorizationEntity.Rule,
alertingAuthorizationFilterOpts
);
const filter = options.filter
? `${options.filter} and alert.attributes.executionStatus.status:(${status})`
: `alert.attributes.executionStatus.status:(${status})`;
const { total } = await this.unsecuredSavedObjectsClient.find<RawAlert>({
...options,
filter:
(authorizationFilter && filter
? nodeBuilder.and([
esKuery.fromKueryExpression(filter),
authorizationFilter as KueryNode,
])
: authorizationFilter) ?? filter,
page: 1,
perPage: 0,
type: 'alert',
});
const { filter: authorizationFilter } = await this.authorization.getFindAuthorizationFilter(
claudiopro marked this conversation as resolved.
Show resolved Hide resolved
AlertingAuthorizationEntity.Rule,
alertingAuthorizationFilterOpts
);
const resp = await this.unsecuredSavedObjectsClient.find<RawAlert, RuleAggregation>({
...options,
filter:
(authorizationFilter && filter
? nodeBuilder.and([esKuery.fromKueryExpression(filter), authorizationFilter as KueryNode])
: authorizationFilter) ?? filter,
page: 1,
perPage: 0,
type: 'alert',
aggs: {
status: {
terms: { field: 'alert.attributes.executionStatus.status' },
},
enabled: {
terms: { field: 'alert.attributes.enabled' },
},
muted: {
terms: { field: 'alert.attributes.muteAll' },
},
},
});

return { [status]: total };
const alertExecutionStatus = resp.aggregations!.status.buckets.map(
claudiopro marked this conversation as resolved.
Show resolved Hide resolved
({ key, doc_count: docCount }) => ({
[key]: docCount,
})
);

return {
const ret: AggregateResult = {
alertExecutionStatus: alertExecutionStatus.reduce(
(acc, curr: { [status: string]: number }) => Object.assign(acc, curr),
{}
),
};

// Fill missing keys
for (const key of AlertExecutionStatusValues) {
if (!ret.alertExecutionStatus.hasOwnProperty(key)) {
ret.alertExecutionStatus[key] = 0;
}
}

const enabledBuckets = resp.aggregations!.enabled.buckets;
ret.ruleEnabledStatus = {
enabled: enabledBuckets.find((bucket) => bucket.key === 1)?.doc_count ?? 0,
disabled: enabledBuckets.find((bucket) => bucket.key === 0)?.doc_count ?? 0,
};

const mutedBuckets = resp.aggregations!.muted.buckets;
ret.ruleMutedStatus = {
muted: mutedBuckets.find((bucket) => bucket.key === 1)?.doc_count ?? 0,
unmuted: mutedBuckets.find((bucket) => bucket.key === 0)?.doc_count ?? 0,
};

return ret;
}

public async delete({ id }: { id: string }) {
Expand Down
136 changes: 78 additions & 58 deletions x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertingAuthorization } from '../../authorization/alerting_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { getBeforeSetup, setGlobalDate } from './lib';
import { AlertExecutionStatusValues } from '../../types';
import { RecoveredActionGroup } from '../../../common';
import { RegistryRuleType } from '../../rule_type_registry';

Expand Down Expand Up @@ -70,37 +69,36 @@ describe('aggregate()', () => {
authorization.getFindAuthorizationFilter.mockResolvedValue({
ensureRuleTypeIsAuthorized() {},
});
unsecuredSavedObjectsClient.find
.mockResolvedValueOnce({
total: 10,
per_page: 0,
page: 1,
saved_objects: [],
})
.mockResolvedValueOnce({
total: 8,
per_page: 0,
page: 1,
saved_objects: [],
})
.mockResolvedValueOnce({
total: 6,
per_page: 0,
page: 1,
saved_objects: [],
})
.mockResolvedValueOnce({
total: 4,
per_page: 0,
page: 1,
saved_objects: [],
})
.mockResolvedValueOnce({
total: 2,
per_page: 0,
page: 1,
saved_objects: [],
});
unsecuredSavedObjectsClient.find.mockResolvedValueOnce({
total: 30,
per_page: 0,
page: 1,
saved_objects: [],
aggregations: {
status: {
buckets: [
{ key: 'active', doc_count: 8 },
{ key: 'error', doc_count: 6 },
{ key: 'ok', doc_count: 10 },
{ key: 'pending', doc_count: 4 },
{ key: 'unknown', doc_count: 2 },
],
},
enabled: {
buckets: [
{ key: 0, key_as_string: '0', doc_count: 2 },
{ key: 1, key_as_string: '1', doc_count: 28 },
],
},
muted: {
buckets: [
{ key: 0, key_as_string: '0', doc_count: 27 },
{ key: 1, key_as_string: '1', doc_count: 3 },
],
},
},
});

ruleTypeRegistry.list.mockReturnValue(listedTypes);
authorization.filterByRuleTypeAuthorization.mockResolvedValue(
new Set([
Expand Down Expand Up @@ -134,41 +132,63 @@ describe('aggregate()', () => {
"pending": 4,
"unknown": 2,
},
"ruleEnabledStatus": Object {
"disabled": 2,
"enabled": 28,
},
"ruleMutedStatus": Object {
"muted": 3,
"unmuted": 27,
},
}
`);
expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(
AlertExecutionStatusValues.length
);
AlertExecutionStatusValues.forEach((status: string, ndx: number) => {
expect(unsecuredSavedObjectsClient.find.mock.calls[ndx]).toEqual([
{
fields: undefined,
filter: `alert.attributes.executionStatus.status:(${status})`,
page: 1,
perPage: 0,
type: 'alert',
expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1);

expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toEqual([
{
filter: undefined,
page: 1,
perPage: 0,
type: 'alert',
aggs: {
status: {
terms: { field: 'alert.attributes.executionStatus.status' },
},
enabled: {
terms: { field: 'alert.attributes.enabled' },
},
muted: {
terms: { field: 'alert.attributes.muteAll' },
},
},
]);
});
},
]);
});

test('supports filters when aggregating', async () => {
const rulesClient = new RulesClient(rulesClientParams);
await rulesClient.aggregate({ options: { filter: 'someTerm' } });

expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(
AlertExecutionStatusValues.length
);
AlertExecutionStatusValues.forEach((status: string, ndx: number) => {
expect(unsecuredSavedObjectsClient.find.mock.calls[ndx]).toEqual([
{
fields: undefined,
filter: `someTerm and alert.attributes.executionStatus.status:(${status})`,
page: 1,
perPage: 0,
type: 'alert',
expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1);
expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toEqual([
{
fields: undefined,
filter: 'someTerm',
page: 1,
perPage: 0,
type: 'alert',
aggs: {
status: {
terms: { field: 'alert.attributes.executionStatus.status' },
},
enabled: {
terms: { field: 'alert.attributes.enabled' },
},
muted: {
terms: { field: 'alert.attributes.muteAll' },
},
},
]);
});
},
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ import { AsApiContract, RewriteRequestCase } from '../../../../../actions/common

const rewriteBodyRes: RewriteRequestCase<AlertAggregations> = ({
rule_execution_status: alertExecutionStatus,
rule_enabled_status: ruleEnabledStatus,
rule_muted_status: ruleMutedStatus,
...rest
}: any) => ({
...rest,
alertExecutionStatus,
ruleEnabledStatus,
ruleMutedStatus,
});

export async function loadAlertAggregations({
Expand Down
Loading