Skip to content

Commit

Permalink
Create new rule HTTP APIs (#93980)
Browse files Browse the repository at this point in the history
* Move current HTTP APIs to legacy folder

* Rename BASE_ALERT_API_PATH to LEGACY_BASE_ALERT_API_PATH

* Fix failing tests and extra files

* Move current alert HTTP APIs to legacy folder (#93943)

* Move current HTTP APIs to legacy folder

* Rename BASE_ALERT_API_PATH to LEGACY_BASE_ALERT_API_PATH

* Fix failing tests and extra files

* Add necessary files

* Create rule route

* Get rule API

* Update rule API

* Delete rule route

* Aggregate rules API

* Disable rule API

* Enable rule API

* Find rules API

* Fix Update API

* Get rule alert summary API

* Get rule state API

* Health API

* Rule types API

* Mute all API

* Mute alert API

* Unmute all API

* Unmute alert route

* Update API key API

* corrected tpye by making it much more complicated

* removed unneeded cocde

* Fixes

* Add back health route

* mutedInstanceIds -> mutedAlertIds

* lastRun -> last_run

* alert_type_state -> rule_type_state & alert_instances -> alerts

Co-authored-by: Gidi Meir Morris <[email protected]>
  • Loading branch information
mikecote and gmmorris authored Mar 17, 2021
1 parent 09e8a3b commit 4c2a619
Show file tree
Hide file tree
Showing 48 changed files with 3,892 additions and 19 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/alerting/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ export interface AlertingFrameworkHealth {
}

export const LEGACY_BASE_ALERT_API_PATH = '/api/alerts';
export const BASE_ALERTING_API_PATH = '/api/alerting';
export const ALERTS_FEATURE_ID = 'alerts';
8 changes: 4 additions & 4 deletions x-pack/plugins/alerting/server/alerts_client/alerts_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ interface IndexType {
[key: string]: unknown;
}

interface AggregateResult {
export interface AggregateResult {
alertExecutionStatus: { [status: string]: number };
}

Expand Down Expand Up @@ -156,7 +156,7 @@ export interface CreateOptions<Params extends AlertTypeParams> {
};
}

interface UpdateOptions<Params extends AlertTypeParams> {
export interface UpdateOptions<Params extends AlertTypeParams> {
id: string;
data: {
name: string;
Expand All @@ -169,7 +169,7 @@ interface UpdateOptions<Params extends AlertTypeParams> {
};
}

interface GetAlertInstanceSummaryParams {
export interface GetAlertInstanceSummaryParams {
id: string;
dateStart?: string;
}
Expand Down Expand Up @@ -228,7 +228,7 @@ export class AlertsClient {
public async create<Params extends AlertTypeParams = never>({
data,
options,
}: CreateOptions<Params>): Promise<Alert<Params>> {
}: CreateOptions<Params>): Promise<SanitizedAlert<Params>> {
const id = options?.id || SavedObjectsUtils.generateId();

try {
Expand Down
17 changes: 17 additions & 0 deletions x-pack/plugins/alerting/server/lib/errors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { ErrorThatHandlesItsOwnResponse } from './types';

export function isErrorThatHandlesItsOwnResponse(
e: ErrorThatHandlesItsOwnResponse
): e is ErrorThatHandlesItsOwnResponse {
return typeof (e as ErrorThatHandlesItsOwnResponse).sendResponse === 'function';
}

export { ErrorThatHandlesItsOwnResponse };
export { AlertTypeDisabledError, AlertTypeDisabledReason } from './alert_type_disabled';
7 changes: 7 additions & 0 deletions x-pack/plugins/alerting/server/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ export { parseDuration, validateDurationSchema } from '../../common/parse_durati
export { ILicenseState, LicenseState } from './license_state';
export { validateAlertTypeParams } from './validate_alert_type_params';
export { getAlertNotifyWhenType } from './get_alert_notify_when_type';
export { verifyApiAccess } from './license_api_access';
export { ErrorWithReason, getReasonFromError, isErrorWithReason } from './error_with_reason';
export {
AlertTypeDisabledError,
AlertTypeDisabledReason,
ErrorThatHandlesItsOwnResponse,
isErrorThatHandlesItsOwnResponse,
} from './errors';
export {
executionStatusFromState,
executionStatusFromError,
Expand Down
150 changes: 150 additions & 0 deletions x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { aggregateRulesRoute } from './aggregate_rules';
import { httpServiceMock } from 'src/core/server/mocks';
import { licenseStateMock } from '../lib/license_state.mock';
import { verifyApiAccess } from '../lib/license_api_access';
import { mockHandlerArguments } from './_mock_handler_arguments';
import { alertsClientMock } from '../alerts_client.mock';

const alertsClient = alertsClientMock.create();

jest.mock('../lib/license_api_access.ts', () => ({
verifyApiAccess: jest.fn(),
}));

beforeEach(() => {
jest.resetAllMocks();
});

describe('aggregateRulesRoute', () => {
it('aggregate rules with proper parameters', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();

aggregateRulesRoute(router, licenseState);

const [config, handler] = router.get.mock.calls[0];

expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rules/_aggregate"`);

const aggregateResult = {
alertExecutionStatus: {
ok: 15,
error: 2,
active: 23,
pending: 1,
unknown: 0,
},
};
alertsClient.aggregate.mockResolvedValueOnce(aggregateResult);

const [context, req, res] = mockHandlerArguments(
{ alertsClient },
{
query: {
default_search_operator: 'AND',
},
},
['ok']
);

expect(await handler(context, req, res)).toMatchInlineSnapshot(`
Object {
"body": Object {
"rule_execution_status": Object {
"active": 23,
"error": 2,
"ok": 15,
"pending": 1,
"unknown": 0,
},
},
}
`);

expect(alertsClient.aggregate).toHaveBeenCalledTimes(1);
expect(alertsClient.aggregate.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"options": Object {
"defaultSearchOperator": "AND",
},
},
]
`);

expect(res.ok).toHaveBeenCalledWith({
body: {
rule_execution_status: {
ok: 15,
error: 2,
active: 23,
pending: 1,
unknown: 0,
},
},
});
});

it('ensures the license allows aggregating rules', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();

aggregateRulesRoute(router, licenseState);

const [, handler] = router.get.mock.calls[0];

alertsClient.aggregate.mockResolvedValueOnce({
alertExecutionStatus: {
ok: 15,
error: 2,
active: 23,
pending: 1,
unknown: 0,
},
});

const [context, req, res] = mockHandlerArguments(
{ alertsClient },
{
query: {
default_search_operator: 'OR',
},
}
);

await handler(context, req, res);

expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
});

it('ensures the license check prevents aggregating rules', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();

(verifyApiAccess as jest.Mock).mockImplementation(() => {
throw new Error('OMG');
});

aggregateRulesRoute(router, licenseState);

const [, handler] = router.get.mock.calls[0];

const [context, req, res] = mockHandlerArguments(
{},
{
query: {},
},
['ok']
);
expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`);

expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
});
});
79 changes: 79 additions & 0 deletions x-pack/plugins/alerting/server/routes/aggregate_rules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { IRouter } from 'kibana/server';
import { schema } from '@kbn/config-schema';
import { ILicenseState } from '../lib';
import { AggregateResult, AggregateOptions } from '../alerts_client';
import { RewriteResponseCase, RewriteRequestCase, verifyAccessAndContext } from './lib';
import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../types';

// config definition
const querySchema = schema.object({
search: schema.maybe(schema.string()),
default_search_operator: schema.oneOf([schema.literal('OR'), schema.literal('AND')], {
defaultValue: 'OR',
}),
search_fields: schema.maybe(schema.arrayOf(schema.string())),
has_reference: schema.maybe(
// use nullable as maybe is currently broken
// in config-schema
schema.nullable(
schema.object({
type: schema.string(),
id: schema.string(),
})
)
),
filter: schema.maybe(schema.string()),
});

const rewriteQueryReq: RewriteRequestCase<AggregateOptions> = ({
default_search_operator: defaultSearchOperator,
has_reference: hasReference,
search_fields: searchFields,
...rest
}) => ({
...rest,
defaultSearchOperator,
...(hasReference ? { hasReference } : {}),
...(searchFields ? { searchFields } : {}),
});
const rewriteBodyRes: RewriteResponseCase<AggregateResult> = ({
alertExecutionStatus,
...rest
}) => ({
...rest,
rule_execution_status: alertExecutionStatus,
});

export const aggregateRulesRoute = (
router: IRouter<AlertingRequestHandlerContext>,
licenseState: ILicenseState
) => {
router.get(
{
path: `${BASE_ALERTING_API_PATH}/rules/_aggregate`,
validate: {
query: querySchema,
},
},
router.handleLegacyErrors(
verifyAccessAndContext(licenseState, async function (context, req, res) {
const alertsClient = context.alerting.getAlertsClient();
const options = rewriteQueryReq({
...req.query,
has_reference: req.query.has_reference || undefined,
});
const aggregateResult = await alertsClient.aggregate({ options });
return res.ok({
body: rewriteBodyRes(aggregateResult),
});
})
)
);
};
Loading

0 comments on commit 4c2a619

Please sign in to comment.