[AO] Add integration test for the Threshold Rule (#160633)

## Summary
It fixes #160510 by:
- Adding integration tests for the Threshold Rule.
- Update the consumer value. 
- Update the action group.
This commit is contained in:
Faisal Kanout 2023-06-30 17:11:58 +03:00 committed by GitHub
parent 73d1ed4ef8
commit 2b45cf628f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 439 additions and 15 deletions

View file

@ -8,9 +8,9 @@
import * as rt from 'io-ts';
import { ML_ANOMALY_THRESHOLD } from '@kbn/ml-anomaly-utils/anomaly_threshold';
import { values } from 'lodash';
import { SerializedSearchSourceFields } from '@kbn/data-plugin/common';
import { Color } from './color_palette';
import { metricsExplorerMetricRT } from './metrics_explorer';
import { TimeUnitChar } from '../utils/formatters/duration';
import { SNAPSHOT_CUSTOM_AGGREGATIONS } from './constants';
@ -201,13 +201,14 @@ export interface MetricAnomalyParams {
// Types for the executor
export interface MetricThresholdParams {
export interface ThresholdParams {
criteria: MetricExpressionParams[];
filterQuery?: string;
filterQueryText?: string;
sourceId?: string;
alertOnNoData?: boolean;
alertOnGroupDisappear?: boolean;
searchConfiguration: SerializedSearchSourceFields;
}
interface BaseMetricExpressionParams {

View file

@ -28,7 +28,7 @@ export function AlertFlyout(props: Props) {
() =>
triggersActionsUI &&
triggersActionsUI.getAddRuleFlyout({
consumer: 'infrastructure',
consumer: 'alerts',
onClose: onCloseFlyout,
canChangeTrigger: false,
ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,

View file

@ -71,8 +71,8 @@ export type MetricThresholdAlertContext = {
value?: Record<string, unknown> | null;
};
export const FIRED_ACTIONS_ID = 'metrics.threshold.fired';
export const NO_DATA_ACTIONS_ID = 'metrics.threshold.nodata';
export const FIRED_ACTIONS_ID = 'threshold.fired';
export const NO_DATA_ACTIONS_ID = 'threshold.nodata';
type MetricThresholdActionGroup =
| typeof FIRED_ACTIONS_ID
@ -408,14 +408,14 @@ export const createMetricThresholdExecutor = ({
};
export const FIRED_ACTIONS = {
id: 'metrics.threshold.fired',
id: 'threshold.fired',
name: i18n.translate('xpack.observability.threshold.rule.alerting.threshold.fired', {
defaultMessage: 'Alert',
}),
};
export const NO_DATA_ACTIONS = {
id: 'metrics.threshold.nodata',
id: 'threshold.nodata',
name: i18n.translate('xpack.observability.threshold.rule.alerting.threshold.nodata', {
defaultMessage: 'No Data',
}),

View file

@ -185,6 +185,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
'--xpack.alerting.healthCheck.interval="1s"',
'--xpack.alerting.rules.minimumScheduleInterval.value="1s"',
'--xpack.alerting.rules.run.alerts.max=20',
'--xpack.observability.unsafe.thresholdRule.enabled=true',
`--xpack.alerting.rules.run.actions.connectorTypeOverrides=${JSON.stringify([
{ id: 'test.capped', max: '1' },
])}`,

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import { InfraRuleType, InfraRuleTypeParams } from '@kbn/infra-plugin/common/alerting/metrics';
import { MetricThresholdParams } from '@kbn/infra-plugin/common/alerting/metrics';
import { ThresholdParams } from '@kbn/observability-plugin/common/threshold_rule/types';
import type { SuperTest, Test } from 'supertest';
export async function createIndexConnector({
@ -31,31 +32,35 @@ export async function createIndexConnector({
return body.id as string;
}
export async function createMetricThresholdRule<T extends InfraRuleType>({
export async function createRule({
supertest,
name,
ruleTypeId,
params,
actions = [],
tags = [],
schedule,
consumer,
}: {
supertest: SuperTest<Test>;
ruleTypeId: T;
ruleTypeId: string;
name: string;
params: InfraRuleTypeParams[T];
params: MetricThresholdParams | ThresholdParams;
actions?: any[];
tags?: any[];
schedule?: { interval: string };
consumer: string;
}) {
const { body } = await supertest
.post(`/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send({
params,
consumer: 'infrastructure',
consumer,
schedule: schedule || {
interval: '5m',
},
tags: ['infrastructure'],
tags,
name,
rule_type_id: ruleTypeId,
actions,

View file

@ -0,0 +1,59 @@
/*
* 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 { SuperTest, Test } from 'supertest';
export const createDataView = async ({
supertest,
id,
name,
title,
}: {
supertest: SuperTest<Test>;
id: string;
name: string;
title: string;
}) => {
const { body } = await supertest
.post(`/api/content_management/rpc/create`)
.set('kbn-xsrf', 'foo')
.send({
contentTypeId: 'index-pattern',
data: {
fieldAttrs: '{}',
title,
timeFieldName: '@timestamp',
sourceFilters: '[]',
fields: '[]',
fieldFormatMap: '{}',
typeMeta: '{}',
runtimeFieldMap: '{}',
name,
},
options: { id },
version: 1,
});
return body;
};
export const deleteDataView = async ({
supertest,
id,
}: {
supertest: SuperTest<Test>;
id: string;
}) => {
const { body } = await supertest
.delete(`/api/content_management/rpc/create`)
.set('kbn-xsrf', 'foo')
.send({
contentTypeId: 'index-pattern',
id,
options: { force: true },
version: 1,
});
return body;
};

View file

@ -0,0 +1,159 @@
/*
* 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 { Client } from '@elastic/elasticsearch';
import {
ApmSynthtraceEsClient,
ApmSynthtraceKibanaClient,
createLogger,
LogLevel,
} from '@kbn/apm-synthtrace';
import { apm, timerange } from '@kbn/apm-synthtrace-client';
export const getSyntraceClient = async ({
kibanaUrl,
esClient,
}: {
kibanaUrl: string;
esClient: Client;
}) => {
const kibanaClient = new ApmSynthtraceKibanaClient({
logger: createLogger(LogLevel.info),
target: kibanaUrl,
});
const packageVersion = await kibanaClient.fetchLatestApmPackageVersion();
return new ApmSynthtraceEsClient({
client: esClient,
logger: createLogger(LogLevel.info),
refreshAfterIndex: true,
version: packageVersion,
});
};
export const dataConfig = {
rate: 10,
transaction: {
name: 'GET /data',
duration: 1000,
},
service: {
name: 'lambda-python-dev-hello',
version: '$LATEST',
runtime: {
name: 'AWS_Lambda_python3.8',
version: '3.8.11',
},
framework: 'AWS Lambda',
agent: {
name: 'python',
version: '6.6.0',
},
},
containerOs: 'linux',
serverless: {
firstFunctionName: 'my-function-1',
secondFunctionName: 'my-function-2',
faasTriggerType: 'other',
},
cloud: {
provider: 'aws',
availabilityZone: 'us-central1-c',
region: 'us-east-1',
machineType: 'e2-standard-4',
projectName: 'elastic-observability',
serviceName: 'lambda',
},
};
export async function generateData({
synthtraceEsClient,
start,
end,
}: {
synthtraceEsClient: ApmSynthtraceEsClient;
start: number;
end: number;
}) {
const { rate, service, containerOs, serverless, cloud, transaction } = dataConfig;
const {
provider,
availabilityZone,
region,
machineType,
projectName,
serviceName: cloudServiceName,
} = cloud;
const { faasTriggerType, firstFunctionName, secondFunctionName } = serverless;
const { version, runtime, framework, agent, name: serviceName } = service;
const { name: serviceRunTimeName, version: serviceRunTimeVersion } = runtime;
const { name: agentName, version: agentVersion } = agent;
const instance = apm
.service({ name: serviceName, environment: 'production', agentName })
.instance('instance-a');
const traceEvents = [
timerange(start, end)
.interval('30s')
.rate(rate)
.generator((timestamp) =>
instance
.containerId('instance-a')
.transaction({ transactionName: transaction.name })
.timestamp(timestamp)
.defaults({
'cloud.provider': provider,
'cloud.project.name': projectName,
'cloud.service.name': cloudServiceName,
'cloud.availability_zone': availabilityZone,
'cloud.machine.type': machineType,
'cloud.region': region,
'faas.id': `arn:aws:lambda:us-west-2:123456789012:function:${firstFunctionName}`,
'faas.trigger.type': faasTriggerType,
'host.os.platform': containerOs,
'kubernetes.pod.uid': '48f4c5a5-0625-4bea-9d94-77ee94a17e70',
'service.version': version,
'service.runtime.name': serviceRunTimeName,
'service.runtime.version': serviceRunTimeVersion,
'service.framework.name': framework,
'agent.version': agentVersion,
})
.duration(transaction.duration)
.success()
),
timerange(start, end)
.interval('30s')
.rate(rate)
.generator((timestamp) =>
instance
.transaction({ transactionName: transaction.name })
.timestamp(timestamp)
.defaults({
'cloud.provider': provider,
'cloud.project.name': projectName,
'cloud.service.name': cloudServiceName,
'cloud.availability_zone': availabilityZone,
'cloud.machine.type': machineType,
'cloud.region': region,
'faas.id': `arn:aws:lambda:us-west-2:123456789012:function:${secondFunctionName}`,
'faas.trigger.type': faasTriggerType,
'host.os.platform': containerOs,
'kubernetes.pod.uid': '48f4c5a5-0625-4bea-9d94-77ee94a17e70',
'service.version': version,
'service.runtime.name': serviceRunTimeName,
'service.runtime.version': serviceRunTimeVersion,
'service.framework.name': framework,
'agent.version': agentVersion,
})
.duration(transaction.duration)
.success()
),
];
await synthtraceEsClient.index(traceEvents);
}

View file

@ -9,5 +9,6 @@
export default function ({ loadTestFile }: any) {
describe('MetricsUI Endpoints', () => {
loadTestFile(require.resolve('./metric_threshold_rule'));
loadTestFile(require.resolve('./threshold_rule'));
});
}

View file

@ -15,7 +15,7 @@ import {
waitForRuleStatus,
} from './helpers/alerting_wait_for_helpers';
import { FtrProviderContext } from '../common/ftr_provider_context';
import { createIndexConnector, createMetricThresholdRule } from './helpers/alerting_api_helper';
import { createIndexConnector, createRule } from './helpers/alerting_api_helper';
// eslint-disable-next-line import/no-default-export
export default function ({ getService }: FtrProviderContext) {
@ -42,9 +42,11 @@ export default function ({ getService }: FtrProviderContext) {
name: 'Index Connector: Metric threshold API test',
indexName: ALERT_ACTION_INDEX,
});
const createdRule = await createMetricThresholdRule({
const createdRule = await createRule({
supertest,
ruleTypeId: InfraRuleType.MetricThreshold,
consumer: 'infrastructure',
tags: ['infrastructure'],
name: 'Metric threshold rule',
params: {
criteria: [

View file

@ -0,0 +1,195 @@
/*
* 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.
*/
/*
* 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 moment from 'moment';
import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import { format } from 'url';
import { Aggregators, Comparator } from '@kbn/observability-plugin/common/threshold_rule/types';
import { FIRED_ACTIONS_ID } from '@kbn/observability-plugin/server/lib/rules/threshold/threshold_executor';
import expect from '@kbn/expect';
import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants';
import { FtrProviderContext } from '../common/ftr_provider_context';
import { createIndexConnector, createRule } from './helpers/alerting_api_helper';
import { createDataView, deleteDataView } from './helpers/data_view';
import { getSyntraceClient, generateData } from './helpers/syntrace';
import { waitForAlertInIndex, waitForRuleStatus } from './helpers/alerting_wait_for_helpers';
// eslint-disable-next-line import/no-default-export
export default function ({ getService }: FtrProviderContext) {
const start = moment(Date.now()).subtract(10, 'minutes').valueOf();
const end = moment(Date.now()).valueOf();
const esClient = getService('es');
const config = getService('config');
const kibanaServerConfig = config.get('servers.kibana');
const kibanaUrl = format(kibanaServerConfig);
const supertest = getService('supertest');
describe('Threshold rule', () => {
const THRESHOLD_RULE_ALERT_INDEX = '.alerts-observability.threshold.alerts-default';
const ALERT_ACTION_INDEX = 'alert-action-threshold';
const DATA_VIEW_ID = 'data-view-id';
let synthtraceEsClient: ApmSynthtraceEsClient;
let actionId: string;
let ruleId: string;
before(async () => {
synthtraceEsClient = await getSyntraceClient({ esClient, kibanaUrl });
await generateData({ synthtraceEsClient, start, end });
await createDataView({
supertest,
name: 'test-data-view',
id: DATA_VIEW_ID,
title: 'traces-apm*,metrics-apm*,logs-apm*',
});
});
after(async () => {
await supertest.delete(`/api/alerting/rule/${ruleId}`).set('kbn-xsrf', 'foo');
await supertest.delete(`/api/actions/connector/${actionId}`).set('kbn-xsrf', 'foo');
await esClient.deleteByQuery({
index: THRESHOLD_RULE_ALERT_INDEX,
query: { term: { 'kibana.alert.rule.uuid': ruleId } },
});
await esClient.deleteByQuery({
index: '.kibana-event-log-*',
query: { term: { 'kibana.alert.rule.consumer': 'alerts' } },
});
await synthtraceEsClient.clean();
await deleteDataView({
supertest,
id: DATA_VIEW_ID,
});
});
describe('Rule creation', () => {
it('creates rule successfully', async () => {
actionId = await createIndexConnector({
supertest,
name: 'Index Connector: Threshold API test',
indexName: ALERT_ACTION_INDEX,
});
const createdRule = await createRule({
supertest,
tags: ['observability'],
consumer: 'alerts',
name: 'Threshold rule',
ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
params: {
criteria: [
{
aggType: Aggregators.CUSTOM,
comparator: Comparator.GT,
threshold: [7500000],
timeSize: 5,
timeUnit: 'm',
customMetrics: [
{ name: 'A', field: 'span.self_time.sum.us', aggType: Aggregators.AVERAGE },
],
},
],
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}}',
},
],
},
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 waitForRuleStatus({
id: ruleId,
expectedStatus: 'active',
supertest,
});
expect(executionStatus.status).to.be('active');
});
it('should set correct information in the alert document', async () => {
const resp = await waitForAlertInIndex({
esClient,
indexName: THRESHOLD_RULE_ALERT_INDEX,
ruleId,
});
expect(resp.hits.hits[0]._source).property(
'kibana.alert.rule.category',
'Threshold (Technical Preview)'
);
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'alerts');
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.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', '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)
.property('kibana.alert.rule.parameters')
.eql({
criteria: [
{
aggType: 'custom',
comparator: '>',
threshold: [7500000],
timeSize: 5,
timeUnit: 'm',
customMetrics: [{ name: 'A', field: 'span.self_time.sum.us', aggType: 'avg' }],
},
],
alertOnNoData: true,
alertOnGroupDisappear: true,
searchConfiguration: { index: 'data-view-id', query: { query: '', language: 'kuery' } },
});
});
});
});
}

View file

@ -63,6 +63,7 @@ export default function createRegisteredRuleTypeTests({ getService }: FtrProvide
'monitoring_alert_elasticsearch_version_mismatch',
'monitoring_ccr_read_exceptions',
'monitoring_shard_size',
'observability.rules.threshold',
'apm.transaction_duration',
'apm.anomaly',
'apm.error_rate',