Skip to content

Commit

Permalink
[Alerts] Uses aggregations in RulesClient.aggregate() method (elastic…
Browse files Browse the repository at this point in the history
…#119852)

* Replaces multiple find requests with aggregations

* Updates unit tests

* Removes commented out code

* Removes unused import

* Adds muted and enabled aggregations

* Updates tests

* Updates snapshot

* Fixes functional test

* Fixes functional test

* Review feedback, fixes API tests

* Logs audit event and updates tests
  • Loading branch information
claudiopro authored and TinLe committed Dec 22, 2021
1 parent 1aa6c31 commit 10744df
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 84 deletions.
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 @@ -56,6 +56,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
3 changes: 3 additions & 0 deletions x-pack/plugins/alerting/server/rules_client/audit_events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export enum RuleAuditAction {
UNMUTE = 'rule_unmute',
MUTE_ALERT = 'rule_alert_mute',
UNMUTE_ALERT = 'rule_alert_unmute',
AGGREGATE = 'rule_aggregate',
}

type VerbsTuple = [string, string, string];
Expand All @@ -40,6 +41,7 @@ const eventVerbs: Record<RuleAuditAction, VerbsTuple> = {
rule_unmute: ['unmute', 'unmuting', 'unmuted'],
rule_alert_mute: ['mute alert of', 'muting alert of', 'muted alert of'],
rule_alert_unmute: ['unmute alert of', 'unmuting alert of', 'unmuted alert of'],
rule_aggregate: ['access', 'accessing', 'accessed'],
};

const eventTypes: Record<RuleAuditAction, EcsEventType> = {
Expand All @@ -56,6 +58,7 @@ const eventTypes: Record<RuleAuditAction, EcsEventType> = {
rule_unmute: 'change',
rule_alert_mute: 'change',
rule_alert_unmute: 'change',
rule_aggregate: 'access',
};

export interface RuleAuditEventParams {
Expand Down
135 changes: 109 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,100 @@ 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',
});
let authorizationTuple;
try {
authorizationTuple = await this.authorization.getFindAuthorizationFilter(
AlertingAuthorizationEntity.Rule,
alertingAuthorizationFilterOpts
);
} catch (error) {
this.auditLogger?.log(
ruleAuditEvent({
action: RuleAuditAction.AGGREGATE,
error,
})
);
throw error;
}
const { filter: authorizationFilter } = authorizationTuple;
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' },
},
},
});

if (!resp.aggregations) {
// Return a placeholder with all zeroes
const placeholder: AggregateResult = {
alertExecutionStatus: {},
ruleEnabledStatus: {
enabled: 0,
disabled: 0,
},
ruleMutedStatus: {
muted: 0,
unmuted: 0,
},
};

for (const key of AlertExecutionStatusValues) {
placeholder.alertExecutionStatus[key] = 0;
}

return placeholder;
}

return { [status]: total };
const alertExecutionStatus = resp.aggregations.status.buckets.map(
({ 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 with zeroes
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
Loading

0 comments on commit 10744df

Please sign in to comment.