mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[ResponseOps][Alerting] Implement and onboard query delay mechanism for Alerting rules (#168735)
Resolves https://github.com/elastic/kibana/issues/167061 ## Summary This PR will merge the query delay feature branch in to main, and includes the following PRs: [[ResponseOps][Alerting] Onboard query delay mechanism for Alerting rules](https://github.com/elastic/kibana/pull/167363) [[ResponseOps][Alerting] Implement a query delay mechanism for Alerting rules](https://github.com/elastic/kibana/pull/167433) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Ying Mao <ying.mao@elastic.co>
This commit is contained in:
parent
8284398023
commit
726558959f
107 changed files with 3394 additions and 1120 deletions
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type { GetQueryDelaySettingsResponse } from './types/latest';
|
||||
|
||||
export type { GetQueryDelaySettingsResponse as GetQueryDelaySettingsResponseV1 } from './types/v1';
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type { GetQueryDelaySettingsResponse } from './v1';
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 type { TypeOf } from '@kbn/config-schema';
|
||||
import { queryDelaySettingsResponseSchemaV1 } from '../../../response';
|
||||
|
||||
export type GetQueryDelaySettingsResponse = TypeOf<typeof queryDelaySettingsResponseSchemaV1>;
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { updateQueryDelaySettingsBodySchema } from './schemas/latest';
|
||||
export type {
|
||||
UpdateQueryDelaySettingsRequestBody,
|
||||
UpdateQueryDelaySettingsResponse,
|
||||
} from './types/latest';
|
||||
|
||||
export { updateQueryDelaySettingsBodySchema as updateQueryDelaySettingsBodySchemaV1 } from './schemas/v1';
|
||||
export type {
|
||||
UpdateQueryDelaySettingsRequestBody as UpdateQueryDelaySettingsRequestBodyV1,
|
||||
UpdateQueryDelaySettingsResponse as UpdateQueryDelaySettingsResponseV1,
|
||||
} from './types/v1';
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './v1';
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
|
||||
export const updateQueryDelaySettingsBodySchema = schema.object({
|
||||
delay: schema.number(),
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './v1';
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 type { TypeOf } from '@kbn/config-schema';
|
||||
import { queryDelaySettingsResponseSchemaV1 } from '../../../response';
|
||||
import { updateQueryDelaySettingsBodySchemaV1 } from '..';
|
||||
|
||||
export type UpdateQueryDelaySettingsRequestBody = TypeOf<
|
||||
typeof updateQueryDelaySettingsBodySchemaV1
|
||||
>;
|
||||
|
||||
export type UpdateQueryDelaySettingsResponse = TypeOf<typeof queryDelaySettingsResponseSchemaV1>;
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { queryDelaySettingsResponseSchema } from './schemas/latest';
|
||||
export type { QueryDelaySettingsResponse } from './types/latest';
|
||||
|
||||
export { queryDelaySettingsResponseSchema as queryDelaySettingsResponseSchemaV1 } from './schemas/v1';
|
||||
export type { QueryDelaySettingsResponse as QueryDelaySettingsResponseV1 } from './types/v1';
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './v1';
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
|
||||
export const queryDelaySettingsResponseBodySchema = schema.object({
|
||||
delay: schema.number(),
|
||||
created_by: schema.nullable(schema.string()),
|
||||
updated_by: schema.nullable(schema.string()),
|
||||
created_at: schema.string(),
|
||||
updated_at: schema.string(),
|
||||
});
|
||||
|
||||
export const queryDelaySettingsResponseSchema = schema.object({
|
||||
body: queryDelaySettingsResponseBodySchema,
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './v1';
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 type { TypeOf } from '@kbn/config-schema';
|
||||
import { queryDelaySettingsResponseSchemaV1 } from '..';
|
||||
|
||||
export type QueryDelaySettingsResponse = TypeOf<typeof queryDelaySettingsResponseSchemaV1>;
|
|
@ -20,29 +20,51 @@ export interface RulesSettingsFlappingProperties {
|
|||
export type RulesSettingsFlapping = RulesSettingsFlappingProperties &
|
||||
RulesSettingsModificationMetadata;
|
||||
|
||||
export interface RulesSettingsQueryDelayProperties {
|
||||
delay: number;
|
||||
}
|
||||
|
||||
export type RulesSettingsQueryDelay = RulesSettingsQueryDelayProperties &
|
||||
RulesSettingsModificationMetadata;
|
||||
|
||||
export interface RulesSettingsProperties {
|
||||
flapping?: RulesSettingsFlappingProperties;
|
||||
queryDelay?: RulesSettingsQueryDelayProperties;
|
||||
}
|
||||
|
||||
export interface RulesSettings {
|
||||
flapping: RulesSettingsFlapping;
|
||||
flapping?: RulesSettingsFlapping;
|
||||
queryDelay?: RulesSettingsQueryDelay;
|
||||
}
|
||||
|
||||
export const MIN_LOOK_BACK_WINDOW = 2;
|
||||
export const MAX_LOOK_BACK_WINDOW = 20;
|
||||
export const MIN_STATUS_CHANGE_THRESHOLD = 2;
|
||||
export const MAX_STATUS_CHANGE_THRESHOLD = 20;
|
||||
export const MIN_QUERY_DELAY = 0;
|
||||
export const MAX_QUERY_DELAY = 60;
|
||||
|
||||
export const RULES_SETTINGS_FEATURE_ID = 'rulesSettings';
|
||||
export const ALL_FLAPPING_SETTINGS_SUB_FEATURE_ID = 'allFlappingSettings';
|
||||
export const READ_FLAPPING_SETTINGS_SUB_FEATURE_ID = 'readFlappingSettings';
|
||||
export const ALL_QUERY_DELAY_SETTINGS_SUB_FEATURE_ID = 'allQueryDelaySettings';
|
||||
export const READ_QUERY_DELAY_SETTINGS_SUB_FEATURE_ID = 'readQueryDelaySettings';
|
||||
|
||||
export const API_PRIVILEGES = {
|
||||
READ_FLAPPING_SETTINGS: 'read-flapping-settings',
|
||||
WRITE_FLAPPING_SETTINGS: 'write-flapping-settings',
|
||||
READ_QUERY_DELAY_SETTINGS: 'read-query-delay-settings',
|
||||
WRITE_QUERY_DELAY_SETTINGS: 'write-query-delay-settings',
|
||||
};
|
||||
|
||||
export const RULES_SETTINGS_SAVED_OBJECT_TYPE = 'rules-settings';
|
||||
export const RULES_SETTINGS_SAVED_OBJECT_ID = 'rules-settings';
|
||||
export const RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID = 'rules-settings';
|
||||
export const RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID = 'query-delay-settings';
|
||||
|
||||
export const DEFAULT_LOOK_BACK_WINDOW = 20;
|
||||
export const DEFAULT_STATUS_CHANGE_THRESHOLD = 4;
|
||||
export const DEFAULT_QUERY_DELAY = 0;
|
||||
export const DEFAULT_SERVERLESS_QUERY_DELAY = 15;
|
||||
|
||||
export const DEFAULT_FLAPPING_SETTINGS: RulesSettingsFlappingProperties = {
|
||||
enabled: true,
|
||||
|
@ -54,3 +76,10 @@ export const DISABLE_FLAPPING_SETTINGS: RulesSettingsFlappingProperties = {
|
|||
...DEFAULT_FLAPPING_SETTINGS,
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
export const DEFAULT_QUERY_DELAY_SETTINGS: RulesSettingsQueryDelayProperties = {
|
||||
delay: DEFAULT_QUERY_DELAY,
|
||||
};
|
||||
export const DEFAULT_SERVERLESS_QUERY_DELAY_SETTINGS: RulesSettingsQueryDelayProperties = {
|
||||
delay: DEFAULT_SERVERLESS_QUERY_DELAY,
|
||||
};
|
||||
|
|
61
x-pack/plugins/alerting/server/lib/get_time_range.test.ts
Normal file
61
x-pack/plugins/alerting/server/lib/get_time_range.test.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { getTimeRange } from './get_time_range';
|
||||
|
||||
describe('getTimeRange', () => {
|
||||
const logger = loggingSystemMock.create().get();
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2023-10-04T00:00:00.000Z'));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('returns time range with no query delay', () => {
|
||||
const { dateStart, dateEnd } = getTimeRange(logger, { delay: 0 }, '5m');
|
||||
expect(dateStart).toBe('2023-10-03T23:55:00.000Z');
|
||||
expect(dateEnd).toBe('2023-10-04T00:00:00.000Z');
|
||||
expect(logger.debug).toHaveBeenCalledWith('Adjusting rule query time range by 0 seconds');
|
||||
});
|
||||
|
||||
test('returns time range with a query delay', () => {
|
||||
const { dateStart, dateEnd } = getTimeRange(logger, { delay: 45 }, '5m');
|
||||
expect(dateStart).toBe('2023-10-03T23:54:15.000Z');
|
||||
expect(dateEnd).toBe('2023-10-03T23:59:15.000Z');
|
||||
expect(logger.debug).toHaveBeenCalledWith('Adjusting rule query time range by 45 seconds');
|
||||
});
|
||||
|
||||
test('returns time range with no query delay and no time range', () => {
|
||||
const { dateStart, dateEnd } = getTimeRange(logger, { delay: 0 });
|
||||
expect(dateStart).toBe('2023-10-04T00:00:00.000Z');
|
||||
expect(dateEnd).toBe('2023-10-04T00:00:00.000Z');
|
||||
expect(logger.debug).toHaveBeenCalledWith('Adjusting rule query time range by 0 seconds');
|
||||
});
|
||||
|
||||
test('returns time range with a query delay and no time range', () => {
|
||||
const { dateStart, dateEnd } = getTimeRange(logger, { delay: 45 });
|
||||
expect(dateStart).toBe('2023-10-03T23:59:15.000Z');
|
||||
expect(dateEnd).toBe('2023-10-03T23:59:15.000Z');
|
||||
expect(logger.debug).toHaveBeenCalledWith('Adjusting rule query time range by 45 seconds');
|
||||
});
|
||||
|
||||
test('throws an error when the time window is invalid', () => {
|
||||
expect(() => getTimeRange(logger, { delay: 45 }, '5k')).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid format for windowSize: \\"5k\\""`
|
||||
);
|
||||
expect(logger.debug).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
40
x-pack/plugins/alerting/server/lib/get_time_range.ts
Normal file
40
x-pack/plugins/alerting/server/lib/get_time_range.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { Logger } from '@kbn/logging';
|
||||
import { parseDuration, RulesSettingsQueryDelayProperties } from '../../common';
|
||||
|
||||
export function getTimeRange(
|
||||
logger: Logger,
|
||||
queryDelaySettings: RulesSettingsQueryDelayProperties,
|
||||
window?: string
|
||||
) {
|
||||
let timeWindow: number = 0;
|
||||
if (window) {
|
||||
try {
|
||||
timeWindow = parseDuration(window);
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.alerting.invalidWindowSizeErrorMessage', {
|
||||
defaultMessage: 'Invalid format for windowSize: "{window}"',
|
||||
values: {
|
||||
window,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
logger.debug(`Adjusting rule query time range by ${queryDelaySettings.delay} seconds`);
|
||||
|
||||
const queryDelay = queryDelaySettings.delay * 1000;
|
||||
const date = Date.now();
|
||||
const dateStart = new Date(date - (timeWindow + queryDelay)).toISOString();
|
||||
const dateEnd = new Date(date - queryDelay).toISOString();
|
||||
|
||||
return { dateStart, dateEnd };
|
||||
}
|
|
@ -190,6 +190,7 @@ export interface AlertingPluginsStart {
|
|||
data: DataPluginStart;
|
||||
dataViews: DataViewsPluginStart;
|
||||
share: SharePluginStart;
|
||||
serverless?: ServerlessPluginSetup;
|
||||
}
|
||||
|
||||
export class AlertingPlugin {
|
||||
|
@ -503,6 +504,7 @@ export class AlertingPlugin {
|
|||
logger: this.logger,
|
||||
savedObjectsService: core.savedObjects,
|
||||
securityPluginStart: plugins.security,
|
||||
isServerless: !!plugins.serverless,
|
||||
});
|
||||
|
||||
maintenanceWindowClientFactory.initialize({
|
||||
|
|
|
@ -62,6 +62,8 @@ import { registerRulesValueSuggestionsRoute } from './suggestions/values_suggest
|
|||
import { registerFieldsRoute } from './suggestions/fields_rules';
|
||||
import { bulkGetMaintenanceWindowRoute } from './maintenance_window/apis/bulk_get/bulk_get_maintenance_windows_route';
|
||||
import { registerAlertsValueSuggestionsRoute } from './suggestions/values_suggestion_alerts';
|
||||
import { getQueryDelaySettingsRoute } from './rules_settings/apis/get/get_query_delay_settings';
|
||||
import { updateQueryDelaySettingsRoute } from './rules_settings/apis/update/update_query_delay_settings';
|
||||
|
||||
export interface RouteOptions {
|
||||
router: IRouter<AlertingRequestHandlerContext>;
|
||||
|
@ -133,4 +135,6 @@ export function defineRoutes(opts: RouteOptions) {
|
|||
bulkGetMaintenanceWindowRoute(router, licenseState);
|
||||
getScheduleFrequencyRoute(router, licenseState);
|
||||
bulkUntrackAlertRoute(router, licenseState);
|
||||
getQueryDelaySettingsRoute(router, licenseState);
|
||||
updateQueryDelaySettingsRoute(router, licenseState);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 { httpServiceMock } from '@kbn/core/server/mocks';
|
||||
import { licenseStateMock } from '../../../../lib/license_state.mock';
|
||||
import { mockHandlerArguments } from '../../../_mock_handler_arguments';
|
||||
import {
|
||||
rulesSettingsClientMock,
|
||||
RulesSettingsClientMock,
|
||||
} from '../../../../rules_settings_client.mock';
|
||||
import { getQueryDelaySettingsRoute } from './get_query_delay_settings';
|
||||
|
||||
let rulesSettingsClient: RulesSettingsClientMock;
|
||||
|
||||
jest.mock('../../../../lib/license_api_access', () => ({
|
||||
verifyApiAccess: jest.fn(),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
rulesSettingsClient = rulesSettingsClientMock.create();
|
||||
});
|
||||
|
||||
describe('getQueryDelaySettingsRoute', () => {
|
||||
test('gets query delay settings', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
getQueryDelaySettingsRoute(router, licenseState);
|
||||
|
||||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"options": Object {
|
||||
"tags": Array [
|
||||
"access:read-query-delay-settings",
|
||||
],
|
||||
},
|
||||
"path": "/internal/alerting/rules/settings/_query_delay",
|
||||
"validate": Object {},
|
||||
}
|
||||
`);
|
||||
|
||||
(rulesSettingsClient.queryDelay().get as jest.Mock).mockResolvedValue({
|
||||
delay: 10,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const [context, req, res] = mockHandlerArguments({ rulesSettingsClient }, {}, ['ok']);
|
||||
|
||||
await handler(context, req, res);
|
||||
|
||||
expect(rulesSettingsClient.queryDelay().get).toHaveBeenCalledTimes(1);
|
||||
expect(res.ok).toHaveBeenCalledWith({
|
||||
body: expect.objectContaining({
|
||||
delay: 10,
|
||||
created_by: 'test name',
|
||||
updated_by: 'test name',
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 '@kbn/core/server';
|
||||
import { ILicenseState } from '../../../../lib';
|
||||
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../../../types';
|
||||
import { verifyAccessAndContext } from '../../../lib';
|
||||
import { API_PRIVILEGES } from '../../../../../common';
|
||||
import { transformQueryDelaySettingsToResponseV1 } from '../../transforms';
|
||||
import { GetQueryDelaySettingsResponseV1 } from '../../../../../common/routes/rules_settings/apis/get';
|
||||
|
||||
export const getQueryDelaySettingsRoute = (
|
||||
router: IRouter<AlertingRequestHandlerContext>,
|
||||
licenseState: ILicenseState
|
||||
) => {
|
||||
router.get(
|
||||
{
|
||||
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/settings/_query_delay`,
|
||||
validate: {},
|
||||
options: {
|
||||
tags: [`access:${API_PRIVILEGES.READ_QUERY_DELAY_SETTINGS}`],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(
|
||||
verifyAccessAndContext(licenseState, async function (context, req, res) {
|
||||
const rulesSettingsClient = (await context.alerting).getRulesSettingsClient();
|
||||
const queryDelaySettings = await rulesSettingsClient.queryDelay().get();
|
||||
const response: GetQueryDelaySettingsResponseV1 =
|
||||
transformQueryDelaySettingsToResponseV1(queryDelaySettings);
|
||||
|
||||
return res.ok(response);
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 { httpServiceMock } from '@kbn/core/server/mocks';
|
||||
import { licenseStateMock } from '../../../../lib/license_state.mock';
|
||||
import { mockHandlerArguments } from '../../../_mock_handler_arguments';
|
||||
import {
|
||||
rulesSettingsClientMock,
|
||||
RulesSettingsClientMock,
|
||||
} from '../../../../rules_settings_client.mock';
|
||||
import { updateQueryDelaySettingsRoute } from './update_query_delay_settings';
|
||||
|
||||
let rulesSettingsClient: RulesSettingsClientMock;
|
||||
|
||||
jest.mock('../../../../lib/license_api_access', () => ({
|
||||
verifyApiAccess: jest.fn(),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
rulesSettingsClient = rulesSettingsClientMock.create();
|
||||
});
|
||||
|
||||
const mockQueryDelaySettings = {
|
||||
delay: 10,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
describe('updateQueryDelaySettingsRoute', () => {
|
||||
test('updates query delay settings', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
updateQueryDelaySettingsRoute(router, licenseState);
|
||||
|
||||
const [config, handler] = router.post.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rules/settings/_query_delay"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:write-query-delay-settings",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
(rulesSettingsClient.queryDelay().get as jest.Mock).mockResolvedValue(mockQueryDelaySettings);
|
||||
(rulesSettingsClient.queryDelay().update as jest.Mock).mockResolvedValue(
|
||||
mockQueryDelaySettings
|
||||
);
|
||||
|
||||
const updateResult = {
|
||||
delay: 6,
|
||||
};
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ rulesSettingsClient },
|
||||
{
|
||||
body: updateResult,
|
||||
},
|
||||
['ok']
|
||||
);
|
||||
|
||||
await handler(context, req, res);
|
||||
|
||||
expect(rulesSettingsClient.queryDelay().update).toHaveBeenCalledTimes(1);
|
||||
expect((rulesSettingsClient.queryDelay().update as jest.Mock).mock.calls[0])
|
||||
.toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"delay": 6,
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(res.ok).toHaveBeenCalledWith({
|
||||
body: expect.objectContaining({
|
||||
delay: 10,
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 '@kbn/core/server';
|
||||
import { ILicenseState } from '../../../../lib';
|
||||
import { verifyAccessAndContext } from '../../../lib';
|
||||
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../../../types';
|
||||
import { API_PRIVILEGES } from '../../../../../common';
|
||||
import {
|
||||
updateQueryDelaySettingsBodySchemaV1,
|
||||
UpdateQueryDelaySettingsRequestBodyV1,
|
||||
UpdateQueryDelaySettingsResponseV1,
|
||||
} from '../../../../../common/routes/rules_settings/apis/update';
|
||||
import { transformQueryDelaySettingsToResponseV1 } from '../../transforms';
|
||||
|
||||
export const updateQueryDelaySettingsRoute = (
|
||||
router: IRouter<AlertingRequestHandlerContext>,
|
||||
licenseState: ILicenseState
|
||||
) => {
|
||||
router.post(
|
||||
{
|
||||
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/settings/_query_delay`,
|
||||
validate: {
|
||||
body: updateQueryDelaySettingsBodySchemaV1,
|
||||
},
|
||||
options: {
|
||||
tags: [`access:${API_PRIVILEGES.WRITE_QUERY_DELAY_SETTINGS}`],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(
|
||||
verifyAccessAndContext(licenseState, async function (context, req, res) {
|
||||
const rulesSettingsClient = (await context.alerting).getRulesSettingsClient();
|
||||
|
||||
const body: UpdateQueryDelaySettingsRequestBodyV1 = req.body;
|
||||
|
||||
const updatedQueryDelaySettings = await rulesSettingsClient.queryDelay().update(body);
|
||||
|
||||
const response: UpdateQueryDelaySettingsResponseV1 =
|
||||
transformQueryDelaySettingsToResponseV1(updatedQueryDelaySettings);
|
||||
|
||||
return res.ok(response);
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { transformQueryDelaySettingsToResponse } from './transform_query_delay_settings_to_response/latest';
|
||||
|
||||
export { transformQueryDelaySettingsToResponse as transformQueryDelaySettingsToResponseV1 } from './transform_query_delay_settings_to_response/v1';
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './v1';
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { RulesSettingsQueryDelay } from '../../../../../common';
|
||||
import { QueryDelaySettingsResponseV1 } from '../../../../../common/routes/rules_settings/response';
|
||||
|
||||
export const transformQueryDelaySettingsToResponse = (
|
||||
settings: RulesSettingsQueryDelay
|
||||
): QueryDelaySettingsResponseV1 => {
|
||||
return {
|
||||
body: {
|
||||
delay: settings.delay,
|
||||
created_by: settings.createdBy,
|
||||
updated_by: settings.updatedBy,
|
||||
created_at: settings.createdAt,
|
||||
updated_at: settings.updatedAt,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -8,11 +8,14 @@
|
|||
import {
|
||||
RulesSettingsClientApi,
|
||||
RulesSettingsFlappingClientApi,
|
||||
RulesSettingsQueryDelayClientApi,
|
||||
DEFAULT_FLAPPING_SETTINGS,
|
||||
DEFAULT_QUERY_DELAY_SETTINGS,
|
||||
} from './types';
|
||||
|
||||
export type RulesSettingsClientMock = jest.Mocked<RulesSettingsClientApi>;
|
||||
export type RulesSettingsFlappingClientMock = jest.Mocked<RulesSettingsFlappingClientApi>;
|
||||
export type RulesSettingsQueryDelayClientMock = jest.Mocked<RulesSettingsQueryDelayClientApi>;
|
||||
|
||||
// Warning: Becareful when resetting all mocks in tests as it would clear
|
||||
// the mock return value on the flapping
|
||||
|
@ -20,11 +23,18 @@ const createRulesSettingsClientMock = () => {
|
|||
const flappingMocked: RulesSettingsFlappingClientMock = {
|
||||
get: jest.fn().mockReturnValue(DEFAULT_FLAPPING_SETTINGS),
|
||||
update: jest.fn(),
|
||||
getSettings: jest.fn(),
|
||||
createSettings: jest.fn(),
|
||||
};
|
||||
const queryDelayMocked: RulesSettingsQueryDelayClientMock = {
|
||||
get: jest.fn().mockReturnValue(DEFAULT_QUERY_DELAY_SETTINGS),
|
||||
update: jest.fn(),
|
||||
getSettings: jest.fn(),
|
||||
createSettings: jest.fn(),
|
||||
};
|
||||
const mocked: RulesSettingsClientMock = {
|
||||
get: jest.fn(),
|
||||
create: jest.fn(),
|
||||
flapping: jest.fn().mockReturnValue(flappingMocked),
|
||||
queryDelay: jest.fn().mockReturnValue(queryDelayMocked),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
|
|
@ -13,10 +13,11 @@ import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mock
|
|||
import {
|
||||
RULES_SETTINGS_FEATURE_ID,
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_SAVED_OBJECT_ID,
|
||||
RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID,
|
||||
DEFAULT_FLAPPING_SETTINGS,
|
||||
RulesSettings,
|
||||
} from '../../../common';
|
||||
import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server';
|
||||
|
||||
const mockDateString = '2019-02-12T21:01:22.479Z';
|
||||
|
||||
|
@ -39,13 +40,6 @@ const getMockRulesSettings = (): RulesSettings => {
|
|||
const rulesSettingsFlappingClientParams: jest.Mocked<RulesSettingsFlappingClientConstructorOptions> =
|
||||
{
|
||||
logger: loggingSystemMock.create().get(),
|
||||
getOrCreate: jest.fn().mockReturnValue({
|
||||
id: RULES_SETTINGS_FEATURE_ID,
|
||||
type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
attributes: getMockRulesSettings(),
|
||||
references: [],
|
||||
version: '123',
|
||||
}),
|
||||
getModificationMetadata: jest.fn(),
|
||||
savedObjectsClient,
|
||||
};
|
||||
|
@ -58,9 +52,21 @@ const updatedMetadata = {
|
|||
};
|
||||
|
||||
describe('RulesSettingsFlappingClient', () => {
|
||||
beforeEach(() =>
|
||||
rulesSettingsFlappingClientParams.getModificationMetadata.mockResolvedValue(updatedMetadata)
|
||||
);
|
||||
beforeEach(() => {
|
||||
rulesSettingsFlappingClientParams.getModificationMetadata.mockResolvedValue(updatedMetadata);
|
||||
savedObjectsClient.get.mockResolvedValue({
|
||||
id: RULES_SETTINGS_FEATURE_ID,
|
||||
type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
attributes: getMockRulesSettings(),
|
||||
references: [],
|
||||
version: '123',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date(mockDateString));
|
||||
|
@ -119,7 +125,7 @@ describe('RulesSettingsFlappingClient', () => {
|
|||
|
||||
expect(savedObjectsClient.update).toHaveBeenCalledWith(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_SAVED_OBJECT_ID,
|
||||
RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID,
|
||||
{
|
||||
flapping: expect.objectContaining({
|
||||
enabled: false,
|
||||
|
@ -192,4 +198,243 @@ describe('RulesSettingsFlappingClient', () => {
|
|||
'Invalid values,lookBackWindow (10) must be equal to or greater than statusChangeThreshold (20).'
|
||||
);
|
||||
});
|
||||
|
||||
test('can create a new flapping settings saved object', async () => {
|
||||
rulesSettingsFlappingClientParams.getModificationMetadata.mockResolvedValueOnce({
|
||||
...updatedMetadata,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
});
|
||||
const client = new RulesSettingsFlappingClient(rulesSettingsFlappingClientParams);
|
||||
const mockAttributes = getMockRulesSettings();
|
||||
|
||||
savedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: RULES_SETTINGS_FEATURE_ID,
|
||||
type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
attributes: mockAttributes,
|
||||
references: [],
|
||||
});
|
||||
|
||||
const result = await client.createSettings();
|
||||
|
||||
expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.create).toHaveBeenCalledWith(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
{
|
||||
flapping: expect.objectContaining({
|
||||
enabled: mockAttributes.flapping?.enabled,
|
||||
lookBackWindow: mockAttributes.flapping?.lookBackWindow,
|
||||
statusChangeThreshold: mockAttributes.flapping?.statusChangeThreshold,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID,
|
||||
overwrite: true,
|
||||
}
|
||||
);
|
||||
expect(result.attributes).toEqual(mockAttributes);
|
||||
});
|
||||
|
||||
test('can get existing flapping settings saved object', async () => {
|
||||
const client = new RulesSettingsFlappingClient(rulesSettingsFlappingClientParams);
|
||||
const mockAttributes = getMockRulesSettings();
|
||||
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: RULES_SETTINGS_FEATURE_ID,
|
||||
type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
attributes: mockAttributes,
|
||||
references: [],
|
||||
});
|
||||
const result = await client.getSettings();
|
||||
expect(result.attributes).toEqual(mockAttributes);
|
||||
});
|
||||
|
||||
test('throws if there is no existing saved object to get', async () => {
|
||||
const client = new RulesSettingsFlappingClient(rulesSettingsFlappingClientParams);
|
||||
|
||||
savedObjectsClient.get.mockRejectedValueOnce(
|
||||
SavedObjectsErrorHelpers.createGenericNotFoundError(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID
|
||||
)
|
||||
);
|
||||
await expect(client.getSettings()).rejects.toThrowError();
|
||||
});
|
||||
|
||||
test('can persist flapping settings when saved object does not exist', async () => {
|
||||
rulesSettingsFlappingClientParams.getModificationMetadata.mockResolvedValueOnce({
|
||||
...updatedMetadata,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
});
|
||||
const client = new RulesSettingsFlappingClient(rulesSettingsFlappingClientParams);
|
||||
const mockAttributes = getMockRulesSettings();
|
||||
savedObjectsClient.get.mockRejectedValueOnce(
|
||||
SavedObjectsErrorHelpers.createGenericNotFoundError(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID
|
||||
)
|
||||
);
|
||||
|
||||
savedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: RULES_SETTINGS_FEATURE_ID,
|
||||
type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
attributes: mockAttributes,
|
||||
references: [],
|
||||
});
|
||||
|
||||
const result = await client.get();
|
||||
|
||||
expect(savedObjectsClient.get).toHaveBeenCalledWith(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID
|
||||
);
|
||||
|
||||
expect(savedObjectsClient.create).toHaveBeenCalledWith(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
{
|
||||
flapping: expect.objectContaining({
|
||||
enabled: mockAttributes.flapping?.enabled,
|
||||
lookBackWindow: mockAttributes.flapping?.lookBackWindow,
|
||||
statusChangeThreshold: mockAttributes.flapping?.statusChangeThreshold,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID,
|
||||
overwrite: true,
|
||||
}
|
||||
);
|
||||
expect(result).toEqual(mockAttributes.flapping);
|
||||
});
|
||||
|
||||
test('can persist flapping settings when saved object already exists', async () => {
|
||||
rulesSettingsFlappingClientParams.getModificationMetadata.mockResolvedValueOnce({
|
||||
...updatedMetadata,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
});
|
||||
const client = new RulesSettingsFlappingClient(rulesSettingsFlappingClientParams);
|
||||
const mockAttributes = getMockRulesSettings();
|
||||
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: RULES_SETTINGS_FEATURE_ID,
|
||||
type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
attributes: mockAttributes,
|
||||
references: [],
|
||||
});
|
||||
|
||||
const result = await client.get();
|
||||
|
||||
expect(savedObjectsClient.get).toHaveBeenCalledWith(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID
|
||||
);
|
||||
expect(savedObjectsClient.create).not.toHaveBeenCalled();
|
||||
expect(result).toEqual(mockAttributes.flapping);
|
||||
});
|
||||
|
||||
test('can update flapping settings when saved object does not exist', async () => {
|
||||
rulesSettingsFlappingClientParams.getModificationMetadata.mockResolvedValueOnce({
|
||||
...updatedMetadata,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
});
|
||||
const client = new RulesSettingsFlappingClient(rulesSettingsFlappingClientParams);
|
||||
const mockAttributes = getMockRulesSettings();
|
||||
|
||||
savedObjectsClient.get.mockRejectedValueOnce(
|
||||
SavedObjectsErrorHelpers.createGenericNotFoundError(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID
|
||||
)
|
||||
);
|
||||
|
||||
const mockResolve = {
|
||||
id: RULES_SETTINGS_FEATURE_ID,
|
||||
type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
attributes: mockAttributes,
|
||||
references: [],
|
||||
version: '123',
|
||||
};
|
||||
|
||||
savedObjectsClient.create.mockResolvedValueOnce(mockResolve);
|
||||
savedObjectsClient.update.mockResolvedValueOnce({
|
||||
...mockResolve,
|
||||
attributes: {
|
||||
flapping: {
|
||||
...mockResolve.attributes.flapping,
|
||||
enabled: false,
|
||||
lookBackWindow: 5,
|
||||
statusChangeThreshold: 5,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Try to update with new values
|
||||
const result = await client.update({
|
||||
enabled: false,
|
||||
lookBackWindow: 5,
|
||||
statusChangeThreshold: 5,
|
||||
});
|
||||
|
||||
// Tried to get first, but no results
|
||||
expect(savedObjectsClient.get).toHaveBeenCalledWith(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID
|
||||
);
|
||||
|
||||
// So create a new entry
|
||||
expect(savedObjectsClient.create).toHaveBeenCalledWith(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
{
|
||||
flapping: expect.objectContaining({
|
||||
enabled: mockAttributes.flapping?.enabled,
|
||||
lookBackWindow: mockAttributes.flapping?.lookBackWindow,
|
||||
statusChangeThreshold: mockAttributes.flapping?.statusChangeThreshold,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID,
|
||||
overwrite: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Try to update with version
|
||||
expect(savedObjectsClient.update).toHaveBeenCalledWith(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID,
|
||||
{
|
||||
flapping: expect.objectContaining({
|
||||
enabled: false,
|
||||
lookBackWindow: 5,
|
||||
statusChangeThreshold: 5,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
}),
|
||||
},
|
||||
{ version: '123' }
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
enabled: false,
|
||||
lookBackWindow: 5,
|
||||
statusChangeThreshold: 5,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,12 @@
|
|||
*/
|
||||
|
||||
import Boom from '@hapi/boom';
|
||||
import { Logger, SavedObjectsClientContract, SavedObject } from '@kbn/core/server';
|
||||
import {
|
||||
Logger,
|
||||
SavedObjectsClientContract,
|
||||
SavedObject,
|
||||
SavedObjectsErrorHelpers,
|
||||
} from '@kbn/core/server';
|
||||
import {
|
||||
RulesSettings,
|
||||
RulesSettingsFlapping,
|
||||
|
@ -17,8 +22,11 @@ import {
|
|||
MIN_STATUS_CHANGE_THRESHOLD,
|
||||
MAX_STATUS_CHANGE_THRESHOLD,
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_SAVED_OBJECT_ID,
|
||||
RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID,
|
||||
DEFAULT_FLAPPING_SETTINGS,
|
||||
} from '../../../common';
|
||||
import { retryIfConflicts } from '../../lib/retry_if_conflicts';
|
||||
import { flappingSchema } from '../schemas';
|
||||
|
||||
const verifyFlappingSettings = (flappingSettings: RulesSettingsFlappingProperties) => {
|
||||
const { lookBackWindow, statusChangeThreshold } = flappingSettings;
|
||||
|
@ -48,30 +56,42 @@ const verifyFlappingSettings = (flappingSettings: RulesSettingsFlappingPropertie
|
|||
export interface RulesSettingsFlappingClientConstructorOptions {
|
||||
readonly logger: Logger;
|
||||
readonly savedObjectsClient: SavedObjectsClientContract;
|
||||
readonly getOrCreate: () => Promise<SavedObject<RulesSettings>>;
|
||||
readonly getModificationMetadata: () => Promise<RulesSettingsModificationMetadata>;
|
||||
}
|
||||
|
||||
export class RulesSettingsFlappingClient {
|
||||
private readonly logger: Logger;
|
||||
private readonly savedObjectsClient: SavedObjectsClientContract;
|
||||
private readonly getOrCreate: () => Promise<SavedObject<RulesSettings>>;
|
||||
private readonly getModificationMetadata: () => Promise<RulesSettingsModificationMetadata>;
|
||||
|
||||
constructor(options: RulesSettingsFlappingClientConstructorOptions) {
|
||||
this.logger = options.logger;
|
||||
this.savedObjectsClient = options.savedObjectsClient;
|
||||
this.getOrCreate = options.getOrCreate;
|
||||
this.getModificationMetadata = options.getModificationMetadata;
|
||||
}
|
||||
|
||||
public async get(): Promise<RulesSettingsFlapping> {
|
||||
const rulesSettings = await this.getOrCreate();
|
||||
if (!rulesSettings.attributes.flapping) {
|
||||
this.logger.error('Failed to get flapping rules setting for current space.');
|
||||
throw new Error(
|
||||
'Failed to get flapping rules setting for current space. Flapping settings are undefined'
|
||||
);
|
||||
}
|
||||
return rulesSettings.attributes.flapping;
|
||||
}
|
||||
|
||||
public async update(newFlappingProperties: RulesSettingsFlappingProperties) {
|
||||
return await retryIfConflicts(
|
||||
this.logger,
|
||||
'ruleSettingsClient.flapping.update()',
|
||||
async () => await this.updateWithOCC(newFlappingProperties)
|
||||
);
|
||||
}
|
||||
|
||||
private async updateWithOCC(newFlappingProperties: RulesSettingsFlappingProperties) {
|
||||
try {
|
||||
flappingSchema.validate(newFlappingProperties);
|
||||
verifyFlappingSettings(newFlappingProperties);
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
|
@ -81,14 +101,16 @@ export class RulesSettingsFlappingClient {
|
|||
}
|
||||
|
||||
const { attributes, version } = await this.getOrCreate();
|
||||
const modificationMetadata = await this.getModificationMetadata();
|
||||
if (!attributes.flapping) {
|
||||
throw new Error('Flapping settings are undefined');
|
||||
}
|
||||
|
||||
const modificationMetadata = await this.getModificationMetadata();
|
||||
try {
|
||||
const result = await this.savedObjectsClient.update(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_SAVED_OBJECT_ID,
|
||||
RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID,
|
||||
{
|
||||
...attributes,
|
||||
flapping: {
|
||||
...attributes.flapping,
|
||||
...newFlappingProperties,
|
||||
|
@ -107,4 +129,55 @@ export class RulesSettingsFlappingClient {
|
|||
throw Boom.boomify(e, { message: errorMessage });
|
||||
}
|
||||
}
|
||||
|
||||
public async getSettings(): Promise<SavedObject<RulesSettings>> {
|
||||
try {
|
||||
return await this.savedObjectsClient.get<RulesSettings>(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`Failed to get flapping rules setting for current space. Error: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async createSettings(): Promise<SavedObject<RulesSettings>> {
|
||||
const modificationMetadata = await this.getModificationMetadata();
|
||||
try {
|
||||
return await this.savedObjectsClient.create<RulesSettings>(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
{
|
||||
flapping: {
|
||||
...DEFAULT_FLAPPING_SETTINGS,
|
||||
...modificationMetadata,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID,
|
||||
overwrite: true,
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`Failed to create flapping rules setting for current space. Error: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to ensure that a rules-settings saved object always exists.
|
||||
* Ensures the creation of the saved object is done lazily during retrieval.
|
||||
*/
|
||||
private async getOrCreate(): Promise<SavedObject<RulesSettings>> {
|
||||
try {
|
||||
return await this.getSettings();
|
||||
} catch (e) {
|
||||
if (SavedObjectsErrorHelpers.isNotFoundError(e)) {
|
||||
this.logger.info('Creating new default flapping rules settings for current space.');
|
||||
return await this.createSettings();
|
||||
}
|
||||
this.logger.error(`Failed to get flapping rules setting for current space. Error: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,3 +7,4 @@
|
|||
|
||||
export * from './rules_settings_client';
|
||||
export * from './flapping/rules_settings_flapping_client';
|
||||
export * from './query_delay/rules_settings_query_delay_client';
|
||||
|
|
|
@ -0,0 +1,385 @@
|
|||
/*
|
||||
* 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 { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server';
|
||||
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import {
|
||||
RULES_SETTINGS_FEATURE_ID,
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID,
|
||||
RulesSettings,
|
||||
DEFAULT_QUERY_DELAY_SETTINGS,
|
||||
} from '../../../common';
|
||||
import {
|
||||
RulesSettingsQueryDelayClient,
|
||||
RulesSettingsQueryDelayClientConstructorOptions,
|
||||
} from './rules_settings_query_delay_client';
|
||||
|
||||
const mockDateString = '2019-02-12T21:01:22.479Z';
|
||||
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
|
||||
const getMockRulesSettings = (): RulesSettings => {
|
||||
return {
|
||||
queryDelay: {
|
||||
delay: DEFAULT_QUERY_DELAY_SETTINGS.delay,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
createdAt: '2023-03-24T00:00:00.000Z',
|
||||
updatedAt: '2023-03-24T00:00:00.000Z',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const rulesSettingsQueryDelayClientParams: jest.Mocked<RulesSettingsQueryDelayClientConstructorOptions> =
|
||||
{
|
||||
logger: loggingSystemMock.create().get(),
|
||||
isServerless: false,
|
||||
getModificationMetadata: jest.fn(),
|
||||
savedObjectsClient,
|
||||
};
|
||||
|
||||
const updatedMetadata = {
|
||||
createdAt: '2023-03-26T00:00:00.000Z',
|
||||
updatedAt: '2023-03-26T00:00:00.000Z',
|
||||
createdBy: 'updated-user',
|
||||
updatedBy: 'updated-user',
|
||||
};
|
||||
|
||||
describe('RulesSettingsQueryDelayClient', () => {
|
||||
beforeEach(() => {
|
||||
rulesSettingsQueryDelayClientParams.getModificationMetadata.mockResolvedValue(updatedMetadata);
|
||||
savedObjectsClient.get.mockResolvedValue({
|
||||
id: RULES_SETTINGS_FEATURE_ID,
|
||||
type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
attributes: getMockRulesSettings(),
|
||||
references: [],
|
||||
version: '123',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date(mockDateString));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('can get query delay settings', async () => {
|
||||
const client = new RulesSettingsQueryDelayClient(rulesSettingsQueryDelayClientParams);
|
||||
const result = await client.get();
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
delay: DEFAULT_QUERY_DELAY_SETTINGS.delay,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('can update query delay settings', async () => {
|
||||
const client = new RulesSettingsQueryDelayClient(rulesSettingsQueryDelayClientParams);
|
||||
|
||||
const mockResolve = {
|
||||
id: RULES_SETTINGS_FEATURE_ID,
|
||||
type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
attributes: getMockRulesSettings(),
|
||||
references: [],
|
||||
version: '123',
|
||||
};
|
||||
|
||||
savedObjectsClient.update.mockResolvedValueOnce({
|
||||
...mockResolve,
|
||||
attributes: {
|
||||
queryDelay: {
|
||||
...mockResolve.attributes.queryDelay,
|
||||
delay: 19,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await client.update({
|
||||
delay: 19,
|
||||
});
|
||||
|
||||
expect(savedObjectsClient.update).toHaveBeenCalledWith(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID,
|
||||
{
|
||||
queryDelay: expect.objectContaining({
|
||||
delay: 19,
|
||||
updatedAt: '2023-03-26T00:00:00.000Z',
|
||||
updatedBy: 'updated-user',
|
||||
createdBy: 'test name',
|
||||
createdAt: '2023-03-24T00:00:00.000Z',
|
||||
}),
|
||||
},
|
||||
{ version: '123' }
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
delay: 19,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('throws if savedObjectsClient failed to update', async () => {
|
||||
const client = new RulesSettingsQueryDelayClient(rulesSettingsQueryDelayClientParams);
|
||||
savedObjectsClient.update.mockRejectedValueOnce(new Error('failed!!'));
|
||||
|
||||
await expect(
|
||||
client.update({
|
||||
delay: 19,
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
'savedObjectsClient errored trying to update query delay settings: failed!!'
|
||||
);
|
||||
});
|
||||
|
||||
test('throws if new query delay setting fails verification', async () => {
|
||||
const client = new RulesSettingsQueryDelayClient(rulesSettingsQueryDelayClientParams);
|
||||
await expect(
|
||||
client.update({
|
||||
delay: 200,
|
||||
})
|
||||
).rejects.toThrowError('Invalid query delay value, must be between 0 and 60, but got: 200.');
|
||||
});
|
||||
|
||||
test('can create a new query delay settings saved object', async () => {
|
||||
rulesSettingsQueryDelayClientParams.getModificationMetadata.mockResolvedValueOnce({
|
||||
...updatedMetadata,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
});
|
||||
const client = new RulesSettingsQueryDelayClient(rulesSettingsQueryDelayClientParams);
|
||||
const mockAttributes = getMockRulesSettings();
|
||||
|
||||
savedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: RULES_SETTINGS_FEATURE_ID,
|
||||
type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
attributes: mockAttributes,
|
||||
references: [],
|
||||
});
|
||||
|
||||
const result = await client.createSettings();
|
||||
|
||||
expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.create).toHaveBeenCalledWith(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
{
|
||||
queryDelay: expect.objectContaining({
|
||||
delay: 0,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID,
|
||||
overwrite: true,
|
||||
}
|
||||
);
|
||||
expect(result.attributes).toEqual(mockAttributes);
|
||||
});
|
||||
|
||||
test('can create a new query delay settings saved object with default serverless value', async () => {
|
||||
rulesSettingsQueryDelayClientParams.getModificationMetadata.mockResolvedValueOnce({
|
||||
...updatedMetadata,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
});
|
||||
const client = new RulesSettingsQueryDelayClient({
|
||||
...rulesSettingsQueryDelayClientParams,
|
||||
isServerless: true,
|
||||
});
|
||||
|
||||
const mockAttributes = getMockRulesSettings();
|
||||
|
||||
savedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: RULES_SETTINGS_FEATURE_ID,
|
||||
type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
attributes: mockAttributes,
|
||||
references: [],
|
||||
});
|
||||
|
||||
const result = await client.createSettings();
|
||||
|
||||
expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.create).toHaveBeenCalledWith(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
{
|
||||
queryDelay: expect.objectContaining({
|
||||
delay: 15,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID,
|
||||
overwrite: true,
|
||||
}
|
||||
);
|
||||
expect(result.attributes).toEqual(mockAttributes);
|
||||
});
|
||||
|
||||
test('can get existing query delay settings saved object', async () => {
|
||||
const client = new RulesSettingsQueryDelayClient(rulesSettingsQueryDelayClientParams);
|
||||
const mockAttributes = getMockRulesSettings();
|
||||
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: RULES_SETTINGS_FEATURE_ID,
|
||||
type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
attributes: mockAttributes,
|
||||
references: [],
|
||||
});
|
||||
const result = await client.getSettings();
|
||||
expect(result.attributes).toEqual(mockAttributes);
|
||||
});
|
||||
|
||||
test('throws if there is no existing saved object to get', async () => {
|
||||
const client = new RulesSettingsQueryDelayClient(rulesSettingsQueryDelayClientParams);
|
||||
|
||||
savedObjectsClient.get.mockRejectedValueOnce(
|
||||
SavedObjectsErrorHelpers.createGenericNotFoundError(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID
|
||||
)
|
||||
);
|
||||
await expect(client.get()).rejects.toThrowError();
|
||||
});
|
||||
|
||||
test('can persist query delay settings when saved object already exists', async () => {
|
||||
rulesSettingsQueryDelayClientParams.getModificationMetadata.mockResolvedValueOnce({
|
||||
...updatedMetadata,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
});
|
||||
const client = new RulesSettingsQueryDelayClient(rulesSettingsQueryDelayClientParams);
|
||||
const mockAttributes = getMockRulesSettings();
|
||||
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: RULES_SETTINGS_FEATURE_ID,
|
||||
type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
attributes: mockAttributes,
|
||||
references: [],
|
||||
});
|
||||
|
||||
const result = await client.get();
|
||||
|
||||
expect(savedObjectsClient.get).toHaveBeenCalledWith(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID
|
||||
);
|
||||
expect(savedObjectsClient.create).not.toHaveBeenCalled();
|
||||
expect(result).toEqual(mockAttributes.queryDelay);
|
||||
});
|
||||
|
||||
test('can update query delay settings when saved object does not exist', async () => {
|
||||
rulesSettingsQueryDelayClientParams.getModificationMetadata.mockResolvedValueOnce({
|
||||
...updatedMetadata,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
});
|
||||
const client = new RulesSettingsQueryDelayClient(rulesSettingsQueryDelayClientParams);
|
||||
const mockAttributes = getMockRulesSettings();
|
||||
|
||||
savedObjectsClient.get.mockRejectedValueOnce(
|
||||
SavedObjectsErrorHelpers.createGenericNotFoundError(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID
|
||||
)
|
||||
);
|
||||
|
||||
const mockResolve = {
|
||||
id: RULES_SETTINGS_FEATURE_ID,
|
||||
type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
attributes: mockAttributes,
|
||||
references: [],
|
||||
version: '123',
|
||||
};
|
||||
|
||||
savedObjectsClient.create.mockResolvedValueOnce(mockResolve);
|
||||
savedObjectsClient.update.mockResolvedValueOnce({
|
||||
...mockResolve,
|
||||
attributes: {
|
||||
queryDelay: {
|
||||
...mockResolve.attributes.queryDelay,
|
||||
delay: 5,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Try to update with new values
|
||||
const result = await client.update({
|
||||
delay: 5,
|
||||
});
|
||||
|
||||
// Tried to get first, but no results
|
||||
expect(savedObjectsClient.get).toHaveBeenCalledWith(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID
|
||||
);
|
||||
|
||||
// So create a new entry
|
||||
expect(savedObjectsClient.create).toHaveBeenCalledWith(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
{
|
||||
queryDelay: expect.objectContaining({
|
||||
delay: mockAttributes.queryDelay?.delay,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID,
|
||||
overwrite: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Try to update with version
|
||||
expect(savedObjectsClient.update).toHaveBeenCalledWith(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID,
|
||||
{
|
||||
queryDelay: expect.objectContaining({
|
||||
delay: 5,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
}),
|
||||
},
|
||||
{ version: '123' }
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
delay: 5,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* 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 Boom from '@hapi/boom';
|
||||
import {
|
||||
Logger,
|
||||
SavedObjectsClientContract,
|
||||
SavedObject,
|
||||
SavedObjectsErrorHelpers,
|
||||
} from '@kbn/core/server';
|
||||
import {
|
||||
RulesSettings,
|
||||
RulesSettingsModificationMetadata,
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID,
|
||||
RulesSettingsQueryDelayProperties,
|
||||
MIN_QUERY_DELAY,
|
||||
MAX_QUERY_DELAY,
|
||||
RulesSettingsQueryDelay,
|
||||
DEFAULT_SERVERLESS_QUERY_DELAY_SETTINGS,
|
||||
DEFAULT_QUERY_DELAY_SETTINGS,
|
||||
} from '../../../common';
|
||||
import { retryIfConflicts } from '../../lib/retry_if_conflicts';
|
||||
import { queryDelaySchema } from '../schemas';
|
||||
|
||||
const verifyQueryDelaySettings = (settings: RulesSettingsQueryDelayProperties) => {
|
||||
const { delay } = settings;
|
||||
|
||||
if (delay < MIN_QUERY_DELAY || delay > MAX_QUERY_DELAY) {
|
||||
throw Boom.badRequest(
|
||||
`Invalid query delay value, must be between ${MIN_QUERY_DELAY} and ${MAX_QUERY_DELAY}, but got: ${delay}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export interface RulesSettingsQueryDelayClientConstructorOptions {
|
||||
readonly logger: Logger;
|
||||
readonly savedObjectsClient: SavedObjectsClientContract;
|
||||
readonly isServerless: boolean;
|
||||
readonly getModificationMetadata: () => Promise<RulesSettingsModificationMetadata>;
|
||||
}
|
||||
|
||||
export class RulesSettingsQueryDelayClient {
|
||||
private readonly logger: Logger;
|
||||
private readonly savedObjectsClient: SavedObjectsClientContract;
|
||||
private readonly isServerless: boolean;
|
||||
private readonly getModificationMetadata: () => Promise<RulesSettingsModificationMetadata>;
|
||||
|
||||
constructor(options: RulesSettingsQueryDelayClientConstructorOptions) {
|
||||
this.logger = options.logger;
|
||||
this.savedObjectsClient = options.savedObjectsClient;
|
||||
this.isServerless = options.isServerless;
|
||||
this.getModificationMetadata = options.getModificationMetadata;
|
||||
}
|
||||
|
||||
public async get(): Promise<RulesSettingsQueryDelay> {
|
||||
const rulesSettings = await this.getOrCreate();
|
||||
if (!rulesSettings.attributes.queryDelay) {
|
||||
this.logger.error('Failed to get query delay rules setting for current space.');
|
||||
throw new Error(
|
||||
'Failed to get query delay rules setting for current space. Query delay settings are undefined'
|
||||
);
|
||||
}
|
||||
return rulesSettings.attributes.queryDelay;
|
||||
}
|
||||
|
||||
public async update(newQueryDelayProperties: RulesSettingsQueryDelayProperties) {
|
||||
return await retryIfConflicts(
|
||||
this.logger,
|
||||
'ruleSettingsClient.queryDelay.update()',
|
||||
async () => await this.updateWithOCC(newQueryDelayProperties)
|
||||
);
|
||||
}
|
||||
|
||||
private async updateWithOCC(newQueryDelayProperties: RulesSettingsQueryDelayProperties) {
|
||||
try {
|
||||
queryDelaySchema.validate(newQueryDelayProperties);
|
||||
verifyQueryDelaySettings(newQueryDelayProperties);
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
`Failed to verify new query delay settings properties when updating. Error: ${e}`
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
|
||||
const { attributes, version } = await this.getOrCreate();
|
||||
if (!attributes.queryDelay) {
|
||||
throw new Error('Query delay settings are undefined');
|
||||
}
|
||||
|
||||
const modificationMetadata = await this.getModificationMetadata();
|
||||
try {
|
||||
const result = await this.savedObjectsClient.update(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID,
|
||||
{
|
||||
queryDelay: {
|
||||
...attributes.queryDelay,
|
||||
...newQueryDelayProperties,
|
||||
updatedAt: modificationMetadata.updatedAt,
|
||||
updatedBy: modificationMetadata.updatedBy,
|
||||
},
|
||||
},
|
||||
{
|
||||
version,
|
||||
}
|
||||
);
|
||||
|
||||
if (!result.attributes.queryDelay) {
|
||||
throw new Error('Query delay settings are undefined');
|
||||
}
|
||||
return result.attributes.queryDelay;
|
||||
} catch (e) {
|
||||
const errorMessage = 'savedObjectsClient errored trying to update query delay settings';
|
||||
this.logger.error(`${errorMessage}: ${e}`);
|
||||
throw Boom.boomify(e, { message: errorMessage });
|
||||
}
|
||||
}
|
||||
|
||||
public async getSettings(): Promise<SavedObject<RulesSettings>> {
|
||||
try {
|
||||
return await this.savedObjectsClient.get<RulesSettings>(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`Failed to get query delay rules setting for current space. Error: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async createSettings(): Promise<SavedObject<RulesSettings>> {
|
||||
const modificationMetadata = await this.getModificationMetadata();
|
||||
const defaultQueryDelaySettings = this.isServerless
|
||||
? DEFAULT_SERVERLESS_QUERY_DELAY_SETTINGS
|
||||
: DEFAULT_QUERY_DELAY_SETTINGS;
|
||||
try {
|
||||
return await this.savedObjectsClient.create<RulesSettings>(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
{
|
||||
queryDelay: {
|
||||
...defaultQueryDelaySettings,
|
||||
...modificationMetadata,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID,
|
||||
overwrite: true,
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
`Failed to create query delay rules setting for current space. Error: ${e}`
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to ensure that a rules-settings saved object always exists.
|
||||
* Ensures the creation of the saved object is done lazily during retrieval.
|
||||
*/
|
||||
private async getOrCreate(): Promise<SavedObject<RulesSettings>> {
|
||||
try {
|
||||
return await this.getSettings();
|
||||
} catch (e) {
|
||||
if (SavedObjectsErrorHelpers.isNotFoundError(e)) {
|
||||
this.logger.info('Creating new default query delay rules settings for current space.');
|
||||
return await this.createSettings();
|
||||
}
|
||||
this.logger.error(`Failed to get query delay rules setting for current space. Error: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,16 +11,7 @@ import {
|
|||
} from './rules_settings_client';
|
||||
import { RulesSettingsFlappingClient } from './flapping/rules_settings_flapping_client';
|
||||
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
import {
|
||||
RULES_SETTINGS_FEATURE_ID,
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_SAVED_OBJECT_ID,
|
||||
DEFAULT_FLAPPING_SETTINGS,
|
||||
RulesSettings,
|
||||
} from '../../common';
|
||||
|
||||
const mockDateString = '2019-02-12T21:01:22.479Z';
|
||||
import { RulesSettingsQueryDelayClient } from './query_delay/rules_settings_query_delay_client';
|
||||
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
|
||||
|
@ -28,258 +19,17 @@ const rulesSettingsClientParams: jest.Mocked<RulesSettingsClientConstructorOptio
|
|||
logger: loggingSystemMock.create().get(),
|
||||
getUserName: jest.fn(),
|
||||
savedObjectsClient,
|
||||
};
|
||||
|
||||
const getMockRulesSettings = (): RulesSettings => {
|
||||
return {
|
||||
flapping: {
|
||||
enabled: DEFAULT_FLAPPING_SETTINGS.enabled,
|
||||
lookBackWindow: DEFAULT_FLAPPING_SETTINGS.lookBackWindow,
|
||||
statusChangeThreshold: DEFAULT_FLAPPING_SETTINGS.statusChangeThreshold,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
isServerless: false,
|
||||
};
|
||||
|
||||
describe('RulesSettingsClient', () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date(mockDateString));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
rulesSettingsClientParams.getUserName.mockResolvedValue('test name');
|
||||
});
|
||||
|
||||
test('can initialize correctly', async () => {
|
||||
const client = new RulesSettingsClient(rulesSettingsClientParams);
|
||||
expect(client.flapping()).toEqual(expect.any(RulesSettingsFlappingClient));
|
||||
});
|
||||
|
||||
test('can create a new rules settings saved object', async () => {
|
||||
const client = new RulesSettingsClient(rulesSettingsClientParams);
|
||||
const mockAttributes = getMockRulesSettings();
|
||||
|
||||
savedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: RULES_SETTINGS_FEATURE_ID,
|
||||
type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
attributes: mockAttributes,
|
||||
references: [],
|
||||
});
|
||||
|
||||
const result = await client.create();
|
||||
|
||||
expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.create).toHaveBeenCalledWith(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
{
|
||||
flapping: expect.objectContaining({
|
||||
enabled: mockAttributes.flapping.enabled,
|
||||
lookBackWindow: mockAttributes.flapping.lookBackWindow,
|
||||
statusChangeThreshold: mockAttributes.flapping.statusChangeThreshold,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: RULES_SETTINGS_SAVED_OBJECT_ID,
|
||||
overwrite: true,
|
||||
}
|
||||
);
|
||||
expect(result.attributes).toEqual(mockAttributes);
|
||||
});
|
||||
|
||||
test('can get existing rules settings saved object', async () => {
|
||||
const client = new RulesSettingsClient(rulesSettingsClientParams);
|
||||
const mockAttributes = getMockRulesSettings();
|
||||
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: RULES_SETTINGS_FEATURE_ID,
|
||||
type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
attributes: mockAttributes,
|
||||
references: [],
|
||||
});
|
||||
const result = await client.get();
|
||||
expect(result.attributes).toEqual(mockAttributes);
|
||||
});
|
||||
|
||||
test('throws if there is no existing saved object to get', async () => {
|
||||
const client = new RulesSettingsClient(rulesSettingsClientParams);
|
||||
|
||||
savedObjectsClient.get.mockRejectedValueOnce(
|
||||
SavedObjectsErrorHelpers.createGenericNotFoundError(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_SAVED_OBJECT_ID
|
||||
)
|
||||
);
|
||||
await expect(client.get()).rejects.toThrowError();
|
||||
});
|
||||
|
||||
test('can persist flapping settings when saved object does not exist', async () => {
|
||||
const client = new RulesSettingsClient(rulesSettingsClientParams);
|
||||
const mockAttributes = getMockRulesSettings();
|
||||
savedObjectsClient.get.mockRejectedValueOnce(
|
||||
SavedObjectsErrorHelpers.createGenericNotFoundError(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_SAVED_OBJECT_ID
|
||||
)
|
||||
);
|
||||
|
||||
savedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: RULES_SETTINGS_FEATURE_ID,
|
||||
type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
attributes: mockAttributes,
|
||||
references: [],
|
||||
});
|
||||
|
||||
const result = await client.flapping().get();
|
||||
|
||||
expect(savedObjectsClient.get).toHaveBeenCalledWith(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_SAVED_OBJECT_ID
|
||||
);
|
||||
|
||||
expect(savedObjectsClient.create).toHaveBeenCalledWith(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
{
|
||||
flapping: expect.objectContaining({
|
||||
enabled: mockAttributes.flapping.enabled,
|
||||
lookBackWindow: mockAttributes.flapping.lookBackWindow,
|
||||
statusChangeThreshold: mockAttributes.flapping.statusChangeThreshold,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: RULES_SETTINGS_SAVED_OBJECT_ID,
|
||||
overwrite: true,
|
||||
}
|
||||
);
|
||||
expect(result).toEqual(mockAttributes.flapping);
|
||||
});
|
||||
|
||||
test('can persist flapping settings when saved object already exists', async () => {
|
||||
const client = new RulesSettingsClient(rulesSettingsClientParams);
|
||||
const mockAttributes = getMockRulesSettings();
|
||||
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: RULES_SETTINGS_FEATURE_ID,
|
||||
type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
attributes: mockAttributes,
|
||||
references: [],
|
||||
});
|
||||
|
||||
const result = await client.flapping().get();
|
||||
|
||||
expect(savedObjectsClient.get).toHaveBeenCalledWith(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_SAVED_OBJECT_ID
|
||||
);
|
||||
expect(savedObjectsClient.create).not.toHaveBeenCalled();
|
||||
expect(result).toEqual(mockAttributes.flapping);
|
||||
});
|
||||
|
||||
test('can update flapping settings when saved object does not exist', async () => {
|
||||
const client = new RulesSettingsClient(rulesSettingsClientParams);
|
||||
const mockAttributes = getMockRulesSettings();
|
||||
|
||||
savedObjectsClient.get.mockRejectedValueOnce(
|
||||
SavedObjectsErrorHelpers.createGenericNotFoundError(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_SAVED_OBJECT_ID
|
||||
)
|
||||
);
|
||||
|
||||
const mockResolve = {
|
||||
id: RULES_SETTINGS_FEATURE_ID,
|
||||
type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
attributes: mockAttributes,
|
||||
references: [],
|
||||
version: '123',
|
||||
};
|
||||
|
||||
savedObjectsClient.create.mockResolvedValueOnce(mockResolve);
|
||||
savedObjectsClient.update.mockResolvedValueOnce({
|
||||
...mockResolve,
|
||||
attributes: {
|
||||
flapping: {
|
||||
...mockResolve.attributes.flapping,
|
||||
enabled: false,
|
||||
lookBackWindow: 5,
|
||||
statusChangeThreshold: 5,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Try to update with new values
|
||||
const result = await client.flapping().update({
|
||||
enabled: false,
|
||||
lookBackWindow: 5,
|
||||
statusChangeThreshold: 5,
|
||||
});
|
||||
|
||||
// Tried to get first, but no results
|
||||
expect(savedObjectsClient.get).toHaveBeenCalledWith(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_SAVED_OBJECT_ID
|
||||
);
|
||||
|
||||
// So create a new entry
|
||||
expect(savedObjectsClient.create).toHaveBeenCalledWith(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
{
|
||||
flapping: expect.objectContaining({
|
||||
enabled: mockAttributes.flapping.enabled,
|
||||
lookBackWindow: mockAttributes.flapping.lookBackWindow,
|
||||
statusChangeThreshold: mockAttributes.flapping.statusChangeThreshold,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: RULES_SETTINGS_SAVED_OBJECT_ID,
|
||||
overwrite: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Try to update with version
|
||||
expect(savedObjectsClient.update).toHaveBeenCalledWith(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_SAVED_OBJECT_ID,
|
||||
{
|
||||
flapping: expect.objectContaining({
|
||||
enabled: false,
|
||||
lookBackWindow: 5,
|
||||
statusChangeThreshold: 5,
|
||||
createdBy: 'test name',
|
||||
updatedBy: 'test name',
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
}),
|
||||
},
|
||||
{ version: '123' }
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
enabled: false,
|
||||
lookBackWindow: 5,
|
||||
statusChangeThreshold: 5,
|
||||
})
|
||||
);
|
||||
expect(client.queryDelay()).toEqual(expect.any(RulesSettingsQueryDelayClient));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,24 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
Logger,
|
||||
SavedObjectsClientContract,
|
||||
SavedObject,
|
||||
SavedObjectsErrorHelpers,
|
||||
} from '@kbn/core/server';
|
||||
import { Logger, SavedObjectsClientContract } from '@kbn/core/server';
|
||||
import { RulesSettingsFlappingClient } from './flapping/rules_settings_flapping_client';
|
||||
import {
|
||||
RulesSettings,
|
||||
DEFAULT_FLAPPING_SETTINGS,
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_SAVED_OBJECT_ID,
|
||||
} from '../../common';
|
||||
import { RulesSettingsQueryDelayClient } from './query_delay/rules_settings_query_delay_client';
|
||||
|
||||
export interface RulesSettingsClientConstructorOptions {
|
||||
readonly logger: Logger;
|
||||
readonly savedObjectsClient: SavedObjectsClientContract;
|
||||
readonly getUserName: () => Promise<string | null>;
|
||||
readonly isServerless: boolean;
|
||||
}
|
||||
|
||||
export class RulesSettingsClient {
|
||||
|
@ -30,16 +21,25 @@ export class RulesSettingsClient {
|
|||
private readonly savedObjectsClient: SavedObjectsClientContract;
|
||||
private readonly getUserName: () => Promise<string | null>;
|
||||
private readonly _flapping: RulesSettingsFlappingClient;
|
||||
private readonly _queryDelay: RulesSettingsQueryDelayClient;
|
||||
private readonly isServerless: boolean;
|
||||
|
||||
constructor(options: RulesSettingsClientConstructorOptions) {
|
||||
this.logger = options.logger;
|
||||
this.savedObjectsClient = options.savedObjectsClient;
|
||||
this.getUserName = options.getUserName;
|
||||
this.isServerless = options.isServerless;
|
||||
|
||||
this._flapping = new RulesSettingsFlappingClient({
|
||||
logger: this.logger,
|
||||
savedObjectsClient: this.savedObjectsClient,
|
||||
getOrCreate: this.getOrCreate.bind(this),
|
||||
getModificationMetadata: this.getModificationMetadata.bind(this),
|
||||
});
|
||||
|
||||
this._queryDelay = new RulesSettingsQueryDelayClient({
|
||||
logger: this.logger,
|
||||
savedObjectsClient: this.savedObjectsClient,
|
||||
isServerless: this.isServerless,
|
||||
getModificationMetadata: this.getModificationMetadata.bind(this),
|
||||
});
|
||||
}
|
||||
|
@ -56,59 +56,11 @@ export class RulesSettingsClient {
|
|||
};
|
||||
}
|
||||
|
||||
public async get(): Promise<SavedObject<RulesSettings>> {
|
||||
try {
|
||||
return await this.savedObjectsClient.get<RulesSettings>(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
RULES_SETTINGS_SAVED_OBJECT_ID
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`Failed to get rules setting for current space. Error: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async create(): Promise<SavedObject<RulesSettings>> {
|
||||
const modificationMetadata = await this.getModificationMetadata();
|
||||
|
||||
try {
|
||||
return await this.savedObjectsClient.create<RulesSettings>(
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
{
|
||||
flapping: {
|
||||
...DEFAULT_FLAPPING_SETTINGS,
|
||||
...modificationMetadata,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: RULES_SETTINGS_SAVED_OBJECT_ID,
|
||||
overwrite: true,
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`Failed to create rules setting for current space. Error: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to ensure that a rules-settings saved object always exists.
|
||||
* Ensures the creation of the saved object is done lazily during retrieval.
|
||||
*/
|
||||
private async getOrCreate(): Promise<SavedObject<RulesSettings>> {
|
||||
try {
|
||||
return await this.get();
|
||||
} catch (e) {
|
||||
if (SavedObjectsErrorHelpers.isNotFoundError(e)) {
|
||||
this.logger.info('Creating new default rules settings for current space.');
|
||||
return await this.create();
|
||||
}
|
||||
this.logger.error(`Failed to persist rules setting for current space. Error: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public flapping(): RulesSettingsFlappingClient {
|
||||
return this._flapping;
|
||||
}
|
||||
|
||||
public queryDelay(): RulesSettingsQueryDelayClient {
|
||||
return this._queryDelay;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
|
||||
export const flappingSchema = schema.object({
|
||||
enabled: schema.boolean(),
|
||||
lookBackWindow: schema.number(),
|
||||
statusChangeThreshold: schema.number(),
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { flappingSchema } from './flapping_schema';
|
||||
export { queryDelaySchema } from './query_delay_schema';
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
|
||||
export const queryDelaySchema = schema.object({
|
||||
delay: schema.number(),
|
||||
});
|
|
@ -30,6 +30,7 @@ const securityPluginStart = securityMock.createStart();
|
|||
const rulesSettingsClientFactoryParams: jest.Mocked<RulesSettingsClientFactoryOpts> = {
|
||||
logger: loggingSystemMock.create().get(),
|
||||
savedObjectsService,
|
||||
isServerless: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -58,6 +59,7 @@ test('creates a rules settings client with proper constructor arguments when sec
|
|||
logger: rulesSettingsClientFactoryParams.logger,
|
||||
savedObjectsClient,
|
||||
getUserName: expect.any(Function),
|
||||
isServerless: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -80,6 +82,7 @@ test('creates a rules settings client with proper constructor arguments', async
|
|||
logger: rulesSettingsClientFactoryParams.logger,
|
||||
savedObjectsClient,
|
||||
getUserName: expect.any(Function),
|
||||
isServerless: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -106,6 +109,7 @@ test('creates an unauthorized rules settings client', async () => {
|
|||
logger: rulesSettingsClientFactoryParams.logger,
|
||||
savedObjectsClient,
|
||||
getUserName: expect.any(Function),
|
||||
isServerless: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import { RULES_SETTINGS_SAVED_OBJECT_TYPE } from '../common';
|
|||
export interface RulesSettingsClientFactoryOpts {
|
||||
logger: Logger;
|
||||
savedObjectsService: SavedObjectsServiceStart;
|
||||
isServerless: boolean;
|
||||
securityPluginStart?: SecurityPluginStart;
|
||||
}
|
||||
|
||||
|
@ -26,6 +27,7 @@ export class RulesSettingsClientFactory {
|
|||
private logger!: Logger;
|
||||
private savedObjectsService!: SavedObjectsServiceStart;
|
||||
private securityPluginStart?: SecurityPluginStart;
|
||||
private isServerless = false;
|
||||
|
||||
public initialize(options: RulesSettingsClientFactoryOpts) {
|
||||
if (this.isInitialized) {
|
||||
|
@ -35,6 +37,7 @@ export class RulesSettingsClientFactory {
|
|||
this.logger = options.logger;
|
||||
this.savedObjectsService = options.savedObjectsService;
|
||||
this.securityPluginStart = options.securityPluginStart;
|
||||
this.isServerless = options.isServerless;
|
||||
}
|
||||
|
||||
private createRulesSettingsClient(request: KibanaRequest, withAuth: boolean) {
|
||||
|
@ -54,6 +57,7 @@ export class RulesSettingsClientFactory {
|
|||
const user = securityPluginStart.authc.getCurrentUser(request);
|
||||
return user ? user.username : null;
|
||||
},
|
||||
isServerless: this.isServerless,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@ import {
|
|||
ALL_FLAPPING_SETTINGS_SUB_FEATURE_ID,
|
||||
API_PRIVILEGES,
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
ALL_QUERY_DELAY_SETTINGS_SUB_FEATURE_ID,
|
||||
READ_QUERY_DELAY_SETTINGS_SUB_FEATURE_ID,
|
||||
} from '../common';
|
||||
|
||||
export const rulesSettingsFeature: KibanaFeatureConfig = {
|
||||
|
@ -87,5 +89,42 @@ export const rulesSettingsFeature: KibanaFeatureConfig = {
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.alerting.feature.queryDelaySettingsSubFeatureName', {
|
||||
defaultMessage: 'Query delay',
|
||||
}),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [
|
||||
API_PRIVILEGES.READ_QUERY_DELAY_SETTINGS,
|
||||
API_PRIVILEGES.WRITE_QUERY_DELAY_SETTINGS,
|
||||
],
|
||||
name: 'All',
|
||||
id: ALL_QUERY_DELAY_SETTINGS_SUB_FEATURE_ID,
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [RULES_SETTINGS_SAVED_OBJECT_TYPE],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeQueryDelaySettingsUI', 'readQueryDelaySettingsUI'],
|
||||
},
|
||||
{
|
||||
api: [API_PRIVILEGES.READ_QUERY_DELAY_SETTINGS],
|
||||
name: 'Read',
|
||||
id: READ_QUERY_DELAY_SETTINGS_SUB_FEATURE_ID,
|
||||
includeIn: 'read',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [RULES_SETTINGS_SAVED_OBJECT_TYPE],
|
||||
},
|
||||
ui: ['readQueryDelaySettingsUI'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -80,6 +80,7 @@ import { RuleResultService } from '../monitoring/rule_result_service';
|
|||
import { LegacyAlertsClient } from '../alerts_client';
|
||||
import { IAlertsClient } from '../alerts_client/types';
|
||||
import { MaintenanceWindow } from '../application/maintenance_window/types';
|
||||
import { getTimeRange } from '../lib/get_time_range';
|
||||
|
||||
const FALLBACK_RETRY_INTERVAL = '5m';
|
||||
const CONNECTIVITY_RETRY_INTERVAL = '5m';
|
||||
|
@ -324,6 +325,7 @@ export class TaskRunner<
|
|||
|
||||
const rulesSettingsClient = this.context.getRulesSettingsClientWithRequest(fakeRequest);
|
||||
const flappingSettings = await rulesSettingsClient.flapping().get();
|
||||
const queryDelaySettings = await rulesSettingsClient.queryDelay().get();
|
||||
|
||||
const alertsClientParams = {
|
||||
logger: this.logger,
|
||||
|
@ -514,6 +516,8 @@ export class TaskRunner<
|
|||
logger: this.logger,
|
||||
flappingSettings,
|
||||
...(maintenanceWindowIds.length ? { maintenanceWindowIds } : {}),
|
||||
getTimeRange: (timeWindow) =>
|
||||
getTimeRange(this.logger, queryDelaySettings, timeWindow),
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -28,7 +28,11 @@ import { Filter } from '@kbn/es-query';
|
|||
import { RuleTypeRegistry as OrigruleTypeRegistry } from './rule_type_registry';
|
||||
import { PluginSetupContract, PluginStartContract } from './plugin';
|
||||
import { RulesClient } from './rules_client';
|
||||
import { RulesSettingsClient, RulesSettingsFlappingClient } from './rules_settings_client';
|
||||
import {
|
||||
RulesSettingsClient,
|
||||
RulesSettingsFlappingClient,
|
||||
RulesSettingsQueryDelayClient,
|
||||
} from './rules_settings_client';
|
||||
import { MaintenanceWindowClient } from './maintenance_window_client';
|
||||
export * from '../common';
|
||||
import {
|
||||
|
@ -135,6 +139,7 @@ export interface RuleExecutorOptions<
|
|||
namespace?: string;
|
||||
flappingSettings: RulesSettingsFlappingProperties;
|
||||
maintenanceWindowIds?: string[];
|
||||
getTimeRange: (timeWindow?: string) => { dateStart: string; dateEnd: string };
|
||||
}
|
||||
|
||||
export interface RuleParamsAndRefs<Params extends RuleTypeParams> {
|
||||
|
@ -372,6 +377,7 @@ export type RulesClientApi = PublicMethodsOf<RulesClient>;
|
|||
|
||||
export type RulesSettingsClientApi = PublicMethodsOf<RulesSettingsClient>;
|
||||
export type RulesSettingsFlappingClientApi = PublicMethodsOf<RulesSettingsFlappingClient>;
|
||||
export type RulesSettingsQueryDelayClientApi = PublicMethodsOf<RulesSettingsQueryDelayClient>;
|
||||
|
||||
export type MaintenanceWindowClientApi = PublicMethodsOf<MaintenanceWindowClient>;
|
||||
|
||||
|
|
|
@ -103,7 +103,11 @@ describe('Transaction duration anomaly alert', () => {
|
|||
ml,
|
||||
});
|
||||
|
||||
const params = { anomalySeverityType: ML_ANOMALY_SEVERITY.MINOR };
|
||||
const params = {
|
||||
anomalySeverityType: ML_ANOMALY_SEVERITY.MINOR,
|
||||
windowSize: 5,
|
||||
windowUnit: 'm',
|
||||
};
|
||||
|
||||
await executor({ params });
|
||||
|
||||
|
|
|
@ -97,7 +97,13 @@ export function registerAnomalyRuleType({
|
|||
producer: 'apm',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
executor: async ({ params, services, spaceId, startedAt }) => {
|
||||
executor: async ({
|
||||
params,
|
||||
services,
|
||||
spaceId,
|
||||
startedAt,
|
||||
getTimeRange,
|
||||
}) => {
|
||||
if (!ml) {
|
||||
return { state: {} };
|
||||
}
|
||||
|
@ -144,12 +150,14 @@ export function registerAnomalyRuleType({
|
|||
}
|
||||
|
||||
// start time must be at least 30, does like this to support rules created before this change where default was 15
|
||||
const startTime = Math.min(
|
||||
datemath.parse('now-30m')!.valueOf(),
|
||||
const window =
|
||||
datemath.parse('now-30m')!.valueOf() >
|
||||
datemath
|
||||
.parse(`now-${ruleParams.windowSize}${ruleParams.windowUnit}`)
|
||||
?.valueOf() || 0
|
||||
);
|
||||
.parse(`now-${ruleParams.windowSize}${ruleParams.windowUnit}`)!
|
||||
.valueOf()
|
||||
? '30m'
|
||||
: `${ruleParams.windowSize}${ruleParams.windowUnit}`;
|
||||
const { dateStart } = getTimeRange(window);
|
||||
|
||||
const jobIds = mlJobs.map((job) => job.jobId);
|
||||
const anomalySearchParams = {
|
||||
|
@ -165,7 +173,7 @@ export function registerAnomalyRuleType({
|
|||
{
|
||||
range: {
|
||||
timestamp: {
|
||||
gte: startTime,
|
||||
gte: dateStart,
|
||||
format: 'epoch_millis',
|
||||
},
|
||||
},
|
||||
|
|
|
@ -104,6 +104,7 @@ export function registerErrorCountRuleType({
|
|||
services,
|
||||
spaceId,
|
||||
startedAt,
|
||||
getTimeRange,
|
||||
}) => {
|
||||
const allGroupByFields = getAllGroupByFields(
|
||||
ApmRuleType.ErrorCount,
|
||||
|
@ -131,6 +132,10 @@ export function registerErrorCountRuleType({
|
|||
]
|
||||
: [];
|
||||
|
||||
const { dateStart } = getTimeRange(
|
||||
`${ruleParams.windowSize}${ruleParams.windowUnit}`
|
||||
);
|
||||
|
||||
const searchParams = {
|
||||
index: indices.error,
|
||||
body: {
|
||||
|
@ -142,7 +147,7 @@ export function registerErrorCountRuleType({
|
|||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: `now-${ruleParams.windowSize}${ruleParams.windowUnit}`,
|
||||
gte: dateStart,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -111,7 +111,12 @@ export function registerTransactionDurationRuleType({
|
|||
producer: APM_SERVER_FEATURE_ID,
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
executor: async ({ params: ruleParams, services, spaceId }) => {
|
||||
executor: async ({
|
||||
params: ruleParams,
|
||||
services,
|
||||
spaceId,
|
||||
getTimeRange,
|
||||
}) => {
|
||||
const allGroupByFields = getAllGroupByFields(
|
||||
ApmRuleType.TransactionDuration,
|
||||
ruleParams.groupBy
|
||||
|
@ -152,6 +157,10 @@ export function registerTransactionDurationRuleType({
|
|||
]
|
||||
: [];
|
||||
|
||||
const { dateStart } = getTimeRange(
|
||||
`${ruleParams.windowSize}${ruleParams.windowUnit}`
|
||||
);
|
||||
|
||||
const searchParams = {
|
||||
index,
|
||||
body: {
|
||||
|
@ -163,7 +172,7 @@ export function registerTransactionDurationRuleType({
|
|||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: `now-${ruleParams.windowSize}${ruleParams.windowUnit}`,
|
||||
gte: dateStart,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -113,6 +113,7 @@ export function registerTransactionErrorRateRuleType({
|
|||
spaceId,
|
||||
params: ruleParams,
|
||||
startedAt,
|
||||
getTimeRange,
|
||||
}) => {
|
||||
const allGroupByFields = getAllGroupByFields(
|
||||
ApmRuleType.TransactionErrorRate,
|
||||
|
@ -154,6 +155,10 @@ export function registerTransactionErrorRateRuleType({
|
|||
]
|
||||
: [];
|
||||
|
||||
const { dateStart } = getTimeRange(
|
||||
`${ruleParams.windowSize}${ruleParams.windowUnit}`
|
||||
);
|
||||
|
||||
const searchParams = {
|
||||
index,
|
||||
body: {
|
||||
|
@ -165,7 +170,7 @@ export function registerTransactionErrorRateRuleType({
|
|||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: `now-${ruleParams.windowSize}${ruleParams.windowUnit}`,
|
||||
gte: dateStart,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -95,6 +95,10 @@ export const createRuleTypeMocks = () => {
|
|||
},
|
||||
startedAt: new Date(),
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange: () => {
|
||||
const date = new Date(Date.now()).toISOString();
|
||||
return { dateStart: date, dateEnd: date };
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -83,6 +83,10 @@ const mockOptions = {
|
|||
},
|
||||
logger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange: () => {
|
||||
const date = new Date().toISOString();
|
||||
return { dateStart: date, dateEnd: date };
|
||||
},
|
||||
};
|
||||
|
||||
const setEvaluationResults = (response: Record<string, ConditionResult>) => {
|
||||
|
|
|
@ -73,6 +73,10 @@ const mockOptions = {
|
|||
},
|
||||
logger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange: () => {
|
||||
const date = STARTED_AT_MOCK_DATE.toISOString();
|
||||
return { dateStart: date, dateEnd: date };
|
||||
},
|
||||
};
|
||||
|
||||
const setEvaluationResults = (response: Array<Record<string, Evaluation>>) => {
|
||||
|
|
|
@ -127,6 +127,10 @@ const mockOptions = {
|
|||
},
|
||||
logger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange: () => {
|
||||
const date = STARTED_AT_MOCK_DATE.toISOString();
|
||||
return { dateStart: date, dateEnd: date };
|
||||
},
|
||||
};
|
||||
|
||||
const setEvaluationResults = (response: Array<Record<string, Evaluation>>) => {
|
||||
|
|
|
@ -127,6 +127,7 @@ export const createMetricThresholdExecutor = ({
|
|||
executionId,
|
||||
spaceId,
|
||||
rule: { id: ruleId },
|
||||
getTimeRange,
|
||||
} = options;
|
||||
|
||||
const { criteria } = params;
|
||||
|
@ -191,6 +192,8 @@ export const createMetricThresholdExecutor = ({
|
|||
throw new Error('The selected data view does not have a timestamp field');
|
||||
}
|
||||
|
||||
// Calculate initial start and end date with no time window, as each criteria has it's own time window
|
||||
const { dateStart, dateEnd } = getTimeRange();
|
||||
const alertResults = await evaluateRule(
|
||||
services.scopedClusterClient.asCurrentUser,
|
||||
params as EvaluatedRuleParams,
|
||||
|
@ -199,8 +202,8 @@ export const createMetricThresholdExecutor = ({
|
|||
compositeSize,
|
||||
alertOnGroupDisappear,
|
||||
logger,
|
||||
{ end: dateEnd, start: dateStart },
|
||||
state.lastRunTimestamp,
|
||||
{ end: startedAt.valueOf() },
|
||||
convertStringsToMissingGroupsRecord(previousMissingGroups)
|
||||
);
|
||||
|
||||
|
|
|
@ -11,123 +11,73 @@ import moment from 'moment';
|
|||
import { createTimerange } from './create_timerange';
|
||||
|
||||
describe('createTimerange(interval, aggType, timeframe)', () => {
|
||||
describe('without timeframe', () => {
|
||||
describe('Basic Metric Aggs', () => {
|
||||
it('should return a second range for last 1 second', () => {
|
||||
const subject = createTimerange(1000, Aggregators.COUNT);
|
||||
expect(subject.end - subject.start).toEqual(1000);
|
||||
});
|
||||
it('should return a minute range for last 1 minute', () => {
|
||||
const subject = createTimerange(60000, Aggregators.COUNT);
|
||||
expect(subject.end - subject.start).toEqual(60000);
|
||||
});
|
||||
it('should return 5 minute range for last 5 minutes', () => {
|
||||
const subject = createTimerange(300000, Aggregators.COUNT);
|
||||
expect(subject.end - subject.start).toEqual(300000);
|
||||
});
|
||||
it('should return a hour range for last 1 hour', () => {
|
||||
const subject = createTimerange(3600000, Aggregators.COUNT);
|
||||
expect(subject.end - subject.start).toEqual(3600000);
|
||||
});
|
||||
it('should return a day range for last 1 day', () => {
|
||||
const subject = createTimerange(86400000, Aggregators.COUNT);
|
||||
expect(subject.end - subject.start).toEqual(86400000);
|
||||
});
|
||||
const end = moment();
|
||||
const timeframe = {
|
||||
start: end.clone().toISOString(),
|
||||
end: end.toISOString(),
|
||||
};
|
||||
describe('Basic Metric Aggs', () => {
|
||||
it('should return a second range for last 1 second', () => {
|
||||
const subject = createTimerange(1000, Aggregators.COUNT, timeframe);
|
||||
expect(subject.end - subject.start).toEqual(1000);
|
||||
});
|
||||
describe('Rate Aggs', () => {
|
||||
it('should return a 20 second range for last 1 second', () => {
|
||||
const subject = createTimerange(1000, Aggregators.RATE);
|
||||
expect(subject.end - subject.start).toEqual(1000 * 2);
|
||||
});
|
||||
it('should return a 5 minute range for last 1 minute', () => {
|
||||
const subject = createTimerange(60000, Aggregators.RATE);
|
||||
expect(subject.end - subject.start).toEqual(60000 * 2);
|
||||
});
|
||||
it('should return 25 minute range for last 5 minutes', () => {
|
||||
const subject = createTimerange(300000, Aggregators.RATE);
|
||||
expect(subject.end - subject.start).toEqual(300000 * 2);
|
||||
});
|
||||
it('should return 5 hour range for last hour', () => {
|
||||
const subject = createTimerange(3600000, Aggregators.RATE);
|
||||
expect(subject.end - subject.start).toEqual(3600000 * 2);
|
||||
});
|
||||
it('should return a 5 day range for last day', () => {
|
||||
const subject = createTimerange(86400000, Aggregators.RATE);
|
||||
expect(subject.end - subject.start).toEqual(86400000 * 2);
|
||||
});
|
||||
it('should return a minute range for last 1 minute', () => {
|
||||
const subject = createTimerange(60000, Aggregators.COUNT, timeframe);
|
||||
expect(subject.end - subject.start).toEqual(60000);
|
||||
});
|
||||
it('should return 5 minute range for last 5 minutes', () => {
|
||||
const subject = createTimerange(300000, Aggregators.COUNT, timeframe);
|
||||
expect(subject.end - subject.start).toEqual(300000);
|
||||
});
|
||||
it('should return a hour range for last 1 hour', () => {
|
||||
const subject = createTimerange(3600000, Aggregators.COUNT, timeframe);
|
||||
expect(subject.end - subject.start).toEqual(3600000);
|
||||
});
|
||||
it('should return a day range for last 1 day', () => {
|
||||
const subject = createTimerange(86400000, Aggregators.COUNT, timeframe);
|
||||
expect(subject.end - subject.start).toEqual(86400000);
|
||||
});
|
||||
});
|
||||
describe('with full timeframe', () => {
|
||||
describe('Basic Metric Aggs', () => {
|
||||
it('should return 5 minute range when given 4 minute timeframe', () => {
|
||||
const end = moment();
|
||||
const timeframe = {
|
||||
start: end.clone().subtract(4, 'minutes').valueOf(),
|
||||
end: end.valueOf(),
|
||||
};
|
||||
const subject = createTimerange(300000, Aggregators.COUNT, timeframe);
|
||||
expect(subject.end - subject.start).toEqual(300000);
|
||||
});
|
||||
it('should return 6 minute range when given 6 minute timeframe', () => {
|
||||
const end = moment();
|
||||
const timeframe = {
|
||||
start: end.clone().subtract(6, 'minutes').valueOf(),
|
||||
end: end.valueOf(),
|
||||
};
|
||||
const subject = createTimerange(300000, Aggregators.COUNT, timeframe);
|
||||
expect(subject.end - subject.start).toEqual(360000);
|
||||
});
|
||||
describe('Rate Aggs', () => {
|
||||
it('should return a 20 second range for last 1 second', () => {
|
||||
const subject = createTimerange(1000, Aggregators.RATE, timeframe);
|
||||
expect(subject.end - subject.start).toEqual(1000 * 2);
|
||||
});
|
||||
describe('Rate Aggs', () => {
|
||||
it('should return 8 minute range when given 4 minute timeframe', () => {
|
||||
const end = moment();
|
||||
const timeframe = {
|
||||
start: end.clone().subtract(4, 'minutes').valueOf(),
|
||||
end: end.valueOf(),
|
||||
};
|
||||
const subject = createTimerange(300000, Aggregators.RATE, timeframe);
|
||||
expect(subject.end - subject.start).toEqual(300000 * 2);
|
||||
});
|
||||
it('should return 12 minute range when given 6 minute timeframe', () => {
|
||||
const end = moment();
|
||||
const timeframe = {
|
||||
start: end.clone().subtract(6, 'minutes').valueOf(),
|
||||
end: end.valueOf(),
|
||||
};
|
||||
const subject = createTimerange(300000, Aggregators.RATE, timeframe);
|
||||
expect(subject.end - subject.start).toEqual(300000 * 2);
|
||||
});
|
||||
it('should return a 5 minute range for last 1 minute', () => {
|
||||
const subject = createTimerange(60000, Aggregators.RATE, timeframe);
|
||||
expect(subject.end - subject.start).toEqual(60000 * 2);
|
||||
});
|
||||
it('should return 25 minute range for last 5 minutes', () => {
|
||||
const subject = createTimerange(300000, Aggregators.RATE, timeframe);
|
||||
expect(subject.end - subject.start).toEqual(300000 * 2);
|
||||
});
|
||||
it('should return 5 hour range for last hour', () => {
|
||||
const subject = createTimerange(3600000, Aggregators.RATE, timeframe);
|
||||
expect(subject.end - subject.start).toEqual(3600000 * 2);
|
||||
});
|
||||
it('should return a 5 day range for last day', () => {
|
||||
const subject = createTimerange(86400000, Aggregators.RATE, timeframe);
|
||||
expect(subject.end - subject.start).toEqual(86400000 * 2);
|
||||
});
|
||||
});
|
||||
describe('with partial timeframe', () => {
|
||||
describe('Basic Metric Aggs', () => {
|
||||
it('should return 5 minute range for last 5 minutes', () => {
|
||||
const end = moment();
|
||||
const timeframe = {
|
||||
end: end.valueOf(),
|
||||
};
|
||||
const subject = createTimerange(300000, Aggregators.AVERAGE, timeframe);
|
||||
expect(subject).toEqual({
|
||||
start: end.clone().subtract(5, 'minutes').valueOf(),
|
||||
end: end.valueOf(),
|
||||
});
|
||||
});
|
||||
describe('With lastPeriodEnd', () => {
|
||||
it('should return a minute and 1 second range for last 1 second when the lastPeriodEnd is less than the timeframe start', () => {
|
||||
const subject = createTimerange(
|
||||
1000,
|
||||
Aggregators.COUNT,
|
||||
timeframe,
|
||||
end.clone().subtract(1, 'minutes').valueOf()
|
||||
);
|
||||
expect(subject.end - subject.start).toEqual(61000);
|
||||
});
|
||||
describe('Rate Aggs', () => {
|
||||
it('should return 10 minute range for last 5 minutes', () => {
|
||||
const end = moment();
|
||||
const timeframe = {
|
||||
end: end.valueOf(),
|
||||
};
|
||||
const subject = createTimerange(300000, Aggregators.RATE, timeframe);
|
||||
expect(subject).toEqual({
|
||||
start: end
|
||||
.clone()
|
||||
.subtract(300 * 2, 'seconds')
|
||||
.valueOf(),
|
||||
end: end.valueOf(),
|
||||
});
|
||||
});
|
||||
it('should return a second range for last 1 second when the lastPeriodEnd is not less than the timeframe start', () => {
|
||||
const subject = createTimerange(
|
||||
1000,
|
||||
Aggregators.COUNT,
|
||||
timeframe,
|
||||
end.clone().add(2, 'seconds').valueOf()
|
||||
);
|
||||
expect(subject.end - subject.start).toEqual(1000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,21 +11,20 @@ import { Aggregators } from '../../../../../common/custom_threshold_rule/types';
|
|||
export const createTimerange = (
|
||||
interval: number,
|
||||
aggType: Aggregators,
|
||||
timeframe?: { end: number; start?: number },
|
||||
timeframe: { end: string; start: string },
|
||||
lastPeriodEnd?: number
|
||||
) => {
|
||||
const to = moment(timeframe ? timeframe.end : Date.now()).valueOf();
|
||||
const end = moment(timeframe.end).valueOf();
|
||||
let start = moment(timeframe.start).valueOf();
|
||||
|
||||
// Rate aggregations need 5 buckets worth of data
|
||||
const minimumBuckets = aggType === Aggregators.RATE ? 2 : 1;
|
||||
const calculatedFrom = lastPeriodEnd ? lastPeriodEnd - interval : to - interval * minimumBuckets;
|
||||
start = start - interval * minimumBuckets;
|
||||
|
||||
// Use either the timeframe.start when the start is less then calculatedFrom
|
||||
// OR use the calculatedFrom
|
||||
const from =
|
||||
timeframe && timeframe.start && timeframe.start <= calculatedFrom
|
||||
? timeframe.start
|
||||
: calculatedFrom;
|
||||
// Use lastPeriodEnd - interval when it's less than start
|
||||
if (lastPeriodEnd && lastPeriodEnd - interval < start) {
|
||||
start = lastPeriodEnd - interval;
|
||||
}
|
||||
|
||||
return { start: from, end: to };
|
||||
return { start, end };
|
||||
};
|
||||
|
|
|
@ -41,8 +41,8 @@ export const evaluateRule = async <Params extends EvaluatedRuleParams = Evaluate
|
|||
compositeSize: number,
|
||||
alertOnGroupDisappear: boolean,
|
||||
logger: Logger,
|
||||
timeframe: { start: string; end: string },
|
||||
lastPeriodEnd?: number,
|
||||
timeframe?: { start?: number; end: number },
|
||||
missingGroups: MissingGroupsRecord[] = []
|
||||
): Promise<Array<Record<string, Evaluation>>> => {
|
||||
const { criteria, groupBy, searchConfiguration } = params;
|
||||
|
|
|
@ -97,6 +97,11 @@ function createFindResponse(sloList: SLO[]): SavedObjectsFindResponse<StoredSLO>
|
|||
};
|
||||
}
|
||||
|
||||
function getTimeRange() {
|
||||
const date = new Date(Date.now()).toISOString();
|
||||
return { dateStart: date, dateEnd: date };
|
||||
}
|
||||
|
||||
describe('BurnRateRuleExecutor', () => {
|
||||
let esClientMock: ElasticsearchClientMock;
|
||||
let soClientMock: jest.Mocked<SavedObjectsClientContract>;
|
||||
|
@ -178,6 +183,7 @@ describe('BurnRateRuleExecutor', () => {
|
|||
spaceId: 'irrelevant',
|
||||
state: {},
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange,
|
||||
})
|
||||
).rejects.toThrowError();
|
||||
});
|
||||
|
@ -198,6 +204,7 @@ describe('BurnRateRuleExecutor', () => {
|
|||
spaceId: 'irrelevant',
|
||||
state: {},
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange,
|
||||
});
|
||||
|
||||
expect(esClientMock.search).not.toHaveBeenCalled();
|
||||
|
@ -246,6 +253,7 @@ describe('BurnRateRuleExecutor', () => {
|
|||
spaceId: 'irrelevant',
|
||||
state: {},
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange,
|
||||
});
|
||||
|
||||
expect(alertWithLifecycleMock).not.toBeCalled();
|
||||
|
@ -291,6 +299,7 @@ describe('BurnRateRuleExecutor', () => {
|
|||
spaceId: 'irrelevant',
|
||||
state: {},
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange,
|
||||
});
|
||||
|
||||
expect(alertWithLifecycleMock).not.toBeCalled();
|
||||
|
@ -339,6 +348,7 @@ describe('BurnRateRuleExecutor', () => {
|
|||
spaceId: 'irrelevant',
|
||||
state: {},
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange,
|
||||
});
|
||||
|
||||
expect(alertWithLifecycleMock).toBeCalledWith({
|
||||
|
@ -436,6 +446,7 @@ describe('BurnRateRuleExecutor', () => {
|
|||
spaceId: 'irrelevant',
|
||||
state: {},
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange,
|
||||
});
|
||||
|
||||
expect(alertWithLifecycleMock).toBeCalledWith({
|
||||
|
|
|
@ -19,7 +19,7 @@ import { LocatorPublic } from '@kbn/share-plugin/common';
|
|||
|
||||
import { upperCase } from 'lodash';
|
||||
import { addSpaceIdToPath } from '@kbn/spaces-plugin/server';
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
import { ALL_VALUE, toDurationUnit } from '@kbn/slo-schema';
|
||||
import { AlertsLocatorParams, getAlertUrl } from '../../../../common';
|
||||
import {
|
||||
SLO_ID_FIELD,
|
||||
|
@ -63,6 +63,7 @@ export const getRuleExecutor = ({
|
|||
params,
|
||||
startedAt,
|
||||
spaceId,
|
||||
getTimeRange,
|
||||
}): ReturnType<
|
||||
ExecutorType<
|
||||
BurnRateRuleParams,
|
||||
|
@ -88,7 +89,22 @@ export const getRuleExecutor = ({
|
|||
return { state: {} };
|
||||
}
|
||||
|
||||
const results = await evaluate(esClient.asCurrentUser, slo, params, startedAt);
|
||||
const burnRateWindows = getBurnRateWindows(params.windows);
|
||||
const longestLookbackWindow = burnRateWindows.reduce((acc, winDef) => {
|
||||
return winDef.longDuration.isShorterThan(acc.longDuration) ? acc : winDef;
|
||||
}, burnRateWindows[0]);
|
||||
const { dateStart, dateEnd } = getTimeRange(
|
||||
`${longestLookbackWindow.longDuration.value}${longestLookbackWindow.longDuration.unit}`
|
||||
);
|
||||
|
||||
const results = await evaluate(
|
||||
esClient.asCurrentUser,
|
||||
slo,
|
||||
params,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
burnRateWindows
|
||||
);
|
||||
|
||||
if (results.length > 0) {
|
||||
for (const result of results) {
|
||||
|
@ -196,6 +212,19 @@ export const getRuleExecutor = ({
|
|||
return { state: {} };
|
||||
};
|
||||
|
||||
export function getBurnRateWindows(windows: WindowSchema[]) {
|
||||
return windows.map((winDef) => {
|
||||
return {
|
||||
...winDef,
|
||||
longDuration: new Duration(winDef.longWindow.value, toDurationUnit(winDef.longWindow.unit)),
|
||||
shortDuration: new Duration(
|
||||
winDef.shortWindow.value,
|
||||
toDurationUnit(winDef.shortWindow.unit)
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getActionGroupName(id: string) {
|
||||
switch (id) {
|
||||
case HIGH_PRIORITY_ACTION.id:
|
||||
|
|
|
@ -8,8 +8,10 @@
|
|||
import { createBurnRateRule } from '../fixtures/rule';
|
||||
import { buildQuery } from './build_query';
|
||||
import { createKQLCustomIndicator, createSLO } from '../../../../services/slo/fixtures/slo';
|
||||
import { getBurnRateWindows } from '../executor';
|
||||
|
||||
const STARTED_AT = new Date('2023-01-01T00:00:00.000Z');
|
||||
const DATE_START = '2022-12-29T00:00:00.000Z';
|
||||
const DATE_END = '2023-01-01T00:00:00.000Z';
|
||||
|
||||
describe('buildQuery()', () => {
|
||||
it('should return a valid query for occurrences', () => {
|
||||
|
@ -18,7 +20,8 @@ describe('buildQuery()', () => {
|
|||
indicator: createKQLCustomIndicator(),
|
||||
});
|
||||
const rule = createBurnRateRule(slo);
|
||||
expect(buildQuery(STARTED_AT, slo, rule)).toMatchSnapshot();
|
||||
const burnRateWindows = getBurnRateWindows(rule.windows);
|
||||
expect(buildQuery(slo, DATE_START, DATE_END, burnRateWindows)).toMatchSnapshot();
|
||||
});
|
||||
it('should return a valid query with afterKey', () => {
|
||||
const slo = createSLO({
|
||||
|
@ -26,7 +29,12 @@ describe('buildQuery()', () => {
|
|||
indicator: createKQLCustomIndicator(),
|
||||
});
|
||||
const rule = createBurnRateRule(slo);
|
||||
expect(buildQuery(STARTED_AT, slo, rule, { instanceId: 'example' })).toMatchSnapshot();
|
||||
const burnRateWindows = getBurnRateWindows(rule.windows);
|
||||
expect(
|
||||
buildQuery(slo, DATE_START, DATE_END, burnRateWindows, {
|
||||
instanceId: 'example',
|
||||
})
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
it('should return a valid query for timeslices', () => {
|
||||
const slo = createSLO({
|
||||
|
@ -35,6 +43,7 @@ describe('buildQuery()', () => {
|
|||
budgetingMethod: 'timeslices',
|
||||
});
|
||||
const rule = createBurnRateRule(slo);
|
||||
expect(buildQuery(STARTED_AT, slo, rule)).toMatchSnapshot();
|
||||
const burnRateWindows = getBurnRateWindows(rule.windows);
|
||||
expect(buildQuery(slo, DATE_START, DATE_END, burnRateWindows)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
|
||||
import moment from 'moment';
|
||||
import { timeslicesBudgetingMethodSchema } from '@kbn/slo-schema';
|
||||
import { Duration, SLO, toDurationUnit, toMomentUnitOfTime } from '../../../../domain/models';
|
||||
import { BurnRateRuleParams, WindowSchema } from '../types';
|
||||
import { Duration, SLO, toMomentUnitOfTime } from '../../../../domain/models';
|
||||
import { WindowSchema } from '../types';
|
||||
|
||||
type BurnRateWindowWithDuration = WindowSchema & {
|
||||
export type BurnRateWindowWithDuration = WindowSchema & {
|
||||
longDuration: Duration;
|
||||
shortDuration: Duration;
|
||||
};
|
||||
|
@ -99,7 +99,11 @@ function buildWindowAgg(
|
|||
};
|
||||
}
|
||||
|
||||
function buildWindowAggs(startedAt: Date, slo: SLO, burnRateWindows: BurnRateWindowWithDuration[]) {
|
||||
function buildWindowAggs(
|
||||
startedAt: string,
|
||||
slo: SLO,
|
||||
burnRateWindows: BurnRateWindowWithDuration[]
|
||||
) {
|
||||
return burnRateWindows.reduce((acc, winDef, index) => {
|
||||
const shortDateRange = getLookbackDateRange(startedAt, winDef.shortDuration);
|
||||
const longDateRange = getLookbackDateRange(startedAt, winDef.longDuration);
|
||||
|
@ -150,27 +154,12 @@ function buildEvaluation(burnRateWindows: BurnRateWindowWithDuration[]) {
|
|||
}
|
||||
|
||||
export function buildQuery(
|
||||
startedAt: Date,
|
||||
slo: SLO,
|
||||
params: BurnRateRuleParams,
|
||||
dateStart: string,
|
||||
dateEnd: string,
|
||||
burnRateWindows: BurnRateWindowWithDuration[],
|
||||
afterKey?: EvaluationAfterKey
|
||||
) {
|
||||
const burnRateWindows = params.windows.map((winDef) => {
|
||||
return {
|
||||
...winDef,
|
||||
longDuration: new Duration(winDef.longWindow.value, toDurationUnit(winDef.longWindow.unit)),
|
||||
shortDuration: new Duration(
|
||||
winDef.shortWindow.value,
|
||||
toDurationUnit(winDef.shortWindow.unit)
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const longestLookbackWindow = burnRateWindows.reduce((acc, winDef) => {
|
||||
return winDef.longDuration.isShorterThan(acc.longDuration) ? acc : winDef;
|
||||
}, burnRateWindows[0]);
|
||||
const longestDateRange = getLookbackDateRange(startedAt, longestLookbackWindow.longDuration);
|
||||
|
||||
return {
|
||||
size: 0,
|
||||
query: {
|
||||
|
@ -181,8 +170,8 @@ export function buildQuery(
|
|||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: longestDateRange.from.toISOString(),
|
||||
lt: longestDateRange.to.toISOString(),
|
||||
gte: dateStart,
|
||||
lt: dateEnd,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -197,7 +186,7 @@ export function buildQuery(
|
|||
sources: [{ instanceId: { terms: { field: 'slo.instanceId' } } }],
|
||||
},
|
||||
aggs: {
|
||||
...buildWindowAggs(startedAt, slo, burnRateWindows),
|
||||
...buildWindowAggs(dateEnd, slo, burnRateWindows),
|
||||
...buildEvaluation(burnRateWindows),
|
||||
},
|
||||
},
|
||||
|
@ -205,9 +194,9 @@ export function buildQuery(
|
|||
};
|
||||
}
|
||||
|
||||
function getLookbackDateRange(startedAt: Date, duration: Duration): { from: Date; to: Date } {
|
||||
function getLookbackDateRange(startedAt: string, duration: Duration): { from: Date; to: Date } {
|
||||
const unit = toMomentUnitOfTime(duration.unit);
|
||||
const now = moment(startedAt).startOf('minute');
|
||||
const now = moment(startedAt);
|
||||
const from = now.clone().subtract(duration.value, unit);
|
||||
const to = now.clone();
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import { BurnRateRuleParams } from '../types';
|
|||
import { SLO_DESTINATION_INDEX_PATTERN } from '../../../../assets/constants';
|
||||
import {
|
||||
buildQuery,
|
||||
BurnRateWindowWithDuration,
|
||||
EvaluationAfterKey,
|
||||
generateAboveThresholdKey,
|
||||
generateBurnRateKey,
|
||||
|
@ -65,12 +66,13 @@ export interface EvalutionAggResults {
|
|||
async function queryAllResults(
|
||||
esClient: ElasticsearchClient,
|
||||
slo: SLO,
|
||||
params: BurnRateRuleParams,
|
||||
startedAt: Date,
|
||||
dateStart: string,
|
||||
dateEnd: string,
|
||||
burnRateWindows: BurnRateWindowWithDuration[],
|
||||
buckets: EvaluationBucket[] = [],
|
||||
lastAfterKey?: { instanceId: string }
|
||||
): Promise<EvaluationBucket[]> {
|
||||
const queryAndAggs = buildQuery(startedAt, slo, params, lastAfterKey);
|
||||
const queryAndAggs = buildQuery(slo, dateStart, dateEnd, burnRateWindows, lastAfterKey);
|
||||
const results = await esClient.search<undefined, EvalutionAggResults>({
|
||||
index: SLO_DESTINATION_INDEX_PATTERN,
|
||||
...queryAndAggs,
|
||||
|
@ -84,8 +86,9 @@ async function queryAllResults(
|
|||
return queryAllResults(
|
||||
esClient,
|
||||
slo,
|
||||
params,
|
||||
startedAt,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
burnRateWindows,
|
||||
[...buckets, ...results.aggregations.instances.buckets],
|
||||
results.aggregations.instances.after_key
|
||||
);
|
||||
|
@ -95,9 +98,11 @@ export async function evaluate(
|
|||
esClient: ElasticsearchClient,
|
||||
slo: SLO,
|
||||
params: BurnRateRuleParams,
|
||||
startedAt: Date
|
||||
dateStart: string,
|
||||
dateEnd: string,
|
||||
burnRateWindows: BurnRateWindowWithDuration[]
|
||||
) {
|
||||
const buckets = await queryAllResults(esClient, slo, params, startedAt);
|
||||
const buckets = await queryAllResults(esClient, slo, dateStart, dateEnd, burnRateWindows);
|
||||
return transformBucketToResults(buckets, params);
|
||||
}
|
||||
|
||||
|
|
|
@ -148,6 +148,10 @@ function createRule(shouldWriteAlerts: boolean = true) {
|
|||
startedAt,
|
||||
state,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange: () => {
|
||||
const date = new Date(Date.now()).toISOString();
|
||||
return { dateStart: date, dateEnd: date };
|
||||
},
|
||||
})) ?? {}) as Record<string, any>);
|
||||
|
||||
previousStartedAt = startedAt;
|
||||
|
|
|
@ -96,4 +96,8 @@ export const createDefaultAlertExecutorOptions = <
|
|||
logger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
...(maintenanceWindowIds ? { maintenanceWindowIds } : {}),
|
||||
getTimeRange: () => {
|
||||
const date = new Date(Date.now()).toISOString();
|
||||
return { dateStart: date, dateEnd: date };
|
||||
},
|
||||
});
|
||||
|
|
|
@ -72,6 +72,10 @@ describe('legacyRules_notification_alert_type', () => {
|
|||
},
|
||||
logger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange: () => {
|
||||
const date = new Date('2019-12-14T16:40:33.400Z').toISOString();
|
||||
return { dateStart: date, dateEnd: date };
|
||||
},
|
||||
};
|
||||
|
||||
alert = legacyRulesNotificationAlertType({
|
||||
|
|
|
@ -284,6 +284,10 @@ export const previewRulesRoute = async (
|
|||
state: statePreview,
|
||||
logger,
|
||||
flappingSettings: DISABLE_FLAPPING_SETTINGS,
|
||||
getTimeRange: () => {
|
||||
const date = startedAt.toISOString();
|
||||
return { dateStart: date, dateEnd: date };
|
||||
},
|
||||
})) as { state: TState });
|
||||
|
||||
const errors = loggedStatusChanges
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
|
||||
import { of } from 'rxjs';
|
||||
import { CoreSetup } from '@kbn/core/server';
|
||||
import { executor, getSearchParams, getValidTimefieldSort, tryToParseAsDate } from './executor';
|
||||
import { ExecutorOptions, OnlyEsQueryRuleParams } from './types';
|
||||
import { executor, getValidTimefieldSort, tryToParseAsDate } from './executor';
|
||||
import { ExecutorOptions } from './types';
|
||||
import { Comparator } from '../../../common/comparator_types';
|
||||
import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
|
@ -117,6 +117,10 @@ describe('es_query executor', () => {
|
|||
state: { latestTimestamp: undefined },
|
||||
spaceId: 'default',
|
||||
logger,
|
||||
getTimeRange: () => {
|
||||
const date = new Date(Date.now()).toISOString();
|
||||
return { dateStart: date, dateEnd: date };
|
||||
},
|
||||
} as unknown as ExecutorOptions<EsQueryRuleParams>;
|
||||
|
||||
it('should throw error for invalid comparator', async () => {
|
||||
|
@ -141,8 +145,6 @@ describe('es_query executor', () => {
|
|||
],
|
||||
truncated: false,
|
||||
},
|
||||
dateStart: new Date().toISOString(),
|
||||
dateEnd: new Date().toISOString(),
|
||||
});
|
||||
await executor(coreMock, defaultExecutorOptions);
|
||||
expect(mockFetchEsQuery).toHaveBeenCalledWith({
|
||||
|
@ -157,6 +159,8 @@ describe('es_query executor', () => {
|
|||
scopedClusterClient: scopedClusterClientMock,
|
||||
logger,
|
||||
},
|
||||
dateStart: new Date().toISOString(),
|
||||
dateEnd: new Date().toISOString(),
|
||||
});
|
||||
expect(mockFetchSearchSourceQuery).not.toHaveBeenCalled();
|
||||
});
|
||||
|
@ -173,8 +177,6 @@ describe('es_query executor', () => {
|
|||
],
|
||||
truncated: false,
|
||||
},
|
||||
dateStart: new Date().toISOString(),
|
||||
dateEnd: new Date().toISOString(),
|
||||
});
|
||||
await executor(coreMock, {
|
||||
...defaultExecutorOptions,
|
||||
|
@ -191,6 +193,8 @@ describe('es_query executor', () => {
|
|||
share: undefined,
|
||||
},
|
||||
spacePrefix: '',
|
||||
dateStart: new Date().toISOString(),
|
||||
dateEnd: new Date().toISOString(),
|
||||
});
|
||||
expect(mockFetchEsQuery).not.toHaveBeenCalled();
|
||||
});
|
||||
|
@ -207,8 +211,6 @@ describe('es_query executor', () => {
|
|||
],
|
||||
truncated: false,
|
||||
},
|
||||
dateStart: new Date().toISOString(),
|
||||
dateEnd: new Date().toISOString(),
|
||||
});
|
||||
await executor(coreMock, {
|
||||
...defaultExecutorOptions,
|
||||
|
@ -222,10 +224,11 @@ describe('es_query executor', () => {
|
|||
scopedClusterClient: scopedClusterClientMock,
|
||||
logger,
|
||||
share: undefined,
|
||||
dataViews: undefined,
|
||||
},
|
||||
spacePrefix: '',
|
||||
publicBaseUrl: 'https://localhost:5601',
|
||||
dateStart: new Date().toISOString(),
|
||||
dateEnd: new Date().toISOString(),
|
||||
});
|
||||
expect(mockFetchEsQuery).not.toHaveBeenCalled();
|
||||
expect(mockFetchSearchSourceQuery).not.toHaveBeenCalled();
|
||||
|
@ -243,8 +246,6 @@ describe('es_query executor', () => {
|
|||
],
|
||||
truncated: false,
|
||||
},
|
||||
dateStart: new Date().toISOString(),
|
||||
dateEnd: new Date().toISOString(),
|
||||
});
|
||||
await executor(coreMock, {
|
||||
...defaultExecutorOptions,
|
||||
|
@ -269,8 +270,6 @@ describe('es_query executor', () => {
|
|||
],
|
||||
truncated: false,
|
||||
},
|
||||
dateStart: new Date().toISOString(),
|
||||
dateEnd: new Date().toISOString(),
|
||||
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
});
|
||||
await executor(coreMock, {
|
||||
|
@ -343,8 +342,6 @@ describe('es_query executor', () => {
|
|||
],
|
||||
truncated: false,
|
||||
},
|
||||
dateStart: new Date().toISOString(),
|
||||
dateEnd: new Date().toISOString(),
|
||||
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
});
|
||||
await executor(coreMock, {
|
||||
|
@ -491,8 +488,6 @@ describe('es_query executor', () => {
|
|||
],
|
||||
truncated: false,
|
||||
},
|
||||
dateStart: new Date().toISOString(),
|
||||
dateEnd: new Date().toISOString(),
|
||||
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
});
|
||||
await executor(coreMock, {
|
||||
|
@ -568,8 +563,6 @@ describe('es_query executor', () => {
|
|||
],
|
||||
truncated: true,
|
||||
},
|
||||
dateStart: new Date().toISOString(),
|
||||
dateEnd: new Date().toISOString(),
|
||||
});
|
||||
await executor(coreMock, {
|
||||
...defaultExecutorOptions,
|
||||
|
@ -611,8 +604,6 @@ describe('es_query executor', () => {
|
|||
],
|
||||
truncated: false,
|
||||
},
|
||||
dateStart: new Date().toISOString(),
|
||||
dateEnd: new Date().toISOString(),
|
||||
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
});
|
||||
await executor(coreMock, {
|
||||
|
@ -673,8 +664,6 @@ describe('es_query executor', () => {
|
|||
]);
|
||||
mockFetchEsQuery.mockResolvedValueOnce({
|
||||
parsedResults: { results: [], truncated: false },
|
||||
dateStart: new Date().toISOString(),
|
||||
dateEnd: new Date().toISOString(),
|
||||
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
});
|
||||
await executor(coreMock, {
|
||||
|
@ -771,8 +760,6 @@ describe('es_query executor', () => {
|
|||
results: [],
|
||||
truncated: false,
|
||||
},
|
||||
dateStart: new Date().toISOString(),
|
||||
dateEnd: new Date().toISOString(),
|
||||
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
});
|
||||
await executor(coreMock, {
|
||||
|
@ -848,36 +835,4 @@ describe('es_query executor', () => {
|
|||
expect(result).toEqual('2018-12-31T19:00:00.000Z');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSearchParams', () => {
|
||||
it('should return search params correctly', () => {
|
||||
const result = getSearchParams(defaultProps as OnlyEsQueryRuleParams);
|
||||
expect(result.parsedQuery.query).toBe('test-query');
|
||||
});
|
||||
|
||||
it('should throw invalid query error', () => {
|
||||
expect(() =>
|
||||
getSearchParams({ ...defaultProps, esQuery: '' } as OnlyEsQueryRuleParams)
|
||||
).toThrow('invalid query specified: "" - query must be JSON');
|
||||
});
|
||||
|
||||
it('should throw invalid query error due to missing query property', () => {
|
||||
expect(() =>
|
||||
getSearchParams({
|
||||
...defaultProps,
|
||||
esQuery: '{ "someProperty": "test-query" }',
|
||||
} as OnlyEsQueryRuleParams)
|
||||
).toThrow('invalid query specified: "{ "someProperty": "test-query" }" - query must be JSON');
|
||||
});
|
||||
|
||||
it('should throw invalid window size error', () => {
|
||||
expect(() =>
|
||||
getSearchParams({
|
||||
...defaultProps,
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'r',
|
||||
} as OnlyEsQueryRuleParams)
|
||||
).toThrow('invalid format for windowSize: "5r"');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
import { sha256 } from 'js-sha256';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CoreSetup } from '@kbn/core/server';
|
||||
import { parseDuration } from '@kbn/alerting-plugin/server';
|
||||
import { isGroupAggregation, UngroupedGroupId } from '@kbn/triggers-actions-ui-plugin/common';
|
||||
import { ALERT_EVALUATION_VALUE, ALERT_REASON, ALERT_URL } from '@kbn/rule-data-utils';
|
||||
|
||||
|
@ -41,6 +40,7 @@ export async function executor(core: CoreSetup, options: ExecutorOptions<EsQuery
|
|||
state,
|
||||
spaceId,
|
||||
logger,
|
||||
getTimeRange,
|
||||
} = options;
|
||||
const { alertsClient, scopedClusterClient, searchSourceClient, share, dataViews } = services;
|
||||
const currentTimestamp = new Date().toISOString();
|
||||
|
@ -60,7 +60,9 @@ export async function executor(core: CoreSetup, options: ExecutorOptions<EsQuery
|
|||
// avoid counting a document multiple times.
|
||||
// latestTimestamp will be ignored if set for grouped queries
|
||||
let latestTimestamp: string | undefined = tryToParseAsDate(state.latestTimestamp);
|
||||
const { parsedResults, dateStart, dateEnd, link } = searchSourceRule
|
||||
const { dateStart, dateEnd } = getTimeRange(`${params.timeWindowSize}${params.timeWindowUnit}`);
|
||||
|
||||
const { parsedResults, link } = searchSourceRule
|
||||
? await fetchSearchSourceQuery({
|
||||
ruleId,
|
||||
alertLimit,
|
||||
|
@ -73,6 +75,8 @@ export async function executor(core: CoreSetup, options: ExecutorOptions<EsQuery
|
|||
logger,
|
||||
dataViews,
|
||||
},
|
||||
dateStart,
|
||||
dateEnd,
|
||||
})
|
||||
: esqlQueryRule
|
||||
? await fetchEsqlQuery({
|
||||
|
@ -85,8 +89,9 @@ export async function executor(core: CoreSetup, options: ExecutorOptions<EsQuery
|
|||
share,
|
||||
scopedClusterClient,
|
||||
logger,
|
||||
dataViews,
|
||||
},
|
||||
dateStart,
|
||||
dateEnd,
|
||||
})
|
||||
: await fetchEsQuery({
|
||||
ruleId,
|
||||
|
@ -100,6 +105,8 @@ export async function executor(core: CoreSetup, options: ExecutorOptions<EsQuery
|
|||
scopedClusterClient,
|
||||
logger,
|
||||
},
|
||||
dateStart,
|
||||
dateEnd,
|
||||
});
|
||||
const unmetGroupValues: Record<string, number> = {};
|
||||
for (const result of parsedResults.results) {
|
||||
|
@ -208,53 +215,6 @@ export async function executor(core: CoreSetup, options: ExecutorOptions<EsQuery
|
|||
return { state: { latestTimestamp } };
|
||||
}
|
||||
|
||||
function getInvalidWindowSizeError(windowValue: string) {
|
||||
return i18n.translate('xpack.stackAlerts.esQuery.invalidWindowSizeErrorMessage', {
|
||||
defaultMessage: 'invalid format for windowSize: "{windowValue}"',
|
||||
values: {
|
||||
windowValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getInvalidQueryError(query: string) {
|
||||
return i18n.translate('xpack.stackAlerts.esQuery.invalidQueryErrorMessage', {
|
||||
defaultMessage: 'invalid query specified: "{query}" - query must be JSON',
|
||||
values: {
|
||||
query,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getSearchParams(queryParams: OnlyEsQueryRuleParams) {
|
||||
const date = Date.now();
|
||||
const { esQuery, timeWindowSize, timeWindowUnit } = queryParams;
|
||||
|
||||
let parsedQuery;
|
||||
try {
|
||||
parsedQuery = JSON.parse(esQuery);
|
||||
} catch (err) {
|
||||
throw new Error(getInvalidQueryError(esQuery));
|
||||
}
|
||||
|
||||
if (parsedQuery && !parsedQuery.query) {
|
||||
throw new Error(getInvalidQueryError(esQuery));
|
||||
}
|
||||
|
||||
const window = `${timeWindowSize}${timeWindowUnit}`;
|
||||
let timeWindow: number;
|
||||
try {
|
||||
timeWindow = parseDuration(window);
|
||||
} catch (err) {
|
||||
throw new Error(getInvalidWindowSizeError(window));
|
||||
}
|
||||
|
||||
const dateStart = new Date(date - timeWindow).toISOString();
|
||||
const dateEnd = new Date(date).toISOString();
|
||||
|
||||
return { parsedQuery, dateStart, dateEnd };
|
||||
}
|
||||
|
||||
export function getValidTimefieldSort(
|
||||
sortValues: Array<string | number | null> = []
|
||||
): undefined | string {
|
||||
|
|
|
@ -10,7 +10,6 @@ import { Comparator } from '../../../../common/comparator_types';
|
|||
import { fetchEsQuery } from './fetch_es_query';
|
||||
import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { getSearchParams } from './get_search_params';
|
||||
|
||||
jest.mock('@kbn/triggers-actions-ui-plugin/common', () => {
|
||||
const actual = jest.requireActual('@kbn/triggers-actions-ui-plugin/common');
|
||||
|
@ -56,7 +55,8 @@ describe('fetchEsQuery', () => {
|
|||
};
|
||||
it('should add time filter if timestamp if defined and excludeHitsFromPreviousRun is true', async () => {
|
||||
const params = defaultParams;
|
||||
const { dateStart, dateEnd } = getSearchParams(params);
|
||||
const date = new Date().toISOString();
|
||||
|
||||
await fetchEsQuery({
|
||||
ruleId: 'abc',
|
||||
name: 'test-rule',
|
||||
|
@ -65,6 +65,8 @@ describe('fetchEsQuery', () => {
|
|||
services,
|
||||
spacePrefix: '',
|
||||
publicBaseUrl: '',
|
||||
dateStart: date,
|
||||
dateEnd: date,
|
||||
});
|
||||
expect(scopedClusterClientMock.asCurrentUser.search).toHaveBeenCalledWith(
|
||||
{
|
||||
|
@ -116,8 +118,8 @@ describe('fetchEsQuery', () => {
|
|||
range: {
|
||||
'@timestamp': {
|
||||
format: 'strict_date_optional_time',
|
||||
gte: dateStart,
|
||||
lte: dateEnd,
|
||||
gte: date,
|
||||
lte: date,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -147,7 +149,8 @@ describe('fetchEsQuery', () => {
|
|||
|
||||
it('should not add time filter if timestamp is undefined', async () => {
|
||||
const params = defaultParams;
|
||||
const { dateStart, dateEnd } = getSearchParams(params);
|
||||
const date = new Date().toISOString();
|
||||
|
||||
await fetchEsQuery({
|
||||
ruleId: 'abc',
|
||||
name: 'test-rule',
|
||||
|
@ -156,6 +159,8 @@ describe('fetchEsQuery', () => {
|
|||
services,
|
||||
spacePrefix: '',
|
||||
publicBaseUrl: '',
|
||||
dateStart: date,
|
||||
dateEnd: date,
|
||||
});
|
||||
expect(scopedClusterClientMock.asCurrentUser.search).toHaveBeenCalledWith(
|
||||
{
|
||||
|
@ -181,8 +186,8 @@ describe('fetchEsQuery', () => {
|
|||
range: {
|
||||
'@timestamp': {
|
||||
format: 'strict_date_optional_time',
|
||||
gte: dateStart,
|
||||
lte: dateEnd,
|
||||
gte: date,
|
||||
lte: date,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -212,7 +217,8 @@ describe('fetchEsQuery', () => {
|
|||
|
||||
it('should not add time filter if excludeHitsFromPreviousRun is false', async () => {
|
||||
const params = { ...defaultParams, excludeHitsFromPreviousRun: false };
|
||||
const { dateStart, dateEnd } = getSearchParams(params);
|
||||
const date = new Date().toISOString();
|
||||
|
||||
await fetchEsQuery({
|
||||
ruleId: 'abc',
|
||||
name: 'test-rule',
|
||||
|
@ -221,6 +227,8 @@ describe('fetchEsQuery', () => {
|
|||
services,
|
||||
spacePrefix: '',
|
||||
publicBaseUrl: '',
|
||||
dateStart: date,
|
||||
dateEnd: date,
|
||||
});
|
||||
expect(scopedClusterClientMock.asCurrentUser.search).toHaveBeenCalledWith(
|
||||
{
|
||||
|
@ -246,8 +254,8 @@ describe('fetchEsQuery', () => {
|
|||
range: {
|
||||
'@timestamp': {
|
||||
format: 'strict_date_optional_time',
|
||||
gte: dateStart,
|
||||
lte: dateEnd,
|
||||
gte: date,
|
||||
lte: date,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -277,7 +285,8 @@ describe('fetchEsQuery', () => {
|
|||
|
||||
it('should set size: 0 and top hits size to size parameter if grouping alerts', async () => {
|
||||
const params = { ...defaultParams, groupBy: 'top', termField: 'host.name', termSize: 10 };
|
||||
const { dateStart, dateEnd } = getSearchParams(params);
|
||||
const date = new Date().toISOString();
|
||||
|
||||
await fetchEsQuery({
|
||||
ruleId: 'abc',
|
||||
name: 'test-rule',
|
||||
|
@ -286,6 +295,8 @@ describe('fetchEsQuery', () => {
|
|||
services,
|
||||
spacePrefix: '',
|
||||
publicBaseUrl: '',
|
||||
dateStart: date,
|
||||
dateEnd: date,
|
||||
});
|
||||
expect(scopedClusterClientMock.asCurrentUser.search).toHaveBeenCalledWith(
|
||||
{
|
||||
|
@ -338,8 +349,8 @@ describe('fetchEsQuery', () => {
|
|||
range: {
|
||||
'@timestamp': {
|
||||
format: 'strict_date_optional_time',
|
||||
gte: dateStart,
|
||||
lte: dateEnd,
|
||||
gte: date,
|
||||
lte: date,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -16,7 +16,7 @@ import { ES_QUERY_ID } from '@kbn/rule-data-utils';
|
|||
import { getComparatorScript } from '../../../../common';
|
||||
import { OnlyEsQueryRuleParams } from '../types';
|
||||
import { buildSortedEventsQuery } from '../../../../common/build_sorted_events_query';
|
||||
import { getSearchParams } from './get_search_params';
|
||||
import { getParsedQuery } from '../util';
|
||||
|
||||
export interface FetchEsQueryOpts {
|
||||
ruleId: string;
|
||||
|
@ -30,6 +30,8 @@ export interface FetchEsQueryOpts {
|
|||
logger: Logger;
|
||||
};
|
||||
alertLimit?: number;
|
||||
dateStart: string;
|
||||
dateEnd: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -44,17 +46,20 @@ export async function fetchEsQuery({
|
|||
timestamp,
|
||||
services,
|
||||
alertLimit,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
}: FetchEsQueryOpts) {
|
||||
const { scopedClusterClient, logger } = services;
|
||||
const esClient = scopedClusterClient.asCurrentUser;
|
||||
const isGroupAgg = isGroupAggregation(params.termField);
|
||||
const isCountAgg = isCountAggregation(params.aggType);
|
||||
const {
|
||||
query,
|
||||
fields,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
parsedQuery: { query, fields, runtime_mappings, _source },
|
||||
dateStart,
|
||||
dateEnd,
|
||||
} = getSearchParams(params);
|
||||
runtime_mappings,
|
||||
_source,
|
||||
} = getParsedQuery(params);
|
||||
|
||||
const filter =
|
||||
timestamp && params.excludeHitsFromPreviousRun
|
||||
|
@ -136,8 +141,6 @@ export async function fetchEsQuery({
|
|||
esResult: searchResult,
|
||||
resultLimit: alertLimit,
|
||||
}),
|
||||
dateStart,
|
||||
dateEnd,
|
||||
link,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,26 +6,15 @@
|
|||
*/
|
||||
|
||||
import { OnlyEsqlQueryRuleParams } from '../types';
|
||||
import { stubbedSavedObjectIndexPattern } from '@kbn/data-views-plugin/common/data_view.stub';
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';
|
||||
import { Comparator } from '../../../../common/comparator_types';
|
||||
import { getEsqlQuery } from './fetch_esql_query';
|
||||
|
||||
const createDataView = () => {
|
||||
const id = 'test-id';
|
||||
const {
|
||||
type,
|
||||
version,
|
||||
attributes: { timeFieldName, fields, title },
|
||||
} = stubbedSavedObjectIndexPattern(id);
|
||||
const getTimeRange = () => {
|
||||
const date = Date.now();
|
||||
const dateStart = new Date(date - 300000).toISOString();
|
||||
const dateEnd = new Date(date).toISOString();
|
||||
|
||||
return new DataView({
|
||||
spec: { id, type, version, timeFieldName, fields: JSON.parse(fields), title },
|
||||
fieldFormats: fieldFormatsMock,
|
||||
shortDotsEnable: false,
|
||||
metaFields: ['_id', '_type', '_score'],
|
||||
});
|
||||
return { dateStart, dateEnd };
|
||||
};
|
||||
|
||||
const defaultParams: OnlyEsqlQueryRuleParams = {
|
||||
|
@ -44,7 +33,6 @@ const defaultParams: OnlyEsqlQueryRuleParams = {
|
|||
|
||||
describe('fetchEsqlQuery', () => {
|
||||
describe('getEsqlQuery', () => {
|
||||
const dataViewMock = createDataView();
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
@ -58,7 +46,8 @@ describe('fetchEsqlQuery', () => {
|
|||
|
||||
it('should generate the correct query', async () => {
|
||||
const params = defaultParams;
|
||||
const { query, dateStart, dateEnd } = getEsqlQuery(dataViewMock, params, undefined);
|
||||
const { dateStart, dateEnd } = getTimeRange();
|
||||
const query = getEsqlQuery(params, undefined, dateStart, dateEnd);
|
||||
|
||||
expect(query).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -80,13 +69,12 @@ describe('fetchEsqlQuery', () => {
|
|||
"query": "from test",
|
||||
}
|
||||
`);
|
||||
expect(dateStart).toMatch('2020-02-09T23:10:41.941Z');
|
||||
expect(dateEnd).toMatch('2020-02-09T23:15:41.941Z');
|
||||
});
|
||||
|
||||
it('should generate the correct query with the alertLimit', async () => {
|
||||
const params = defaultParams;
|
||||
const { query, dateStart, dateEnd } = getEsqlQuery(dataViewMock, params, 100);
|
||||
const { dateStart, dateEnd } = getTimeRange();
|
||||
const query = getEsqlQuery(params, 100, dateStart, dateEnd);
|
||||
|
||||
expect(query).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -108,8 +96,6 @@ describe('fetchEsqlQuery', () => {
|
|||
"query": "from test | limit 100",
|
||||
}
|
||||
`);
|
||||
expect(dateStart).toMatch('2020-02-09T23:10:41.941Z');
|
||||
expect(dateEnd).toMatch('2020-02-09T23:15:41.941Z');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DataView, DataViewsContract, getTime } from '@kbn/data-plugin/common';
|
||||
import { parseAggregationResults } from '@kbn/triggers-actions-ui-plugin/common';
|
||||
import { SharePluginStart } from '@kbn/share-plugin/server';
|
||||
import { IScopedClusterClient, Logger } from '@kbn/core/server';
|
||||
|
@ -22,8 +21,9 @@ export interface FetchEsqlQueryOpts {
|
|||
logger: Logger;
|
||||
scopedClusterClient: IScopedClusterClient;
|
||||
share: SharePluginStart;
|
||||
dataViews: DataViewsContract;
|
||||
};
|
||||
dateStart: string;
|
||||
dateEnd: string;
|
||||
}
|
||||
|
||||
export async function fetchEsqlQuery({
|
||||
|
@ -33,14 +33,12 @@ export async function fetchEsqlQuery({
|
|||
services,
|
||||
spacePrefix,
|
||||
publicBaseUrl,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
}: FetchEsqlQueryOpts) {
|
||||
const { logger, scopedClusterClient, dataViews } = services;
|
||||
const { logger, scopedClusterClient } = services;
|
||||
const esClient = scopedClusterClient.asCurrentUser;
|
||||
const dataView = await dataViews.create({
|
||||
timeFieldName: params.timeField,
|
||||
});
|
||||
|
||||
const { query, dateStart, dateEnd } = getEsqlQuery(dataView, params, alertLimit);
|
||||
const query = getEsqlQuery(params, alertLimit, dateStart, dateEnd);
|
||||
|
||||
logger.debug(`ES|QL query rule (${ruleId}) query: ${JSON.stringify(query)}`);
|
||||
|
||||
|
@ -66,23 +64,15 @@ export async function fetchEsqlQuery({
|
|||
},
|
||||
resultLimit: alertLimit,
|
||||
}),
|
||||
dateStart,
|
||||
dateEnd,
|
||||
};
|
||||
}
|
||||
|
||||
export const getEsqlQuery = (
|
||||
dataView: DataView,
|
||||
params: OnlyEsqlQueryRuleParams,
|
||||
alertLimit: number | undefined
|
||||
alertLimit: number | undefined,
|
||||
dateStart: string,
|
||||
dateEnd: string
|
||||
) => {
|
||||
const timeRange = {
|
||||
from: `now-${params.timeWindowSize}${params.timeWindowUnit}`,
|
||||
to: 'now',
|
||||
};
|
||||
const timerangeFilter = getTime(dataView, timeRange);
|
||||
const dateStart = timerangeFilter?.query.range[params.timeField].gte;
|
||||
const dateEnd = timerangeFilter?.query.range[params.timeField].lte;
|
||||
const rangeFilter: unknown[] = [
|
||||
{
|
||||
range: {
|
||||
|
@ -103,9 +93,5 @@ export const getEsqlQuery = (
|
|||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
query,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
};
|
||||
return query;
|
||||
};
|
||||
|
|
|
@ -32,6 +32,14 @@ const createDataView = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const getTimeRange = () => {
|
||||
const date = Date.now();
|
||||
const dateStart = new Date(date - 300000).toISOString();
|
||||
const dateEnd = new Date(date).toISOString();
|
||||
|
||||
return { dateStart, dateEnd };
|
||||
};
|
||||
|
||||
const defaultParams: OnlySearchSourceRuleParams = {
|
||||
size: 100,
|
||||
timeWindowSize: 5,
|
||||
|
@ -65,11 +73,14 @@ describe('fetchSearchSourceQuery', () => {
|
|||
|
||||
const searchSourceInstance = createSearchSourceMock({ index: dataViewMock });
|
||||
|
||||
const { searchSource, dateStart, dateEnd } = updateSearchSource(
|
||||
const { dateStart, dateEnd } = getTimeRange();
|
||||
const searchSource = updateSearchSource(
|
||||
searchSourceInstance,
|
||||
dataViewMock,
|
||||
params,
|
||||
undefined
|
||||
undefined,
|
||||
dateStart,
|
||||
dateEnd
|
||||
);
|
||||
const searchRequest = searchSource.getSearchRequestBody();
|
||||
expect(searchRequest.size).toMatchInlineSnapshot(`100`);
|
||||
|
@ -94,8 +105,6 @@ describe('fetchSearchSourceQuery', () => {
|
|||
}
|
||||
`);
|
||||
expect(searchRequest.aggs).toMatchInlineSnapshot(`Object {}`);
|
||||
expect(dateStart).toMatch('2020-02-09T23:10:41.941Z');
|
||||
expect(dateEnd).toMatch('2020-02-09T23:15:41.941Z');
|
||||
});
|
||||
|
||||
it('with latest timestamp in between the given time range ', async () => {
|
||||
|
@ -103,11 +112,14 @@ describe('fetchSearchSourceQuery', () => {
|
|||
|
||||
const searchSourceInstance = createSearchSourceMock({ index: dataViewMock });
|
||||
|
||||
const { searchSource } = updateSearchSource(
|
||||
const { dateStart, dateEnd } = getTimeRange();
|
||||
const searchSource = updateSearchSource(
|
||||
searchSourceInstance,
|
||||
dataViewMock,
|
||||
params,
|
||||
'2020-02-09T23:12:41.941Z'
|
||||
'2020-02-09T23:12:41.941Z',
|
||||
dateStart,
|
||||
dateEnd
|
||||
);
|
||||
const searchRequest = searchSource.getSearchRequestBody();
|
||||
expect(searchRequest.size).toMatchInlineSnapshot(`100`);
|
||||
|
@ -147,11 +159,14 @@ describe('fetchSearchSourceQuery', () => {
|
|||
|
||||
const searchSourceInstance = createSearchSourceMock({ index: dataViewMock });
|
||||
|
||||
const { searchSource } = updateSearchSource(
|
||||
const { dateStart, dateEnd } = getTimeRange();
|
||||
const searchSource = updateSearchSource(
|
||||
searchSourceInstance,
|
||||
dataViewMock,
|
||||
params,
|
||||
'2020-01-09T22:12:41.941Z'
|
||||
'2020-01-09T22:12:41.941Z',
|
||||
dateStart,
|
||||
dateEnd
|
||||
);
|
||||
const searchRequest = searchSource.getSearchRequestBody();
|
||||
expect(searchRequest.size).toMatchInlineSnapshot(`100`);
|
||||
|
@ -183,11 +198,14 @@ describe('fetchSearchSourceQuery', () => {
|
|||
|
||||
const searchSourceInstance = createSearchSourceMock({ index: dataViewMock });
|
||||
|
||||
const { searchSource } = updateSearchSource(
|
||||
const { dateStart, dateEnd } = getTimeRange();
|
||||
const searchSource = updateSearchSource(
|
||||
searchSourceInstance,
|
||||
dataViewMock,
|
||||
params,
|
||||
'2020-02-09T23:12:41.941Z'
|
||||
'2020-02-09T23:12:41.941Z',
|
||||
dateStart,
|
||||
dateEnd
|
||||
);
|
||||
const searchRequest = searchSource.getSearchRequestBody();
|
||||
expect(searchRequest.size).toMatchInlineSnapshot(`100`);
|
||||
|
@ -225,11 +243,14 @@ describe('fetchSearchSourceQuery', () => {
|
|||
|
||||
const searchSourceInstance = createSearchSourceMock({ index: dataViewMock });
|
||||
|
||||
const { searchSource } = updateSearchSource(
|
||||
const { dateStart, dateEnd } = getTimeRange();
|
||||
const searchSource = updateSearchSource(
|
||||
searchSourceInstance,
|
||||
dataViewMock,
|
||||
params,
|
||||
'2020-02-09T23:12:41.941Z'
|
||||
'2020-02-09T23:12:41.941Z',
|
||||
dateStart,
|
||||
dateEnd
|
||||
);
|
||||
const searchRequest = searchSource.getSearchRequestBody();
|
||||
expect(searchRequest.size).toMatchInlineSnapshot(`0`);
|
||||
|
|
|
@ -9,7 +9,6 @@ import { buildRangeFilter, Filter } from '@kbn/es-query';
|
|||
import {
|
||||
DataView,
|
||||
DataViewsContract,
|
||||
getTime,
|
||||
ISearchSource,
|
||||
ISearchStartSearchSource,
|
||||
SortDirection,
|
||||
|
@ -40,6 +39,8 @@ export interface FetchSearchSourceQueryOpts {
|
|||
share: SharePluginStart;
|
||||
dataViews: DataViewsContract;
|
||||
};
|
||||
dateStart: string;
|
||||
dateEnd: string;
|
||||
}
|
||||
|
||||
export async function fetchSearchSourceQuery({
|
||||
|
@ -49,6 +50,8 @@ export async function fetchSearchSourceQuery({
|
|||
latestTimestamp,
|
||||
spacePrefix,
|
||||
services,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
}: FetchSearchSourceQueryOpts) {
|
||||
const { logger, searchSourceClient } = services;
|
||||
const isGroupAgg = isGroupAggregation(params.termField);
|
||||
|
@ -57,11 +60,13 @@ export async function fetchSearchSourceQuery({
|
|||
const initialSearchSource = await searchSourceClient.create(params.searchConfiguration);
|
||||
|
||||
const index = initialSearchSource.getField('index') as DataView;
|
||||
const { searchSource, dateStart, dateEnd } = updateSearchSource(
|
||||
const searchSource = updateSearchSource(
|
||||
initialSearchSource,
|
||||
index,
|
||||
params,
|
||||
latestTimestamp,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
alertLimit
|
||||
);
|
||||
|
||||
|
@ -87,8 +92,6 @@ export async function fetchSearchSourceQuery({
|
|||
numMatches: Number(searchResult.hits.total),
|
||||
searchResult,
|
||||
parsedResults: parseAggregationResults({ isCountAgg, isGroupAgg, esResult: searchResult }),
|
||||
dateStart,
|
||||
dateEnd,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -97,6 +100,8 @@ export function updateSearchSource(
|
|||
index: DataView,
|
||||
params: OnlySearchSourceRuleParams,
|
||||
latestTimestamp: string | undefined,
|
||||
dateStart: string,
|
||||
dateEnd: string,
|
||||
alertLimit?: number
|
||||
) {
|
||||
const isGroupAgg = isGroupAggregation(params.termField);
|
||||
|
@ -108,20 +113,19 @@ export function updateSearchSource(
|
|||
|
||||
searchSource.setField('size', isGroupAgg ? 0 : params.size);
|
||||
|
||||
const timeRange = {
|
||||
from: `now-${params.timeWindowSize}${params.timeWindowUnit}`,
|
||||
to: 'now',
|
||||
};
|
||||
const timerangeFilter = getTime(index, timeRange);
|
||||
const dateStart = timerangeFilter?.query.range[timeFieldName].gte;
|
||||
const dateEnd = timerangeFilter?.query.range[timeFieldName].lte;
|
||||
const filters = [timerangeFilter];
|
||||
const field = index.fields.find((f) => f.name === timeFieldName);
|
||||
const filters = [
|
||||
buildRangeFilter(
|
||||
field!,
|
||||
{ lte: dateEnd, gte: dateStart, format: 'strict_date_optional_time' },
|
||||
index
|
||||
),
|
||||
];
|
||||
|
||||
if (params.excludeHitsFromPreviousRun) {
|
||||
if (latestTimestamp && latestTimestamp > dateStart) {
|
||||
// add additional filter for documents with a timestamp greater then
|
||||
// the timestamp of the previous run, so that those documents are not counted twice
|
||||
const field = index.fields.find((f) => f.name === timeFieldName);
|
||||
const addTimeRangeField = buildRangeFilter(
|
||||
field!,
|
||||
{ gt: latestTimestamp, format: 'strict_date_optional_time' },
|
||||
|
@ -159,11 +163,7 @@ export function updateSearchSource(
|
|||
...(isGroupAgg ? { topHitsSize: params.size } : {}),
|
||||
})
|
||||
);
|
||||
return {
|
||||
searchSource: searchSourceChild,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
};
|
||||
return searchSourceChild;
|
||||
}
|
||||
|
||||
async function generateLink(
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { parseDuration } from '@kbn/alerting-plugin/common';
|
||||
import { OnlyEsQueryRuleParams } from '../types';
|
||||
|
||||
export function getSearchParams(queryParams: OnlyEsQueryRuleParams) {
|
||||
const date = Date.now();
|
||||
const { esQuery, timeWindowSize, timeWindowUnit } = queryParams;
|
||||
|
||||
let parsedQuery;
|
||||
try {
|
||||
parsedQuery = JSON.parse(esQuery);
|
||||
} catch (err) {
|
||||
throw new Error(getInvalidQueryError(esQuery));
|
||||
}
|
||||
|
||||
if (parsedQuery && !parsedQuery.query) {
|
||||
throw new Error(getInvalidQueryError(esQuery));
|
||||
}
|
||||
|
||||
const window = `${timeWindowSize}${timeWindowUnit}`;
|
||||
let timeWindow: number;
|
||||
try {
|
||||
timeWindow = parseDuration(window);
|
||||
} catch (err) {
|
||||
throw new Error(getInvalidWindowSizeError(window));
|
||||
}
|
||||
|
||||
const dateStart = new Date(date - timeWindow).toISOString();
|
||||
const dateEnd = new Date(date).toISOString();
|
||||
|
||||
return { parsedQuery, dateStart, dateEnd };
|
||||
}
|
||||
|
||||
function getInvalidWindowSizeError(windowValue: string) {
|
||||
return i18n.translate('xpack.stackAlerts.esQuery.invalidWindowSizeErrorMessage', {
|
||||
defaultMessage: 'invalid format for windowSize: "{windowValue}"',
|
||||
values: {
|
||||
windowValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getInvalidQueryError(query: string) {
|
||||
return i18n.translate('xpack.stackAlerts.esQuery.invalidQueryErrorMessage', {
|
||||
defaultMessage: 'invalid query specified: "{query}" - query must be JSON',
|
||||
values: {
|
||||
query,
|
||||
},
|
||||
});
|
||||
}
|
|
@ -10,8 +10,6 @@ import type { Writable } from '@kbn/utility-types';
|
|||
import { RuleExecutorServices } from '@kbn/alerting-plugin/server';
|
||||
import { RuleExecutorServicesMock, alertsMock } from '@kbn/alerting-plugin/server/mocks';
|
||||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import type { DataViewSpec } from '@kbn/data-views-plugin/common';
|
||||
import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub';
|
||||
import { getRuleType } from './rule_type';
|
||||
import { EsQueryRuleParams, EsQueryRuleState } from './rule_type_params';
|
||||
import { ActionContext } from './action_context';
|
||||
|
@ -566,31 +564,32 @@ describe('ruleType', () => {
|
|||
});
|
||||
|
||||
describe('search source query', () => {
|
||||
const dataViewMock = createStubDataView({
|
||||
spec: {
|
||||
id: 'test-id',
|
||||
title: 'test-title',
|
||||
timeFieldName: 'time-field',
|
||||
fields: {
|
||||
message: {
|
||||
name: 'message',
|
||||
type: 'string',
|
||||
scripted: false,
|
||||
searchable: false,
|
||||
aggregatable: false,
|
||||
readFromDocValues: false,
|
||||
},
|
||||
timestamp: {
|
||||
name: 'timestamp',
|
||||
type: 'date',
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: false,
|
||||
readFromDocValues: false,
|
||||
},
|
||||
const dataViewMock = {
|
||||
id: 'test-id',
|
||||
title: 'test-title',
|
||||
timeFieldName: 'timestamp',
|
||||
fields: [
|
||||
{
|
||||
name: 'message',
|
||||
type: 'string',
|
||||
displayName: 'message',
|
||||
scripted: false,
|
||||
filterable: false,
|
||||
aggregatable: false,
|
||||
},
|
||||
{
|
||||
name: 'timestamp',
|
||||
type: 'date',
|
||||
displayName: 'timestamp',
|
||||
scripted: false,
|
||||
filterable: false,
|
||||
aggregatable: false,
|
||||
},
|
||||
],
|
||||
toSpec: () => {
|
||||
return { id: 'test-id', title: 'test-title', timeFieldName: 'timestamp', fields: [] };
|
||||
},
|
||||
});
|
||||
};
|
||||
const defaultParams: OnlySearchSourceRuleParams = {
|
||||
size: 100,
|
||||
timeWindowSize: 5,
|
||||
|
@ -632,9 +631,11 @@ describe('ruleType', () => {
|
|||
const searchResult: ESSearchResponse<unknown, {}> = generateResults([]);
|
||||
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
|
||||
(ruleServices.dataViews.create as jest.Mock).mockImplementationOnce((spec: DataViewSpec) =>
|
||||
createStubDataView({ spec })
|
||||
);
|
||||
(ruleServices.dataViews.create as jest.Mock).mockResolvedValueOnce({
|
||||
...dataViewMock.toSpec(),
|
||||
toSpec: () => dataViewMock.toSpec(),
|
||||
toMinimalSpec: () => dataViewMock.toSpec(),
|
||||
});
|
||||
(searchSourceInstanceMock.getField as jest.Mock).mockImplementation((name: string) => {
|
||||
if (name === 'index') {
|
||||
return dataViewMock;
|
||||
|
@ -669,9 +670,11 @@ describe('ruleType', () => {
|
|||
const params = { ...defaultParams, thresholdComparator: Comparator.GT_OR_EQ, threshold: [3] };
|
||||
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
|
||||
(ruleServices.dataViews.create as jest.Mock).mockImplementationOnce((spec: DataViewSpec) =>
|
||||
createStubDataView({ spec })
|
||||
);
|
||||
(ruleServices.dataViews.create as jest.Mock).mockResolvedValueOnce({
|
||||
...dataViewMock.toSpec(),
|
||||
toSpec: () => dataViewMock.toSpec(),
|
||||
toMinimalSpec: () => dataViewMock.toSpec(),
|
||||
});
|
||||
(searchSourceInstanceMock.getField as jest.Mock).mockImplementation((name: string) => {
|
||||
if (name === 'index') {
|
||||
return dataViewMock;
|
||||
|
@ -711,32 +714,6 @@ describe('ruleType', () => {
|
|||
});
|
||||
|
||||
describe('ESQL query', () => {
|
||||
const dataViewMock = {
|
||||
id: 'test-id',
|
||||
title: 'test-title',
|
||||
timeFieldName: 'time-field',
|
||||
fields: [
|
||||
{
|
||||
name: 'message',
|
||||
type: 'string',
|
||||
displayName: 'message',
|
||||
scripted: false,
|
||||
filterable: false,
|
||||
aggregatable: false,
|
||||
},
|
||||
{
|
||||
name: 'timestamp',
|
||||
type: 'date',
|
||||
displayName: 'timestamp',
|
||||
scripted: false,
|
||||
filterable: false,
|
||||
aggregatable: false,
|
||||
},
|
||||
],
|
||||
toSpec: () => {
|
||||
return { id: 'test-id', title: 'test-title', timeFieldName: 'timestamp', fields: [] };
|
||||
},
|
||||
};
|
||||
const defaultParams: OnlyEsqlQueryRuleParams = {
|
||||
size: 100,
|
||||
timeWindowSize: 5,
|
||||
|
@ -777,12 +754,6 @@ describe('ruleType', () => {
|
|||
it('rule executor handles no documents returned by ES', async () => {
|
||||
const params = defaultParams;
|
||||
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
|
||||
(ruleServices.dataViews.create as jest.Mock).mockResolvedValueOnce({
|
||||
...dataViewMock.toSpec(),
|
||||
toSpec: () => dataViewMock.toSpec(),
|
||||
});
|
||||
|
||||
const searchResult = {
|
||||
columns: [
|
||||
{ name: 'timestamp', type: 'date' },
|
||||
|
@ -801,12 +772,6 @@ describe('ruleType', () => {
|
|||
it('rule executor schedule actions when condition met', async () => {
|
||||
const params = defaultParams;
|
||||
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
|
||||
(ruleServices.dataViews.create as jest.Mock).mockResolvedValueOnce({
|
||||
...dataViewMock.toSpec(),
|
||||
toSpec: () => dataViewMock.toSpec(),
|
||||
});
|
||||
|
||||
const searchResult = {
|
||||
columns: [
|
||||
{ name: 'timestamp', type: 'date' },
|
||||
|
@ -932,5 +897,9 @@ async function invokeExecutor({
|
|||
},
|
||||
logger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange: () => {
|
||||
const date = new Date(Date.now()).toISOString();
|
||||
return { dateStart: date, dateEnd: date };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { OnlyEsQueryRuleParams } from './types';
|
||||
import { Comparator } from '../../../common/comparator_types';
|
||||
import { getParsedQuery } from './util';
|
||||
|
||||
describe('es_query utils', () => {
|
||||
const defaultProps = {
|
||||
size: 3,
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'm',
|
||||
threshold: [],
|
||||
thresholdComparator: '>=' as Comparator,
|
||||
esQuery: '{ "query": "test-query" }',
|
||||
index: ['test-index'],
|
||||
timeField: '',
|
||||
searchType: 'esQuery',
|
||||
excludeHitsFromPreviousRun: true,
|
||||
aggType: 'count',
|
||||
groupBy: 'all',
|
||||
searchConfiguration: {},
|
||||
esqlQuery: { esql: 'test-query' },
|
||||
};
|
||||
|
||||
describe('getParsedQuery', () => {
|
||||
it('should return search params correctly', () => {
|
||||
const parsedQuery = getParsedQuery(defaultProps as OnlyEsQueryRuleParams);
|
||||
expect(parsedQuery.query).toBe('test-query');
|
||||
});
|
||||
|
||||
it('should throw invalid query error', () => {
|
||||
expect(() =>
|
||||
getParsedQuery({ ...defaultProps, esQuery: '' } as OnlyEsQueryRuleParams)
|
||||
).toThrow('invalid query specified: "" - query must be JSON');
|
||||
});
|
||||
|
||||
it('should throw invalid query error due to missing query property', () => {
|
||||
expect(() =>
|
||||
getParsedQuery({
|
||||
...defaultProps,
|
||||
esQuery: '{ "someProperty": "test-query" }',
|
||||
} as OnlyEsQueryRuleParams)
|
||||
).toThrow('invalid query specified: "{ "someProperty": "test-query" }" - query must be JSON');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { OnlyEsQueryRuleParams } from './types';
|
||||
import { EsQueryRuleParams } from './rule_type_params';
|
||||
|
||||
export function isEsQueryRule(searchType: EsQueryRuleParams['searchType']) {
|
||||
|
@ -18,3 +20,29 @@ export function isSearchSourceRule(searchType: EsQueryRuleParams['searchType'])
|
|||
export function isEsqlQueryRule(searchType: EsQueryRuleParams['searchType']) {
|
||||
return searchType === 'esqlQuery';
|
||||
}
|
||||
|
||||
export function getParsedQuery(queryParams: OnlyEsQueryRuleParams) {
|
||||
const { esQuery } = queryParams;
|
||||
|
||||
let parsedQuery;
|
||||
try {
|
||||
parsedQuery = JSON.parse(esQuery);
|
||||
} catch (err) {
|
||||
throw new Error(getInvalidQueryError(esQuery));
|
||||
}
|
||||
|
||||
if (parsedQuery && !parsedQuery.query) {
|
||||
throw new Error(getInvalidQueryError(esQuery));
|
||||
}
|
||||
|
||||
return parsedQuery;
|
||||
}
|
||||
|
||||
function getInvalidQueryError(query: string) {
|
||||
return i18n.translate('xpack.stackAlerts.esQuery.invalidQueryErrorMessage', {
|
||||
defaultMessage: 'invalid query specified: "{query}" - query must be JSON',
|
||||
values: {
|
||||
query,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -20,6 +20,11 @@ import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_set
|
|||
|
||||
let fakeTimer: sinon.SinonFakeTimers;
|
||||
|
||||
function getTimeRange() {
|
||||
const date = new Date(Date.now()).toISOString();
|
||||
return { dateStart: date, dateEnd: date };
|
||||
}
|
||||
|
||||
describe('ruleType', () => {
|
||||
const logger = loggingSystemMock.create().get();
|
||||
const data = {
|
||||
|
@ -224,6 +229,7 @@ describe('ruleType', () => {
|
|||
},
|
||||
logger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange,
|
||||
});
|
||||
|
||||
expect(alertServices.alertsClient.report).toHaveBeenCalledWith({
|
||||
|
@ -318,6 +324,7 @@ describe('ruleType', () => {
|
|||
},
|
||||
logger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange,
|
||||
});
|
||||
|
||||
expect(customAlertServices.alertFactory.create).not.toHaveBeenCalled();
|
||||
|
@ -386,6 +393,7 @@ describe('ruleType', () => {
|
|||
},
|
||||
logger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange,
|
||||
});
|
||||
|
||||
expect(customAlertServices.alertFactory.create).not.toHaveBeenCalled();
|
||||
|
@ -453,6 +461,7 @@ describe('ruleType', () => {
|
|||
},
|
||||
logger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange,
|
||||
});
|
||||
|
||||
expect(data.timeSeriesQuery).toHaveBeenCalledWith(
|
||||
|
|
|
@ -219,6 +219,7 @@ export function getRuleType(
|
|||
services,
|
||||
params,
|
||||
logger,
|
||||
getTimeRange,
|
||||
} = options;
|
||||
const { alertsClient, scopedClusterClient } = services;
|
||||
|
||||
|
@ -237,7 +238,8 @@ export function getRuleType(
|
|||
}
|
||||
|
||||
const esClient = scopedClusterClient.asCurrentUser;
|
||||
const date = new Date().toISOString();
|
||||
const { dateStart, dateEnd } = getTimeRange(`${params.timeWindowSize}${params.timeWindowUnit}`);
|
||||
|
||||
// the undefined values below are for config-schema optional types
|
||||
const queryParams: TimeSeriesQuery = {
|
||||
index: params.index,
|
||||
|
@ -247,8 +249,8 @@ export function getRuleType(
|
|||
groupBy: params.groupBy,
|
||||
termField: params.termField,
|
||||
termSize: params.termSize,
|
||||
dateStart: date,
|
||||
dateEnd: date,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
timeWindowSize: params.timeWindowSize,
|
||||
timeWindowUnit: params.timeWindowUnit,
|
||||
interval: undefined,
|
||||
|
@ -269,6 +271,7 @@ export function getRuleType(
|
|||
TIME_SERIES_BUCKET_SELECTOR_FIELD
|
||||
),
|
||||
},
|
||||
useCalculatedDateRange: false,
|
||||
});
|
||||
logger.debug(`rule ${ID}:${ruleId} "${name}" query result: ${JSON.stringify(result)}`);
|
||||
|
||||
|
@ -309,7 +312,7 @@ export function getRuleType(
|
|||
)} ${params.threshold.join(' and ')}`;
|
||||
|
||||
const baseContext: BaseActionContext = {
|
||||
date,
|
||||
date: dateEnd,
|
||||
group: alertId,
|
||||
value,
|
||||
conditions: humanFn,
|
||||
|
@ -338,7 +341,7 @@ export function getRuleType(
|
|||
const alertId = recoveredAlert.getId();
|
||||
logger.debug(`setting context for recovered alert ${alertId}`);
|
||||
const baseContext: BaseActionContext = {
|
||||
date,
|
||||
date: dateEnd,
|
||||
value: unmetGroupValues[alertId] ?? 'unknown',
|
||||
group: alertId,
|
||||
conditions: `${agg} is NOT ${getHumanReadableComparator(
|
||||
|
|
|
@ -35063,7 +35063,6 @@
|
|||
"xpack.stackAlerts.esQuery.invalidQueryErrorMessage": "recherche spécifiée non valide : \"{query}\" - la recherche doit être au format JSON",
|
||||
"xpack.stackAlerts.esQuery.invalidTermSizeMaximumErrorMessage": "[termSize] : doit être inférieure ou égale à {maxGroups}",
|
||||
"xpack.stackAlerts.esQuery.invalidThreshold2ErrorMessage": "[threshold] : requiert deux éléments pour le comparateur \"{thresholdComparator}\"",
|
||||
"xpack.stackAlerts.esQuery.invalidWindowSizeErrorMessage": "format non valide pour windowSize : \"{windowValue}\"",
|
||||
"xpack.stackAlerts.esQuery.ui.numQueryMatchesText": "La recherche correspondait à {count} documents dans le/la/les dernier(s)/dernière(s) {window}.",
|
||||
"xpack.stackAlerts.esQuery.ui.queryError": "Erreur lors du test de la recherche : {message}",
|
||||
"xpack.stackAlerts.esQuery.ui.testQueryGroupedResponse": "La recherche groupée correspondait à {groups} groupes dans le/la/les dernier(s)/dernière(s) {window}.",
|
||||
|
@ -37791,7 +37790,6 @@
|
|||
"xpack.triggersActionsUI.rulesSettings.modal.flappingDetectionDescription": "Détectez les alertes qui passent rapidement de l'état actif à l'état récupéré et réduisez le bruit non souhaité de ces alertes bagotantes.",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.flappingOffLabel": "Désactivé",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.flappingOnLabel": "Activé (recommandé)",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.getRulesSettingsError": "Impossible de récupérer les paramètres des règles.",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.saveButton": "Enregistrer",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.title": "Paramètres de règle",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.updateRulesSettingsFailure": "Impossible de mettre à jour les paramètres des règles.",
|
||||
|
|
|
@ -35062,7 +35062,6 @@
|
|||
"xpack.stackAlerts.esQuery.invalidQueryErrorMessage": "無効なクエリが指定されました: \"{query}\" - クエリはJSONでなければなりません",
|
||||
"xpack.stackAlerts.esQuery.invalidTermSizeMaximumErrorMessage": "[termSize]:{maxGroups}以下でなければなりません",
|
||||
"xpack.stackAlerts.esQuery.invalidThreshold2ErrorMessage": "[threshold]:「{thresholdComparator}」比較子の場合には2つの要素が必要です",
|
||||
"xpack.stackAlerts.esQuery.invalidWindowSizeErrorMessage": "windowSizeの無効な形式:\"{windowValue}\"",
|
||||
"xpack.stackAlerts.esQuery.ui.numQueryMatchesText": "前回の{window}でクエリが{count}個のドキュメントと一致しました。",
|
||||
"xpack.stackAlerts.esQuery.ui.queryError": "クエリのテストエラー:{message}",
|
||||
"xpack.stackAlerts.esQuery.ui.testQueryGroupedResponse": "グループ化されたクエリは、直近の{window}件に{groups}グループと一致しました。",
|
||||
|
@ -37782,7 +37781,6 @@
|
|||
"xpack.triggersActionsUI.rulesSettings.modal.flappingDetectionDescription": "アクティブと回復済みの状態がすばやく切り替わるアラートを検出し、これらのフラップアラートに対する不要なノイズを低減します。",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.flappingOffLabel": "オフ",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.flappingOnLabel": "オン(推奨)",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.getRulesSettingsError": "ルール設定を取得できませんでした。",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.saveButton": "保存",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.title": "ルール設定",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.updateRulesSettingsFailure": "ルール設定を更新できませんでした。",
|
||||
|
|
|
@ -35056,7 +35056,6 @@
|
|||
"xpack.stackAlerts.esQuery.invalidQueryErrorMessage": "指定的查询无效:“{query}”- 查询必须为 JSON",
|
||||
"xpack.stackAlerts.esQuery.invalidTermSizeMaximumErrorMessage": "[termSize]:必须小于或等于 {maxGroups}",
|
||||
"xpack.stackAlerts.esQuery.invalidThreshold2ErrorMessage": "[threshold]:对于“{thresholdComparator}”比较运算符,必须包含两个元素",
|
||||
"xpack.stackAlerts.esQuery.invalidWindowSizeErrorMessage": "windowSize 的格式无效:“{windowValue}”",
|
||||
"xpack.stackAlerts.esQuery.ui.numQueryMatchesText": "查询在过去 {window} 匹配 {count} 个文档。",
|
||||
"xpack.stackAlerts.esQuery.ui.queryError": "测试查询时出错:{message}",
|
||||
"xpack.stackAlerts.esQuery.ui.testQueryGroupedResponse": "过去 {window} 与 {groups} 个组匹配的分组查询。",
|
||||
|
@ -37776,7 +37775,6 @@
|
|||
"xpack.triggersActionsUI.rulesSettings.modal.flappingDetectionDescription": "检测在“活动”和“已恢复”状态之间快速切换的告警,并为这些摆动告警减少不必要噪音。",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.flappingOffLabel": "关闭",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.flappingOnLabel": "开(建议)",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.getRulesSettingsError": "无法获取规则设置。",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.saveButton": "保存",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.title": "规则设置",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.updateRulesSettingsFailure": "无法更新规则设置。",
|
||||
|
|
|
@ -8,19 +8,7 @@
|
|||
import React, { memo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiFormRowProps,
|
||||
EuiIconTip,
|
||||
EuiRange,
|
||||
EuiRangeProps,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
EuiPanel,
|
||||
} from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiText, EuiPanel } from '@elastic/eui';
|
||||
import {
|
||||
RulesSettingsFlappingProperties,
|
||||
MIN_LOOK_BACK_WINDOW,
|
||||
|
@ -28,7 +16,7 @@ import {
|
|||
MAX_LOOK_BACK_WINDOW,
|
||||
MAX_STATUS_CHANGE_THRESHOLD,
|
||||
} from '@kbn/alerting-plugin/common';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { RulesSettingsRange } from '../rules_settings_range';
|
||||
|
||||
type OnChangeKey = keyof Omit<RulesSettingsFlappingProperties, 'enabled'>;
|
||||
|
||||
|
@ -81,16 +69,6 @@ const getStatusChangeThresholdRuleRuns = (amount: number) => {
|
|||
);
|
||||
};
|
||||
|
||||
export interface RulesSettingsRangeProps {
|
||||
label: EuiFormRowProps['label'];
|
||||
labelPopoverText?: string;
|
||||
min: number;
|
||||
max: number;
|
||||
value: number;
|
||||
disabled?: EuiRangeProps['disabled'];
|
||||
onChange?: EuiRangeProps['onChange'];
|
||||
}
|
||||
|
||||
export const RulesSettingsFlappingTitle = () => {
|
||||
return (
|
||||
<EuiTitle size="xs">
|
||||
|
@ -115,58 +93,19 @@ export const RulesSettingsFlappingDescription = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export const RulesSettingsRange = memo((props: RulesSettingsRangeProps) => {
|
||||
const { label, labelPopoverText, min, max, value, disabled, onChange, ...rest } = props;
|
||||
|
||||
const renderLabel = () => {
|
||||
return (
|
||||
<div>
|
||||
{label}
|
||||
|
||||
<EuiIconTip color="subdued" size="s" type="questionInCircle" content={labelPopoverText} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFormRow label={renderLabel()}>
|
||||
<EuiRange
|
||||
min={min}
|
||||
max={max}
|
||||
step={1}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
showLabels
|
||||
showValue
|
||||
{...rest}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
});
|
||||
|
||||
export interface RulesSettingsFlappingFormSectionProps {
|
||||
flappingSettings: RulesSettingsFlappingProperties;
|
||||
compressed?: boolean;
|
||||
onChange: (key: OnChangeKey, value: number) => void;
|
||||
canWrite: boolean;
|
||||
}
|
||||
|
||||
export const RulesSettingsFlappingFormSection = memo(
|
||||
(props: RulesSettingsFlappingFormSectionProps) => {
|
||||
const { flappingSettings, compressed = false, onChange } = props;
|
||||
const { flappingSettings, compressed = false, onChange, canWrite } = props;
|
||||
|
||||
const { lookBackWindow, statusChangeThreshold } = flappingSettings;
|
||||
|
||||
const {
|
||||
application: { capabilities },
|
||||
} = useKibana().services;
|
||||
|
||||
const {
|
||||
rulesSettings: { writeFlappingSettingsUI },
|
||||
} = capabilities;
|
||||
|
||||
const canWriteFlappingSettings = writeFlappingSettingsUI;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
{compressed && (
|
||||
|
@ -193,7 +132,7 @@ export const RulesSettingsFlappingFormSection = memo(
|
|||
onChange={(e) => onChange('lookBackWindow', parseInt(e.currentTarget.value, 10))}
|
||||
label={lookBackWindowLabel}
|
||||
labelPopoverText={lookBackWindowHelp}
|
||||
disabled={!canWriteFlappingSettings}
|
||||
disabled={!canWrite}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -205,7 +144,7 @@ export const RulesSettingsFlappingFormSection = memo(
|
|||
onChange={(e) => onChange('statusChangeThreshold', parseInt(e.currentTarget.value, 10))}
|
||||
label={statusChangeThresholdLabel}
|
||||
labelPopoverText={statusChangeThresholdHelp}
|
||||
disabled={!canWriteFlappingSettings}
|
||||
disabled={!canWrite}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* 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 React, { memo } from 'react';
|
||||
import { RulesSettingsFlappingProperties } from '@kbn/alerting-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiSwitchProps,
|
||||
EuiPanel,
|
||||
EuiText,
|
||||
EuiEmptyPrompt,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
RulesSettingsFlappingFormSection,
|
||||
RulesSettingsFlappingFormSectionProps,
|
||||
RulesSettingsFlappingTitle,
|
||||
} from './rules_settings_flapping_form_section';
|
||||
|
||||
const flappingDescription = i18n.translate(
|
||||
'xpack.triggersActionsUI.rulesSettings.modal.flappingDetectionDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Detect alerts that switch quickly between active and recovered states and reduce unwanted noise for these flapping alerts.',
|
||||
}
|
||||
);
|
||||
|
||||
const flappingOnLabel = i18n.translate(
|
||||
'xpack.triggersActionsUI.rulesSettings.modal.flappingOnLabel',
|
||||
{
|
||||
defaultMessage: 'On (recommended)',
|
||||
}
|
||||
);
|
||||
|
||||
const flappingOffLabel = i18n.translate(
|
||||
'xpack.triggersActionsUI.rulesSettings.modal.flappingOffLabel',
|
||||
{
|
||||
defaultMessage: 'Off',
|
||||
}
|
||||
);
|
||||
|
||||
export const RulesSettingsFlappingErrorPrompt = memo(() => {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
data-test-subj="rulesSettingsFlappingErrorPrompt"
|
||||
color="danger"
|
||||
iconType="warning"
|
||||
title={
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.rulesSettings.modal.flappingErrorPromptTitle"
|
||||
defaultMessage="Unable to load your flapping settings"
|
||||
/>
|
||||
</h4>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.rulesSettings.modal.flappingErrorPromptBody"
|
||||
defaultMessage="There was an error loading your flapping settings. Contact your administrator for help"
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
interface RulesSettingsFlappingFormLeftProps {
|
||||
settings: RulesSettingsFlappingProperties;
|
||||
onChange: EuiSwitchProps['onChange'];
|
||||
isSwitchDisabled: boolean;
|
||||
}
|
||||
|
||||
export const RulesSettingsFlappingFormLeft = memo((props: RulesSettingsFlappingFormLeftProps) => {
|
||||
const { settings, onChange, isSwitchDisabled } = props;
|
||||
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color="subdued" size="s">
|
||||
<p>{flappingDescription}</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSwitch
|
||||
data-test-subj="rulesSettingsFlappingEnableSwitch"
|
||||
label={settings!.enabled ? flappingOnLabel : flappingOffLabel}
|
||||
checked={settings!.enabled}
|
||||
disabled={isSwitchDisabled}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
});
|
||||
|
||||
interface RulesSettingsFlappingFormRightProps {
|
||||
settings: RulesSettingsFlappingProperties;
|
||||
onChange: RulesSettingsFlappingFormSectionProps['onChange'];
|
||||
canWrite: boolean;
|
||||
}
|
||||
|
||||
export const RulesSettingsFlappingFormRight = memo((props: RulesSettingsFlappingFormRightProps) => {
|
||||
const { settings, onChange, canWrite } = props;
|
||||
|
||||
if (!settings) {
|
||||
return null;
|
||||
}
|
||||
if (!settings.enabled) {
|
||||
return (
|
||||
<EuiFlexItem data-test-subj="rulesSettingsFlappingOffPrompt">
|
||||
<EuiPanel borderRadius="none" color="subdued" grow={false}>
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.rulesSettings.flapping.flappingSettingsOffDescription"
|
||||
defaultMessage="Alert flapping detection is off. Alerts will be generated based on the rule interval, which might result in higher alert volumes."
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<RulesSettingsFlappingFormSection
|
||||
flappingSettings={settings}
|
||||
onChange={onChange}
|
||||
canWrite={canWrite}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
});
|
||||
|
||||
export interface RulesSettingsFlappingSectionProps {
|
||||
onChange: (key: keyof RulesSettingsFlappingProperties, value: number | boolean) => void;
|
||||
settings: RulesSettingsFlappingProperties;
|
||||
canShow: boolean | Readonly<{ [x: string]: boolean }>;
|
||||
canWrite: boolean;
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
export const RulesSettingsFlappingSection = memo((props: RulesSettingsFlappingSectionProps) => {
|
||||
const { onChange, settings, hasError, canShow, canWrite } = props;
|
||||
|
||||
if (!canShow) {
|
||||
return null;
|
||||
}
|
||||
if (hasError) {
|
||||
return <RulesSettingsFlappingErrorPrompt />;
|
||||
}
|
||||
return (
|
||||
<EuiForm data-test-subj="rulesSettingsFlappingSection">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<RulesSettingsFlappingTitle />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup>
|
||||
<RulesSettingsFlappingFormLeft
|
||||
isSwitchDisabled={!canWrite}
|
||||
settings={settings}
|
||||
onChange={(e) => onChange('enabled', e.target.checked)}
|
||||
/>
|
||||
<RulesSettingsFlappingFormRight
|
||||
settings={settings}
|
||||
onChange={(key, value) => onChange(key, value)}
|
||||
canWrite={canWrite}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
</EuiForm>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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 React, { memo } from 'react';
|
||||
import {
|
||||
MAX_QUERY_DELAY,
|
||||
MIN_QUERY_DELAY,
|
||||
RulesSettingsQueryDelayProperties,
|
||||
} from '@kbn/alerting-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiEmptyPrompt,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { RulesSettingsRange } from '../rules_settings_range';
|
||||
|
||||
const queryDelayDescription = i18n.translate(
|
||||
'xpack.triggersActionsUI.rulesSettings.modal.queryDelayDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Delay all rule queries to mitigate the impact of index refresh intervals on data availability.',
|
||||
}
|
||||
);
|
||||
|
||||
const queryDelayLabel = i18n.translate('xpack.triggersActionsUI.rulesSettings.queryDelayLabel', {
|
||||
defaultMessage: 'Query delay length (seconds)',
|
||||
});
|
||||
|
||||
export const RulesSettingsQueryDelayErrorPrompt = memo(() => {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
data-test-subj="rulesSettingsQueryDelayErrorPrompt"
|
||||
color="danger"
|
||||
iconType="warning"
|
||||
title={
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.rulesSettings.modal.queryDelayErrorPromptTitle"
|
||||
defaultMessage="Unable to load your query delay settings"
|
||||
/>
|
||||
</h4>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.rulesSettings.modal.queryDelayErrorPromptBody"
|
||||
defaultMessage="There was an error loading your query delay settings. Contact your administrator for help"
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const RulesSettingsQueryDelayTitle = () => {
|
||||
return (
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.rulesSettings.queryDelayTitle"
|
||||
defaultMessage="Query delay"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
);
|
||||
};
|
||||
|
||||
export interface RulesSettingsQueryDelaySectionProps {
|
||||
onChange: (key: keyof RulesSettingsQueryDelayProperties, value: number | boolean) => void;
|
||||
settings: RulesSettingsQueryDelayProperties;
|
||||
canShow: boolean | Readonly<{ [x: string]: boolean }>;
|
||||
canWrite: boolean;
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
export const RulesSettingsQueryDelaySection = memo((props: RulesSettingsQueryDelaySectionProps) => {
|
||||
const { onChange, settings, hasError, canShow, canWrite } = props;
|
||||
|
||||
if (!canShow) {
|
||||
return null;
|
||||
}
|
||||
if (hasError) {
|
||||
return <RulesSettingsQueryDelayErrorPrompt />;
|
||||
}
|
||||
return (
|
||||
<EuiForm data-test-subj="rulesSettingsQueryDelaySection">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<RulesSettingsQueryDelayTitle />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued" size="s">
|
||||
<p>{queryDelayDescription}</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow>
|
||||
<RulesSettingsRange
|
||||
data-test-subj="queryDelayRangeInput"
|
||||
min={MIN_QUERY_DELAY}
|
||||
max={MAX_QUERY_DELAY}
|
||||
value={settings.delay}
|
||||
onChange={(e) => onChange('delay', parseInt(e.currentTarget.value, 10))}
|
||||
label={queryDelayLabel}
|
||||
disabled={!canWrite}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiForm>
|
||||
);
|
||||
});
|
|
@ -36,6 +36,8 @@ withAllPermission.decorators = [
|
|||
save: true,
|
||||
readFlappingSettingsUI: true,
|
||||
writeFlappingSettingsUI: true,
|
||||
readQueryDelaySettingsUI: true,
|
||||
writeQueryDelaySettingsUI: true,
|
||||
},
|
||||
}),
|
||||
}}
|
||||
|
@ -58,6 +60,8 @@ withReadPermission.decorators = [
|
|||
save: false,
|
||||
readFlappingSettingsUI: true,
|
||||
writeFlappingSettingsUI: false,
|
||||
readQueryDelaySettingsUI: true,
|
||||
writeQueryDelaySettingsUI: false,
|
||||
},
|
||||
}),
|
||||
}}
|
||||
|
@ -80,6 +84,8 @@ withNoPermission.decorators = [
|
|||
save: false,
|
||||
readFlappingSettingsUI: false,
|
||||
writeFlappingSettingsUI: false,
|
||||
readQueryDelaySettingsUI: false,
|
||||
writeQueryDelaySettingsUI: false,
|
||||
},
|
||||
}),
|
||||
}}
|
||||
|
|
|
@ -11,18 +11,18 @@ import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
|||
import { render, cleanup, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { RulesSettingsFlapping } from '@kbn/alerting-plugin/common';
|
||||
import { RulesSettingsFlapping, RulesSettingsQueryDelay } from '@kbn/alerting-plugin/common';
|
||||
import { RulesSettingsLink } from './rules_settings_link';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { getFlappingSettings } from '../../lib/rule_api/get_flapping_settings';
|
||||
import { updateFlappingSettings } from '../../lib/rule_api/update_flapping_settings';
|
||||
import { getQueryDelaySettings } from '../../lib/rule_api/get_query_delay_settings';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
jest.mock('../../lib/rule_api/get_flapping_settings', () => ({
|
||||
getFlappingSettings: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../lib/rule_api/update_flapping_settings', () => ({
|
||||
updateFlappingSettings: jest.fn(),
|
||||
jest.mock('../../lib/rule_api/get_query_delay_settings', () => ({
|
||||
getQueryDelaySettings: jest.fn(),
|
||||
}));
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
|
@ -41,8 +41,8 @@ const mocks = coreMock.createSetup();
|
|||
const getFlappingSettingsMock = getFlappingSettings as unknown as jest.MockedFunction<
|
||||
typeof getFlappingSettings
|
||||
>;
|
||||
const updateFlappingSettingsMock = updateFlappingSettings as unknown as jest.MockedFunction<
|
||||
typeof updateFlappingSettings
|
||||
const getQueryDelaySettingsMock = getQueryDelaySettings as unknown as jest.MockedFunction<
|
||||
typeof getQueryDelaySettings
|
||||
>;
|
||||
|
||||
const mockFlappingSetting: RulesSettingsFlapping = {
|
||||
|
@ -54,6 +54,13 @@ const mockFlappingSetting: RulesSettingsFlapping = {
|
|||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
const mockQueryDelaySetting: RulesSettingsQueryDelay = {
|
||||
delay: 10,
|
||||
createdBy: 'test user',
|
||||
updatedBy: 'test user',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const RulesSettingsLinkWithProviders: React.FunctionComponent<{}> = () => (
|
||||
<IntlProvider locale="en">
|
||||
|
@ -77,10 +84,12 @@ describe('rules_settings_link', () => {
|
|||
show: true,
|
||||
writeFlappingSettingsUI: true,
|
||||
readFlappingSettingsUI: true,
|
||||
writeQueryDelaySettingsUI: true,
|
||||
readQueryDelaySettingsUI: true,
|
||||
},
|
||||
};
|
||||
getFlappingSettingsMock.mockResolvedValue(mockFlappingSetting);
|
||||
updateFlappingSettingsMock.mockResolvedValue(mockFlappingSetting);
|
||||
getQueryDelaySettingsMock.mockResolvedValue(mockQueryDelaySetting);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -98,6 +107,58 @@ describe('rules_settings_link', () => {
|
|||
expect(result.queryByTestId('rulesSettingsModal')).toBe(null);
|
||||
});
|
||||
|
||||
test('renders the rules setting link correctly (readFlappingSettingsUI = true)', async () => {
|
||||
const [
|
||||
{
|
||||
application: { capabilities },
|
||||
},
|
||||
] = await mocks.getStartServices();
|
||||
useKibanaMock().services.application.capabilities = {
|
||||
...capabilities,
|
||||
rulesSettings: {
|
||||
save: true,
|
||||
show: true,
|
||||
writeFlappingSettingsUI: true,
|
||||
readFlappingSettingsUI: true,
|
||||
writeQueryDelaySettingsUI: true,
|
||||
readQueryDelaySettingsUI: false,
|
||||
},
|
||||
};
|
||||
|
||||
const result = render(<RulesSettingsLinkWithProviders />);
|
||||
await waitFor(() => {
|
||||
expect(result.getByText('Settings')).toBeInTheDocument();
|
||||
});
|
||||
expect(result.getByText('Settings')).not.toBeDisabled();
|
||||
expect(result.queryByTestId('rulesSettingsModal')).toBe(null);
|
||||
});
|
||||
|
||||
test('renders the rules setting link correctly (readQueryDelaySettingsUI = true)', async () => {
|
||||
const [
|
||||
{
|
||||
application: { capabilities },
|
||||
},
|
||||
] = await mocks.getStartServices();
|
||||
useKibanaMock().services.application.capabilities = {
|
||||
...capabilities,
|
||||
rulesSettings: {
|
||||
save: true,
|
||||
show: true,
|
||||
writeFlappingSettingsUI: true,
|
||||
readFlappingSettingsUI: false,
|
||||
writeQueryDelaySettingsUI: true,
|
||||
readQueryDelaySettingsUI: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = render(<RulesSettingsLinkWithProviders />);
|
||||
await waitFor(() => {
|
||||
expect(result.getByText('Settings')).toBeInTheDocument();
|
||||
});
|
||||
expect(result.getByText('Settings')).not.toBeDisabled();
|
||||
expect(result.queryByTestId('rulesSettingsModal')).toBe(null);
|
||||
});
|
||||
|
||||
test('clicking the settings link opens the rules settings modal', async () => {
|
||||
const result = render(<RulesSettingsLinkWithProviders />);
|
||||
await waitFor(() => {
|
||||
|
@ -124,6 +185,8 @@ describe('rules_settings_link', () => {
|
|||
show: false,
|
||||
writeFlappingSettingsUI: true,
|
||||
readFlappingSettingsUI: true,
|
||||
writeQueryDelaySettingsUI: true,
|
||||
readQueryDelaySettingsUI: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -139,6 +202,8 @@ describe('rules_settings_link', () => {
|
|||
show: true,
|
||||
writeFlappingSettingsUI: true,
|
||||
readFlappingSettingsUI: false,
|
||||
writeQueryDelaySettingsUI: true,
|
||||
readQueryDelaySettingsUI: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -17,9 +17,9 @@ export const RulesSettingsLink = () => {
|
|||
application: { capabilities },
|
||||
} = useKibana().services;
|
||||
|
||||
const { show, readFlappingSettingsUI } = capabilities.rulesSettings;
|
||||
const { show, readFlappingSettingsUI, readQueryDelaySettingsUI } = capabilities.rulesSettings;
|
||||
|
||||
if (!show || !readFlappingSettingsUI) {
|
||||
if (!show || (!readFlappingSettingsUI && !readQueryDelaySettingsUI)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -12,11 +12,13 @@ import { render, fireEvent, cleanup, waitFor } from '@testing-library/react';
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { IToasts } from '@kbn/core/public';
|
||||
import { RulesSettingsFlapping } from '@kbn/alerting-plugin/common';
|
||||
import { RulesSettingsFlapping, RulesSettingsQueryDelay } from '@kbn/alerting-plugin/common';
|
||||
import { RulesSettingsModal, RulesSettingsModalProps } from './rules_settings_modal';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { getFlappingSettings } from '../../lib/rule_api/get_flapping_settings';
|
||||
import { updateFlappingSettings } from '../../lib/rule_api/update_flapping_settings';
|
||||
import { getQueryDelaySettings } from '../../lib/rule_api/get_query_delay_settings';
|
||||
import { updateQueryDelaySettings } from '../../lib/rule_api/update_query_delay_settings';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
jest.mock('../../lib/rule_api/get_flapping_settings', () => ({
|
||||
|
@ -25,6 +27,12 @@ jest.mock('../../lib/rule_api/get_flapping_settings', () => ({
|
|||
jest.mock('../../lib/rule_api/update_flapping_settings', () => ({
|
||||
updateFlappingSettings: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../lib/rule_api/get_query_delay_settings', () => ({
|
||||
getQueryDelaySettings: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../lib/rule_api/update_query_delay_settings', () => ({
|
||||
updateQueryDelaySettings: jest.fn(),
|
||||
}));
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
|
@ -45,6 +53,12 @@ const getFlappingSettingsMock = getFlappingSettings as unknown as jest.MockedFun
|
|||
const updateFlappingSettingsMock = updateFlappingSettings as unknown as jest.MockedFunction<
|
||||
typeof updateFlappingSettings
|
||||
>;
|
||||
const getQueryDelaySettingsMock = getQueryDelaySettings as unknown as jest.MockedFunction<
|
||||
typeof getQueryDelaySettings
|
||||
>;
|
||||
const updateQueryDelaySettingsMock = updateQueryDelaySettings as unknown as jest.MockedFunction<
|
||||
typeof updateQueryDelaySettings
|
||||
>;
|
||||
|
||||
const mockFlappingSetting: RulesSettingsFlapping = {
|
||||
enabled: true,
|
||||
|
@ -55,6 +69,13 @@ const mockFlappingSetting: RulesSettingsFlapping = {
|
|||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
const mockQueryDelaySetting: RulesSettingsQueryDelay = {
|
||||
delay: 10,
|
||||
createdBy: 'test user',
|
||||
updatedBy: 'test user',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const modalProps: RulesSettingsModalProps = {
|
||||
isVisible: true,
|
||||
|
@ -87,6 +108,8 @@ describe('rules_settings_modal', () => {
|
|||
show: true,
|
||||
writeFlappingSettingsUI: true,
|
||||
readFlappingSettingsUI: true,
|
||||
writeQueryDelaySettingsUI: true,
|
||||
readQueryDelaySettingsUI: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -99,6 +122,8 @@ describe('rules_settings_modal', () => {
|
|||
|
||||
getFlappingSettingsMock.mockResolvedValue(mockFlappingSetting);
|
||||
updateFlappingSettingsMock.mockResolvedValue(mockFlappingSetting);
|
||||
getQueryDelaySettingsMock.mockResolvedValue(mockQueryDelaySetting);
|
||||
updateQueryDelaySettingsMock.mockResolvedValue(mockQueryDelaySetting);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -113,9 +138,9 @@ describe('rules_settings_modal', () => {
|
|||
await waitFor(() => {
|
||||
expect(result.queryByTestId('centerJustifiedSpinner')).toBe(null);
|
||||
});
|
||||
expect(result.getByTestId('rulesSettingsModalEnableSwitch').getAttribute('aria-checked')).toBe(
|
||||
'true'
|
||||
);
|
||||
expect(
|
||||
result.getByTestId('rulesSettingsFlappingEnableSwitch').getAttribute('aria-checked')
|
||||
).toBe('true');
|
||||
expect(result.getByTestId('lookBackWindowRangeInput').getAttribute('value')).toBe('10');
|
||||
expect(result.getByTestId('statusChangeThresholdRangeInput').getAttribute('value')).toBe('10');
|
||||
|
||||
|
@ -190,6 +215,15 @@ describe('rules_settings_modal', () => {
|
|||
expect(result.queryByTestId('centerJustifiedSpinner')).toBe(null);
|
||||
});
|
||||
|
||||
const lookBackWindowInput = result.getByTestId('lookBackWindowRangeInput');
|
||||
const statusChangeThresholdInput = result.getByTestId('statusChangeThresholdRangeInput');
|
||||
|
||||
fireEvent.change(lookBackWindowInput, { target: { value: 20 } });
|
||||
fireEvent.change(statusChangeThresholdInput, { target: { value: 5 } });
|
||||
|
||||
expect(lookBackWindowInput.getAttribute('value')).toBe('20');
|
||||
expect(statusChangeThresholdInput.getAttribute('value')).toBe('5');
|
||||
|
||||
// Try saving
|
||||
userEvent.click(result.getByTestId('rulesSettingsModalSaveButton'));
|
||||
await waitFor(() => {
|
||||
|
@ -207,9 +241,9 @@ describe('rules_settings_modal', () => {
|
|||
expect(result.queryByTestId('centerJustifiedSpinner')).toBe(null);
|
||||
});
|
||||
|
||||
expect(result.queryByTestId('rulesSettingsModalFlappingOffPrompt')).toBe(null);
|
||||
userEvent.click(result.getByTestId('rulesSettingsModalEnableSwitch'));
|
||||
expect(result.queryByTestId('rulesSettingsModalFlappingOffPrompt')).not.toBe(null);
|
||||
expect(result.queryByTestId('rulesSettingsFlappingOffPrompt')).toBe(null);
|
||||
userEvent.click(result.getByTestId('rulesSettingsFlappingEnableSwitch'));
|
||||
expect(result.queryByTestId('rulesSettingsFlappingOffPrompt')).not.toBe(null);
|
||||
});
|
||||
|
||||
test('form elements are disabled when provided with insufficient write permissions', async () => {
|
||||
|
@ -232,7 +266,7 @@ describe('rules_settings_modal', () => {
|
|||
expect(result.queryByTestId('centerJustifiedSpinner')).toBe(null);
|
||||
});
|
||||
|
||||
expect(result.getByTestId('rulesSettingsModalEnableSwitch')).toBeDisabled();
|
||||
expect(result.getByTestId('rulesSettingsFlappingEnableSwitch')).toBeDisabled();
|
||||
expect(result.getByTestId('lookBackWindowRangeInput')).toBeDisabled();
|
||||
expect(result.getByTestId('statusChangeThresholdRangeInput')).toBeDisabled();
|
||||
expect(result.getByTestId('rulesSettingsModalSaveButton')).toBeDisabled();
|
||||
|
@ -259,6 +293,118 @@ describe('rules_settings_modal', () => {
|
|||
expect(result.queryByTestId('centerJustifiedSpinner')).toBe(null);
|
||||
});
|
||||
|
||||
expect(result.getByTestId('rulesSettingsErrorPrompt')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('rulesSettingsFlappingSection')).toBe(null);
|
||||
});
|
||||
|
||||
test('renders query delay settings correctly', async () => {
|
||||
const result = render(<RulesSettingsModalWithProviders {...modalProps} />);
|
||||
expect(getQueryDelaySettingsMock).toHaveBeenCalledTimes(1);
|
||||
await waitFor(() => {
|
||||
expect(result.queryByTestId('centerJustifiedSpinner')).toBe(null);
|
||||
});
|
||||
expect(result.getByTestId('queryDelayRangeInput').getAttribute('value')).toBe('10');
|
||||
|
||||
expect(result.getByTestId('rulesSettingsModalCancelButton')).toBeInTheDocument();
|
||||
expect(result.getByTestId('rulesSettingsModalSaveButton').getAttribute('disabled')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('can save query delay settings', async () => {
|
||||
const result = render(<RulesSettingsModalWithProviders {...modalProps} />);
|
||||
await waitFor(() => {
|
||||
expect(result.queryByTestId('centerJustifiedSpinner')).toBe(null);
|
||||
});
|
||||
|
||||
const queryDelayRangeInput = result.getByTestId('queryDelayRangeInput');
|
||||
fireEvent.change(queryDelayRangeInput, { target: { value: 20 } });
|
||||
expect(queryDelayRangeInput.getAttribute('value')).toBe('20');
|
||||
|
||||
// Try saving
|
||||
userEvent.click(result.getByTestId('rulesSettingsModalSaveButton'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(modalProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
|
||||
});
|
||||
expect(modalProps.onClose).toHaveBeenCalledTimes(1);
|
||||
expect(updateQueryDelaySettingsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
queryDelaySettings: {
|
||||
delay: 20,
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(useKibanaMock().services.notifications.toasts.addSuccess).toHaveBeenCalledTimes(1);
|
||||
expect(modalProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
|
||||
expect(modalProps.onSave).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('handles errors when saving query delay settings', async () => {
|
||||
updateQueryDelaySettingsMock.mockRejectedValue('failed!');
|
||||
|
||||
const result = render(<RulesSettingsModalWithProviders {...modalProps} />);
|
||||
await waitFor(() => {
|
||||
expect(result.queryByTestId('centerJustifiedSpinner')).toBe(null);
|
||||
});
|
||||
|
||||
const queryDelayRangeInput = result.getByTestId('queryDelayRangeInput');
|
||||
fireEvent.change(queryDelayRangeInput, { target: { value: 20 } });
|
||||
expect(queryDelayRangeInput.getAttribute('value')).toBe('20');
|
||||
|
||||
// Try saving
|
||||
userEvent.click(result.getByTestId('rulesSettingsModalSaveButton'));
|
||||
await waitFor(() => {
|
||||
expect(modalProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
|
||||
});
|
||||
expect(modalProps.onClose).toHaveBeenCalledTimes(1);
|
||||
expect(useKibanaMock().services.notifications.toasts.addDanger).toHaveBeenCalledTimes(1);
|
||||
expect(modalProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
|
||||
expect(modalProps.onSave).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('query delay form elements are disabled when provided with insufficient write permissions', async () => {
|
||||
const [
|
||||
{
|
||||
application: { capabilities },
|
||||
},
|
||||
] = await mocks.getStartServices();
|
||||
useKibanaMock().services.application.capabilities = {
|
||||
...capabilities,
|
||||
rulesSettings: {
|
||||
save: true,
|
||||
show: true,
|
||||
writeQueryDelaySettingsUI: false,
|
||||
readQueryDelaySettingsUI: true,
|
||||
},
|
||||
};
|
||||
const result = render(<RulesSettingsModalWithProviders {...modalProps} />);
|
||||
await waitFor(() => {
|
||||
expect(result.queryByTestId('centerJustifiedSpinner')).toBe(null);
|
||||
});
|
||||
|
||||
expect(result.getByTestId('queryDelayRangeInput')).toBeDisabled();
|
||||
expect(result.getByTestId('rulesSettingsModalSaveButton')).toBeDisabled();
|
||||
});
|
||||
|
||||
test('query delay form elements are not visible when provided with insufficient read permissions', async () => {
|
||||
const [
|
||||
{
|
||||
application: { capabilities },
|
||||
},
|
||||
] = await mocks.getStartServices();
|
||||
useKibanaMock().services.application.capabilities = {
|
||||
...capabilities,
|
||||
rulesSettings: {
|
||||
save: true,
|
||||
show: false,
|
||||
writeQueryDelaySettingsUI: true,
|
||||
readQueryDelaySettingsUI: false,
|
||||
},
|
||||
};
|
||||
|
||||
const result = render(<RulesSettingsModalWithProviders {...modalProps} />);
|
||||
await waitFor(() => {
|
||||
expect(result.queryByTestId('centerJustifiedSpinner')).toBe(null);
|
||||
});
|
||||
|
||||
expect(result.queryByTestId('rulesSettingsQueryDelaySection')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
*/
|
||||
|
||||
import React, { memo, useState } from 'react';
|
||||
import { RulesSettingsFlappingProperties } from '@kbn/alerting-plugin/common';
|
||||
import {
|
||||
RulesSettingsFlappingProperties,
|
||||
RulesSettingsProperties,
|
||||
RulesSettingsQueryDelayProperties,
|
||||
} from '@kbn/alerting-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
|
@ -14,53 +18,22 @@ import {
|
|||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiHorizontalRule,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiModal,
|
||||
EuiModalHeader,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeaderTitle,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiSwitchProps,
|
||||
EuiPanel,
|
||||
EuiText,
|
||||
EuiEmptyPrompt,
|
||||
} from '@elastic/eui';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import {
|
||||
RulesSettingsFlappingFormSection,
|
||||
RulesSettingsFlappingFormSectionProps,
|
||||
RulesSettingsFlappingTitle,
|
||||
} from './rules_settings_flapping_form_section';
|
||||
import { useGetFlappingSettings } from '../../hooks/use_get_flapping_settings';
|
||||
import { useUpdateFlappingSettings } from '../../hooks/use_update_flapping_settings';
|
||||
import { RulesSettingsFlappingSection } from './flapping/rules_settings_flapping_section';
|
||||
import { RulesSettingsQueryDelaySection } from './query_delay/rules_settings_query_delay_section';
|
||||
import { useGetQueryDelaySettings } from '../../hooks/use_get_query_delay_settings';
|
||||
import { useUpdateRuleSettings } from '../../hooks/use_update_rules_settings';
|
||||
import { CenterJustifiedSpinner } from '../center_justified_spinner';
|
||||
|
||||
const flappingDescription = i18n.translate(
|
||||
'xpack.triggersActionsUI.rulesSettings.modal.flappingDetectionDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Detect alerts that switch quickly between active and recovered states and reduce unwanted noise for these flapping alerts.',
|
||||
}
|
||||
);
|
||||
|
||||
const flappingOnLabel = i18n.translate(
|
||||
'xpack.triggersActionsUI.rulesSettings.modal.flappingOnLabel',
|
||||
{
|
||||
defaultMessage: 'On (recommended)',
|
||||
}
|
||||
);
|
||||
|
||||
const flappingOffLabel = i18n.translate(
|
||||
'xpack.triggersActionsUI.rulesSettings.modal.flappingOffLabel',
|
||||
{
|
||||
defaultMessage: 'Off',
|
||||
}
|
||||
);
|
||||
|
||||
export const RulesSettingsErrorPrompt = memo(() => {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
|
@ -87,70 +60,6 @@ export const RulesSettingsErrorPrompt = memo(() => {
|
|||
);
|
||||
});
|
||||
|
||||
interface RulesSettingsModalFormLeftProps {
|
||||
settings: RulesSettingsFlappingProperties;
|
||||
onChange: EuiSwitchProps['onChange'];
|
||||
isSwitchDisabled: boolean;
|
||||
}
|
||||
|
||||
export const RulesSettingsModalFormLeft = memo((props: RulesSettingsModalFormLeftProps) => {
|
||||
const { settings, onChange, isSwitchDisabled } = props;
|
||||
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color="subdued" size="s">
|
||||
<p>{flappingDescription}</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSwitch
|
||||
data-test-subj="rulesSettingsModalEnableSwitch"
|
||||
label={settings!.enabled ? flappingOnLabel : flappingOffLabel}
|
||||
checked={settings!.enabled}
|
||||
disabled={isSwitchDisabled}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
});
|
||||
|
||||
interface RulesSettingsModalFormRightProps {
|
||||
settings: RulesSettingsFlappingProperties;
|
||||
onChange: RulesSettingsFlappingFormSectionProps['onChange'];
|
||||
}
|
||||
|
||||
export const RulesSettingsModalFormRight = memo((props: RulesSettingsModalFormRightProps) => {
|
||||
const { settings, onChange } = props;
|
||||
|
||||
if (!settings) {
|
||||
return null;
|
||||
}
|
||||
if (!settings.enabled) {
|
||||
return (
|
||||
<EuiFlexItem data-test-subj="rulesSettingsModalFlappingOffPrompt">
|
||||
<EuiPanel borderRadius="none" color="subdued" grow={false}>
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.rulesSettings.flapping.flappingSettingsOffDescription"
|
||||
defaultMessage="Alert flapping detection is off. Alerts will be generated based on the rule interval, which might result in higher alert volumes."
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<RulesSettingsFlappingFormSection flappingSettings={settings} onChange={onChange} />
|
||||
</EuiFlexItem>
|
||||
);
|
||||
});
|
||||
|
||||
export interface RulesSettingsModalProps {
|
||||
isVisible: boolean;
|
||||
setUpdatingRulesSettings?: (isUpdating: boolean) => void;
|
||||
|
@ -165,16 +74,27 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => {
|
|||
application: { capabilities },
|
||||
} = useKibana().services;
|
||||
const {
|
||||
rulesSettings: { show, save, writeFlappingSettingsUI, readFlappingSettingsUI },
|
||||
rulesSettings: {
|
||||
show,
|
||||
save,
|
||||
writeFlappingSettingsUI,
|
||||
readFlappingSettingsUI,
|
||||
writeQueryDelaySettingsUI,
|
||||
readQueryDelaySettingsUI,
|
||||
},
|
||||
} = capabilities;
|
||||
|
||||
const [settings, setSettings] = useState<RulesSettingsFlappingProperties>();
|
||||
const [flappingSettings, setFlappingSettings] = useState<RulesSettingsFlappingProperties>();
|
||||
const [hasFlappingChanged, setHasFlappingChanged] = useState<boolean>(false);
|
||||
|
||||
const { isLoading, isError: hasError } = useGetFlappingSettings({
|
||||
const [queryDelaySettings, setQueryDelaySettings] = useState<RulesSettingsQueryDelayProperties>();
|
||||
const [hasQueryDelayChanged, setHasQueryDelayChanged] = useState<boolean>(false);
|
||||
|
||||
const { isLoading: isFlappingLoading, isError: hasFlappingError } = useGetFlappingSettings({
|
||||
enabled: isVisible,
|
||||
onSuccess: (fetchedSettings) => {
|
||||
if (!settings) {
|
||||
setSettings({
|
||||
if (!flappingSettings) {
|
||||
setFlappingSettings({
|
||||
enabled: fetchedSettings.enabled,
|
||||
lookBackWindow: fetchedSettings.lookBackWindow,
|
||||
statusChangeThreshold: fetchedSettings.statusChangeThreshold,
|
||||
|
@ -183,7 +103,18 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => {
|
|||
},
|
||||
});
|
||||
|
||||
const { mutate } = useUpdateFlappingSettings({
|
||||
const { isLoading: isQueryDelayLoading, isError: hasQueryDelayError } = useGetQueryDelaySettings({
|
||||
enabled: isVisible,
|
||||
onSuccess: (fetchedSettings) => {
|
||||
if (!queryDelaySettings) {
|
||||
setQueryDelaySettings({
|
||||
delay: fetchedSettings.delay,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate } = useUpdateRuleSettings({
|
||||
onSave,
|
||||
onClose,
|
||||
setUpdatingRulesSettings,
|
||||
|
@ -192,36 +123,56 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => {
|
|||
// In the future when we have more settings sub-features, we should
|
||||
// disassociate the rule settings capabilities (save, show) from the
|
||||
// sub-feature capabilities (writeXSettingsUI).
|
||||
const canWriteFlappingSettings = save && writeFlappingSettingsUI && !hasError;
|
||||
const canWriteFlappingSettings = save && writeFlappingSettingsUI && !hasFlappingError;
|
||||
const canShowFlappingSettings = show && readFlappingSettingsUI;
|
||||
const canWriteQueryDelaySettings = save && writeQueryDelaySettingsUI && !hasQueryDelayError;
|
||||
const canShowQueryDelaySettings = show && readQueryDelaySettingsUI;
|
||||
|
||||
const handleSettingsChange = (
|
||||
key: keyof RulesSettingsFlappingProperties,
|
||||
value: number | boolean
|
||||
setting: keyof RulesSettingsProperties,
|
||||
key: keyof RulesSettingsFlappingProperties | keyof RulesSettingsQueryDelayProperties,
|
||||
value: boolean | number
|
||||
) => {
|
||||
if (!settings) {
|
||||
return;
|
||||
if (setting === 'flapping') {
|
||||
if (!flappingSettings) {
|
||||
return;
|
||||
}
|
||||
const newSettings = {
|
||||
...flappingSettings,
|
||||
[key]: value,
|
||||
};
|
||||
setFlappingSettings({
|
||||
...newSettings,
|
||||
statusChangeThreshold: Math.min(
|
||||
newSettings.lookBackWindow,
|
||||
newSettings.statusChangeThreshold
|
||||
),
|
||||
});
|
||||
setHasFlappingChanged(true);
|
||||
}
|
||||
|
||||
const newSettings = {
|
||||
...settings,
|
||||
[key]: value,
|
||||
};
|
||||
|
||||
setSettings({
|
||||
...newSettings,
|
||||
statusChangeThreshold: Math.min(
|
||||
newSettings.lookBackWindow,
|
||||
newSettings.statusChangeThreshold
|
||||
),
|
||||
});
|
||||
if (setting === 'queryDelay') {
|
||||
if (!queryDelaySettings) {
|
||||
return;
|
||||
}
|
||||
const newSettings = {
|
||||
...queryDelaySettings,
|
||||
[key]: value,
|
||||
};
|
||||
setQueryDelaySettings(newSettings);
|
||||
setHasQueryDelayChanged(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!settings) {
|
||||
return;
|
||||
const updatedSettings: RulesSettingsProperties = {};
|
||||
if (canWriteFlappingSettings && hasFlappingChanged) {
|
||||
updatedSettings.flapping = flappingSettings;
|
||||
}
|
||||
mutate(settings);
|
||||
if (canWriteQueryDelaySettings && hasQueryDelayChanged) {
|
||||
updatedSettings.queryDelay = queryDelaySettings;
|
||||
}
|
||||
mutate(updatedSettings);
|
||||
};
|
||||
|
||||
if (!isVisible) {
|
||||
|
@ -229,32 +180,36 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => {
|
|||
}
|
||||
|
||||
const maybeRenderForm = () => {
|
||||
if (hasError || !canShowFlappingSettings) {
|
||||
if (!canShowFlappingSettings && !canShowQueryDelaySettings) {
|
||||
return <RulesSettingsErrorPrompt />;
|
||||
}
|
||||
if (!settings || isLoading) {
|
||||
if (isFlappingLoading || isQueryDelayLoading) {
|
||||
return <CenterJustifiedSpinner />;
|
||||
}
|
||||
return (
|
||||
<EuiForm>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<RulesSettingsFlappingTitle />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup>
|
||||
<RulesSettingsModalFormLeft
|
||||
isSwitchDisabled={!canWriteFlappingSettings}
|
||||
settings={settings}
|
||||
onChange={(e) => handleSettingsChange('enabled', e.target.checked)}
|
||||
<>
|
||||
{flappingSettings && (
|
||||
<RulesSettingsFlappingSection
|
||||
onChange={(key, value) => handleSettingsChange('flapping', key, value)}
|
||||
settings={flappingSettings}
|
||||
canWrite={canWriteFlappingSettings}
|
||||
canShow={canShowFlappingSettings}
|
||||
hasError={hasFlappingError}
|
||||
/>
|
||||
<RulesSettingsModalFormRight
|
||||
settings={settings}
|
||||
onChange={(key, value) => handleSettingsChange(key, value)}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
</EuiForm>
|
||||
)}
|
||||
{queryDelaySettings && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<RulesSettingsQueryDelaySection
|
||||
onChange={(key, value) => handleSettingsChange('queryDelay', key, value)}
|
||||
settings={queryDelaySettings}
|
||||
canWrite={canWriteQueryDelaySettings}
|
||||
canShow={canShowQueryDelaySettings}
|
||||
hasError={hasQueryDelayError}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -291,7 +246,7 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => {
|
|||
fill
|
||||
data-test-subj="rulesSettingsModalSaveButton"
|
||||
onClick={handleSave}
|
||||
disabled={!canWriteFlappingSettings}
|
||||
disabled={!canWriteFlappingSettings && !canWriteQueryDelaySettings}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.rulesSettings.modal.saveButton"
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 React, { memo } from 'react';
|
||||
import { EuiFormRow, EuiFormRowProps, EuiIconTip, EuiRange, EuiRangeProps } from '@elastic/eui';
|
||||
|
||||
export interface RulesSettingsRangeProps {
|
||||
label: EuiFormRowProps['label'];
|
||||
labelPopoverText?: string;
|
||||
min: number;
|
||||
max: number;
|
||||
value: number;
|
||||
disabled?: EuiRangeProps['disabled'];
|
||||
onChange?: EuiRangeProps['onChange'];
|
||||
}
|
||||
|
||||
export const RulesSettingsRange = memo((props: RulesSettingsRangeProps) => {
|
||||
const { label, labelPopoverText, min, max, value, disabled, onChange, ...rest } = props;
|
||||
|
||||
const renderLabel = () => {
|
||||
return (
|
||||
<div>
|
||||
{label}
|
||||
|
||||
{labelPopoverText && (
|
||||
<EuiIconTip color="subdued" size="s" type="questionInCircle" content={labelPopoverText} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFormRow label={renderLabel()}>
|
||||
<EuiRange
|
||||
min={min}
|
||||
max={max}
|
||||
step={1}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
showLabels
|
||||
showValue
|
||||
{...rest}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
});
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { RulesSettingsFlapping } from '@kbn/alerting-plugin/common';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
|
@ -18,27 +17,15 @@ interface UseGetFlappingSettingsProps {
|
|||
|
||||
export const useGetFlappingSettings = (props: UseGetFlappingSettingsProps) => {
|
||||
const { enabled, onSuccess } = props;
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const queryFn = () => {
|
||||
return getFlappingSettings({ http });
|
||||
};
|
||||
|
||||
const onErrorFn = () => {
|
||||
toasts.addDanger(
|
||||
i18n.translate('xpack.triggersActionsUI.rulesSettings.modal.getRulesSettingsError', {
|
||||
defaultMessage: 'Failed to get rules Settings.',
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const { data, isFetching, isError, isLoadingError, isLoading } = useQuery({
|
||||
queryKey: ['getFlappingSettings'],
|
||||
queryFn,
|
||||
onError: onErrorFn,
|
||||
onSuccess,
|
||||
enabled,
|
||||
refetchOnWindowFocus: false,
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { renderHook } from '@testing-library/react-hooks/dom';
|
||||
import { waitFor } from '@testing-library/dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { useGetQueryDelaySettings } from './use_get_query_delay_settings';
|
||||
|
||||
jest.mock('../lib/rule_api/get_query_delay_settings', () => ({
|
||||
getQueryDelaySettings: jest.fn(),
|
||||
}));
|
||||
|
||||
const { getQueryDelaySettings } = jest.requireMock('../lib/rule_api/get_query_delay_settings');
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
cacheTime: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
const wrapper = ({ children }: { children: Node }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
describe('useGetQueryDelaySettings', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call getQueryDelaySettings', async () => {
|
||||
renderHook(() => useGetQueryDelaySettings({ enabled: true, onSuccess: () => {} }), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getQueryDelaySettings).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return isError = true if api fails', async () => {
|
||||
getQueryDelaySettings.mockRejectedValue('This is an error.');
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useGetQueryDelaySettings({ enabled: true, onSuccess: () => {} }),
|
||||
{
|
||||
wrapper,
|
||||
}
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
import { RulesSettingsQueryDelay } from '@kbn/alerting-plugin/common';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { getQueryDelaySettings } from '../lib/rule_api/get_query_delay_settings';
|
||||
|
||||
interface UseGetQueryDelaySettingsProps {
|
||||
enabled: boolean;
|
||||
onSuccess: (settings: RulesSettingsQueryDelay) => void;
|
||||
}
|
||||
|
||||
export const useGetQueryDelaySettings = (props: UseGetQueryDelaySettingsProps) => {
|
||||
const { enabled, onSuccess } = props;
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const queryFn = () => {
|
||||
return getQueryDelaySettings({ http });
|
||||
};
|
||||
|
||||
const { data, isFetching, isError, isLoadingError, isLoading } = useQuery({
|
||||
queryKey: ['getQueryDelaySettings'],
|
||||
queryFn,
|
||||
onSuccess,
|
||||
enabled,
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
return {
|
||||
isLoading: isLoading || isFetching,
|
||||
isError: isError || isLoadingError,
|
||||
data,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { act, renderHook } from '@testing-library/react-hooks/dom';
|
||||
import { waitFor } from '@testing-library/dom';
|
||||
import { useUpdateRuleSettings } from './use_update_rules_settings';
|
||||
|
||||
const mockAddDanger = jest.fn();
|
||||
const mockAddSuccess = jest.fn();
|
||||
|
||||
jest.mock('../../common/lib/kibana', () => {
|
||||
const originalModule = jest.requireActual('../../common/lib/kibana');
|
||||
return {
|
||||
...originalModule,
|
||||
useKibana: () => {
|
||||
const { services } = originalModule.useKibana();
|
||||
return {
|
||||
services: {
|
||||
...services,
|
||||
notifications: { toasts: { addSuccess: mockAddSuccess, addDanger: mockAddDanger } },
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
jest.mock('../lib/rule_api/update_query_delay_settings', () => ({
|
||||
updateQueryDelaySettings: jest.fn(),
|
||||
}));
|
||||
jest.mock('../lib/rule_api/update_flapping_settings', () => ({
|
||||
updateFlappingSettings: jest.fn(),
|
||||
}));
|
||||
|
||||
const { updateQueryDelaySettings } = jest.requireMock(
|
||||
'../lib/rule_api/update_query_delay_settings'
|
||||
);
|
||||
const { updateFlappingSettings } = jest.requireMock('../lib/rule_api/update_flapping_settings');
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
cacheTime: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
const wrapper = ({ children }: { children: Node }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
describe('useUpdateRuleSettings', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call onSuccess if api succeeds', async () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useUpdateRuleSettings({
|
||||
onSave: () => {},
|
||||
onClose: () => {},
|
||||
setUpdatingRulesSettings: () => {},
|
||||
}),
|
||||
{
|
||||
wrapper,
|
||||
}
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutate({
|
||||
flapping: { enabled: true, lookBackWindow: 3, statusChangeThreshold: 3 },
|
||||
queryDelay: { delay: 2 },
|
||||
});
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(mockAddSuccess).toBeCalledWith('Rules settings updated successfully.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onError if api fails', async () => {
|
||||
updateQueryDelaySettings.mockRejectedValue('');
|
||||
updateFlappingSettings.mockRejectedValue('');
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useUpdateRuleSettings({
|
||||
onSave: () => {},
|
||||
onClose: () => {},
|
||||
setUpdatingRulesSettings: () => {},
|
||||
}),
|
||||
{
|
||||
wrapper,
|
||||
}
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutate({
|
||||
flapping: { enabled: true, lookBackWindow: 3, statusChangeThreshold: 3 },
|
||||
queryDelay: { delay: 2 },
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => expect(mockAddDanger).toBeCalledWith('Failed to update rules settings.'));
|
||||
});
|
||||
});
|
|
@ -7,17 +7,18 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { RulesSettingsFlappingProperties } from '@kbn/alerting-plugin/common';
|
||||
import { RulesSettingsProperties } from '@kbn/alerting-plugin/common';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { updateFlappingSettings } from '../lib/rule_api/update_flapping_settings';
|
||||
import { updateQueryDelaySettings } from '../lib/rule_api/update_query_delay_settings';
|
||||
|
||||
interface UseUpdateFlappingSettingsProps {
|
||||
interface UseUpdateRuleSettingsProps {
|
||||
onClose: () => void;
|
||||
onSave?: () => void;
|
||||
setUpdatingRulesSettings?: (isUpdating: boolean) => void;
|
||||
}
|
||||
|
||||
export const useUpdateFlappingSettings = (props: UseUpdateFlappingSettingsProps) => {
|
||||
export const useUpdateRuleSettings = (props: UseUpdateRuleSettingsProps) => {
|
||||
const { onSave, onClose, setUpdatingRulesSettings } = props;
|
||||
|
||||
const {
|
||||
|
@ -25,8 +26,17 @@ export const useUpdateFlappingSettings = (props: UseUpdateFlappingSettingsProps)
|
|||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
const mutationFn = (flappingSettings: RulesSettingsFlappingProperties) => {
|
||||
return updateFlappingSettings({ http, flappingSettings });
|
||||
const mutationFn = async (settings: RulesSettingsProperties) => {
|
||||
const updates = [];
|
||||
if (settings.flapping) {
|
||||
updates.push(updateFlappingSettings({ http, flappingSettings: settings.flapping }));
|
||||
}
|
||||
|
||||
if (settings.queryDelay) {
|
||||
updates.push(updateQueryDelaySettings({ http, queryDelaySettings: settings.queryDelay }));
|
||||
}
|
||||
|
||||
return await Promise.all(updates);
|
||||
};
|
||||
|
||||
return useMutation({
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { httpServiceMock } from '@kbn/core/public/mocks';
|
||||
import { getQueryDelaySettings } from './get_query_delay_settings';
|
||||
|
||||
const http = httpServiceMock.createStartContract();
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('getQueryDelaySettings', () => {
|
||||
test('should call get query delay settings api', async () => {
|
||||
const apiResponse = {
|
||||
delay: 10,
|
||||
};
|
||||
http.get.mockResolvedValueOnce(apiResponse);
|
||||
|
||||
const result = await getQueryDelaySettings({ http });
|
||||
expect(result).toEqual({ delay: 10 });
|
||||
expect(http.get.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"/internal/alerting/rules/settings/_query_delay",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { HttpSetup } from '@kbn/core/public';
|
||||
import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common';
|
||||
import { RulesSettingsQueryDelay } from '@kbn/alerting-plugin/common';
|
||||
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';
|
||||
|
||||
const rewriteBodyRes: RewriteRequestCase<RulesSettingsQueryDelay> = ({ ...rest }: any) => ({
|
||||
...rest,
|
||||
});
|
||||
|
||||
export const getQueryDelaySettings = async ({ http }: { http: HttpSetup }) => {
|
||||
const res = await http.get<AsApiContract<RulesSettingsQueryDelay>>(
|
||||
`${INTERNAL_BASE_ALERTING_API_PATH}/rules/settings/_query_delay`
|
||||
);
|
||||
return rewriteBodyRes(res);
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { httpServiceMock } from '@kbn/core/public/mocks';
|
||||
import { updateQueryDelaySettings } from './update_query_delay_settings';
|
||||
|
||||
const http = httpServiceMock.createStartContract();
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('updateQueryDelaySettings', () => {
|
||||
test('should call update query delay settings api', async () => {
|
||||
const apiResponse = {
|
||||
delay: 10,
|
||||
};
|
||||
http.post.mockResolvedValueOnce(apiResponse);
|
||||
|
||||
const result = await updateQueryDelaySettings({ http, queryDelaySettings: { delay: 10 } });
|
||||
expect(result).toEqual({ delay: 10 });
|
||||
expect(http.post.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"/internal/alerting/rules/settings/_query_delay",
|
||||
Object {
|
||||
"body": "{\\"delay\\":10}",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { HttpSetup } from '@kbn/core/public';
|
||||
import {
|
||||
RulesSettingsQueryDelay,
|
||||
RulesSettingsQueryDelayProperties,
|
||||
} from '@kbn/alerting-plugin/common';
|
||||
import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common';
|
||||
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';
|
||||
|
||||
const rewriteBodyRes: RewriteRequestCase<RulesSettingsQueryDelay> = ({ ...rest }: any) => ({
|
||||
...rest,
|
||||
});
|
||||
|
||||
export const updateQueryDelaySettings = async ({
|
||||
http,
|
||||
queryDelaySettings,
|
||||
}: {
|
||||
http: HttpSetup;
|
||||
queryDelaySettings: RulesSettingsQueryDelayProperties;
|
||||
}) => {
|
||||
let body: string;
|
||||
try {
|
||||
body = JSON.stringify({
|
||||
delay: queryDelaySettings.delay,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(`Unable to parse query delay settings update params: ${e}`);
|
||||
}
|
||||
|
||||
const res = await http.post<AsApiContract<RulesSettingsQueryDelay>>(
|
||||
`${INTERNAL_BASE_ALERTING_API_PATH}/rules/settings/_query_delay`,
|
||||
{
|
||||
body,
|
||||
}
|
||||
);
|
||||
|
||||
return rewriteBodyRes(res);
|
||||
};
|
|
@ -713,6 +713,28 @@ describe('timeSeriesQuery', () => {
|
|||
{ ignore: [404], meta: true }
|
||||
);
|
||||
});
|
||||
|
||||
it('uses the passed in date parms when useCalculatedDateRange = false param is passed', async () => {
|
||||
await timeSeriesQuery({
|
||||
...params,
|
||||
useCalculatedDateRange: false,
|
||||
query: {
|
||||
...params.query,
|
||||
dateStart: '2023-10-12T00:00:00Z',
|
||||
dateEnd: '2023-10-12T00:00:00Z',
|
||||
},
|
||||
});
|
||||
// @ts-ignore
|
||||
expect(esClient.search.mock.calls[0]![0].body.query.bool.filter[0]).toEqual({
|
||||
range: {
|
||||
'time-field': {
|
||||
format: 'strict_date_time',
|
||||
gte: '2023-10-12T00:00:00Z',
|
||||
lt: '2023-10-12T00:00:00Z',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getResultFromEs', () => {
|
||||
|
|
|
@ -33,12 +33,19 @@ export interface TimeSeriesQueryParameters {
|
|||
esClient: ElasticsearchClient;
|
||||
query: TimeSeriesQuery;
|
||||
condition?: TimeSeriesCondition;
|
||||
useCalculatedDateRange?: boolean;
|
||||
}
|
||||
|
||||
export async function timeSeriesQuery(
|
||||
params: TimeSeriesQueryParameters
|
||||
): Promise<TimeSeriesResult> {
|
||||
const { logger, esClient, query: queryParams, condition: conditionParams } = params;
|
||||
const {
|
||||
logger,
|
||||
esClient,
|
||||
query: queryParams,
|
||||
condition: conditionParams,
|
||||
useCalculatedDateRange = true,
|
||||
} = params;
|
||||
const {
|
||||
index,
|
||||
timeWindowSize,
|
||||
|
@ -67,8 +74,8 @@ export async function timeSeriesQuery(
|
|||
{
|
||||
range: {
|
||||
[timeField]: {
|
||||
gte: dateRangeInfo.dateStart,
|
||||
lt: dateRangeInfo.dateEnd,
|
||||
gte: useCalculatedDateRange ? dateRangeInfo.dateStart : dateStart,
|
||||
lt: useCalculatedDateRange ? dateRangeInfo.dateEnd : dateEnd,
|
||||
format: 'strict_date_time',
|
||||
},
|
||||
},
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue