diff --git a/x-pack/plugins/alerting/common/index.ts b/x-pack/plugins/alerting/common/index.ts index 05b9e84ee2b7a..e2e9e477cc4cc 100644 --- a/x-pack/plugins/alerting/common/index.ts +++ b/x-pack/plugins/alerting/common/index.ts @@ -34,7 +34,6 @@ export * from './parse_duration'; export * from './execution_log_types'; export * from './rule_snooze_type'; export * from './rrule_type'; -export * from './default_rule_aggregation'; export * from './rule_tags_aggregation'; export * from './iso_weekdays'; export * from './saved_objects/rules/mappings'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/aggregate/index.ts b/x-pack/plugins/alerting/common/routes/rule/apis/aggregate/index.ts new file mode 100644 index 0000000000000..0bbdbc0ea4069 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/aggregate/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { + aggregateRulesRequestBodySchema, + aggregateRulesResponseBodySchema, +} from './schemas/latest'; +export type { AggregateRulesRequestBody, AggregateRulesResponseBody } from './types/latest'; + +export { + aggregateRulesRequestBodySchema as aggregateRulesRequestBodySchemaV1, + aggregateRulesResponseBodySchema as aggregateRulesResponseBodySchemaV1, +} from './schemas/v1'; +export type { + AggregateRulesRequestBody as AggregateRulesRequestBodyV1, + AggregateRulesResponseBody as AggregateRulesResponseBodyV1, + AggregateRulesResponse as AggregateRulesResponseV1, +} from './types/v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/aggregate/schemas/latest.ts b/x-pack/plugins/alerting/common/routes/rule/apis/aggregate/schemas/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/aggregate/schemas/latest.ts @@ -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'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/aggregate/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/rule/apis/aggregate/schemas/v1.ts new file mode 100644 index 0000000000000..d49ccb090d53d --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/aggregate/schemas/v1.ts @@ -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 { schema } from '@kbn/config-schema'; + +export const aggregateRulesRequestBodySchema = schema.object({ + search: schema.maybe(schema.string()), + default_search_operator: schema.oneOf([schema.literal('OR'), schema.literal('AND')], { + defaultValue: 'OR', + }), + search_fields: schema.maybe(schema.arrayOf(schema.string())), + has_reference: schema.maybe( + // use nullable as maybe is currently broken + // in config-schema + schema.nullable( + schema.object({ + type: schema.string(), + id: schema.string(), + }) + ) + ), + filter: schema.maybe(schema.string()), +}); + +export const aggregateRulesResponseBodySchema = schema.object({ + rule_execution_status: schema.recordOf(schema.string(), schema.number()), + rule_last_run_outcome: schema.recordOf(schema.string(), schema.number()), + rule_enabled_status: schema.object({ + enabled: schema.number(), + disabled: schema.number(), + }), + rule_muted_status: schema.object({ + muted: schema.number(), + unmuted: schema.number(), + }), + rule_snoozed_status: schema.object({ + snoozed: schema.number(), + }), + rule_tags: schema.arrayOf(schema.string()), +}); diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/aggregate/types/latest.ts b/x-pack/plugins/alerting/common/routes/rule/apis/aggregate/types/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/aggregate/types/latest.ts @@ -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'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/aggregate/types/v1.ts b/x-pack/plugins/alerting/common/routes/rule/apis/aggregate/types/v1.ts new file mode 100644 index 0000000000000..2dc21a72b3783 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/aggregate/types/v1.ts @@ -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 { aggregateRulesRequestBodySchemaV1, aggregateRulesResponseBodySchemaV1 } from '..'; + +export type AggregateRulesRequestBody = TypeOf; +export type AggregateRulesResponseBody = TypeOf; + +export interface AggregateRulesResponse { + body: AggregateRulesResponseBody; +} diff --git a/x-pack/plugins/alerting/common/rule.ts b/x-pack/plugins/alerting/common/rule.ts index 22692a091a38c..5a8e1b275ef7e 100644 --- a/x-pack/plugins/alerting/common/rule.ts +++ b/x-pack/plugins/alerting/common/rule.ts @@ -10,7 +10,7 @@ import type { SavedObjectAttributes, SavedObjectsResolveResponse, } from '@kbn/core/server'; -import type { Filter, KueryNode } from '@kbn/es-query'; +import type { Filter } from '@kbn/es-query'; import { IsoWeekday } from './iso_weekdays'; import { RuleNotifyWhenType } from './rule_notify_when_type'; import { RuleSnooze } from './rule_snooze_type'; @@ -118,28 +118,6 @@ export interface RuleAction { alertsFilter?: AlertsFilter; } -export interface AggregateOptions { - search?: string; - defaultSearchOperator?: 'AND' | 'OR'; - searchFields?: string[]; - hasReference?: { - type: string; - id: string; - }; - filter?: string | KueryNode; - page?: number; - perPage?: number; -} - -export interface RuleAggregationFormattedResult { - ruleExecutionStatus: { [status: string]: number }; - ruleLastRunOutcome: { [status: string]: number }; - ruleEnabledStatus: { enabled: number; disabled: number }; - ruleMutedStatus: { muted: number; unmuted: number }; - ruleSnoozedStatus: { snoozed: number }; - ruleTags: string[]; -} - export interface RuleLastRun { outcome: RuleLastRunOutcomes; outcomeOrder?: number; diff --git a/x-pack/plugins/alerting/common/rule_tags_aggregation.ts b/x-pack/plugins/alerting/common/rule_tags_aggregation.ts index d7b8fc4089c27..04cf6676059d8 100644 --- a/x-pack/plugins/alerting/common/rule_tags_aggregation.ts +++ b/x-pack/plugins/alerting/common/rule_tags_aggregation.ts @@ -10,7 +10,7 @@ import type { AggregationsCompositeAggregation, AggregationsAggregateOrder, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { AggregateOptions } from './rule'; +import type { AggregateOptions } from '../server/application/rule/methods/aggregate/types'; export type RuleTagsAggregationOptions = Pick & { after?: AggregationsCompositeAggregation['after']; diff --git a/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/aggregate_rules.test.ts similarity index 91% rename from x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts rename to x-pack/plugins/alerting/server/application/rule/methods/aggregate/aggregate_rules.test.ts index 64f7325092691..b8fbc0427b7f4 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/aggregate_rules.test.ts @@ -5,45 +5,43 @@ * 2.0. */ -import { RulesClient, ConstructorOptions } from '../rules_client'; +import { RulesClient, ConstructorOptions } from '../../../../rules_client'; import { savedObjectsClientMock, loggingSystemMock, savedObjectsRepositoryMock, } from '@kbn/core/server/mocks'; import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; -import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; -import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; +import { ruleTypeRegistryMock } from '../../../../rule_type_registry.mock'; +import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks'; -import { AlertingAuthorization } from '../../authorization/alerting_authorization'; +import { AlertingAuthorization } from '../../../../authorization/alerting_authorization'; import { ActionsAuthorization } from '@kbn/actions-plugin/server'; import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks'; -import { getBeforeSetup, setGlobalDate } from './lib'; -import { - RecoveredActionGroup, - getDefaultRuleAggregation, - DefaultRuleAggregationResult, -} from '../../../common'; -import { RegistryRuleType } from '../../rule_type_registry'; +import { getBeforeSetup, setGlobalDate } from '../../../../rules_client/tests/lib'; + +import { RegistryRuleType } from '../../../../rule_type_registry'; import { fromKueryExpression, nodeTypes } from '@kbn/es-query'; +import { RecoveredActionGroup } from '../../../../../common'; +import { DefaultRuleAggregationResult } from '../../../../routes/rule/apis/aggregate/types'; +import { defaultRuleAggregationFactory } from '.'; const taskManager = taskManagerMock.createStart(); const ruleTypeRegistry = ruleTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); +const internalSavedObjectsRepository = savedObjectsRepositoryMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); const auditLogger = auditLoggerMock.create(); -const internalSavedObjectsRepository = savedObjectsRepositoryMock.create(); const kibanaVersion = 'v7.10.0'; const rulesClientParams: jest.Mocked = { taskManager, ruleTypeRegistry, unsecuredSavedObjectsClient, - maxScheduledPerMinute: 10000, minimumScheduleInterval: { value: '1m', enforce: false }, authorization: authorization as unknown as AlertingAuthorization, actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization, @@ -52,13 +50,14 @@ const rulesClientParams: jest.Mocked = { getUserName: jest.fn(), createAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), - internalSavedObjectsRepository, encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), getEventLogClient: jest.fn(), kibanaVersion, isAuthenticationTypeAPIKey: jest.fn(), getAuthenticationAPIKey: jest.fn(), + maxScheduledPerMinute: 1000, + internalSavedObjectsRepository, }; beforeEach(() => { @@ -176,7 +175,7 @@ describe('aggregate()', () => { const rulesClient = new RulesClient(rulesClientParams); const result = await rulesClient.aggregate({ options: {}, - aggs: getDefaultRuleAggregation(), + aggs: defaultRuleAggregationFactory(), }); expect(result).toMatchInlineSnapshot(` @@ -332,7 +331,7 @@ describe('aggregate()', () => { const rulesClient = new RulesClient(rulesClientParams); await rulesClient.aggregate({ options: { filter: 'foo: someTerm' }, - aggs: getDefaultRuleAggregation(), + aggs: defaultRuleAggregationFactory(), }); expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1); @@ -385,7 +384,9 @@ describe('aggregate()', () => { const rulesClient = new RulesClient({ ...rulesClientParams, auditLogger }); authorization.getFindAuthorizationFilter.mockRejectedValue(new Error('Unauthorized')); - await expect(rulesClient.aggregate({ aggs: getDefaultRuleAggregation() })).rejects.toThrow(); + await expect( + rulesClient.aggregate({ aggs: defaultRuleAggregationFactory() }) + ).rejects.toThrow(); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ @@ -404,7 +405,7 @@ describe('aggregate()', () => { test('sets to default (50) if it is not provided', async () => { const rulesClient = new RulesClient(rulesClientParams); - await rulesClient.aggregate({ aggs: getDefaultRuleAggregation() }); + await rulesClient.aggregate({ aggs: defaultRuleAggregationFactory() }); expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchObject([ { @@ -421,7 +422,7 @@ describe('aggregate()', () => { const rulesClient = new RulesClient(rulesClientParams); await rulesClient.aggregate({ - aggs: getDefaultRuleAggregation({ maxTags: 1000 }), + aggs: defaultRuleAggregationFactory({ maxTags: 1000 }), }); expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchObject([ diff --git a/x-pack/plugins/alerting/server/application/rule/methods/aggregate/aggregate_rules.ts b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/aggregate_rules.ts new file mode 100644 index 0000000000000..f992f5cead705 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/aggregate_rules.ts @@ -0,0 +1,62 @@ +/* + * 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 { KueryNode, nodeBuilder } from '@kbn/es-query'; +import { findRulesSo } from '../../../../data/rule'; +import { AlertingAuthorizationEntity } from '../../../../authorization'; +import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events'; +import { buildKueryNodeFilter } from '../../../../rules_client/common'; +import { alertingAuthorizationFilterOpts } from '../../../../rules_client/common/constants'; +import { RulesClientContext } from '../../../../rules_client/types'; +import { aggregateOptionsSchema } from './schemas'; +import type { AggregateParams } from './types'; +import { validateRuleAggregationFields } from './validation'; + +export async function aggregateRules>( + context: RulesClientContext, + params: AggregateParams +): Promise { + const { options = {}, aggs } = params; + const { filter, page = 1, perPage = 0, ...restOptions } = options; + + let authorizationTuple; + try { + authorizationTuple = await context.authorization.getFindAuthorizationFilter( + AlertingAuthorizationEntity.Rule, + alertingAuthorizationFilterOpts + ); + validateRuleAggregationFields(aggs); + aggregateOptionsSchema.validate(options); + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.AGGREGATE, + error, + }) + ); + throw error; + } + + const { filter: authorizationFilter } = authorizationTuple; + const filterKueryNode = buildKueryNodeFilter(filter); + + const { aggregations } = await findRulesSo({ + savedObjectsClient: context.unsecuredSavedObjectsClient, + savedObjectsFindOptions: { + ...restOptions, + filter: + authorizationFilter && filterKueryNode + ? nodeBuilder.and([filterKueryNode, authorizationFilter as KueryNode]) + : authorizationFilter, + page, + perPage, + aggs, + }, + }); + + return aggregations!; +} diff --git a/x-pack/plugins/alerting/server/application/rule/methods/aggregate/factories/default_rule_aggregation_factory/latest.ts b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/factories/default_rule_aggregation_factory/latest.ts new file mode 100644 index 0000000000000..210b2ee194850 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/factories/default_rule_aggregation_factory/latest.ts @@ -0,0 +1,7 @@ +/* + * 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 { defaultRuleAggregationFactory } from './v1'; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/aggregate/factories/default_rule_aggregation_factory/v1.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/factories/default_rule_aggregation_factory/v1.test.ts new file mode 100644 index 0000000000000..2accb33511681 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/factories/default_rule_aggregation_factory/v1.test.ts @@ -0,0 +1,24 @@ +/* + * 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 { defaultRuleAggregationFactory } from './v1'; + +describe('getDefaultRuleAggregation', () => { + it('should return aggregation with default maxTags', () => { + const result = defaultRuleAggregationFactory(); + expect(result.tags).toEqual({ + terms: { field: 'alert.attributes.tags', order: { _key: 'asc' }, size: 50 }, + }); + }); + + it('should return aggregation with custom maxTags', () => { + const result = defaultRuleAggregationFactory({ maxTags: 100 }); + expect(result.tags).toEqual({ + terms: { field: 'alert.attributes.tags', order: { _key: 'asc' }, size: 100 }, + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/application/rule/methods/aggregate/factories/default_rule_aggregation_factory/v1.ts b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/factories/default_rule_aggregation_factory/v1.ts new file mode 100644 index 0000000000000..371c12c1fada3 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/factories/default_rule_aggregation_factory/v1.ts @@ -0,0 +1,46 @@ +/* + * 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 { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/types'; +import { DefaultRuleAggregationParams } from '../../types'; + +export const defaultRuleAggregationFactory = ( + params?: DefaultRuleAggregationParams +): Record => { + const { maxTags = 50 } = params || {}; + return { + status: { + terms: { field: 'alert.attributes.executionStatus.status' }, + }, + outcome: { + terms: { field: 'alert.attributes.lastRun.outcome' }, + }, + enabled: { + terms: { field: 'alert.attributes.enabled' }, + }, + muted: { + terms: { field: 'alert.attributes.muteAll' }, + }, + tags: { + terms: { field: 'alert.attributes.tags', order: { _key: 'asc' }, size: maxTags }, + }, + snoozed: { + nested: { + path: 'alert.attributes.snoozeSchedule', + }, + aggs: { + count: { + filter: { + exists: { + field: 'alert.attributes.snoozeSchedule.duration', + }, + }, + }, + }, + }, + }; +}; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/aggregate/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/index.ts new file mode 100644 index 0000000000000..ccb5ca61daf9a --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { defaultRuleAggregationFactory } from './factories/default_rule_aggregation_factory/latest'; +export { defaultRuleAggregationFactory as defaultRuleAggregationFactoryV1 } from './factories/default_rule_aggregation_factory/v1'; +// export { aggregateOptionsSchema } from './schemas'; +// export type { AggregateOptions, AggregateParams, DefaultRuleAggregationParams } from './types'; + +export { aggregateRules } from './aggregate_rules'; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/aggregate/schemas/aggregate_options_schema.ts b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/schemas/aggregate_options_schema.ts new file mode 100644 index 0000000000000..3ac244b808752 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/schemas/aggregate_options_schema.ts @@ -0,0 +1,25 @@ +/* + * 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 aggregateOptionsSchema = schema.object({ + search: schema.maybe(schema.string()), + defaultSearchOperator: schema.maybe(schema.oneOf([schema.literal('AND'), schema.literal('OR')])), + searchFields: schema.maybe(schema.arrayOf(schema.string())), + hasReference: schema.maybe( + schema.object({ + type: schema.string(), + id: schema.string(), + }) + ), + // filter type is `string | KueryNode`, but `KueryNode` has no schema to import yet + filter: schema.maybe( + schema.oneOf([schema.string(), schema.recordOf(schema.string(), schema.any())]) + ), + page: schema.maybe(schema.number()), + perPage: schema.maybe(schema.number()), +}); diff --git a/x-pack/plugins/alerting/server/application/rule/methods/aggregate/schemas/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/schemas/index.ts new file mode 100644 index 0000000000000..c29f688596ef4 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/schemas/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { aggregateOptionsSchema } from './aggregate_options_schema'; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/aggregate/types/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/types/index.ts new file mode 100644 index 0000000000000..3733a49003bba --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/types/index.ts @@ -0,0 +1,48 @@ +/* + * 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 { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { TypeOf } from '@kbn/config-schema'; +import { KueryNode } from '@kbn/es-query'; +import { aggregateOptionsSchema } from '../schemas'; + +type AggregateOptionsSchemaTypes = TypeOf; +export type AggregateOptions = TypeOf & { + search?: AggregateOptionsSchemaTypes['search']; + defaultSearchOperator?: AggregateOptionsSchemaTypes['defaultSearchOperator']; + searchFields?: AggregateOptionsSchemaTypes['searchFields']; + hasReference?: AggregateOptionsSchemaTypes['hasReference']; + // Adding filter as in schema it's defined as any instead of KueryNode + filter?: string | KueryNode; + page?: AggregateOptionsSchemaTypes['page']; + perPage?: AggregateOptionsSchemaTypes['perPage']; +}; + +export interface AggregateParams { + options?: AggregateOptions; + aggs: Record; +} + +export interface DefaultRuleAggregationParams { + maxTags?: number; +} + +export interface RuleAggregationFormattedResult { + ruleExecutionStatus: Record; + ruleLastRunOutcome: Record; + ruleEnabledStatus: { + enabled: number; + disabled: number; + }; + ruleMutedStatus: { + muted: number; + unmuted: number; + }; + ruleSnoozedStatus: { + snoozed: number; + }; + ruleTags: string[]; +} diff --git a/x-pack/plugins/alerting/server/application/rule/methods/aggregate/validation/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/validation/index.ts new file mode 100644 index 0000000000000..bce1dab4756fe --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/validation/index.ts @@ -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 { validateRuleAggregationFields } from './validate_rule_aggregation_fields'; diff --git a/x-pack/plugins/alerting/server/rules_client/lib/validate_rule_aggregation_fields.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/validation/validate_rule_aggregation_fields.test.ts similarity index 97% rename from x-pack/plugins/alerting/server/rules_client/lib/validate_rule_aggregation_fields.test.ts rename to x-pack/plugins/alerting/server/application/rule/methods/aggregate/validation/validate_rule_aggregation_fields.test.ts index 599938de4b1fc..8bb6e75b88f72 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/validate_rule_aggregation_fields.test.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/validation/validate_rule_aggregation_fields.test.ts @@ -5,8 +5,10 @@ * 2.0. */ -import { getRuleTagsAggregation, getDefaultRuleAggregation } from '../../../common'; import type { AggregationsAggregateOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { getRuleTagsAggregation } from '../../../../../../common'; +import { defaultRuleAggregationFactory } from '..'; + import { validateRuleAggregationFields } from './validate_rule_aggregation_fields'; describe('validateAggregationTerms', () => { @@ -95,7 +97,7 @@ describe('validateAggregationTerms', () => { }); it('should allow for default and tags aggregations', () => { - expect(() => validateRuleAggregationFields(getDefaultRuleAggregation())).not.toThrowError(); + expect(() => validateRuleAggregationFields(defaultRuleAggregationFactory())).not.toThrowError(); expect(() => validateRuleAggregationFields(getRuleTagsAggregation())).not.toThrowError(); }); diff --git a/x-pack/plugins/alerting/server/rules_client/lib/validate_rule_aggregation_fields.ts b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/validation/validate_rule_aggregation_fields.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_client/lib/validate_rule_aggregation_fields.ts rename to x-pack/plugins/alerting/server/application/rule/methods/aggregate/validation/validate_rule_aggregation_fields.ts diff --git a/x-pack/plugins/alerting/server/routes/aggregate_rules.ts b/x-pack/plugins/alerting/server/routes/aggregate_rules.ts deleted file mode 100644 index ea3bb22bd0d17..0000000000000 --- a/x-pack/plugins/alerting/server/routes/aggregate_rules.ts +++ /dev/null @@ -1,133 +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 { IRouter } from '@kbn/core/server'; -import { schema } from '@kbn/config-schema'; -import { UsageCounter } from '@kbn/usage-collection-plugin/server'; -import { - AggregateOptions, - DefaultRuleAggregationResult, - formatDefaultAggregationResult, - getDefaultRuleAggregation, - RuleAggregationFormattedResult, -} from '../../common'; -import { ILicenseState } from '../lib'; -import { RewriteResponseCase, RewriteRequestCase, verifyAccessAndContext } from './lib'; -import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types'; -import { trackLegacyTerminology } from './lib/track_legacy_terminology'; - -// config definition -const querySchema = schema.object({ - search: schema.maybe(schema.string()), - default_search_operator: schema.oneOf([schema.literal('OR'), schema.literal('AND')], { - defaultValue: 'OR', - }), - search_fields: schema.maybe(schema.arrayOf(schema.string())), - has_reference: schema.maybe( - // use nullable as maybe is currently broken - // in config-schema - schema.nullable( - schema.object({ - type: schema.string(), - id: schema.string(), - }) - ) - ), - filter: schema.maybe(schema.string()), -}); - -const rewriteQueryReq: RewriteRequestCase = ({ - default_search_operator: defaultSearchOperator, - has_reference: hasReference, - search_fields: searchFields, - ...rest -}) => ({ - ...rest, - defaultSearchOperator, - ...(hasReference ? { hasReference } : {}), - ...(searchFields ? { searchFields } : {}), -}); -const rewriteBodyRes: RewriteResponseCase = ({ - ruleExecutionStatus, - ruleLastRunOutcome, - ruleEnabledStatus, - ruleMutedStatus, - ruleSnoozedStatus, - ruleTags, - ...rest -}) => ({ - ...rest, - rule_execution_status: ruleExecutionStatus, - rule_last_run_outcome: ruleLastRunOutcome, - rule_enabled_status: ruleEnabledStatus, - rule_muted_status: ruleMutedStatus, - rule_snoozed_status: ruleSnoozedStatus, - rule_tags: ruleTags, -}); - -export const aggregateRulesRoute = ( - router: IRouter, - licenseState: ILicenseState, - usageCounter?: UsageCounter -) => { - router.get( - { - path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`, - validate: { - query: querySchema, - }, - }, - router.handleLegacyErrors( - verifyAccessAndContext(licenseState, async function (context, req, res) { - const rulesClient = (await context.alerting).getRulesClient(); - const options = rewriteQueryReq({ - ...req.query, - has_reference: req.query.has_reference || undefined, - }); - trackLegacyTerminology( - [req.query.search, req.query.search_fields].filter(Boolean) as string[], - usageCounter - ); - const aggregateResult = await rulesClient.aggregate({ - aggs: getDefaultRuleAggregation(), - options, - }); - return res.ok({ - body: rewriteBodyRes(formatDefaultAggregationResult(aggregateResult)), - }); - }) - ) - ); - router.post( - { - path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`, - validate: { - body: querySchema, - }, - }, - router.handleLegacyErrors( - verifyAccessAndContext(licenseState, async function (context, req, res) { - const rulesClient = (await context.alerting).getRulesClient(); - const options = rewriteQueryReq({ - ...req.body, - has_reference: req.body.has_reference || undefined, - }); - trackLegacyTerminology( - [req.body.search, req.body.search_fields].filter(Boolean) as string[], - usageCounter - ); - const aggregateResult = await rulesClient.aggregate({ - aggs: getDefaultRuleAggregation(), - options, - }); - return res.ok({ - body: rewriteBodyRes(formatDefaultAggregationResult(aggregateResult)), - }); - }) - ) - ); -}; diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts index bdc9dcee21895..1bffc87e7b60a 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -17,7 +17,7 @@ import { createRuleRoute } from './rule/apis/create'; import { getRuleRoute, getInternalRuleRoute } from './get_rule'; import { updateRuleRoute } from './update_rule'; import { deleteRuleRoute } from './delete_rule'; -import { aggregateRulesRoute } from './aggregate_rules'; +import { aggregateRulesRoute } from './rule/apis/aggregate/aggregate_rules_route'; import { disableRuleRoute } from './disable_rule'; import { enableRuleRoute } from './enable_rule'; import { findRulesRoute, findInternalRulesRoute } from './find_rules'; diff --git a/x-pack/plugins/alerting/server/routes/legacy/aggregate.test.ts b/x-pack/plugins/alerting/server/routes/legacy/aggregate.test.ts deleted file mode 100644 index 9ba0bbd86da3f..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/aggregate.test.ts +++ /dev/null @@ -1,301 +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 { aggregateAlertRoute } from './aggregate'; -import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../../lib/license_state.mock'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { mockHandlerArguments } from '../_mock_handler_arguments'; -import { rulesClientMock } from '../../rules_client.mock'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { trackLegacyTerminology } from '../lib/track_legacy_terminology'; -import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; - -const rulesClient = rulesClientMock.create(); -const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); -const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - -jest.mock('../../lib/track_legacy_route_usage', () => ({ - trackLegacyRouteUsage: jest.fn(), -})); - -jest.mock('../../lib/license_api_access', () => ({ - verifyApiAccess: jest.fn(), -})); - -jest.mock('../lib/track_legacy_terminology', () => ({ - trackLegacyTerminology: jest.fn(), -})); - -jest.mock('../../../common', () => ({ - ...jest.requireActual('../../../common'), - formatDefaultAggregationResult: jest.fn(), -})); - -const { formatDefaultAggregationResult } = jest.requireMock('../../../common'); - -beforeEach(() => { - jest.resetAllMocks(); -}); - -describe('aggregateAlertRoute', () => { - it('aggregate alerts with proper parameters', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - aggregateAlertRoute(router, licenseState); - - const [config, handler] = router.get.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/_aggregate"`); - - const aggregateResult = { - ruleExecutionStatus: { - ok: 15, - error: 2, - active: 23, - pending: 1, - unknown: 0, - }, - ruleLastRunOutcome: { - succeeded: 1, - failed: 2, - warning: 3, - }, - }; - formatDefaultAggregationResult.mockReturnValueOnce(aggregateResult); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - query: { - default_search_operator: 'AND', - }, - }, - ['ok'] - ); - - expect(await handler(context, req, res)).toMatchInlineSnapshot(` - Object { - "body": Object { - "alertExecutionStatus": Object { - "active": 23, - "error": 2, - "ok": 15, - "pending": 1, - "unknown": 0, - }, - "ruleLastRunOutcome": Object { - "failed": 2, - "succeeded": 1, - "warning": 3, - }, - }, - } - `); - - expect(rulesClient.aggregate).toHaveBeenCalledTimes(1); - expect(rulesClient.aggregate.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "aggs": Object { - "enabled": Object { - "terms": Object { - "field": "alert.attributes.enabled", - }, - }, - "muted": Object { - "terms": Object { - "field": "alert.attributes.muteAll", - }, - }, - "outcome": Object { - "terms": Object { - "field": "alert.attributes.lastRun.outcome", - }, - }, - "snoozed": Object { - "aggs": Object { - "count": Object { - "filter": Object { - "exists": Object { - "field": "alert.attributes.snoozeSchedule.duration", - }, - }, - }, - }, - "nested": Object { - "path": "alert.attributes.snoozeSchedule", - }, - }, - "status": Object { - "terms": Object { - "field": "alert.attributes.executionStatus.status", - }, - }, - "tags": Object { - "terms": Object { - "field": "alert.attributes.tags", - "order": Object { - "_key": "asc", - }, - "size": 50, - }, - }, - }, - "options": Object { - "defaultSearchOperator": "AND", - }, - }, - ] - `); - - expect(res.ok).toHaveBeenCalledWith({ - body: { - ruleLastRunOutcome: aggregateResult.ruleLastRunOutcome, - alertExecutionStatus: aggregateResult.ruleExecutionStatus, - }, - }); - }); - - it('ensures the license allows aggregating alerts', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - aggregateAlertRoute(router, licenseState); - - const [, handler] = router.get.mock.calls[0]; - - formatDefaultAggregationResult.mockReturnValueOnce({ - ruleExecutionStatus: { - ok: 15, - error: 2, - active: 23, - pending: 1, - unknown: 0, - }, - ruleLastRunOutcome: { - succeeded: 1, - failed: 2, - warning: 3, - }, - }); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - query: { - default_search_operator: 'OR', - }, - } - ); - - await handler(context, req, res); - - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); - }); - - it('ensures the license check prevents aggregating alerts', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - (verifyApiAccess as jest.Mock).mockImplementation(() => { - throw new Error('OMG'); - }); - - aggregateAlertRoute(router, licenseState); - - const [, handler] = router.get.mock.calls[0]; - - const [context, req, res] = mockHandlerArguments( - {}, - { - query: {}, - }, - ['ok'] - ); - expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); - - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); - }); - - it('should track every call', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - aggregateAlertRoute(router, licenseState, mockUsageCounter); - - formatDefaultAggregationResult.mockReturnValueOnce({ - ruleExecutionStatus: { - ok: 15, - error: 2, - active: 23, - pending: 1, - unknown: 0, - }, - ruleLastRunOutcome: { - succeeded: 1, - failed: 2, - warning: 3, - }, - }); - - const [, handler] = router.get.mock.calls[0]; - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - query: { - default_search_operator: 'AND', - }, - }, - ['ok'] - ); - await handler(context, req, res); - expect(trackLegacyRouteUsage).toHaveBeenCalledWith('aggregate', mockUsageCounter); - }); - - it('should track calls with deprecated param values', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - aggregateAlertRoute(router, licenseState, mockUsageCounter); - - formatDefaultAggregationResult.mockReturnValueOnce({ - ruleExecutionStatus: { - ok: 15, - error: 2, - active: 23, - pending: 1, - unknown: 0, - }, - ruleLastRunOutcome: { - succeeded: 1, - failed: 2, - warning: 3, - }, - }); - - const [, handler] = router.get.mock.calls[0]; - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: {}, - query: { - search_fields: ['alertTypeId:1', 'message:foo'], - search: 'alertTypeId:2', - }, - }, - ['ok'] - ); - await handler(context, req, res); - expect(trackLegacyTerminology).toHaveBeenCalledTimes(1); - expect((trackLegacyTerminology as jest.Mock).mock.calls[0][0]).toStrictEqual([ - 'alertTypeId:2', - ['alertTypeId:1', 'message:foo'], - ]); - }); -}); diff --git a/x-pack/plugins/alerting/server/routes/legacy/aggregate.ts b/x-pack/plugins/alerting/server/routes/legacy/aggregate.ts deleted file mode 100644 index 44559830a24e9..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/aggregate.ts +++ /dev/null @@ -1,98 +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 { schema } from '@kbn/config-schema'; -import { UsageCounter } from '@kbn/usage-collection-plugin/server'; -import type { AlertingRouter } from '../../types'; -import { ILicenseState } from '../../lib/license_state'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { - LEGACY_BASE_ALERT_API_PATH, - DefaultRuleAggregationResult, - getDefaultRuleAggregation, - formatDefaultAggregationResult, -} from '../../../common'; -import { renameKeys } from '../lib/rename_keys'; -import { FindOptions } from '../../rules_client'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { trackLegacyTerminology } from '../lib/track_legacy_terminology'; - -// config definition -const querySchema = schema.object({ - search: schema.maybe(schema.string()), - default_search_operator: schema.oneOf([schema.literal('OR'), schema.literal('AND')], { - defaultValue: 'OR', - }), - search_fields: schema.maybe(schema.oneOf([schema.arrayOf(schema.string()), schema.string()])), - has_reference: schema.maybe( - // use nullable as maybe is currently broken - // in config-schema - schema.nullable( - schema.object({ - type: schema.string(), - id: schema.string(), - }) - ) - ), - filter: schema.maybe(schema.string()), -}); - -export const aggregateAlertRoute = ( - router: AlertingRouter, - licenseState: ILicenseState, - usageCounter?: UsageCounter -) => { - router.get( - { - path: `${LEGACY_BASE_ALERT_API_PATH}/_aggregate`, - validate: { - query: querySchema, - }, - }, - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - if (!context.alerting) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); - } - const rulesClient = (await context.alerting).getRulesClient(); - - trackLegacyRouteUsage('aggregate', usageCounter); - trackLegacyTerminology( - [req.query.search, req.query.search_fields].filter(Boolean) as string[], - usageCounter - ); - - const query = req.query; - const renameMap = { - default_search_operator: 'defaultSearchOperator', - has_reference: 'hasReference', - search: 'search', - filter: 'filter', - }; - - const options = renameKeys>(renameMap, query); - - if (query.search_fields) { - options.searchFields = Array.isArray(query.search_fields) - ? query.search_fields - : [query.search_fields]; - } - - const aggregateResult = await rulesClient.aggregate({ - options, - aggs: getDefaultRuleAggregation(), - }); - const { ruleExecutionStatus, ...rest } = formatDefaultAggregationResult(aggregateResult); - return res.ok({ - body: { - ...rest, - alertExecutionStatus: ruleExecutionStatus, - }, - }); - }) - ); -}; diff --git a/x-pack/plugins/alerting/server/routes/legacy/index.ts b/x-pack/plugins/alerting/server/routes/legacy/index.ts index a89ccf0920b29..f915cff705318 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/index.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { aggregateAlertRoute } from './aggregate'; import { createAlertRoute } from './create'; import { deleteAlertRoute } from './delete'; import { findAlertRoute } from './find'; @@ -28,7 +27,6 @@ export function defineLegacyRoutes(opts: RouteOptions) { const { router, licenseState, encryptedSavedObjects, usageCounter } = opts; createAlertRoute(opts); - aggregateAlertRoute(router, licenseState, usageCounter); deleteAlertRoute(router, licenseState, usageCounter); findAlertRoute(router, licenseState, usageCounter); getAlertRoute(router, licenseState, usageCounter); diff --git a/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts b/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/aggregate_rules_route.test.ts similarity index 90% rename from x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts rename to x-pack/plugins/alerting/server/routes/rule/apis/aggregate/aggregate_rules_route.test.ts index c9bfbdca4aa0b..51a7da620dfb3 100644 --- a/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/aggregate_rules_route.test.ts @@ -5,24 +5,24 @@ * 2.0. */ -import { aggregateRulesRoute } from './aggregate_rules'; +import { aggregateRulesRoute } from './aggregate_rules_route'; import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { mockHandlerArguments } from './_mock_handler_arguments'; -import { rulesClientMock } from '../rules_client.mock'; -import { trackLegacyTerminology } from './lib/track_legacy_terminology'; +import { licenseStateMock } from '../../../../lib/license_state.mock'; +import { verifyApiAccess } from '../../../../lib/license_api_access'; +import { mockHandlerArguments } from '../../../_mock_handler_arguments'; +import { rulesClientMock } from '../../../../rules_client.mock'; +import { trackLegacyTerminology } from '../../../lib/track_legacy_terminology'; import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; const rulesClient = rulesClientMock.create(); const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); -jest.mock('../lib/license_api_access', () => ({ +jest.mock('../../../../lib/license_api_access', () => ({ verifyApiAccess: jest.fn(), })); -jest.mock('./lib/track_legacy_terminology', () => ({ +jest.mock('../../../lib/track_legacy_terminology', () => ({ trackLegacyTerminology: jest.fn(), })); @@ -134,7 +134,7 @@ describe('aggregateRulesRoute', () => { aggregateRulesRoute(router, licenseState); - const [config, handler] = router.get.mock.calls[0]; + const [config, handler] = router.post.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rules/_aggregate"`); @@ -143,7 +143,7 @@ describe('aggregateRulesRoute', () => { const [context, req, res] = mockHandlerArguments( { rulesClient }, { - query: { + body: { default_search_operator: 'AND', }, }, @@ -279,14 +279,14 @@ describe('aggregateRulesRoute', () => { aggregateRulesRoute(router, licenseState); - const [, handler] = router.get.mock.calls[0]; + const [, handler] = router.post.mock.calls[0]; rulesClient.aggregate.mockResolvedValueOnce(aggregateResult); const [context, req, res] = mockHandlerArguments( { rulesClient }, { - query: { + body: { default_search_operator: 'OR', }, } @@ -307,12 +307,12 @@ describe('aggregateRulesRoute', () => { aggregateRulesRoute(router, licenseState); - const [, handler] = router.get.mock.calls[0]; + const [, handler] = router.post.mock.calls[0]; const [context, req, res] = mockHandlerArguments( {}, { - query: {}, + body: {}, }, ['ok'] ); @@ -326,7 +326,7 @@ describe('aggregateRulesRoute', () => { const router = httpServiceMock.createRouter(); aggregateRulesRoute(router, licenseState, mockUsageCounter); - const [, handler] = router.get.mock.calls[0]; + const [, handler] = router.post.mock.calls[0]; rulesClient.aggregate.mockResolvedValueOnce(aggregateResult); @@ -334,7 +334,7 @@ describe('aggregateRulesRoute', () => { { rulesClient }, { params: {}, - query: { + body: { search_fields: ['alertTypeId:1', 'message:foo'], search: 'alertTypeId:2', }, diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/aggregate_rules_route.ts b/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/aggregate_rules_route.ts new file mode 100644 index 0000000000000..a8f7a20baa4cb --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/aggregate_rules_route.ts @@ -0,0 +1,63 @@ +/* + * 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 { UsageCounter } from '@kbn/usage-collection-plugin/server'; + +import { defaultRuleAggregationFactoryV1 } from '../../../../application/rule/methods/aggregate'; +import { ILicenseState } from '../../../../lib'; +import { verifyAccessAndContext } from '../../../lib'; +import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../../../types'; +import { trackLegacyTerminology } from '../../../lib/track_legacy_terminology'; +import { + aggregateRulesRequestBodySchemaV1, + AggregateRulesRequestBodyV1, + AggregateRulesResponseV1, +} from '../../../../../common/routes/rule/apis/aggregate'; +import { formatDefaultAggregationResult } from './transforms'; +import { transformAggregateQueryRequestV1, transformAggregateBodyResponseV1 } from './transforms'; +import { DefaultRuleAggregationResult } from './types'; + +export const aggregateRulesRoute = ( + router: IRouter, + licenseState: ILicenseState, + usageCounter?: UsageCounter +) => { + router.post( + { + path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`, + validate: { + body: aggregateRulesRequestBodySchemaV1, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const rulesClient = (await context.alerting).getRulesClient(); + const body: AggregateRulesRequestBodyV1 = req.body; + const options = transformAggregateQueryRequestV1({ + ...body, + has_reference: body.has_reference || undefined, + }); + trackLegacyTerminology( + [body.search, body.search_fields].filter(Boolean) as string[], + usageCounter + ); + + const aggregateResult = await rulesClient.aggregate({ + aggs: defaultRuleAggregationFactoryV1(), + options, + }); + + const responsePayload: AggregateRulesResponseV1 = { + body: transformAggregateBodyResponseV1(formatDefaultAggregationResult(aggregateResult)), + }; + + return res.ok(responsePayload); + }) + ) + ); +}; diff --git a/x-pack/plugins/alerting/common/default_rule_aggregation.test.ts b/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/transforms/format_default_aggregation_result/index.test.ts similarity index 77% rename from x-pack/plugins/alerting/common/default_rule_aggregation.test.ts rename to x-pack/plugins/alerting/server/routes/rule/apis/aggregate/transforms/format_default_aggregation_result/index.test.ts index e1c38587d2dd0..0ea0ea3c22a7d 100644 --- a/x-pack/plugins/alerting/common/default_rule_aggregation.test.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/transforms/format_default_aggregation_result/index.test.ts @@ -5,26 +5,7 @@ * 2.0. */ -import { - getDefaultRuleAggregation, - formatDefaultAggregationResult, -} from './default_rule_aggregation'; - -describe('getDefaultRuleAggregation', () => { - it('should return aggregation with default maxTags', () => { - const result = getDefaultRuleAggregation(); - expect(result.tags).toEqual({ - terms: { field: 'alert.attributes.tags', order: { _key: 'asc' }, size: 50 }, - }); - }); - - it('should return aggregation with custom maxTags', () => { - const result = getDefaultRuleAggregation({ maxTags: 100 }); - expect(result.tags).toEqual({ - terms: { field: 'alert.attributes.tags', order: { _key: 'asc' }, size: 100 }, - }); - }); -}); +import { formatDefaultAggregationResult } from '.'; describe('formatDefaultAggregationResult', () => { it('should format aggregation result', () => { diff --git a/x-pack/plugins/alerting/common/default_rule_aggregation.ts b/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/transforms/format_default_aggregation_result/index.ts similarity index 57% rename from x-pack/plugins/alerting/common/default_rule_aggregation.ts rename to x-pack/plugins/alerting/server/routes/rule/apis/aggregate/transforms/format_default_aggregation_result/index.ts index 1b72657ce2dbe..d751875e104d7 100644 --- a/x-pack/plugins/alerting/common/default_rule_aggregation.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/transforms/format_default_aggregation_result/index.ts @@ -4,93 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { - RuleExecutionStatusValues, - RuleLastRunOutcomeValues, - RuleAggregationFormattedResult, -} from './rule'; -export interface DefaultRuleAggregationResult { - status: { - buckets: Array<{ - key: string; - doc_count: number; - }>; - }; - outcome: { - buckets: Array<{ - key: string; - doc_count: number; - }>; - }; - muted: { - buckets: Array<{ - key: number; - key_as_string: string; - doc_count: number; - }>; - }; - enabled: { - buckets: Array<{ - key: number; - key_as_string: string; - doc_count: number; - }>; - }; - snoozed: { - count: { - doc_count: number; - }; - }; - tags: { - buckets: Array<{ - key: string; - doc_count: number; - }>; - }; -} - -interface GetDefaultRuleAggregationParams { - maxTags?: number; -} - -export const getDefaultRuleAggregation = ( - params?: GetDefaultRuleAggregationParams -): Record => { - const { maxTags = 50 } = params || {}; - return { - status: { - terms: { field: 'alert.attributes.executionStatus.status' }, - }, - outcome: { - terms: { field: 'alert.attributes.lastRun.outcome' }, - }, - enabled: { - terms: { field: 'alert.attributes.enabled' }, - }, - muted: { - terms: { field: 'alert.attributes.muteAll' }, - }, - tags: { - terms: { field: 'alert.attributes.tags', order: { _key: 'asc' }, size: maxTags }, - }, - snoozed: { - nested: { - path: 'alert.attributes.snoozeSchedule', - }, - aggs: { - count: { - filter: { - exists: { - field: 'alert.attributes.snoozeSchedule.duration', - }, - }, - }, - }, - }, - }; -}; +import { RuleAggregationFormattedResult } from '../../../../../../application/rule/methods/aggregate/types'; +import { RuleExecutionStatusValues, RuleLastRunOutcomeValues } from '../../../../../../../common'; +import { DefaultRuleAggregationResult } from '../../types'; export const formatDefaultAggregationResult = ( aggregations: DefaultRuleAggregationResult @@ -166,7 +83,9 @@ export const formatDefaultAggregationResult = ( } const tagsBuckets = aggregations.tags?.buckets || []; - result.ruleTags = tagsBuckets.map((bucket) => bucket.key); + tagsBuckets.forEach((bucket) => { + result.ruleTags.push(bucket.key); + }); return result; }; diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/transforms/index.ts b/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/transforms/index.ts new file mode 100644 index 0000000000000..bd9e8015cf8f4 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/transforms/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { formatDefaultAggregationResult } from './format_default_aggregation_result'; +export { transformAggregateQueryRequest } from './transform_aggregate_query_request/latest'; +export { transformAggregateBodyResponse } from './transform_aggregate_body_response/latest'; + +export { transformAggregateQueryRequest as transformAggregateQueryRequestV1 } from './transform_aggregate_query_request/v1'; +export { transformAggregateBodyResponse as transformAggregateBodyResponseV1 } from './transform_aggregate_body_response/v1'; diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/transforms/transform_aggregate_body_response/latest.ts b/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/transforms/transform_aggregate_body_response/latest.ts new file mode 100644 index 0000000000000..1d31867d87b28 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/transforms/transform_aggregate_body_response/latest.ts @@ -0,0 +1,7 @@ +/* + * 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 { transformAggregateBodyResponse } from './v1'; diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/transforms/transform_aggregate_body_response/v1.ts b/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/transforms/transform_aggregate_body_response/v1.ts new file mode 100644 index 0000000000000..87bd99af3a665 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/transforms/transform_aggregate_body_response/v1.ts @@ -0,0 +1,25 @@ +/* + * 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 { RuleAggregationFormattedResult } from '../../../../../../application/rule/methods/aggregate/types'; +import { AggregateRulesResponseBodyV1 } from '../../../../../../../common/routes/rule/apis/aggregate'; + +export const transformAggregateBodyResponse = ({ + ruleExecutionStatus, + ruleEnabledStatus, + ruleLastRunOutcome, + ruleMutedStatus, + ruleSnoozedStatus, + ruleTags, +}: RuleAggregationFormattedResult): AggregateRulesResponseBodyV1 => ({ + rule_execution_status: ruleExecutionStatus, + rule_last_run_outcome: ruleLastRunOutcome, + rule_enabled_status: ruleEnabledStatus, + rule_muted_status: ruleMutedStatus, + rule_snoozed_status: ruleSnoozedStatus, + rule_tags: ruleTags, +}); diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/transforms/transform_aggregate_query_request/latest.ts b/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/transforms/transform_aggregate_query_request/latest.ts new file mode 100644 index 0000000000000..9ecc4c35b6049 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/transforms/transform_aggregate_query_request/latest.ts @@ -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 { transformAggregateQueryRequest } from './v1'; diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/transforms/transform_aggregate_query_request/v1.ts b/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/transforms/transform_aggregate_query_request/v1.ts new file mode 100644 index 0000000000000..7b5879227ce97 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/transforms/transform_aggregate_query_request/v1.ts @@ -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 { RewriteRequestCase } from '@kbn/actions-plugin/common'; +import { AggregateOptions } from '../../../../../../application/rule/methods/aggregate/types'; + +export const transformAggregateQueryRequest: RewriteRequestCase = ({ + search, + default_search_operator: defaultSearchOperator, + search_fields: searchFields, + has_reference: hasReference, + filter, +}) => ({ + defaultSearchOperator, + ...(hasReference ? { hasReference } : {}), + ...(searchFields ? { searchFields } : {}), + ...(search ? { search } : {}), + ...(filter ? { filter } : {}), +}); diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/types/index.ts b/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/types/index.ts new file mode 100644 index 0000000000000..a8eccab7f1e00 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/types/index.ts @@ -0,0 +1,46 @@ +/* + * 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 interface DefaultRuleAggregationResult { + status: { + buckets: Array<{ + key: string; + doc_count: number; + }>; + }; + outcome: { + buckets: Array<{ + key: string; + doc_count: number; + }>; + }; + muted: { + buckets: Array<{ + key: number; + key_as_string: string; + doc_count: number; + }>; + }; + enabled: { + buckets: Array<{ + key: number; + key_as_string: string; + doc_count: number; + }>; + }; + snoozed: { + count: { + doc_count: number; + }; + }; + tags: { + buckets: Array<{ + key: string; + doc_count: number; + }>; + }; +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/aggregate.ts b/x-pack/plugins/alerting/server/rules_client/methods/aggregate.ts deleted file mode 100644 index 6dd26c1a7e197..0000000000000 --- a/x-pack/plugins/alerting/server/rules_client/methods/aggregate.ts +++ /dev/null @@ -1,64 +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 { KueryNode, nodeBuilder } from '@kbn/es-query'; -import type { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { AlertingAuthorizationEntity } from '../../authorization'; -import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; -import { buildKueryNodeFilter } from '../common'; -import { alertingAuthorizationFilterOpts } from '../common/constants'; -import { RulesClientContext } from '../types'; -import { RawRule, AggregateOptions } from '../../types'; -import { validateRuleAggregationFields } from '../lib/validate_rule_aggregation_fields'; - -export interface AggregateParams { - options?: AggregateOptions; - aggs: Record; -} - -export async function aggregate>( - context: RulesClientContext, - params: AggregateParams -): Promise { - const { options = {}, aggs } = params; - const { filter, page = 1, perPage = 0, ...restOptions } = options; - - let authorizationTuple; - try { - authorizationTuple = await context.authorization.getFindAuthorizationFilter( - AlertingAuthorizationEntity.Rule, - alertingAuthorizationFilterOpts - ); - validateRuleAggregationFields(aggs); - } catch (error) { - context.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.AGGREGATE, - error, - }) - ); - throw error; - } - - const { filter: authorizationFilter } = authorizationTuple; - const filterKueryNode = buildKueryNodeFilter(filter); - - const result = await context.unsecuredSavedObjectsClient.find({ - ...restOptions, - filter: - authorizationFilter && filterKueryNode - ? nodeBuilder.and([filterKueryNode, authorizationFilter as KueryNode]) - : authorizationFilter, - page, - perPage, - type: 'alert', - aggs, - }); - - // params. - return result.aggregations!; -} diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index b27bb9b5a6747..f2274648d3981 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -33,7 +33,8 @@ import { GetRuleExecutionKPIParams, } from './methods/get_execution_kpi'; import { find, FindParams } from './methods/find'; -import { aggregate, AggregateParams } from './methods/aggregate'; +import { AggregateParams } from '../application/rule/methods/aggregate/types'; +import { aggregateRules } from '../application/rule/methods/aggregate'; import { deleteRule } from './methods/delete'; import { update, UpdateOptions } from './methods/update'; import { bulkDeleteRules } from './methods/bulk_delete'; @@ -108,7 +109,7 @@ export class RulesClient { } public aggregate = >(params: AggregateParams): Promise => - aggregate(this.context, params); + aggregateRules(this.context, params); public clone = (...args: CloneArguments) => clone(this.context, ...args); public create = (params: CreateRuleParams) => diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts index 6e2465e58c2fd..a2f3e1eb80392 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts @@ -5,7 +5,7 @@ * 2.0. */ import { AsApiContract } from '@kbn/actions-plugin/common'; -import { RuleAggregationFormattedResult } from '@kbn/alerting-plugin/common'; +import { AggregateRulesResponseBody } from '@kbn/alerting-plugin/common/routes/rule/apis/aggregate'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; import { mapFiltersToKql } from './map_filters_to_kql'; import { @@ -14,6 +14,7 @@ import { rewriteBodyRes, rewriteTagsBodyRes, GetRuleTagsResponse, + AggregateRulesResponse, } from './aggregate_helpers'; export async function loadRuleTags({ @@ -43,7 +44,7 @@ export async function loadRuleAggregations({ ruleExecutionStatusesFilter, ruleStatusesFilter, tagsFilter, -}: LoadRuleAggregationsProps): Promise { +}: LoadRuleAggregationsProps): Promise { const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, @@ -51,7 +52,7 @@ export async function loadRuleAggregations({ ruleStatusesFilter, tagsFilter, }); - const res = await http.post>( + const res = await http.post( `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`, { body: JSON.stringify({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_helpers.ts index 61fff1e95a451..d093649f73740 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_helpers.ts @@ -7,19 +7,34 @@ import { HttpSetup } from '@kbn/core/public'; import { RewriteRequestCase } from '@kbn/actions-plugin/common'; -import { RuleAggregationFormattedResult } from '@kbn/alerting-plugin/common'; +import { AggregateRulesResponseBody } from '@kbn/alerting-plugin/common/routes/rule/apis/aggregate'; import { RuleStatus } from '../../../types'; -export const rewriteBodyRes: RewriteRequestCase = ({ +export interface AggregateRulesResponse { + ruleExecutionStatus: Record; + ruleLastRunOutcome: Record; + ruleEnabledStatus: { + enabled: number; + disabled: number; + }; + ruleMutedStatus: { + muted: number; + unmuted: number; + }; + ruleSnoozedStatus: { + snoozed: number; + }; + ruleTags: string[]; +} + +export const rewriteBodyRes = ({ rule_execution_status: ruleExecutionStatus, rule_last_run_outcome: ruleLastRunOutcome, rule_enabled_status: ruleEnabledStatus, rule_muted_status: ruleMutedStatus, rule_snoozed_status: ruleSnoozedStatus, rule_tags: ruleTags, - ...rest -}: any) => ({ - ...rest, +}: AggregateRulesResponseBody): AggregateRulesResponse => ({ ruleExecutionStatus, ruleEnabledStatus, ruleMutedStatus, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_kuery_filter.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_kuery_filter.ts index 20d1fc9281b48..23941ae36ccc5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_kuery_filter.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_kuery_filter.ts @@ -4,10 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { AsApiContract } from '@kbn/actions-plugin/common'; -import { RuleAggregationFormattedResult } from '@kbn/alerting-plugin/common'; +import { AggregateRulesResponseBody } from '@kbn/alerting-plugin/common/routes/rule/apis/aggregate'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; -import { LoadRuleAggregationsProps, rewriteBodyRes } from './aggregate_helpers'; +import { + AggregateRulesResponse, + LoadRuleAggregationsProps, + rewriteBodyRes, +} from './aggregate_helpers'; import { mapFiltersToKueryNode } from './map_filters_to_kuery_node'; export async function loadRuleAggregationsWithKueryFilter({ @@ -18,7 +21,7 @@ export async function loadRuleAggregationsWithKueryFilter({ ruleExecutionStatusesFilter, ruleStatusesFilter, tagsFilter, -}: LoadRuleAggregationsProps): Promise { +}: LoadRuleAggregationsProps): Promise { const filtersKueryNode = mapFiltersToKueryNode({ typesFilter, actionTypesFilter, @@ -28,7 +31,7 @@ export async function loadRuleAggregationsWithKueryFilter({ searchText, }); - const res = await http.post>( + const res = await http.post( `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`, { body: JSON.stringify({ @@ -37,5 +40,6 @@ export async function loadRuleAggregationsWithKueryFilter({ }), } ); + return rewriteBodyRes(res); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/aggregate.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/aggregate.ts deleted file mode 100644 index 63fdfa8d498fa..0000000000000 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/aggregate.ts +++ /dev/null @@ -1,281 +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 expect from '@kbn/expect'; -import { Spaces } from '../../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../../common/lib'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -// eslint-disable-next-line import/no-default-export -export default function createAggregateTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const retry = getService('retry'); - - const getEventLogWithRetry = async (id: string) => { - await retry.try(async () => { - return await getEventLog({ - getService, - spaceId: Spaces.space1.id, - type: 'alert', - id, - provider: 'alerting', - actions: new Map([['execute', { equal: 1 }]]), - }); - }); - }; - - describe('aggregate', () => { - const objectRemover = new ObjectRemover(supertest); - - afterEach(() => objectRemover.removeAll()); - - it('should aggregate when there are no alerts', async () => { - const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_aggregate` - ); - expect(response.status).to.eql(200); - expect(response.body).to.eql({ - rule_enabled_status: { - disabled: 0, - enabled: 0, - }, - rule_execution_status: { - ok: 0, - active: 0, - error: 0, - pending: 0, - unknown: 0, - warning: 0, - }, - rule_last_run_outcome: { - succeeded: 0, - warning: 0, - failed: 0, - }, - rule_muted_status: { - muted: 0, - unmuted: 0, - }, - rule_snoozed_status: { - snoozed: 0, - }, - rule_tags: [], - }); - }); - - it('should aggregate alert status totals', async () => { - const NumOkAlerts = 4; - const NumActiveAlerts = 1; - const NumErrorAlerts = 2; - - const okAlertIds: string[] = []; - const activeAlertIds: string[] = []; - const errorAlertIds: string[] = []; - - await Promise.all( - [...Array(NumOkAlerts)].map(async () => { - const okAlertId = await createTestAlert({ - rule_type_id: 'test.noop', - schedule: { interval: '24h' }, - }); - okAlertIds.push(okAlertId); - objectRemover.add(Spaces.space1.id, okAlertId, 'rule', 'alerting'); - }) - ); - - await Promise.all(okAlertIds.map((id) => getEventLogWithRetry(id))); - - await Promise.all( - [...Array(NumActiveAlerts)].map(async () => { - const activeAlertId = await createTestAlert({ - rule_type_id: 'test.patternFiring', - schedule: { interval: '24h' }, - params: { - pattern: { instance: new Array(100).fill(true) }, - }, - }); - activeAlertIds.push(activeAlertId); - objectRemover.add(Spaces.space1.id, activeAlertId, 'rule', 'alerting'); - }) - ); - - await Promise.all(activeAlertIds.map((id) => getEventLogWithRetry(id))); - - await Promise.all( - [...Array(NumErrorAlerts)].map(async () => { - const errorAlertId = await createTestAlert({ - rule_type_id: 'test.throw', - schedule: { interval: '24h' }, - }); - errorAlertIds.push(errorAlertId); - objectRemover.add(Spaces.space1.id, errorAlertId, 'rule', 'alerting'); - }) - ); - - await Promise.all(errorAlertIds.map((id) => getEventLogWithRetry(id))); - - await retry.try(async () => { - const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_aggregate` - ); - - expect(response.status).to.eql(200); - expect(response.body).to.eql({ - rule_enabled_status: { - disabled: 0, - enabled: 7, - }, - rule_execution_status: { - ok: NumOkAlerts, - active: NumActiveAlerts, - error: NumErrorAlerts, - pending: 0, - unknown: 0, - warning: 0, - }, - rule_last_run_outcome: { - succeeded: 5, - warning: 0, - failed: 2, - }, - rule_muted_status: { - muted: 0, - unmuted: 7, - }, - rule_snoozed_status: { - snoozed: 0, - }, - rule_tags: ['foo'], - }); - }); - }); - - describe('tags limit', () => { - it('should be 50 be default', async () => { - const numOfAlerts = 3; - const numOfTagsPerAlert = 30; - - await Promise.all( - [...Array(numOfAlerts)].map(async (_, alertIndex) => { - const okAlertId = await createTestAlert({ - rule_type_id: 'test.noop', - schedule: { interval: '24h' }, - tags: [...Array(numOfTagsPerAlert)].map( - (__, i) => `tag-${i + numOfTagsPerAlert * alertIndex}` - ), - }); - objectRemover.add(Spaces.space1.id, okAlertId, 'rule', 'alerting'); - }) - ); - - const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_aggregate` - ); - - expect(response.body.rule_tags.length).to.eql(50); - }); - }); - - describe('legacy', () => { - it('should aggregate alert status totals', async () => { - const NumOkAlerts = 4; - const NumActiveAlerts = 1; - const NumErrorAlerts = 2; - - const okAlertIds: string[] = []; - const activeAlertIds: string[] = []; - const errorAlertIds: string[] = []; - - await Promise.all( - [...Array(NumOkAlerts)].map(async () => { - const okAlertId = await createTestAlert({ - rule_type_id: 'test.noop', - schedule: { interval: '24h' }, - tags: ['a', 'b'], - }); - okAlertIds.push(okAlertId); - objectRemover.add(Spaces.space1.id, okAlertId, 'rule', 'alerting'); - }) - ); - await Promise.all(okAlertIds.map((id) => getEventLogWithRetry(id))); - - await Promise.all( - [...Array(NumActiveAlerts)].map(async () => { - const activeAlertId = await createTestAlert({ - rule_type_id: 'test.patternFiring', - schedule: { interval: '24h' }, - params: { - pattern: { instance: new Array(100).fill(true) }, - }, - tags: ['a', 'c', 'f'], - }); - activeAlertIds.push(activeAlertId); - objectRemover.add(Spaces.space1.id, activeAlertId, 'rule', 'alerting'); - }) - ); - await Promise.all(activeAlertIds.map((id) => getEventLogWithRetry(id))); - - await Promise.all( - [...Array(NumErrorAlerts)].map(async () => { - const errorAlertId = await createTestAlert({ - rule_type_id: 'test.throw', - schedule: { interval: '24h' }, - tags: ['b', 'c', 'd'], - }); - errorAlertIds.push(errorAlertId); - objectRemover.add(Spaces.space1.id, errorAlertId, 'rule', 'alerting'); - }) - ); - await Promise.all(errorAlertIds.map((id) => getEventLogWithRetry(id))); - - await retry.try(async () => { - const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/api/alerts/_aggregate` - ); - - expect(response.status).to.eql(200); - expect(response.body).to.eql({ - alertExecutionStatus: { - ok: NumOkAlerts, - active: NumActiveAlerts, - error: NumErrorAlerts, - pending: 0, - unknown: 0, - warning: 0, - }, - ruleEnabledStatus: { - disabled: 0, - enabled: 7, - }, - ruleLastRunOutcome: { - succeeded: 5, - warning: 0, - failed: 2, - }, - ruleMutedStatus: { - muted: 0, - unmuted: 7, - }, - ruleSnoozedStatus: { - snoozed: 0, - }, - ruleTags: ['a', 'b', 'c', 'd', 'f'], - }); - }); - }); - }); - }); - - async function createTestAlert(testAlertOverrides = {}) { - const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send(getTestRuleData(testAlertOverrides)) - .expect(200); - return createdAlert.id; - } -} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/index.ts index 6dacd17642a10..7a0c24878d07f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/index.ts @@ -14,7 +14,6 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC before(async () => await buildUp(getService)); after(async () => await tearDown(getService)); - loadTestFile(require.resolve('./aggregate')); loadTestFile(require.resolve('./aggregate_post')); loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete'));