Skip to content

Commit ae007c2

Browse files
ymao1kibanamachine
andauthored
[Alerting] Return alert execution status rollup from _find API (#81819)
* wip * wip * Adding aggregation option to find function and using those results in UI * Requesting aggregations from client instead of hard-coding in route * alert_api test * i18n fix * Adding functional test * Adding unit test for filters * Splitting into two API endpoints * Fixing test * Fixing test * Adding comment Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
1 parent c2af3aa commit ae007c2

File tree

14 files changed

+900
-29
lines changed

14 files changed

+900
-29
lines changed

x-pack/plugins/alerts/common/alert.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ export interface AlertAction {
4646
params: AlertActionParams;
4747
}
4848

49+
export interface AlertAggregations {
50+
alertExecutionStatus: { [status: string]: number };
51+
}
52+
4953
export interface Alert {
5054
id: string;
5155
enabled: boolean;

x-pack/plugins/alerts/server/alerts_client.mock.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export type AlertsClientMock = jest.Mocked<Schema>;
1111

1212
const createAlertsClientMock = () => {
1313
const mocked: AlertsClientMock = {
14+
aggregate: jest.fn(),
1415
create: jest.fn(),
1516
get: jest.fn(),
1617
getAlertState: jest.fn(),

x-pack/plugins/alerts/server/alerts_client/alerts_client.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
SanitizedAlert,
2828
AlertTaskState,
2929
AlertInstanceSummary,
30+
AlertExecutionStatusValues,
3031
} from '../types';
3132
import { validateAlertTypeParams, alertExecutionStatusFromRaw } from '../lib';
3233
import {
@@ -98,10 +99,25 @@ export interface FindOptions extends IndexType {
9899
filter?: string;
99100
}
100101

102+
export interface AggregateOptions extends IndexType {
103+
search?: string;
104+
defaultSearchOperator?: 'AND' | 'OR';
105+
searchFields?: string[];
106+
hasReference?: {
107+
type: string;
108+
id: string;
109+
};
110+
filter?: string;
111+
}
112+
101113
interface IndexType {
102114
[key: string]: unknown;
103115
}
104116

117+
interface AggregateResult {
118+
alertExecutionStatus: { [status: string]: number };
119+
}
120+
105121
export interface FindResult {
106122
page: number;
107123
perPage: number;
@@ -400,6 +416,44 @@ export class AlertsClient {
400416
};
401417
}
402418

419+
public async aggregate({
420+
options: { fields, ...options } = {},
421+
}: { options?: AggregateOptions } = {}): Promise<AggregateResult> {
422+
// Replace this when saved objects supports aggregations https://github.com/elastic/kibana/pull/64002
423+
const alertExecutionStatus = await Promise.all(
424+
AlertExecutionStatusValues.map(async (status: string) => {
425+
const {
426+
filter: authorizationFilter,
427+
logSuccessfulAuthorization,
428+
} = await this.authorization.getFindAuthorizationFilter();
429+
const filter = options.filter
430+
? `${options.filter} and alert.attributes.executionStatus.status:(${status})`
431+
: `alert.attributes.executionStatus.status:(${status})`;
432+
const { total } = await this.unsecuredSavedObjectsClient.find<RawAlert>({
433+
...options,
434+
filter:
435+
(authorizationFilter && filter
436+
? and([esKuery.fromKueryExpression(filter), authorizationFilter])
437+
: authorizationFilter) ?? filter,
438+
page: 1,
439+
perPage: 0,
440+
type: 'alert',
441+
});
442+
443+
logSuccessfulAuthorization();
444+
445+
return { [status]: total };
446+
})
447+
);
448+
449+
return {
450+
alertExecutionStatus: alertExecutionStatus.reduce(
451+
(acc, curr: { [status: string]: number }) => Object.assign(acc, curr),
452+
{}
453+
),
454+
};
455+
}
456+
403457
public async delete({ id }: { id: string }) {
404458
let taskIdToRemove: string | undefined | null;
405459
let apiKeyToInvalidate: string | null = null;
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
import { AlertsClient, ConstructorOptions } from '../alerts_client';
7+
import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks';
8+
import { taskManagerMock } from '../../../../task_manager/server/mocks';
9+
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
10+
import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock';
11+
import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks';
12+
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
13+
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
14+
import { ActionsAuthorization } from '../../../../actions/server';
15+
import { getBeforeSetup, setGlobalDate } from './lib';
16+
import { AlertExecutionStatusValues } from '../../types';
17+
18+
const taskManager = taskManagerMock.createStart();
19+
const alertTypeRegistry = alertTypeRegistryMock.create();
20+
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
21+
22+
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
23+
const authorization = alertsAuthorizationMock.create();
24+
const actionsAuthorization = actionsAuthorizationMock.create();
25+
26+
const kibanaVersion = 'v7.10.0';
27+
const alertsClientParams: jest.Mocked<ConstructorOptions> = {
28+
taskManager,
29+
alertTypeRegistry,
30+
unsecuredSavedObjectsClient,
31+
authorization: (authorization as unknown) as AlertsAuthorization,
32+
actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization,
33+
spaceId: 'default',
34+
namespace: 'default',
35+
getUserName: jest.fn(),
36+
createAPIKey: jest.fn(),
37+
invalidateAPIKey: jest.fn(),
38+
logger: loggingSystemMock.create().get(),
39+
encryptedSavedObjectsClient: encryptedSavedObjects,
40+
getActionsClient: jest.fn(),
41+
getEventLogClient: jest.fn(),
42+
kibanaVersion,
43+
};
44+
45+
beforeEach(() => {
46+
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
47+
});
48+
49+
setGlobalDate();
50+
51+
describe('aggregate()', () => {
52+
const listedTypes = new Set([
53+
{
54+
actionGroups: [],
55+
actionVariables: undefined,
56+
defaultActionGroupId: 'default',
57+
id: 'myType',
58+
name: 'myType',
59+
producer: 'myApp',
60+
},
61+
]);
62+
beforeEach(() => {
63+
authorization.getFindAuthorizationFilter.mockResolvedValue({
64+
ensureAlertTypeIsAuthorized() {},
65+
logSuccessfulAuthorization() {},
66+
});
67+
unsecuredSavedObjectsClient.find
68+
.mockResolvedValueOnce({
69+
total: 10,
70+
per_page: 0,
71+
page: 1,
72+
saved_objects: [],
73+
})
74+
.mockResolvedValueOnce({
75+
total: 8,
76+
per_page: 0,
77+
page: 1,
78+
saved_objects: [],
79+
})
80+
.mockResolvedValueOnce({
81+
total: 6,
82+
per_page: 0,
83+
page: 1,
84+
saved_objects: [],
85+
})
86+
.mockResolvedValueOnce({
87+
total: 4,
88+
per_page: 0,
89+
page: 1,
90+
saved_objects: [],
91+
})
92+
.mockResolvedValueOnce({
93+
total: 2,
94+
per_page: 0,
95+
page: 1,
96+
saved_objects: [],
97+
});
98+
alertTypeRegistry.list.mockReturnValue(listedTypes);
99+
authorization.filterByAlertTypeAuthorization.mockResolvedValue(
100+
new Set([
101+
{
102+
id: 'myType',
103+
name: 'Test',
104+
actionGroups: [{ id: 'default', name: 'Default' }],
105+
defaultActionGroupId: 'default',
106+
producer: 'alerts',
107+
authorizedConsumers: {
108+
myApp: { read: true, all: true },
109+
},
110+
},
111+
])
112+
);
113+
});
114+
115+
test('calls saved objects client with given params to perform aggregation', async () => {
116+
const alertsClient = new AlertsClient(alertsClientParams);
117+
const result = await alertsClient.aggregate({ options: {} });
118+
expect(result).toMatchInlineSnapshot(`
119+
Object {
120+
"alertExecutionStatus": Object {
121+
"active": 8,
122+
"error": 6,
123+
"ok": 10,
124+
"pending": 4,
125+
"unknown": 2,
126+
},
127+
}
128+
`);
129+
expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(
130+
AlertExecutionStatusValues.length
131+
);
132+
AlertExecutionStatusValues.forEach((status: string, ndx: number) => {
133+
expect(unsecuredSavedObjectsClient.find.mock.calls[ndx]).toEqual([
134+
{
135+
fields: undefined,
136+
filter: `alert.attributes.executionStatus.status:(${status})`,
137+
page: 1,
138+
perPage: 0,
139+
type: 'alert',
140+
},
141+
]);
142+
});
143+
});
144+
145+
test('supports filters when aggregating', async () => {
146+
const alertsClient = new AlertsClient(alertsClientParams);
147+
await alertsClient.aggregate({ options: { filter: 'someTerm' } });
148+
149+
expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(
150+
AlertExecutionStatusValues.length
151+
);
152+
AlertExecutionStatusValues.forEach((status: string, ndx: number) => {
153+
expect(unsecuredSavedObjectsClient.find.mock.calls[ndx]).toEqual([
154+
{
155+
fields: undefined,
156+
filter: `someTerm and alert.attributes.executionStatus.status:(${status})`,
157+
page: 1,
158+
perPage: 0,
159+
type: 'alert',
160+
},
161+
]);
162+
});
163+
});
164+
});

x-pack/plugins/alerts/server/plugin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
} from '../../../../src/core/server';
3434

3535
import {
36+
aggregateAlertRoute,
3637
createAlertRoute,
3738
deleteAlertRoute,
3839
findAlertRoute,
@@ -190,6 +191,7 @@ export class AlertingPlugin {
190191
// Routes
191192
const router = core.http.createRouter();
192193
// Register routes
194+
aggregateAlertRoute(router, this.licenseState);
193195
createAlertRoute(router, this.licenseState);
194196
deleteAlertRoute(router, this.licenseState);
195197
findAlertRoute(router, this.licenseState);

0 commit comments

Comments
 (0)