diff --git a/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts b/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts index fb5aef4e3ddcb..804ae09e5ee9e 100644 --- a/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts +++ b/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts @@ -155,6 +155,7 @@ const mockedIndex = { const mockedDataView = { getIndexPattern: () => 'mockedIndexPattern', getName: () => 'mockedDataViewName', + getRuntimeMappings: () => undefined, ...mockedIndex, }; const mockedSearchSource = { @@ -971,7 +972,7 @@ describe('The custom threshold alert type', () => { stateResult2 ); expect(stateResult3.missingGroups).toEqual([{ key: 'b', bucketKey: { groupBy0: 'b' } }]); - expect(mockedEvaluateRule.mock.calls[2][10]).toEqual([ + expect(mockedEvaluateRule.mock.calls[2][11]).toEqual([ { bucketKey: { groupBy0: 'b' }, key: 'b' }, ]); }); @@ -2961,7 +2962,7 @@ describe('The custom threshold alert type', () => { stateResult2 ); expect(stateResult3.missingGroups).toEqual([{ key: 'b', bucketKey: { groupBy0: 'b' } }]); - expect(mockedEvaluateRule.mock.calls[2][10]).toEqual([ + expect(mockedEvaluateRule.mock.calls[2][11]).toEqual([ { bucketKey: { groupBy0: 'b' }, key: 'b' }, ]); }); diff --git a/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts b/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts index 72c9795122dc8..9c9a1d2c33647 100644 --- a/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts +++ b/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts @@ -126,6 +126,7 @@ export const createCustomThresholdExecutor = ({ const initialSearchSource = await searchSourceClient.create(params.searchConfiguration); const dataView = initialSearchSource.getField('index')!; const { id: dataViewId, timeFieldName } = dataView; + const runtimeMappings = dataView.getRuntimeMappings(); const dataViewIndexPattern = dataView.getIndexPattern(); const dataViewName = dataView.getName(); if (!dataViewIndexPattern) { @@ -147,6 +148,7 @@ export const createCustomThresholdExecutor = ({ logger, { end: dateEnd, start: dateStart }, esQueryConfig, + runtimeMappings, state.lastRunTimestamp, previousMissingGroups ); diff --git a/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/check_missing_group.ts b/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/check_missing_group.ts index 8c5b75f00003a..2d8952a98fa78 100644 --- a/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/check_missing_group.ts +++ b/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/check_missing_group.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ElasticsearchClient } from '@kbn/core/server'; import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import type { EsQueryConfig } from '@kbn/es-query'; @@ -16,6 +17,7 @@ import { } from '../../../../../common/custom_threshold_rule/types'; import type { BucketKey } from './get_data'; import { calculateCurrentTimeFrame, createBoolQuery } from './metric_query'; +import { isPopulatedObject } from './is_populated_object'; export interface MissingGroupsRecord { key: string; @@ -32,7 +34,8 @@ export const checkMissingGroups = async ( logger: Logger, timeframe: { start: number; end: number }, esQueryConfig: EsQueryConfig, - missingGroups: MissingGroupsRecord[] = [] + missingGroups: MissingGroupsRecord[] = [], + runtimeMappings?: estypes.MappingRuntimeFields ): Promise => { if (missingGroups.length === 0) { return missingGroups; @@ -65,6 +68,7 @@ export const checkMissingGroups = async ( terminate_after: 1, track_total_hits: true, query, + ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), }, ]; }); diff --git a/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/evaluate_rule.ts b/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/evaluate_rule.ts index 2e2d1e5af48b2..e0d861f53ae38 100644 --- a/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/evaluate_rule.ts +++ b/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/evaluate_rule.ts @@ -6,6 +6,7 @@ */ import moment from 'moment'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ElasticsearchClient } from '@kbn/core/server'; import { EsQueryConfig } from '@kbn/es-query'; import type { Logger } from '@kbn/logging'; @@ -45,6 +46,7 @@ export const evaluateRule = async >> => { @@ -77,6 +79,7 @@ export const evaluateRule = async @@ -170,6 +172,7 @@ export const getData = async ( alertOnGroupDisappear, timeframe, logger, + runtimeMappings, lastPeriodEnd, previous, nextAfterKey @@ -209,6 +212,7 @@ export const getData = async ( alertOnGroupDisappear, searchConfiguration, esQueryConfig, + runtimeMappings, lastPeriodEnd, groupBy, afterKey, diff --git a/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/is_populated_object.test.ts b/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/is_populated_object.test.ts new file mode 100644 index 0000000000000..fdbe8d9fa5f21 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/is_populated_object.test.ts @@ -0,0 +1,52 @@ +/* + * 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 { isPopulatedObject } from './is_populated_object'; + +describe('isPopulatedObject', () => { + it('does not allow numbers', () => { + expect(isPopulatedObject(0)).toBe(false); + }); + it('does not allow strings', () => { + expect(isPopulatedObject('')).toBe(false); + }); + it('does not allow null', () => { + expect(isPopulatedObject(null)).toBe(false); + }); + it('does not allow an empty object', () => { + expect(isPopulatedObject({})).toBe(false); + }); + it('allows an object with an attribute', () => { + expect(isPopulatedObject({ attribute: 'value' })).toBe(true); + }); + it('does not allow an object with a non-existing required attribute', () => { + expect(isPopulatedObject({ attribute: 'value' }, ['otherAttribute'])).toBe(false); + }); + it('allows an object with an existing required attribute', () => { + expect(isPopulatedObject({ attribute: 'value' }, ['attribute'])).toBe(true); + }); + it('allows an object with two existing required attributes', () => { + expect( + isPopulatedObject({ attribute1: 'value1', attribute2: 'value2' }, [ + 'attribute1', + 'attribute2', + ]) + ).toBe(true); + }); + it('does not allow an object with two required attributes where one does not exist', () => { + expect( + isPopulatedObject({ attribute1: 'value1', attribute2: 'value2' }, [ + 'attribute1', + 'otherAttribute', + ]) + ).toBe(false); + }); + it('does not allow an object with a required attribute in the prototype ', () => { + const testObject = { attribute: 'value', __proto__: { otherAttribute: 'value' } }; + expect(isPopulatedObject(testObject, ['otherAttribute'])).toBe(false); + }); +}); diff --git a/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/is_populated_object.ts b/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/is_populated_object.ts new file mode 100644 index 0000000000000..7fee714dbd6a0 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/is_populated_object.ts @@ -0,0 +1,35 @@ +/* + * 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. + */ + +/** + * A type guard to check record like object structures. + * + * Examples: + * - `isPopulatedObject({...})` + * Limits type to Record + * + * - `isPopulatedObject({...}, ['attribute'])` + * Limits type to Record<'attribute', unknown> + * + * - `isPopulatedObject({...})` + * Limits type to a record with keys of the given interface. + * Note that you might want to add keys from the interface to the + * array of requiredAttributes to satisfy runtime requirements. + * Otherwise you'd just satisfy TS requirements but might still + * run into runtime issues. + */ +export const isPopulatedObject = ( + arg: unknown, + requiredAttributes: U[] = [] +): arg is Record => { + return ( + typeof arg === 'object' && + arg !== null && + Object.keys(arg).length > 0 && + (requiredAttributes.length === 0 || requiredAttributes.every((d) => Object.hasOwn(arg, d))) + ); +}; diff --git a/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/metric_query.test.ts b/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/metric_query.test.ts index 53955b7130c54..a6981b4c02f38 100644 --- a/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/metric_query.test.ts +++ b/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/metric_query.test.ts @@ -64,6 +64,7 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { true, searchConfiguration, esQueryConfig, + undefined, void 0, groupBy ); @@ -121,6 +122,7 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { true, currentSearchConfiguration, esQueryConfig, + undefined, void 0, groupBy ); @@ -233,6 +235,7 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { true, currentSearchConfiguration, esQueryConfig, + undefined, void 0, groupBy ); diff --git a/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/metric_query.ts b/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/metric_query.ts index 4e7fc236bfdf5..8cf6867aa4a01 100644 --- a/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/metric_query.ts +++ b/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/metric_query.ts @@ -6,6 +6,7 @@ */ import moment from 'moment'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import type { EsQueryConfig, Filter } from '@kbn/es-query'; import { @@ -24,6 +25,7 @@ import { } from '../utils'; import { createBucketSelector } from './create_bucket_selector'; import { wrapInCurrentPeriod } from './wrap_in_period'; +import { isPopulatedObject } from './is_populated_object'; export const calculateCurrentTimeFrame = ( metricParams: CustomMetricExpressionParams, @@ -76,6 +78,7 @@ export const getElasticsearchMetricQuery = ( alertOnGroupDisappear: boolean, searchConfiguration: SearchConfigurationType, esQueryConfig: EsQueryConfig, + runtimeMappings?: estypes.MappingRuntimeFields, lastPeriodEnd?: number, groupBy?: string | string[], afterKey?: Record, @@ -211,6 +214,7 @@ export const getElasticsearchMetricQuery = ( return { track_total_hits: true, query, + ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), size: 0, aggs, }; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/cardinality_runtime_field_fired.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/cardinality_runtime_field_fired.ts new file mode 100644 index 0000000000000..64b54f2f159d3 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/cardinality_runtime_field_fired.ts @@ -0,0 +1,291 @@ +/* + * 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 { omit } from 'lodash'; +import expect from '@kbn/expect'; +import { cleanup, generate, Dataset, PartialConfig } from '@kbn/data-forge'; +import { Aggregators } from '@kbn/observability-plugin/common/custom_threshold_rule/types'; +import { FIRED_ACTIONS_ID } from '@kbn/observability-plugin/server/lib/rules/custom_threshold/constants'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; +import { parseSearchParams } from '@kbn/share-plugin/common/url_service'; +import { COMPARATORS } from '@kbn/alerting-comparators'; +import { kbnTestConfig } from '@kbn/test'; +import type { InternalRequestHeader, RoleCredentials } from '@kbn/ftr-common-functional-services'; +import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { ISO_DATE_REGEX } from './constants'; +import { ActionDocument, LogsExplorerLocatorParsedParams } from './types'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const esClient = getService('es'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esDeleteAllIndices = getService('esDeleteAllIndices'); + const logger = getService('log'); + const alertingApi = getService('alertingApi'); + const dataViewApi = getService('dataViewApi'); + const samlAuth = getService('samlAuth'); + let roleAuthc: RoleCredentials; + let internalReqHeader: InternalRequestHeader; + const config = getService('config'); + const isServerless = config.get('serverless'); + const expectedConsumer = isServerless ? 'observability' : 'logs'; + + describe('CARDINALITY - RUNTIME FIELD - FIRED', () => { + const CUSTOM_THRESHOLD_RULE_ALERT_INDEX = '.alerts-observability.threshold.alerts-default'; + const ALERT_ACTION_INDEX = 'alert-action-threshold'; + const DATA_VIEW = 'kbn-data-forge-fake_hosts.fake_hosts-*'; + const DATA_VIEW_ID = 'data-view-id'; + const DATA_VIEW_NAME = 'data-view-name'; + const runtimeMappings = { + runtimeHostName: { + type: 'keyword', + script: { + source: + "String runtimeHostName = doc['host.name'].value;\n" + '\n' + 'emit(runtimeHostName);', + }, + }, + }; + let dataForgeConfig: PartialConfig; + let dataForgeIndices: string[]; + let actionId: string; + let ruleId: string; + let alertId: string; + + before(async () => { + roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + internalReqHeader = samlAuth.getInternalRequestHeader(); + dataForgeConfig = { + schedule: [ + { + template: 'good', + start: 'now-10m', + end: 'now+5m', + metrics: [ + { name: 'system.cpu.user.pct', method: 'linear', start: 2.5, end: 2.5 }, + { name: 'system.cpu.total.pct', method: 'linear', start: 0.5, end: 0.5 }, + { name: 'system.cpu.total.norm.pct', method: 'linear', start: 0.8, end: 0.8 }, + ], + }, + ], + indexing: { + dataset: 'fake_hosts' as Dataset, + eventsPerCycle: 1, + interval: 60000, + alignEventsToInterval: true, + }, + }; + dataForgeIndices = await generate({ client: esClient, config: dataForgeConfig, logger }); + await alertingApi.waitForDocumentInIndex({ + indexName: dataForgeIndices.join(','), + docCountTarget: 45, + }); + await dataViewApi.create({ + name: DATA_VIEW_NAME, + id: DATA_VIEW_ID, + title: DATA_VIEW, + roleAuthc, + data: { + runtimeFieldMap: JSON.stringify(runtimeMappings), + }, + }); + }); + + after(async () => { + await supertestWithoutAuth + .delete(`/api/alerting/rule/${ruleId}`) + .set(roleAuthc.apiKeyHeader) + .set(internalReqHeader); + await supertestWithoutAuth + .delete(`/api/actions/connector/${actionId}`) + .set(roleAuthc.apiKeyHeader) + .set(internalReqHeader); + await esClient.deleteByQuery({ + index: CUSTOM_THRESHOLD_RULE_ALERT_INDEX, + query: { term: { 'kibana.alert.rule.uuid': ruleId } }, + conflicts: 'proceed', + }); + await esClient.deleteByQuery({ + index: '.kibana-event-log-*', + query: { term: { 'rule.id': ruleId } }, + conflicts: 'proceed', + }); + await dataViewApi.delete({ + id: DATA_VIEW_ID, + roleAuthc, + }); + await esDeleteAllIndices([ALERT_ACTION_INDEX, ...dataForgeIndices]); + await cleanup({ client: esClient, config: dataForgeConfig, logger }); + await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); + }); + + describe('Rule creation', () => { + it('creates rule successfully', async () => { + actionId = await alertingApi.createIndexConnector({ + roleAuthc, + name: 'Index Connector: Threshold API test', + indexName: ALERT_ACTION_INDEX, + }); + + const createdRule = await alertingApi.createRule({ + roleAuthc, + tags: ['observability'], + consumer: expectedConsumer, + name: 'Threshold rule', + ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, + params: { + criteria: [ + { + comparator: COMPARATORS.GREATER_THAN, + threshold: [0], + timeSize: 1, + timeUnit: 'm', + metrics: [ + { name: 'A', field: 'runtimeHostName', aggType: Aggregators.CARDINALITY }, + ], + }, + ], + alertOnNoData: true, + alertOnGroupDisappear: true, + searchConfiguration: { + query: { + query: '', + language: 'kuery', + }, + index: DATA_VIEW_ID, + }, + }, + actions: [ + { + group: FIRED_ACTIONS_ID, + id: actionId, + params: { + documents: [ + { + ruleType: '{{rule.type}}', + alertDetailsUrl: '{{context.alertDetailsUrl}}', + reason: '{{context.reason}}', + value: '{{context.value}}', + viewInAppUrl: '{{context.viewInAppUrl}}', + }, + ], + }, + frequency: { + notify_when: 'onActionGroupChange', + throttle: null, + summary: false, + }, + }, + ], + }); + ruleId = createdRule.id; + expect(ruleId).not.to.be(undefined); + }); + + it('should be active', async () => { + const executionStatus = await alertingApi.waitForRuleStatus({ + roleAuthc, + ruleId, + expectedStatus: 'active', + }); + expect(executionStatus).to.be('active'); + }); + + it('should find the created rule with correct information about the consumer', async () => { + const match = await alertingApi.findInRules(roleAuthc, ruleId); + expect(match).not.to.be(undefined); + expect(match.consumer).to.be(expectedConsumer); + }); + + it('should set correct information in the alert document', async () => { + const resp = await alertingApi.waitForAlertInIndex({ + indexName: CUSTOM_THRESHOLD_RULE_ALERT_INDEX, + ruleId, + }); + alertId = (resp.hits.hits[0]._source as any)['kibana.alert.uuid']; + + expect(resp.hits.hits[0]._source).property( + 'kibana.alert.rule.category', + 'Custom threshold' + ); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', expectedConsumer); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.name', 'Threshold rule'); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.producer', 'observability'); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.revision', 0); + expect(resp.hits.hits[0]._source).property( + 'kibana.alert.rule.rule_type_id', + 'observability.rules.custom_threshold' + ); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.uuid', ruleId); + expect(resp.hits.hits[0]._source).property('kibana.space_ids').contain('default'); + expect(resp.hits.hits[0]._source) + .property('kibana.alert.rule.tags') + .contain('observability'); + expect(resp.hits.hits[0]._source).property( + 'kibana.alert.action_group', + 'custom_threshold.fired' + ); + expect(resp.hits.hits[0]._source).property('tags').contain('observability'); + expect(resp.hits.hits[0]._source).property('kibana.alert.instance.id', '*'); + expect(resp.hits.hits[0]._source).property('kibana.alert.workflow_status', 'open'); + expect(resp.hits.hits[0]._source).property('event.kind', 'signal'); + expect(resp.hits.hits[0]._source).property('event.action', 'open'); + expect(resp.hits.hits[0]._source).not.have.property('kibana.alert.group'); + expect(resp.hits.hits[0]._source).property('kibana.alert.evaluation.threshold').eql([0]); + expect(resp.hits.hits[0]._source) + .property('kibana.alert.rule.parameters') + .eql({ + criteria: [ + { + comparator: COMPARATORS.GREATER_THAN, + threshold: [0], + timeSize: 1, + timeUnit: 'm', + metrics: [{ name: 'A', field: 'runtimeHostName', aggType: 'cardinality' }], + }, + ], + alertOnNoData: true, + alertOnGroupDisappear: true, + searchConfiguration: { + index: 'data-view-id', + query: { query: '', language: 'kuery' }, + }, + }); + }); + + it('should set correct action variables', async () => { + const resp = await alertingApi.waitForDocumentInIndex({ + indexName: ALERT_ACTION_INDEX, + docCountTarget: 1, + }); + + const { protocol, hostname, port } = kbnTestConfig.getUrlPartsWithStrippedDefaultPort(); + + expect(resp.hits.hits[0]._source?.ruleType).eql('observability.rules.custom_threshold'); + expect(resp.hits.hits[0]._source?.alertDetailsUrl).eql( + `${protocol}://${hostname}${port ? `:${port}` : ''}/app/observability/alerts/${alertId}` + ); + + expect(resp.hits.hits[0]._source?.reason).eql( + `Cardinality of the runtimeHostName is 1, above the threshold of 0. (duration: 1 min, data view: ${DATA_VIEW_NAME})` + ); + expect(resp.hits.hits[0]._source?.value).eql('1'); + + const parsedViewInAppUrl = parseSearchParams( + new URL(resp.hits.hits[0]._source?.viewInAppUrl || '').search + ); + + expect(resp.hits.hits[0]._source?.viewInAppUrl).contain('LOGS_EXPLORER_LOCATOR'); + expect(omit(parsedViewInAppUrl.params, 'timeRange.from')).eql({ + dataset: DATA_VIEW_ID, + timeRange: { to: 'now' }, + query: { query: '', language: 'kuery' }, + filters: [], + }); + expect(parsedViewInAppUrl.params.timeRange.from).match(ISO_DATE_REGEX); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/index.ts index 45a8f2d8b1b40..ae9eb4a8d2419 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/index.ts @@ -12,6 +12,7 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) loadTestFile(require.resolve('./avg_pct_fired')); loadTestFile(require.resolve('./avg_pct_no_data')); loadTestFile(require.resolve('./avg_ticks_fired')); + loadTestFile(require.resolve('./cardinality_runtime_field_fired')); loadTestFile(require.resolve('./custom_eq_avg_bytes_fired')); loadTestFile(require.resolve('./documents_count_fired')); loadTestFile(require.resolve('./group_by_fired')); diff --git a/x-pack/test/api_integration/deployment_agnostic/services/data_view_api.ts b/x-pack/test/api_integration/deployment_agnostic/services/data_view_api.ts index 6b03bdf46b273..6f3c38b872f19 100644 --- a/x-pack/test/api_integration/deployment_agnostic/services/data_view_api.ts +++ b/x-pack/test/api_integration/deployment_agnostic/services/data_view_api.ts @@ -19,12 +19,14 @@ export function DataViewApiProvider({ getService }: DeploymentAgnosticFtrProvide name, title, spaceId, + data, }: { roleAuthc: RoleCredentials; id: string; name: string; title: string; spaceId?: string; + data?: Record; }) { const { body } = await supertestWithoutAuth .post(`${spaceId ? '/s/' + spaceId : ''}/api/content_management/rpc/create`) @@ -43,6 +45,7 @@ export function DataViewApiProvider({ getService }: DeploymentAgnosticFtrProvide typeMeta: '{}', runtimeFieldMap: '{}', name, + ...(data ? data : {}), }, options: { id }, version: 1,