diff --git a/src/platform/packages/shared/kbn-object-utils/src/flatten_object.test.ts b/src/platform/packages/shared/kbn-object-utils/src/flatten_object.test.ts index ecad01d51a057..18c47d94abc6f 100644 --- a/src/platform/packages/shared/kbn-object-utils/src/flatten_object.test.ts +++ b/src/platform/packages/shared/kbn-object-utils/src/flatten_object.test.ts @@ -29,6 +29,33 @@ describe('flattenObject', () => { beta: 3, }); }); + + it('does not flatten an array item', () => { + const data = { + key1: { + item1: 'value 1', + item2: { itemA: 'value 2' }, + }, + key2: { + item3: { itemA: { itemAB: 'value AB' } }, + item4: 'value 4', + item5: [1], + item6: { itemA: [1, 2, 3] }, + }, + key3: ['item7', 'item8'], + }; + + const flatten = flattenObject(data); + expect(flatten).toEqual({ + key3: ['item7', 'item8'], + 'key2.item3.itemA.itemAB': 'value AB', + 'key2.item4': 'value 4', + 'key2.item5': [1], + 'key2.item6.itemA': [1, 2, 3], + 'key1.item1': 'value 1', + 'key1.item2.itemA': 'value 2', + }); + }); }); describe('flattenObjectNestedLast', () => { diff --git a/x-pack/platform/packages/shared/alerting-rule-utils/index.ts b/x-pack/platform/packages/shared/alerting-rule-utils/index.ts index 9f98f5a2b10fb..921c04c4e03b8 100644 --- a/x-pack/platform/packages/shared/alerting-rule-utils/index.ts +++ b/x-pack/platform/packages/shared/alerting-rule-utils/index.ts @@ -6,4 +6,5 @@ */ export { getEcsGroups } from './src/get_ecs_groups'; -export type { Group } from './src/get_ecs_groups'; +export { getGroupByObject, getFormattedGroupBy } from './src/group_by_object_utils'; +export type { Group, FieldsObject } from './src/types'; diff --git a/x-pack/platform/packages/shared/alerting-rule-utils/src/get_ecs_groups.ts b/x-pack/platform/packages/shared/alerting-rule-utils/src/get_ecs_groups.ts index 7c24ae9deb723..30918ca935234 100644 --- a/x-pack/platform/packages/shared/alerting-rule-utils/src/get_ecs_groups.ts +++ b/x-pack/platform/packages/shared/alerting-rule-utils/src/get_ecs_groups.ts @@ -6,11 +6,7 @@ */ import { ecsFieldMap } from '@kbn/alerts-as-data-utils'; - -export interface Group { - field: string; - value: string; -} +import { Group } from './types'; export const getEcsGroups = (groups: Group[] = []): Record => { const ecsGroups = groups.filter((group) => { diff --git a/x-pack/platform/packages/shared/alerting-rule-utils/src/group_by_object_utils.test.ts b/x-pack/platform/packages/shared/alerting-rule-utils/src/group_by_object_utils.test.ts new file mode 100644 index 0000000000000..3735cb44183bf --- /dev/null +++ b/x-pack/platform/packages/shared/alerting-rule-utils/src/group_by_object_utils.test.ts @@ -0,0 +1,78 @@ +/* + * 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 { getFormattedGroupBy, getGroupByObject } from './group_by_object_utils'; + +describe('getFormattedGroupBy', () => { + it('should format groupBy correctly for empty input', () => { + expect(getFormattedGroupBy(undefined, new Set())).toEqual({}); + }); + + it('should format groupBy correctly for multiple groups', () => { + expect( + getFormattedGroupBy( + ['host.name', 'host.mac', 'tags', 'container.name'], + new Set([ + 'host-0,00-00-5E-00-53-23,event-0,container-name', + 'host-0,00-00-5E-00-53-23,group-0,container-name', + 'host-0,00-00-5E-00-53-24,event-0,container-name', + 'host-0,00-00-5E-00-53-24,group-0,container-name', + ]) + ) + ).toEqual({ + 'host-0,00-00-5E-00-53-23,event-0,container-name': [ + { field: 'host.name', value: 'host-0' }, + { field: 'host.mac', value: '00-00-5E-00-53-23' }, + { field: 'tags', value: 'event-0' }, + { field: 'container.name', value: 'container-name' }, + ], + 'host-0,00-00-5E-00-53-23,group-0,container-name': [ + { field: 'host.name', value: 'host-0' }, + { field: 'host.mac', value: '00-00-5E-00-53-23' }, + { field: 'tags', value: 'group-0' }, + { field: 'container.name', value: 'container-name' }, + ], + 'host-0,00-00-5E-00-53-24,event-0,container-name': [ + { field: 'host.name', value: 'host-0' }, + { field: 'host.mac', value: '00-00-5E-00-53-24' }, + { field: 'tags', value: 'event-0' }, + { field: 'container.name', value: 'container-name' }, + ], + 'host-0,00-00-5E-00-53-24,group-0,container-name': [ + { field: 'host.name', value: 'host-0' }, + { field: 'host.mac', value: '00-00-5E-00-53-24' }, + { field: 'tags', value: 'group-0' }, + { field: 'container.name', value: 'container-name' }, + ], + }); + }); +}); + +describe('getGroupByObject', () => { + it('should return empty object for undefined groupBy', () => { + expect(getFormattedGroupBy(undefined, new Set())).toEqual({}); + }); + + it('should return an object containing groups for one groupBy field', () => { + expect(getGroupByObject('host.name', new Set(['host-0', 'host-1']))).toEqual({ + 'host-0': { host: { name: 'host-0' } }, + 'host-1': { host: { name: 'host-1' } }, + }); + }); + + it('should return an object containing groups for multiple groupBy fields', () => { + expect( + getGroupByObject( + ['host.name', 'container.id'], + new Set(['host-0,container-0', 'host-1,container-1']) + ) + ).toEqual({ + 'host-0,container-0': { container: { id: 'container-0' }, host: { name: 'host-0' } }, + 'host-1,container-1': { container: { id: 'container-1' }, host: { name: 'host-1' } }, + }); + }); +}); diff --git a/x-pack/platform/packages/shared/alerting-rule-utils/src/group_by_object_utils.ts b/x-pack/platform/packages/shared/alerting-rule-utils/src/group_by_object_utils.ts new file mode 100644 index 0000000000000..2dbe77cbb93fc --- /dev/null +++ b/x-pack/platform/packages/shared/alerting-rule-utils/src/group_by_object_utils.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 { unflattenObject } from '@kbn/object-utils'; +import { Group } from './types'; + +export const getGroupByObject = ( + groupBy: string | string[] | undefined, + groupValueSet: Set +): Record => { + const groupKeyValueMappingsObject: Record = {}; + if (groupBy) { + groupValueSet.forEach((groupValueStr) => { + const groupValueArray = groupValueStr.split(','); + groupKeyValueMappingsObject[groupValueStr] = unflattenObject( + Array.isArray(groupBy) + ? groupBy.reduce((result, groupKey, index) => { + return { ...result, [groupKey]: groupValueArray[index]?.trim() }; + }, {}) + : { [groupBy]: groupValueStr } + ); + }); + } + return groupKeyValueMappingsObject; +}; + +export const getFormattedGroupBy = ( + groupBy: string | string[] | undefined, + groupSet: Set +): Record => { + const groupByKeysObjectMapping: Record = {}; + if (groupBy) { + groupSet.forEach((group) => { + const groupSetKeys = group.split(','); + groupByKeysObjectMapping[group] = Array.isArray(groupBy) + ? groupBy.reduce((result: Group[], groupByItem, index) => { + result.push({ field: groupByItem, value: groupSetKeys[index]?.trim() }); + return result; + }, []) + : [{ field: groupBy, value: group }]; + }); + } + return groupByKeysObjectMapping; +}; diff --git a/x-pack/platform/packages/shared/alerting-rule-utils/src/types.ts b/x-pack/platform/packages/shared/alerting-rule-utils/src/types.ts new file mode 100644 index 0000000000000..93167dc83818a --- /dev/null +++ b/x-pack/platform/packages/shared/alerting-rule-utils/src/types.ts @@ -0,0 +1,15 @@ +/* + * 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 Group { + field: string; + value: string; +} + +export interface FieldsObject { + [x: string]: any; +} diff --git a/x-pack/platform/packages/shared/alerting-rule-utils/tsconfig.json b/x-pack/platform/packages/shared/alerting-rule-utils/tsconfig.json index 62dd08bda923e..1ed94d2a6ab1d 100644 --- a/x-pack/platform/packages/shared/alerting-rule-utils/tsconfig.json +++ b/x-pack/platform/packages/shared/alerting-rule-utils/tsconfig.json @@ -14,6 +14,7 @@ "target/**/*" ], "kbn_references": [ - "@kbn/alerts-as-data-utils" + "@kbn/alerts-as-data-utils", + "@kbn/object-utils" ] } diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/action_context.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/action_context.ts index be5f55161864e..c465e2e98efa3 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/action_context.ts +++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/action_context.ts @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import type * as estypes from '@elastic/elasticsearch/lib/api/types'; import type { AlertInstanceContext } from '@kbn/alerting-plugin/server'; import type { EsQueryRuleParams } from '@kbn/response-ops-rule-params/es_query'; +import type { FieldsObject } from '@kbn/alerting-rule-utils'; import type { Comparator } from '../../../common/comparator_types'; import { getHumanReadableComparator } from '../../../common'; import { isEsqlQueryRule } from './util'; @@ -35,6 +36,7 @@ export interface EsQueryRuleActionContext extends AlertInstanceContext { // a link which navigates to stack management in case of Elastic query rule link: string; sourceFields: string[]; + grouping?: FieldsObject; } interface AddMessagesOpts { diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/executor.test.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/executor.test.ts index e1e080803d75d..f39c07ad7264a 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/executor.test.ts +++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/executor.test.ts @@ -115,7 +115,7 @@ describe('es_query executor', () => { params: defaultProps, services, rule: { id: 'test-rule-id', name: 'test-rule-name' }, - state: { latestTimestamp: undefined }, + state: { latestTimestamp: undefined, grouping: undefined }, spaceId: 'default', logger, getTimeRange: () => { @@ -295,6 +295,7 @@ describe('es_query executor', () => { dateEnd: new Date(mockNow).toISOString(), dateStart: new Date(mockNow).toISOString(), latestTimestamp: undefined, + grouping: undefined, }, payload: { 'kibana.alert.evaluation.conditions': @@ -353,6 +354,7 @@ describe('es_query executor', () => { dateEnd: new Date(mockNow).toISOString(), dateStart: new Date(mockNow).toISOString(), latestTimestamp: undefined, + grouping: undefined, }, payload: { 'kibana.alert.evaluation.conditions': @@ -379,18 +381,33 @@ describe('es_query executor', () => { groups: [{ field: 'host.name', value: 'host-1' }], count: 291, hits: [], + groupingObject: { + host: { + name: 'host-1', + }, + }, }, { group: 'host-2', groups: [{ field: 'host.name', value: 'host-2' }], count: 477, hits: [], + groupingObject: { + host: { + name: 'host-2', + }, + }, }, { group: 'host-3', groups: [{ field: 'host.name', value: 'host-3' }], count: 999, hits: [], + groupingObject: { + host: { + name: 'host-3', + }, + }, }, ], truncated: false, @@ -417,6 +434,11 @@ describe('es_query executor', () => { conditions: 'Number of matching documents for group "host-1" is greater than or equal to 200', date: new Date(mockNow).toISOString(), + grouping: { + host: { + name: 'host-1', + }, + }, hits: [], link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', message: @@ -429,6 +451,11 @@ describe('es_query executor', () => { dateEnd: new Date(mockNow).toISOString(), dateStart: new Date(mockNow).toISOString(), latestTimestamp: undefined, + grouping: { + host: { + name: 'host-1', + }, + }, }, payload: { 'host.name': 'host-1', @@ -449,6 +476,11 @@ describe('es_query executor', () => { conditions: 'Number of matching documents for group "host-2" is greater than or equal to 200', date: new Date(mockNow).toISOString(), + grouping: { + host: { + name: 'host-2', + }, + }, hits: [], link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', message: @@ -461,6 +493,11 @@ describe('es_query executor', () => { dateEnd: new Date(mockNow).toISOString(), dateStart: new Date(mockNow).toISOString(), latestTimestamp: undefined, + grouping: { + host: { + name: 'host-2', + }, + }, }, payload: { 'host.name': 'host-2', @@ -481,6 +518,11 @@ describe('es_query executor', () => { conditions: 'Number of matching documents for group "host-3" is greater than or equal to 200', date: new Date(mockNow).toISOString(), + grouping: { + host: { + name: 'host-3', + }, + }, hits: [], link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', message: @@ -493,6 +535,11 @@ describe('es_query executor', () => { dateEnd: new Date(mockNow).toISOString(), dateStart: new Date(mockNow).toISOString(), latestTimestamp: undefined, + grouping: { + host: { + name: 'host-3', + }, + }, }, payload: { 'host.name': 'host-3', @@ -562,6 +609,7 @@ describe('es_query executor', () => { dateEnd: new Date(mockNow).toISOString(), dateStart: new Date(mockNow).toISOString(), latestTimestamp: undefined, + grouping: undefined, }, }); expect(mockSetLimitReached).toHaveBeenCalledTimes(1); @@ -617,6 +665,11 @@ describe('es_query executor', () => { { alert: { getId: () => 'query matched', + getState: () => { + return { + grouping: undefined, + }; + }, }, }, ]); @@ -674,11 +727,29 @@ describe('es_query executor', () => { { alert: { getId: () => 'host-1', + getState: () => { + return { + grouping: { + host: { + name: 'host-1', + }, + }, + }; + }, }, }, { alert: { getId: () => 'host-2', + getState: () => { + return { + grouping: { + host: { + name: 'host-2', + }, + }, + }; + }, }, }, ]); @@ -713,6 +784,11 @@ describe('es_query executor', () => { title: "rule 'test-rule-name' recovered", value: 0, sourceFields: [], + grouping: { + host: { + name: 'host-1', + }, + }, }, payload: { 'kibana.alert.evaluation.conditions': @@ -738,6 +814,11 @@ describe('es_query executor', () => { title: "rule 'test-rule-name' recovered", value: 0, sourceFields: [], + grouping: { + host: { + name: 'host-2', + }, + }, }, payload: { 'kibana.alert.evaluation.conditions': @@ -760,6 +841,11 @@ describe('es_query executor', () => { { alert: { getId: () => 'query matched', + getState: () => { + return { + grouping: undefined, + }; + }, }, }, ]); @@ -821,6 +907,11 @@ describe('es_query executor', () => { 'host.id': ['1'], 'host.name': ['host-1'], }, + groupingObject: { + host: { + name: 'host-1', + }, + }, }, ], truncated: false, @@ -852,6 +943,11 @@ describe('es_query executor', () => { conditions: 'Number of matching documents for group "host-1" is greater than or equal to 200', date: new Date(mockNow).toISOString(), + grouping: { + host: { + name: 'host-1', + }, + }, hits: [], link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', message: @@ -869,6 +965,11 @@ describe('es_query executor', () => { dateEnd: new Date(mockNow).toISOString(), dateStart: new Date(mockNow).toISOString(), latestTimestamp: undefined, + grouping: { + host: { + name: 'host-1', + }, + }, }, payload: { 'kibana.alert.evaluation.conditions': diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/executor.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/executor.ts index 487dbb999494a..d54e8e5bafe33 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/executor.ts +++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/executor.ts @@ -25,6 +25,7 @@ import { AlertsClientError } from '@kbn/alerting-plugin/server'; import type { EsQueryRuleParams } from '@kbn/response-ops-rule-params/es_query'; import { ComparatorFns } from '@kbn/response-ops-rule-params/common'; +import { unflattenObject } from '@kbn/object-utils'; import type { EsQueryRuleActionContext } from './action_context'; import { addMessages, getContextConditionsDescription } from './action_context'; import type { @@ -124,8 +125,17 @@ export async function executor(core: CoreSetup, options: ExecutorOptions(); + for (const result of parsedResults.results) { + resultGroupSet.add(result.group); + } + const unmetGroupValues: Record = {}; for (const result of parsedResults.results) { + const groupingObject = result.groupingObject + ? unflattenObject(result.groupingObject) + : undefined; const alertId = result.group; const value = result.value ?? result.count; @@ -144,6 +154,7 @@ export async function executor(core: CoreSetup, options: ExecutorOptions { "name": "link", "usesPublicBaseUrl": true, }, + Object { + "description": "The object containing groups that are reporting data", + "name": "grouping", + }, ], "params": Array [ Object { diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/rule_type.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/rule_type.ts index 8aef80e17cace..480e4fdd7fad2 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/rule_type.ts +++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/rule_type.ts @@ -149,6 +149,13 @@ export function getRuleType( } ); + const actionVariableContextGroupingLabel = i18n.translate( + 'xpack.stackAlerts.esQuery.actionVariableContextGroupingLabel', + { + defaultMessage: 'The object containing groups that are reporting data', + } + ); + return { id: ES_QUERY_ID, name: ruleTypeName, @@ -180,6 +187,7 @@ export function getRuleType( { name: 'hits', description: actionVariableContextHitsLabel }, { name: 'conditions', description: actionVariableContextConditionsLabel }, { name: 'link', description: actionVariableContextLinkLabel, usesPublicBaseUrl: true }, + { name: 'grouping', description: actionVariableContextGroupingLabel }, ], params: [ { name: 'size', description: actionVariableContextSizeLabel }, diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/rule_type_params.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/rule_type_params.ts index 3b685b0fd85ca..f96e44bb6dfdf 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/rule_type_params.ts +++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/rule_type_params.ts @@ -9,12 +9,15 @@ import { i18n } from '@kbn/i18n'; import type { RuleTypeState } from '@kbn/alerting-plugin/server'; import { type EsQueryRuleParams } from '@kbn/response-ops-rule-params/es_query'; import type { SerializedSearchSourceFields } from '@kbn/data-plugin/common'; +import type { AlertInstanceState as AlertState } from '@kbn/alerting-plugin/common'; import { ES_QUERY_MAX_HITS_PER_EXECUTION_SERVERLESS } from '../../../common'; export interface EsQueryRuleState extends RuleTypeState { latestTimestamp: string | undefined; } +export type EsQueryAlertState = AlertState; + export type EsQueryRuleParamsExtractedParams = Omit & { searchConfiguration: SerializedSearchSourceFields & { indexRefName: string; diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/types.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/types.ts index 0b25fd749bb27..9881cc0d0663d 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/types.ts +++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/types.ts @@ -8,7 +8,7 @@ import type { EsQueryRuleParams } from '@kbn/response-ops-rule-params/es_query'; import type { RuleExecutorOptions, RuleTypeParams } from '../../types'; import type { ActionContext } from './action_context'; -import type { EsQueryRuleState } from './rule_type_params'; +import type { EsQueryAlertState, EsQueryRuleState } from './rule_type_params'; import type { ActionGroupId } from '../../../common/es_query'; import type { StackAlertType } from '../types'; @@ -35,7 +35,7 @@ export type OnlyEsqlQueryRuleParams = Omit< export type ExecutorOptions

= RuleExecutorOptions< P, EsQueryRuleState, - {}, + EsQueryAlertState, ActionContext, typeof ActionGroupId, StackAlertType diff --git a/x-pack/platform/plugins/shared/stack_alerts/tsconfig.json b/x-pack/platform/plugins/shared/stack_alerts/tsconfig.json index b6c287ed5cf05..e4aef51a08ac0 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/tsconfig.json +++ b/x-pack/platform/plugins/shared/stack_alerts/tsconfig.json @@ -51,6 +51,7 @@ "@kbn/core-saved-objects-server", "@kbn/alerting-rule-utils", "@kbn/response-ops-rule-params", + "@kbn/object-utils", "@kbn/expressions-plugin", "@kbn/esql-ast", "@kbn/esql-validation-autocomplete", diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/common/data/lib/parse_aggregation_results.test.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/common/data/lib/parse_aggregation_results.test.ts index 170537cb83984..89e71c1ad1c62 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/common/data/lib/parse_aggregation_results.test.ts +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/common/data/lib/parse_aggregation_results.test.ts @@ -187,6 +187,9 @@ describe('parseAggregationResults', () => { value: 'execute', }, ], + groupingObject: { + event: 'execute', + }, count: 120, hits: [], sourceFields: {}, @@ -199,6 +202,9 @@ describe('parseAggregationResults', () => { value: 'execute-start', }, ], + groupingObject: { + event: 'execute-start', + }, count: 120, hits: [], sourceFields: {}, @@ -211,6 +217,9 @@ describe('parseAggregationResults', () => { value: 'active-instance', }, ], + groupingObject: { + event: 'active-instance', + }, count: 100, hits: [], sourceFields: {}, @@ -223,6 +232,9 @@ describe('parseAggregationResults', () => { value: 'execute-action', }, ], + groupingObject: { + event: 'execute-action', + }, count: 100, hits: [], sourceFields: {}, @@ -235,6 +247,9 @@ describe('parseAggregationResults', () => { value: 'new-instance', }, ], + groupingObject: { + event: 'new-instance', + }, count: 100, hits: [], sourceFields: {}, @@ -345,6 +360,9 @@ describe('parseAggregationResults', () => { value: 'execute', }, ], + groupingObject: { + event: 'execute', + }, count: 120, hits: [sampleHit], sourceFields: {}, @@ -357,6 +375,9 @@ describe('parseAggregationResults', () => { value: 'execute-start', }, ], + groupingObject: { + event: 'execute-start', + }, count: 120, hits: [sampleHit], sourceFields: {}, @@ -369,6 +390,9 @@ describe('parseAggregationResults', () => { value: 'active-instance', }, ], + groupingObject: { + event: 'active-instance', + }, count: 100, hits: [sampleHit], sourceFields: {}, @@ -381,6 +405,9 @@ describe('parseAggregationResults', () => { value: 'execute-action', }, ], + groupingObject: { + event: 'execute-action', + }, count: 100, hits: [sampleHit], sourceFields: {}, @@ -393,6 +420,9 @@ describe('parseAggregationResults', () => { value: 'new-instance', }, ], + groupingObject: { + event: 'new-instance', + }, count: 100, hits: [sampleHit], sourceFields: {}, @@ -499,6 +529,9 @@ describe('parseAggregationResults', () => { value: 'execute-action', }, ], + groupingObject: { + event: 'execute-action', + }, count: 120, hits: [], value: null, @@ -512,6 +545,9 @@ describe('parseAggregationResults', () => { value: 'execute-start', }, ], + groupingObject: { + event: 'execute-start', + }, count: 139, hits: [], value: null, @@ -525,6 +561,9 @@ describe('parseAggregationResults', () => { value: 'starting', }, ], + groupingObject: { + event: 'starting', + }, count: 1, hits: [], value: null, @@ -538,6 +577,9 @@ describe('parseAggregationResults', () => { value: 'recovered-instance', }, ], + groupingObject: { + event: 'recovered-instance', + }, count: 120, hits: [], value: 12837500000, @@ -551,6 +593,9 @@ describe('parseAggregationResults', () => { value: 'execute', }, ], + groupingObject: { + event: 'execute', + }, count: 139, hits: [], value: 137647482.0143885, @@ -631,6 +676,10 @@ describe('parseAggregationResults', () => { value: 'action1', }, ], + groupingObject: { + event: 'execute-action', + action: 'action1', + }, count: 120, hits: [], value: null, @@ -648,6 +697,10 @@ describe('parseAggregationResults', () => { value: 'action2', }, ], + groupingObject: { + event: 'execute-start', + action: 'action2', + }, count: 139, hits: [], value: null, @@ -665,6 +718,10 @@ describe('parseAggregationResults', () => { value: 'action3', }, ], + groupingObject: { + event: 'starting', + action: 'action3', + }, count: 1, hits: [], value: null, @@ -682,6 +739,10 @@ describe('parseAggregationResults', () => { value: 'action4', }, ], + groupingObject: { + event: 'recovered-instance', + action: 'action4', + }, count: 120, hits: [], value: 12837500000, @@ -699,6 +760,10 @@ describe('parseAggregationResults', () => { value: 'action5', }, ], + groupingObject: { + event: 'execute', + action: 'action5', + }, count: 139, hits: [], value: 137647482.0143885, @@ -825,6 +890,9 @@ describe('parseAggregationResults', () => { value: 'execute-action', }, ], + groupingObject: { + event: 'execute-action', + }, count: 120, hits: [sampleHit], value: null, @@ -838,6 +906,9 @@ describe('parseAggregationResults', () => { value: 'execute-start', }, ], + groupingObject: { + event: 'execute-start', + }, count: 139, hits: [sampleHit], value: null, @@ -851,6 +922,9 @@ describe('parseAggregationResults', () => { value: 'starting', }, ], + groupingObject: { + event: 'starting', + }, count: 1, hits: [sampleHit], value: null, @@ -864,6 +938,9 @@ describe('parseAggregationResults', () => { value: 'recovered-instance', }, ], + groupingObject: { + event: 'recovered-instance', + }, count: 120, hits: [sampleHit], value: 12837500000, @@ -877,6 +954,9 @@ describe('parseAggregationResults', () => { value: 'execute', }, ], + groupingObject: { + event: 'execute', + }, count: 139, hits: [sampleHit], value: 137647482.0143885, @@ -942,6 +1022,9 @@ describe('parseAggregationResults', () => { value: 'execute', }, ], + groupingObject: { + event: 'execute', + }, count: 120, hits: [], sourceFields: {}, @@ -954,6 +1037,9 @@ describe('parseAggregationResults', () => { value: 'execute-start', }, ], + groupingObject: { + event: 'execute-start', + }, count: 120, hits: [], sourceFields: {}, @@ -966,6 +1052,9 @@ describe('parseAggregationResults', () => { value: 'active-instance', }, ], + groupingObject: { + event: 'active-instance', + }, count: 100, hits: [], sourceFields: {}, @@ -1084,6 +1173,9 @@ describe('parseAggregationResults', () => { value: 'host-1', }, ], + groupingObject: { + 'host.name': 'host-1', + }, hits: [ sampleSourceFieldsHit, sampleSourceFieldsHit, diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/common/data/lib/parse_aggregation_results.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/common/data/lib/parse_aggregation_results.ts index 6aa68a5736cdf..2f19149b4c267 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/common/data/lib/parse_aggregation_results.ts +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/common/data/lib/parse_aggregation_results.ts @@ -20,6 +20,7 @@ export interface ParsedAggregationGroup { hits: Array>; sourceFields: string[]; groups?: Group[]; + groupingObject?: Record; value?: number; } @@ -87,16 +88,28 @@ export const parseAggregationResults = ({ if (resultLimit && results.results.length === resultLimit) break; const groupName = `${groupBucket?.key}`; + const groupKeys = [termField ?? []].flat(); + const groupValues = [groupBucket.key].flat(); + const groups = termField && groupBucket?.key - ? [termField].flat().reduce((resultGroups, groupByItem, groupIndex) => { + ? groupKeys.reduce((resultGroups, groupByItem, groupIndex) => { resultGroups.push({ field: groupByItem, - value: [groupBucket.key].flat()[groupIndex], + value: groupValues[groupIndex], }); return resultGroups; }, []) : undefined; + + const groupingObject = + termField && groupBucket?.key + ? groupKeys.reduce>((resultGroups, groupByItem, groupIndex) => { + resultGroups[groupByItem] = groupValues[groupIndex]; + return resultGroups; + }, {}) + : undefined; + const sourceFields: { [key: string]: string[] } = {}; sourceFieldsParams.forEach((field) => { @@ -120,6 +133,7 @@ export const parseAggregationResults = ({ const groupResult: any = { group: groupName, groups, + groupingObject, count: groupBucket?.doc_count, hits: groupBucket?.topHitsAgg?.hits?.hits ?? [], ...(!isCountAgg ? { value: groupBucket?.metricAgg?.value } : {}), diff --git a/x-pack/solutions/observability/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts b/x-pack/solutions/observability/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts index 3367af37f4997..ec8d674acf31e 100644 --- a/x-pack/solutions/observability/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts +++ b/x-pack/solutions/observability/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts @@ -35,6 +35,7 @@ import { getParsedFilterQuery, termQuery } from '@kbn/observability-plugin/serve import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; import { asyncForEach } from '@kbn/std'; import { errorCountParamsSchema } from '@kbn/response-ops-rule-params/error_count'; +import { unflattenObject } from '@kbn/object-utils'; import { getEnvironmentEsField } from '../../../../../common/environment_filter_values'; import { ERROR_GROUP_ID, @@ -65,7 +66,6 @@ import { import { getGroupByTerms } from '../utils/get_groupby_terms'; import { getGroupByActionVariables } from '../utils/get_groupby_action_variables'; import { getAllGroupByFields } from '../../../../../common/rules/get_all_groupby_fields'; -import { unflattenObject } from '../utils/unflatten_object'; const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.ErrorCount]; diff --git a/x-pack/solutions/observability/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts b/x-pack/solutions/observability/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts index 48898322f7ce8..d5ec12796631d 100644 --- a/x-pack/solutions/observability/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts +++ b/x-pack/solutions/observability/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts @@ -36,6 +36,7 @@ import { import type { ObservabilityApmAlert } from '@kbn/alerts-as-data-utils'; import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; import { transactionDurationParamsSchema } from '@kbn/response-ops-rule-params/transaction_duration'; +import { unflattenObject } from '@kbn/object-utils'; import { getGroupByTerms } from '../utils/get_groupby_terms'; import { SearchAggregatedTransactionSetting } from '../../../../../common/aggregated_transactions'; import { getEnvironmentEsField } from '../../../../../common/environment_filter_values'; @@ -76,7 +77,6 @@ import { import { averageOrPercentileAgg, getMultiTermsSortOrder } from './average_or_percentile_agg'; import { getGroupByActionVariables } from '../utils/get_groupby_action_variables'; import { getAllGroupByFields } from '../../../../../common/rules/get_all_groupby_fields'; -import { unflattenObject } from '../utils/unflatten_object'; const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.TransactionDuration]; diff --git a/x-pack/solutions/observability/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts b/x-pack/solutions/observability/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts index 376cd059cac76..6c080a07feb5a 100644 --- a/x-pack/solutions/observability/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts +++ b/x-pack/solutions/observability/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts @@ -36,6 +36,7 @@ import type { ObservabilityApmAlert } from '@kbn/alerts-as-data-utils'; import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; import { asyncForEach } from '@kbn/std'; import { transactionErrorRateParamsSchema } from '@kbn/response-ops-rule-params/transaction_error_rate'; +import { unflattenObject } from '@kbn/object-utils'; import { SearchAggregatedTransactionSetting } from '../../../../../common/aggregated_transactions'; import { getEnvironmentEsField } from '../../../../../common/environment_filter_values'; import { @@ -71,7 +72,6 @@ import { import { getGroupByTerms } from '../utils/get_groupby_terms'; import { getGroupByActionVariables } from '../utils/get_groupby_action_variables'; import { getAllGroupByFields } from '../../../../../common/rules/get_all_groupby_fields'; -import { unflattenObject } from '../utils/unflatten_object'; const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.TransactionErrorRate]; diff --git a/x-pack/solutions/observability/plugins/apm/server/routes/alerts/rule_types/utils/unflatten_object.test.ts b/x-pack/solutions/observability/plugins/apm/server/routes/alerts/rule_types/utils/unflatten_object.test.ts deleted file mode 100644 index 7af92b997ca53..0000000000000 --- a/x-pack/solutions/observability/plugins/apm/server/routes/alerts/rule_types/utils/unflatten_object.test.ts +++ /dev/null @@ -1,26 +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 { unflattenObject } from './unflatten_object'; - -describe('unflatten group-by fields', () => { - it('returns unflattened group-by fields', () => { - const groupByFields = { - 'service.name': 'foo', - 'service.environment': 'env-foo', - 'transaction.type': 'tx-type-foo', - 'transaction.name': 'tx-name-foo', - }; - - const unflattenedGroupByFields = unflattenObject(groupByFields); - - expect(unflattenedGroupByFields).toEqual({ - service: { environment: 'env-foo', name: 'foo' }, - transaction: { name: 'tx-name-foo', type: 'tx-type-foo' }, - }); - }); -}); diff --git a/x-pack/solutions/observability/plugins/apm/server/routes/alerts/rule_types/utils/unflatten_object.ts b/x-pack/solutions/observability/plugins/apm/server/routes/alerts/rule_types/utils/unflatten_object.ts deleted file mode 100644 index 4893da8297516..0000000000000 --- a/x-pack/solutions/observability/plugins/apm/server/routes/alerts/rule_types/utils/unflatten_object.ts +++ /dev/null @@ -1,18 +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 { set } from '@kbn/safer-lodash-set'; - -export interface GroupByFields { - [x: string]: any; -} - -export const unflattenObject = (object: object): T => - Object.entries(object).reduce((acc, [key, value]) => { - set(acc, key, value); - return acc; - }, {} as T); diff --git a/x-pack/solutions/observability/plugins/apm/tsconfig.json b/x-pack/solutions/observability/plugins/apm/tsconfig.json index cdc1f4ecd0bf4..cf62abd54db3f 100644 --- a/x-pack/solutions/observability/plugins/apm/tsconfig.json +++ b/x-pack/solutions/observability/plugins/apm/tsconfig.json @@ -138,7 +138,8 @@ "@kbn/event-stacktrace", "@kbn/response-ops-rule-form", "@kbn/fields-metadata-plugin", - "@kbn/deeplinks-analytics" + "@kbn/deeplinks-analytics", + "@kbn/object-utils", ], "exclude": ["target/**/*"] } diff --git a/x-pack/solutions/observability/plugins/infra/server/lib/alerting/common/utils.test.ts b/x-pack/solutions/observability/plugins/infra/server/lib/alerting/common/utils.test.ts deleted file mode 100644 index 65c3e468e5e7e..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/server/lib/alerting/common/utils.test.ts +++ /dev/null @@ -1,60 +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 { flattenObject } from './utils'; - -describe('FlattenObject', () => { - it('flattens multi level item', () => { - const data = { - key0: 'value', - key1: { - item1: 'value 1', - item2: { itemA: 'value 2' }, - }, - key2: { - item3: { itemA: { itemAB: 'value AB' } }, - item4: 'value 4', - }, - }; - - const flatten = flattenObject(data); - expect(flatten).toEqual({ - key0: 'value', - 'key2.item3.itemA.itemAB': 'value AB', - 'key2.item4': 'value 4', - 'key1.item1': 'value 1', - 'key1.item2.itemA': 'value 2', - }); - }); - - it('does not flatten an array item', () => { - const data = { - key1: { - item1: 'value 1', - item2: { itemA: 'value 2' }, - }, - key2: { - item3: { itemA: { itemAB: 'value AB' } }, - item4: 'value 4', - item5: [1], - item6: { itemA: [1, 2, 3] }, - }, - key3: ['item7', 'item8'], - }; - - const flatten = flattenObject(data); - expect(flatten).toEqual({ - key3: ['item7', 'item8'], - 'key2.item3.itemA.itemAB': 'value AB', - 'key2.item4': 'value 4', - 'key2.item5': [1], - 'key2.item6.itemA': [1, 2, 3], - 'key1.item1': 'value 1', - 'key1.item2.itemA': 'value 2', - }); - }); -}); diff --git a/x-pack/solutions/observability/plugins/infra/server/lib/alerting/common/utils.ts b/x-pack/solutions/observability/plugins/infra/server/lib/alerting/common/utils.ts index 187dd89b8ce3f..af5588a70e0e6 100644 --- a/x-pack/solutions/observability/plugins/infra/server/lib/alerting/common/utils.ts +++ b/x-pack/solutions/observability/plugins/infra/server/lib/alerting/common/utils.ts @@ -13,9 +13,8 @@ import { ALERT_RULE_PARAMETERS, TIMESTAMP } from '@kbn/rule-data-utils'; import type { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common/parse_technical_fields'; import { parseTechnicalFields } from '@kbn/rule-registry-plugin/common/parse_technical_fields'; import { ES_FIELD_TYPES } from '@kbn/field-types'; -import { set } from '@kbn/safer-lodash-set'; import type { Alert } from '@kbn/alerts-as-data-utils'; -import { type Group } from '@kbn/alerting-rule-utils'; +import { flattenObject, unflattenObject } from '@kbn/object-utils'; import type { ParsedExperimentalFields } from '@kbn/rule-registry-plugin/common/parse_experimental_fields'; import type { LocatorPublic } from '@kbn/share-plugin/common'; import type { @@ -237,7 +236,7 @@ export const getContextForRecoveredAlerts = < >( alertHitSource: Partial | undefined | null ): AdditionalContext => { - const alert = alertHitSource ? unflattenObject(alertHitSource) : undefined; + const alert = alertHitSource ? unflattenObject(alertHitSource) : undefined; return { cloud: alert?.[ALERT_CONTEXT_CLOUD], @@ -248,70 +247,3 @@ export const getContextForRecoveredAlerts = < tags: alert?.[ALERT_CONTEXT_TAGS], }; }; - -export const unflattenObject = (object: object): T => - Object.entries(object).reduce((acc, [key, value]) => { - set(acc, key, value); - return acc; - }, {} as T); - -export const flattenObject = (obj: AdditionalContext, prefix: string = ''): AdditionalContext => - Object.keys(obj).reduce((acc, key) => { - const nextValue = obj[key]; - - if (nextValue) { - if (typeof nextValue === 'object' && !Array.isArray(nextValue)) { - const dotSuffix = '.'; - if (Object.keys(nextValue).length > 0) { - return { - ...acc, - ...flattenObject(nextValue, `${prefix}${key}${dotSuffix}`), - }; - } - } - - const fullPath = `${prefix}${key}`; - acc[fullPath] = nextValue; - } - - return acc; - }, {}); - -export const getGroupByObject = ( - groupBy: string | string[] | undefined, - resultGroupSet: Set -): Record => { - const groupByKeysObjectMapping: Record = {}; - if (groupBy) { - resultGroupSet.forEach((groupSet) => { - const groupSetKeys = groupSet.split(','); - groupByKeysObjectMapping[groupSet] = unflattenObject( - Array.isArray(groupBy) - ? groupBy.reduce((result, group, index) => { - return { ...result, [group]: groupSetKeys[index]?.trim() }; - }, {}) - : { [groupBy]: groupSet } - ); - }); - } - return groupByKeysObjectMapping; -}; - -export const getFormattedGroupBy = ( - groupBy: string | string[] | undefined, - groupSet: Set -): Record => { - const groupByKeysObjectMapping: Record = {}; - if (groupBy) { - groupSet.forEach((group) => { - const groupSetKeys = group.split(','); - groupByKeysObjectMapping[group] = Array.isArray(groupBy) - ? groupBy.reduce((result: Group[], groupByItem, index) => { - result.push({ field: groupByItem, value: groupSetKeys[index]?.trim() }); - return result; - }, []) - : [{ field: groupBy, value: group }]; - }); - } - return groupByKeysObjectMapping; -}; diff --git a/x-pack/solutions/observability/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/solutions/observability/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index 1e0a9408e4aa0..dc91227ab3b6a 100644 --- a/x-pack/solutions/observability/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/solutions/observability/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -31,8 +31,8 @@ import type { PublicAlertsClient, RecoveredAlertData, } from '@kbn/alerting-plugin/server/alerts_client/types'; -import { getEcsGroups, type Group } from '@kbn/alerting-rule-utils'; - +import { getEcsGroups, getGroupByObject, type Group } from '@kbn/alerting-rule-utils'; +import { unflattenObject } from '@kbn/object-utils'; import { ecsFieldMap } from '@kbn/rule-registry-plugin/common/assets/field_maps/ecs_field_map'; import { decodeOrThrow } from '@kbn/io-ts-utils'; import { getChartGroupNames } from '../../../../common/utils/get_chart_group_names'; @@ -65,8 +65,6 @@ import type { AdditionalContext } from '../common/utils'; import { flattenAdditionalContext, getContextForRecoveredAlerts, - getGroupByObject, - unflattenObject, UNGROUPED_FACTORY_KEY, } from '../common/utils'; import { diff --git a/x-pack/solutions/observability/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/solutions/observability/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 53f4fec84d896..5d24ea95f141d 100644 --- a/x-pack/solutions/observability/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/solutions/observability/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -25,7 +25,12 @@ import type { TimeUnitChar } from '@kbn/observability-plugin/common'; import { getAlertDetailsUrl } from '@kbn/observability-plugin/common'; import type { ObservabilityMetricsAlert } from '@kbn/alerts-as-data-utils'; import type { COMPARATORS } from '@kbn/alerting-comparators'; -import { getEcsGroups, type Group } from '@kbn/alerting-rule-utils'; +import { + getEcsGroups, + getFormattedGroupBy, + getGroupByObject, + type Group, +} from '@kbn/alerting-rule-utils'; import { convertToBuiltInComparators } from '@kbn/observability-plugin/common/utils/convert_legacy_outside_comparator'; import { getOriginalActionGroup } from '../../../utils/get_original_action_group'; import { AlertStates } from '../../../../common/alerting/metrics'; @@ -47,8 +52,6 @@ import { hasAdditionalContext, validGroupByForContext, flattenAdditionalContext, - getGroupByObject, - getFormattedGroupBy, } from '../common/utils'; import { getEvaluationValues, getThresholds } from '../common/get_values'; diff --git a/x-pack/solutions/observability/plugins/infra/tsconfig.json b/x-pack/solutions/observability/plugins/infra/tsconfig.json index 4189ceb36f0a7..c7202179019d7 100644 --- a/x-pack/solutions/observability/plugins/infra/tsconfig.json +++ b/x-pack/solutions/observability/plugins/infra/tsconfig.json @@ -124,7 +124,8 @@ "@kbn/response-ops-rule-form", "@kbn/traced-es-client", "@kbn/fields-metadata-plugin", - "@kbn/deeplinks-analytics" + "@kbn/deeplinks-analytics", + "@kbn/object-utils" ], "exclude": ["target/**/*"] } diff --git a/x-pack/solutions/observability/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts b/x-pack/solutions/observability/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts index 1ea5296ee5fe5..83c053028d4d7 100644 --- a/x-pack/solutions/observability/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts +++ b/x-pack/solutions/observability/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts @@ -18,7 +18,7 @@ import { LocatorPublic } from '@kbn/share-plugin/common'; import { RecoveredActionGroup } from '@kbn/alerting-plugin/common'; import { IBasePath, Logger } from '@kbn/core/server'; import { AlertsClientError, RuleExecutorOptions } from '@kbn/alerting-plugin/server'; -import { getEcsGroups } from '@kbn/alerting-rule-utils'; +import { getEcsGroups, getFormattedGroupBy, getGroupByObject } from '@kbn/alerting-rule-utils'; import { getEsQueryConfig } from '../../../utils/get_es_query_config'; import { AlertsLocatorParams, getAlertDetailsUrl } from '../../../../common'; import { getViewInAppUrl } from '../../../../common/custom_threshold_rule/get_view_in_app_url'; @@ -41,9 +41,7 @@ import { hasAdditionalContext, validGroupByForContext, flattenAdditionalContext, - getFormattedGroupBy, getContextForRecoveredAlerts, - getGroupByObject, } from './utils'; import { formatAlertResult, getLabel } from './lib/format_alert_result'; @@ -302,7 +300,7 @@ export const createCustomThresholdExecutor = ({ alertsClient.setAlertLimitReached(hasReachedLimit); const recoveredAlerts = alertsClient.getRecoveredAlerts() ?? []; - let groupingObjectForRecovered: Record = {}; + let groupingObjectForRecovered: Record = {}; // extracing group by fields from kibana.alert.rule.params, // since all recovered alert documents will have same group by fields, diff --git a/x-pack/solutions/observability/plugins/observability/server/lib/rules/custom_threshold/utils.test.ts b/x-pack/solutions/observability/plugins/observability/server/lib/rules/custom_threshold/utils.test.ts index da1c1fa19d5f7..f8b87c35fa4dc 100644 --- a/x-pack/solutions/observability/plugins/observability/server/lib/rules/custom_threshold/utils.test.ts +++ b/x-pack/solutions/observability/plugins/observability/server/lib/rules/custom_threshold/utils.test.ts @@ -5,62 +5,7 @@ * 2.0. */ -import { - flattenObject, - getFormattedGroupBy, - getGroupByObject, - validateKQLStringFilter, -} from './utils'; - -describe('FlattenObject', () => { - it('flattens multi level item', () => { - const data = { - key1: { - item1: 'value 1', - item2: { itemA: 'value 2' }, - }, - key2: { - item3: { itemA: { itemAB: 'value AB' } }, - item4: 'value 4', - }, - }; - - const flatten = flattenObject(data); - expect(flatten).toEqual({ - 'key2.item3.itemA.itemAB': 'value AB', - 'key2.item4': 'value 4', - 'key1.item1': 'value 1', - 'key1.item2.itemA': 'value 2', - }); - }); - - it('does not flatten an array item', () => { - const data = { - key1: { - item1: 'value 1', - item2: { itemA: 'value 2' }, - }, - key2: { - item3: { itemA: { itemAB: 'value AB' } }, - item4: 'value 4', - item5: [1], - item6: { itemA: [1, 2, 3] }, - }, - key3: ['item7', 'item8'], - }; - - const flatten = flattenObject(data); - expect(flatten).toEqual({ - key3: ['item7', 'item8'], - 'key2.item3.itemA.itemAB': 'value AB', - 'key2.item4': 'value 4', - 'key2.item5': [1], - 'key2.item6.itemA': [1, 2, 3], - 'key1.item1': 'value 1', - 'key1.item2.itemA': 'value 2', - }); - }); -}); +import { validateKQLStringFilter } from './utils'; describe('validateKQLStringFilter', () => { const data = [ @@ -84,73 +29,3 @@ describe('validateKQLStringFilter', () => { expect(validateKQLStringFilter(input)).toContain(output); }); }); - -describe('getFormattedGroupBy', () => { - it('should format groupBy correctly for empty input', () => { - expect(getFormattedGroupBy(undefined, new Set())).toEqual({}); - }); - - it('should format groupBy correctly for multiple groups', () => { - expect( - getFormattedGroupBy( - ['host.name', 'host.mac', 'tags', 'container.name'], - new Set([ - 'host-0,00-00-5E-00-53-23,event-0,container-name', - 'host-0,00-00-5E-00-53-23,group-0,container-name', - 'host-0,00-00-5E-00-53-24,event-0,container-name', - 'host-0,00-00-5E-00-53-24,group-0,container-name', - ]) - ) - ).toEqual({ - 'host-0,00-00-5E-00-53-23,event-0,container-name': [ - { field: 'host.name', value: 'host-0' }, - { field: 'host.mac', value: '00-00-5E-00-53-23' }, - { field: 'tags', value: 'event-0' }, - { field: 'container.name', value: 'container-name' }, - ], - 'host-0,00-00-5E-00-53-23,group-0,container-name': [ - { field: 'host.name', value: 'host-0' }, - { field: 'host.mac', value: '00-00-5E-00-53-23' }, - { field: 'tags', value: 'group-0' }, - { field: 'container.name', value: 'container-name' }, - ], - 'host-0,00-00-5E-00-53-24,event-0,container-name': [ - { field: 'host.name', value: 'host-0' }, - { field: 'host.mac', value: '00-00-5E-00-53-24' }, - { field: 'tags', value: 'event-0' }, - { field: 'container.name', value: 'container-name' }, - ], - 'host-0,00-00-5E-00-53-24,group-0,container-name': [ - { field: 'host.name', value: 'host-0' }, - { field: 'host.mac', value: '00-00-5E-00-53-24' }, - { field: 'tags', value: 'group-0' }, - { field: 'container.name', value: 'container-name' }, - ], - }); - }); -}); - -describe('getGroupByObject', () => { - it('should return empty object for undefined groupBy', () => { - expect(getFormattedGroupBy(undefined, new Set())).toEqual({}); - }); - - it('should return an object containing groups for one groupBy field', () => { - expect(getGroupByObject('host.name', new Set(['host-0', 'host-1']))).toEqual({ - 'host-0': { host: { name: 'host-0' } }, - 'host-1': { host: { name: 'host-1' } }, - }); - }); - - it('should return an object containing groups for multiple groupBy fields', () => { - expect( - getGroupByObject( - ['host.name', 'container.id'], - new Set(['host-0,container-0', 'host-1,container-1']) - ) - ).toEqual({ - 'host-0,container-0': { container: { id: 'container-0' }, host: { name: 'host-0' } }, - 'host-1,container-1': { container: { id: 'container-1' }, host: { name: 'host-1' } }, - }); - }); -}); diff --git a/x-pack/solutions/observability/plugins/observability/server/lib/rules/custom_threshold/utils.ts b/x-pack/solutions/observability/plugins/observability/server/lib/rules/custom_threshold/utils.ts index a1edf9b3b6095..ce763c111b7b8 100644 --- a/x-pack/solutions/observability/plugins/observability/server/lib/rules/custom_threshold/utils.ts +++ b/x-pack/solutions/observability/plugins/observability/server/lib/rules/custom_threshold/utils.ts @@ -13,11 +13,10 @@ import { Logger, LogMeta } from '@kbn/logging'; import type { ElasticsearchClient, IBasePath } from '@kbn/core/server'; import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; import { ES_FIELD_TYPES } from '@kbn/field-types'; -import { set } from '@kbn/safer-lodash-set'; import { ParsedExperimentalFields } from '@kbn/rule-registry-plugin/common/parse_experimental_fields'; import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; import { Alert } from '@kbn/alerts-as-data-utils'; -import type { Group } from '../../../../common/typings'; +import { flattenObject, unflattenObject } from '@kbn/object-utils'; import { ObservabilityConfig } from '../../..'; import { AlertExecutionDetails } from './types'; @@ -194,7 +193,7 @@ export const getContextForRecoveredAlerts = < >( alertHitSource: Partial | undefined | null ): AdditionalContext => { - const alert = alertHitSource ? unflattenObject(alertHitSource) : undefined; + const alert = alertHitSource ? unflattenObject(alertHitSource) : undefined; return { cloud: alert?.[ALERT_CONTEXT_CLOUD], @@ -206,73 +205,6 @@ export const getContextForRecoveredAlerts = < }; }; -export const unflattenObject = (object: object): T => - Object.entries(object).reduce((acc, [key, value]) => { - set(acc, key, value); - return acc; - }, {} as T); - -export const flattenObject = (obj: AdditionalContext, prefix: string = ''): AdditionalContext => - Object.keys(obj).reduce((acc, key) => { - const nextValue = obj[key]; - - if (nextValue) { - if (typeof nextValue === 'object' && !Array.isArray(nextValue)) { - const dotSuffix = '.'; - if (Object.keys(nextValue).length > 0) { - return { - ...acc, - ...flattenObject(nextValue, `${prefix}${key}${dotSuffix}`), - }; - } - } - - const fullPath = `${prefix}${key}`; - acc[fullPath] = nextValue; - } - - return acc; - }, {}); - -export const getGroupByObject = ( - groupBy: string | string[] | undefined, - resultGroupSet: Set -): Record => { - const groupByKeysObjectMapping: Record = {}; - if (groupBy) { - resultGroupSet.forEach((groupSet) => { - const groupSetKeys = groupSet.split(','); - groupByKeysObjectMapping[groupSet] = unflattenObject( - Array.isArray(groupBy) - ? groupBy.reduce((result, group, index) => { - return { ...result, [group]: groupSetKeys[index]?.trim() }; - }, {}) - : { [groupBy]: groupSet } - ); - }); - } - return groupByKeysObjectMapping; -}; - -export const getFormattedGroupBy = ( - groupBy: string | string[] | undefined, - groupSet: Set -): Record => { - const groupByKeysObjectMapping: Record = {}; - if (groupBy) { - groupSet.forEach((group) => { - const groupSetKeys = group.split(','); - groupByKeysObjectMapping[group] = Array.isArray(groupBy) - ? groupBy.reduce((result: Group[], groupByItem, index) => { - result.push({ field: groupByItem, value: groupSetKeys[index]?.trim() }); - return result; - }, []) - : [{ field: groupBy, value: group }]; - }); - } - return groupByKeysObjectMapping; -}; - // TO BE MOVED export const INFRA_ALERT_PREVIEW_PATH = '/api/infra/alerting/preview'; diff --git a/x-pack/solutions/observability/plugins/observability/tsconfig.json b/x-pack/solutions/observability/plugins/observability/tsconfig.json index 8208c9100fd72..239a1329f47e7 100644 --- a/x-pack/solutions/observability/plugins/observability/tsconfig.json +++ b/x-pack/solutions/observability/plugins/observability/tsconfig.json @@ -102,7 +102,6 @@ "@kbn/securitysolution-io-ts-utils", "@kbn/core-elasticsearch-server", "@kbn/logging", - "@kbn/safer-lodash-set", "@kbn/features-plugin", "@kbn/files-plugin", "@kbn/server-route-repository", @@ -118,6 +117,7 @@ "@kbn/ebt-tools", "@kbn/dashboard-plugin", "@kbn/fields-metadata-plugin", + "@kbn/object-utils" ], "exclude": ["target/**/*"] } diff --git a/x-pack/solutions/observability/plugins/slo/server/lib/rules/slo_burn_rate/executor.ts b/x-pack/solutions/observability/plugins/slo/server/lib/rules/slo_burn_rate/executor.ts index e7d3d9c619c85..9db2bd8961b7d 100644 --- a/x-pack/solutions/observability/plugins/slo/server/lib/rules/slo_burn_rate/executor.ts +++ b/x-pack/solutions/observability/plugins/slo/server/lib/rules/slo_burn_rate/executor.ts @@ -113,6 +113,7 @@ export const getRuleExecutor = (basePath: IBasePath) => for (const result of results) { const { instanceId, + groupings, shouldAlert, longWindowDuration, longWindowBurnRate, @@ -167,6 +168,7 @@ export const getRuleExecutor = (basePath: IBasePath) => actionGroup, state: { alertState: AlertStates.ALERT, + grouping: groupings, }, payload: { [ALERT_REASON]: reason, @@ -199,6 +201,7 @@ export const getRuleExecutor = (basePath: IBasePath) => sloErrorBudgetRemaining: sloSummary?.errorBudgetRemaining ?? 1, sloErrorBudgetConsumed: sloSummary?.errorBudgetConsumed ?? 0, suppressedAction: shouldSuppress ? windowDef.actionGroup : null, + grouping: groupings, }; alertsClient.setAlertData({ id: alertId, context }); @@ -210,6 +213,7 @@ export const getRuleExecutor = (basePath: IBasePath) => } const recoveredAlerts = alertsClient.getRecoveredAlerts() ?? []; + for (const recoveredAlert of recoveredAlerts) { const alertId = recoveredAlert.alert.getId(); const alertUuid = recoveredAlert.alert.getUuid(); @@ -222,6 +226,8 @@ export const getRuleExecutor = (basePath: IBasePath) => `/app/observability/slos/${slo.id}${urlQuery}` ); + const recoveredAlertState = recoveredAlert.alert.getState(); + const context = { timestamp: startedAt.toISOString(), viewInAppUrl, @@ -229,6 +235,7 @@ export const getRuleExecutor = (basePath: IBasePath) => sloId: slo.id, sloName: slo.name, sloInstanceId: alertId, + grouping: recoveredAlertState?.grouping, }; alertsClient.setAlertData({ diff --git a/x-pack/solutions/observability/plugins/slo/server/lib/rules/slo_burn_rate/lib/__snapshots__/build_query.test.ts.snap b/x-pack/solutions/observability/plugins/slo/server/lib/rules/slo_burn_rate/lib/__snapshots__/build_query.test.ts.snap index 245078ccd627a..dddbf695377c5 100644 --- a/x-pack/solutions/observability/plugins/slo/server/lib/rules/slo_burn_rate/lib/__snapshots__/build_query.test.ts.snap +++ b/x-pack/solutions/observability/plugins/slo/server/lib/rules/slo_burn_rate/lib/__snapshots__/build_query.test.ts.snap @@ -414,6 +414,14 @@ Object { }, }, }, + "groupings": Object { + "top_hits": Object { + "_source": Array [ + "slo.groupings", + ], + "size": 1, + }, + }, }, "composite": Object { "size": 1000, @@ -879,6 +887,14 @@ Object { }, }, }, + "groupings": Object { + "top_hits": Object { + "_source": Array [ + "slo.groupings", + ], + "size": 1, + }, + }, }, "composite": Object { "size": 1000, @@ -1336,6 +1352,14 @@ Object { }, }, }, + "groupings": Object { + "top_hits": Object { + "_source": Array [ + "slo.groupings", + ], + "size": 1, + }, + }, }, "composite": Object { "after": Object { diff --git a/x-pack/solutions/observability/plugins/slo/server/lib/rules/slo_burn_rate/lib/build_query.ts b/x-pack/solutions/observability/plugins/slo/server/lib/rules/slo_burn_rate/lib/build_query.ts index 071dfb046b157..b5ea8a175825c 100644 --- a/x-pack/solutions/observability/plugins/slo/server/lib/rules/slo_burn_rate/lib/build_query.ts +++ b/x-pack/solutions/observability/plugins/slo/server/lib/rules/slo_burn_rate/lib/build_query.ts @@ -179,6 +179,17 @@ function buildEvaluation(burnRateWindows: BurnRateWindowWithDuration[]) { }; } +function buildGroupingAgg() { + return { + groupings: { + top_hits: { + size: 1, + _source: ['slo.groupings'], + }, + }, + }; +} + export function buildQuery( startedAt: Date, slo: SLODefinition, @@ -234,6 +245,7 @@ export function buildQuery( aggs: { ...buildWindowAggs(startedAt, slo, burnRateWindows, delayInSeconds), ...buildEvaluation(burnRateWindows), + ...buildGroupingAgg(), }, }, }, diff --git a/x-pack/solutions/observability/plugins/slo/server/lib/rules/slo_burn_rate/lib/evaluate.ts b/x-pack/solutions/observability/plugins/slo/server/lib/rules/slo_burn_rate/lib/evaluate.ts index 957157cecda29..a0ff9a085a6bb 100644 --- a/x-pack/solutions/observability/plugins/slo/server/lib/rules/slo_burn_rate/lib/evaluate.ts +++ b/x-pack/solutions/observability/plugins/slo/server/lib/rules/slo_burn_rate/lib/evaluate.ts @@ -7,6 +7,7 @@ import { ElasticsearchClient } from '@kbn/core/server'; import { get } from 'lodash'; +import { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { Duration, SLODefinition, toDurationUnit } from '../../../../domain/models'; import { BurnRateRuleParams } from '../types'; import { SLI_DESTINATION_INDEX_PATTERN } from '../../../../../common/constants'; @@ -26,8 +27,15 @@ export interface EvaluationWindowStats { total: { value: number }; } +export interface TopHitsAggResults { + slo: { + groupings: Record; + }; +} + export interface EvaluationBucket { key: EvaluationAfterKey; + groupings: SearchResponse; doc_count: number; WINDOW_0_SHORT?: EvaluationWindowStats; WINDOW_1_SHORT?: EvaluationWindowStats; @@ -117,6 +125,7 @@ function transformBucketToResults(buckets: EvaluationBucket[], params: BurnRateR if (isShortWindowTriggering && isLongWindowTriggering) { return { instanceId: bucket.key.instanceId, + groupings: transformGroupings(bucket.groupings), shouldAlert: true, longWindowBurnRate: get( bucket, @@ -144,3 +153,12 @@ function transformBucketToResults(buckets: EvaluationBucket[], params: BurnRateR throw new Error(`Evaluation query for ${bucket.key.instanceId} failed.`); }); } + +function transformGroupings( + groupings: SearchResponse +): Record | undefined { + if (groupings && groupings.hits && groupings.hits.hits && groupings.hits.hits.length > 0) { + const topHit = groupings.hits.hits[0]; + return topHit._source?.slo?.groupings; + } +} diff --git a/x-pack/solutions/observability/plugins/slo/server/lib/rules/slo_burn_rate/register.ts b/x-pack/solutions/observability/plugins/slo/server/lib/rules/slo_burn_rate/register.ts index 02b450aeac6ed..f0f749d76d645 100644 --- a/x-pack/solutions/observability/plugins/slo/server/lib/rules/slo_burn_rate/register.ts +++ b/x-pack/solutions/observability/plugins/slo/server/lib/rules/slo_burn_rate/register.ts @@ -85,6 +85,10 @@ export function sloBurnRateRuleType( name: 'sloErrorBudgetConsumed', description: sloErrorBudgetConsumedActionVariableDescription, }, + { + name: 'grouping', + description: groupingObjectActionVariableDescription, + }, ], }, alerts: { @@ -193,3 +197,10 @@ export const sloErrorBudgetConsumedActionVariableDescription = i18n.translate( defaultMessage: 'The consumed error budget at the time of firing the alert.', } ); + +export const groupingObjectActionVariableDescription = i18n.translate( + 'xpack.slo.alerting.groupingObjectActionVariableDescription', + { + defaultMessage: 'The object containing groups that are reporting data', + } +); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query/query_dsl_with_group_by.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query/query_dsl_with_group_by.ts index db91ba5780dad..1ac679da7a708 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query/query_dsl_with_group_by.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query/query_dsl_with_group_by.ts @@ -129,6 +129,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { tags: '{{rule.tags}}', alertId: '{{alert.id}}', alertActionGroup: '{{alert.actionGroup}}', + grouping: '{{context.grouping}}', instanceContextValue: '{{context.instanceContextValue}}', instanceStateValue: '{{state.instanceStateValue}}', }, @@ -201,6 +202,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { '{"size":100,"thresholdComparator":">","threshold":[1],"index":["kbn-data-forge-fake_hosts.fake_hosts-*"],"timeField":"@timestamp","esQuery":"{\\n \\"query\\":{\\n \\"match_all\\" : {}\\n }\\n}","timeWindowSize":1,"timeWindowUnit":"m","groupBy":"top","termField":"host.name","termSize":1,"excludeHitsFromPreviousRun":true,"aggType":"count","searchType":"esQuery"}' ); expect(resp.hits.hits[0]._source?.alertActionGroup).eql('query matched'); + expect(resp.hits.hits[0]._source?.grouping).eql('{"host":{"name":"host-0"}}'); }); }); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query/types.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query/types.ts index 939ff17b775d7..046ac60e2ace3 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query/types.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query/types.ts @@ -13,4 +13,5 @@ export interface ActionDocument { tags: string; alertId: string; alertActionGroup: string; + grouping: string; }