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
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