mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[8.17] [Custom threshold] Fix using data view runtime fields during rule execution for the custom threshold rule (#209133) (#209280)
# Backport This will backport the following commits from `main` to `8.17`: - [[Custom threshold] Fix using data view runtime fields during rule execution for the custom threshold rule (#209133)](https://github.com/elastic/kibana/pull/209133) <!--- Backport version: 9.6.4 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Maryam Saeidi","email":"maryam.saeidi@elastic.co"},"sourceCommit":{"committedDate":"2025-02-03T11:09:29Z","message":"[Custom threshold] Fix using data view runtime fields during rule execution for the custom threshold rule (#209133)\n\nFixes #200772\r\n\r\n## 🐉 Summary\r\n\r\n\r\nThis PR fixes supporting data view runtime fields during rule execution\r\nfor the custom threshold rule.\r\n\r\n## 🧪 How to test\r\n\r\n1. Create a runtime field as shown below:\r\n\r\n |Runtime field| Preview|\r\n |---|---|\r\n\r\n||\r\n\r\n2. Make sure alerts are generated as expected both for regular and\r\nno-data alerts:\r\n\r\n\r\n\r\n### TODO\r\n- [x] Add an API integration test\r\n - [x] Test on MKI","sha":"8fe5738b24048972f801dc96e243d5a3d5d72eb3","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:fix","backport:prev-major","ci:project-deploy-observability","Team:obs-ux-management","v9.1.0"],"title":"[Custom threshold] Fix using data view runtime fields during rule execution for the custom threshold rule","number":209133,"url":"https://github.com/elastic/kibana/pull/209133","mergeCommit":{"message":"[Custom threshold] Fix using data view runtime fields during rule execution for the custom threshold rule (#209133)\n\nFixes #200772\r\n\r\n## 🐉 Summary\r\n\r\n\r\nThis PR fixes supporting data view runtime fields during rule execution\r\nfor the custom threshold rule.\r\n\r\n## 🧪 How to test\r\n\r\n1. Create a runtime field as shown below:\r\n\r\n |Runtime field| Preview|\r\n |---|---|\r\n\r\n||\r\n\r\n2. Make sure alerts are generated as expected both for regular and\r\nno-data alerts:\r\n\r\n\r\n\r\n### TODO\r\n- [x] Add an API integration test\r\n - [x] Test on MKI","sha":"8fe5738b24048972f801dc96e243d5a3d5d72eb3"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/209133","number":209133,"mergeCommit":{"message":"[Custom threshold] Fix using data view runtime fields during rule execution for the custom threshold rule (#209133)\n\nFixes #200772\r\n\r\n## 🐉 Summary\r\n\r\n\r\nThis PR fixes supporting data view runtime fields during rule execution\r\nfor the custom threshold rule.\r\n\r\n## 🧪 How to test\r\n\r\n1. Create a runtime field as shown below:\r\n\r\n |Runtime field| Preview|\r\n |---|---|\r\n\r\n||\r\n\r\n2. Make sure alerts are generated as expected both for regular and\r\nno-data alerts:\r\n\r\n\r\n\r\n### TODO\r\n- [x] Add an API integration test\r\n - [x] Test on MKI","sha":"8fe5738b24048972f801dc96e243d5a3d5d72eb3"}},{"url":"https://github.com/elastic/kibana/pull/209253","number":209253,"branch":"8.18","state":"OPEN"},{"url":"https://github.com/elastic/kibana/pull/209254","number":209254,"branch":"8.x","state":"OPEN"}]}] BACKPORT-->
This commit is contained in:
parent
6957fe270c
commit
732ea4f84a
12 changed files with 408 additions and 4 deletions
|
@ -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' },
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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<MissingGroupsRecord[]> => {
|
||||
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 } : {}),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
|
|
@ -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 <Params extends EvaluatedRuleParams = Evaluate
|
|||
logger: Logger,
|
||||
timeframe: { start: string; end: string },
|
||||
esQueryConfig: EsQueryConfig,
|
||||
runtimeMappings?: estypes.MappingRuntimeFields,
|
||||
lastPeriodEnd?: number,
|
||||
missingGroups: MissingGroupsRecord[] = []
|
||||
): Promise<Array<Record<string, Evaluation>>> => {
|
||||
|
@ -77,6 +79,7 @@ export const evaluateRule = async <Params extends EvaluatedRuleParams = Evaluate
|
|||
alertOnGroupDisappear,
|
||||
calculatedTimerange,
|
||||
logger,
|
||||
runtimeMappings,
|
||||
lastPeriodEnd
|
||||
);
|
||||
|
||||
|
@ -90,7 +93,8 @@ export const evaluateRule = async <Params extends EvaluatedRuleParams = Evaluate
|
|||
logger,
|
||||
calculatedTimerange,
|
||||
esQueryConfig,
|
||||
missingGroups
|
||||
missingGroups,
|
||||
runtimeMappings
|
||||
);
|
||||
|
||||
for (const missingGroup of verifiedMissingGroups) {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { SearchResponse, AggregationsAggregate } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { ElasticsearchClient } from '@kbn/core/server';
|
||||
import type { EsQueryConfig } from '@kbn/es-query';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
|
@ -110,6 +111,7 @@ export const getData = async (
|
|||
alertOnGroupDisappear: boolean,
|
||||
timeframe: { start: number; end: number },
|
||||
logger: Logger,
|
||||
runtimeMappings?: estypes.MappingRuntimeFields,
|
||||
lastPeriodEnd?: number,
|
||||
previousResults: GetDataResponse = {},
|
||||
afterKey?: Record<string, string>
|
||||
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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<string, unknown>
|
||||
*
|
||||
* - `isPopulatedObject({...}, ['attribute'])`
|
||||
* Limits type to Record<'attribute', unknown>
|
||||
*
|
||||
* - `isPopulatedObject<keyof MyInterface>({...})`
|
||||
* 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 = <U extends string = string, T extends unknown = unknown>(
|
||||
arg: unknown,
|
||||
requiredAttributes: U[] = []
|
||||
): arg is Record<U, T> => {
|
||||
return (
|
||||
typeof arg === 'object' &&
|
||||
arg !== null &&
|
||||
Object.keys(arg).length > 0 &&
|
||||
(requiredAttributes.length === 0 || requiredAttributes.every((d) => Object.hasOwn(arg, d)))
|
||||
);
|
||||
};
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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<string, string>,
|
||||
|
@ -211,6 +214,7 @@ export const getElasticsearchMetricQuery = (
|
|||
return {
|
||||
track_total_hits: true,
|
||||
query,
|
||||
...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}),
|
||||
size: 0,
|
||||
aggs,
|
||||
};
|
||||
|
|
|
@ -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<ActionDocument>({
|
||||
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<LogsExplorerLocatorParsedParams>(
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
|
|
|
@ -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<string, string>;
|
||||
}) {
|
||||
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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue