diff --git a/x-pack/plugins/observability/common/threshold_rule/types.ts b/x-pack/plugins/observability/common/threshold_rule/types.ts index 0762fef113ea..9cf5f48fae1d 100644 --- a/x-pack/plugins/observability/common/threshold_rule/types.ts +++ b/x-pack/plugins/observability/common/threshold_rule/types.ts @@ -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 { diff --git a/x-pack/plugins/observability/public/components/threshold/components/alert_flyout.tsx b/x-pack/plugins/observability/public/components/threshold/components/alert_flyout.tsx index cc84ccd5081b..e0d24d58eb0d 100644 --- a/x-pack/plugins/observability/public/components/threshold/components/alert_flyout.tsx +++ b/x-pack/plugins/observability/public/components/threshold/components/alert_flyout.tsx @@ -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, diff --git a/x-pack/plugins/observability/server/lib/rules/threshold/threshold_executor.ts b/x-pack/plugins/observability/server/lib/rules/threshold/threshold_executor.ts index f7387a5764ce..6fd08c0a15b8 100644 --- a/x-pack/plugins/observability/server/lib/rules/threshold/threshold_executor.ts +++ b/x-pack/plugins/observability/server/lib/rules/threshold/threshold_executor.ts @@ -71,8 +71,8 @@ export type MetricThresholdAlertContext = { value?: Record | 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', }), diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index f77542dc0958..851bc653d625 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -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' }, ])}`, diff --git a/x-pack/test/alerting_api_integration/observability/helpers/alerting_api_helper.ts b/x-pack/test/alerting_api_integration/observability/helpers/alerting_api_helper.ts index d89908b9bef7..a50e1b4e85c1 100644 --- a/x-pack/test/alerting_api_integration/observability/helpers/alerting_api_helper.ts +++ b/x-pack/test/alerting_api_integration/observability/helpers/alerting_api_helper.ts @@ -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({ +export async function createRule({ supertest, name, ruleTypeId, params, actions = [], + tags = [], schedule, + consumer, }: { supertest: SuperTest; - 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, diff --git a/x-pack/test/alerting_api_integration/observability/helpers/data_view.ts b/x-pack/test/alerting_api_integration/observability/helpers/data_view.ts new file mode 100644 index 000000000000..e8a03df2d071 --- /dev/null +++ b/x-pack/test/alerting_api_integration/observability/helpers/data_view.ts @@ -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; + 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; + 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; +}; diff --git a/x-pack/test/alerting_api_integration/observability/helpers/syntrace.ts b/x-pack/test/alerting_api_integration/observability/helpers/syntrace.ts new file mode 100644 index 000000000000..259924e80d64 --- /dev/null +++ b/x-pack/test/alerting_api_integration/observability/helpers/syntrace.ts @@ -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); +} diff --git a/x-pack/test/alerting_api_integration/observability/index.ts b/x-pack/test/alerting_api_integration/observability/index.ts index 675119da9192..ad137560c903 100644 --- a/x-pack/test/alerting_api_integration/observability/index.ts +++ b/x-pack/test/alerting_api_integration/observability/index.ts @@ -9,5 +9,6 @@ export default function ({ loadTestFile }: any) { describe('MetricsUI Endpoints', () => { loadTestFile(require.resolve('./metric_threshold_rule')); + loadTestFile(require.resolve('./threshold_rule')); }); } diff --git a/x-pack/test/alerting_api_integration/observability/metric_threshold_rule.ts b/x-pack/test/alerting_api_integration/observability/metric_threshold_rule.ts index d99437eae68e..2b727820ade7 100644 --- a/x-pack/test/alerting_api_integration/observability/metric_threshold_rule.ts +++ b/x-pack/test/alerting_api_integration/observability/metric_threshold_rule.ts @@ -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: [ diff --git a/x-pack/test/alerting_api_integration/observability/threshold_rule.ts b/x-pack/test/alerting_api_integration/observability/threshold_rule.ts new file mode 100644 index 000000000000..ab1907f95174 --- /dev/null +++ b/x-pack/test/alerting_api_integration/observability/threshold_rule.ts @@ -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' } }, + }); + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/check_registered_rule_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/check_registered_rule_types.ts index cb74808e3d63..40197f1e1878 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/check_registered_rule_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/check_registered_rule_types.ts @@ -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',