[APM] Cleanup alerting api tests (#164438)

This PR cleans up and refactors the APM API tests for rules and
alerting.

- introduces some new helper methods like `deleteRuleById`
- removes dependency on index actions to test alerts (we can just use
the alert index)
- improve flaky tests and ensure that tests can be run in isolation and
in any order
This commit is contained in:
Søren Louv-Jansen 2023-08-24 02:22:26 +02:00 committed by GitHub
parent 950ac6ea25
commit d4402886a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1012 additions and 932 deletions

View file

@ -57,6 +57,20 @@ import { getAllGroupByFields } from '../../../../../common/rules/get_all_groupby
const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.ErrorCount];
export const errorCountActionVariables = [
apmActionVariables.alertDetailsUrl,
apmActionVariables.environment,
apmActionVariables.errorGroupingKey,
apmActionVariables.errorGroupingName,
apmActionVariables.interval,
apmActionVariables.reason,
apmActionVariables.serviceName,
apmActionVariables.threshold,
apmActionVariables.transactionName,
apmActionVariables.triggerValue,
apmActionVariables.viewInAppUrl,
];
export function registerErrorCountRuleType({
alerting,
alertsLocator,
@ -78,19 +92,7 @@ export function registerErrorCountRuleType({
defaultActionGroupId: ruleTypeConfig.defaultActionGroupId,
validate: { params: errorCountParamsSchema },
actionVariables: {
context: [
apmActionVariables.alertDetailsUrl,
apmActionVariables.environment,
apmActionVariables.interval,
apmActionVariables.reason,
apmActionVariables.serviceName,
apmActionVariables.transactionName,
apmActionVariables.errorGroupingKey,
apmActionVariables.errorGroupingName,
apmActionVariables.threshold,
apmActionVariables.triggerValue,
apmActionVariables.viewInAppUrl,
],
context: errorCountActionVariables,
},
producer: APM_SERVER_FEATURE_ID,
minimumLicenseRequired: 'basic',

View file

@ -71,6 +71,19 @@ import { getAllGroupByFields } from '../../../../../common/rules/get_all_groupby
const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.TransactionDuration];
export const transactionDurationActionVariables = [
apmActionVariables.alertDetailsUrl,
apmActionVariables.environment,
apmActionVariables.interval,
apmActionVariables.reason,
apmActionVariables.serviceName,
apmActionVariables.threshold,
apmActionVariables.transactionName,
apmActionVariables.transactionType,
apmActionVariables.triggerValue,
apmActionVariables.viewInAppUrl,
];
export function registerTransactionDurationRuleType({
alerting,
ruleDataClient,
@ -91,18 +104,7 @@ export function registerTransactionDurationRuleType({
defaultActionGroupId: ruleTypeConfig.defaultActionGroupId,
validate: { params: transactionDurationParamsSchema },
actionVariables: {
context: [
apmActionVariables.alertDetailsUrl,
apmActionVariables.environment,
apmActionVariables.interval,
apmActionVariables.reason,
apmActionVariables.serviceName,
apmActionVariables.transactionType,
apmActionVariables.transactionName,
apmActionVariables.threshold,
apmActionVariables.triggerValue,
apmActionVariables.viewInAppUrl,
],
context: transactionDurationActionVariables,
},
producer: APM_SERVER_FEATURE_ID,
minimumLicenseRequired: 'basic',

View file

@ -66,6 +66,19 @@ import { getAllGroupByFields } from '../../../../../common/rules/get_all_groupby
const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.TransactionErrorRate];
export const transactionErrorRateActionVariables = [
apmActionVariables.alertDetailsUrl,
apmActionVariables.environment,
apmActionVariables.interval,
apmActionVariables.reason,
apmActionVariables.serviceName,
apmActionVariables.threshold,
apmActionVariables.transactionName,
apmActionVariables.transactionType,
apmActionVariables.triggerValue,
apmActionVariables.viewInAppUrl,
];
export function registerTransactionErrorRateRuleType({
alerting,
alertsLocator,
@ -88,19 +101,7 @@ export function registerTransactionErrorRateRuleType({
defaultActionGroupId: ruleTypeConfig.defaultActionGroupId,
validate: { params: transactionErrorRateParamsSchema },
actionVariables: {
context: [
apmActionVariables.alertDetailsUrl,
apmActionVariables.environment,
apmActionVariables.interval,
apmActionVariables.reason,
apmActionVariables.serviceName,
apmActionVariables.transactionName,
apmActionVariables.threshold,
apmActionVariables.transactionType,
apmActionVariables.transactionName,
apmActionVariables.triggerValue,
apmActionVariables.viewInAppUrl,
],
context: transactionErrorRateActionVariables,
},
producer: APM_SERVER_FEATURE_ID,
minimumLicenseRequired: 'basic',

View file

@ -1,121 +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 type { SuperTest, Test } from 'supertest';
import { ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types';
import { ApmRuleParamsType } from '@kbn/apm-plugin/common/rules/schema';
import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type';
import { RollupInterval } from '@kbn/apm-plugin/common/rollup';
import { ApmApiClient } from '../../common/config';
export async function createIndexConnector({
supertest,
name,
indexName,
}: {
supertest: SuperTest<Test>;
name: string;
indexName: string;
}) {
const { body } = await supertest
.post(`/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.send({
name,
config: {
index: indexName,
refresh: true,
},
connector_type_id: '.index',
});
return body.id as string;
}
export async function createApmRule<T extends ApmRuleType>({
supertest,
name,
ruleTypeId,
params,
actions = [],
}: {
supertest: SuperTest<Test>;
ruleTypeId: T;
name: string;
params: ApmRuleParamsType[T];
actions?: any[];
}) {
try {
const { body } = await supertest
.post(`/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send({
params,
consumer: 'apm',
schedule: {
interval: '1m',
},
tags: ['apm'],
name,
rule_type_id: ruleTypeId,
actions,
});
return body;
} catch (error: any) {
throw new Error(`[Rule] Creating a rule failed: ${error}`);
}
}
function getTimerange() {
return {
start: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
end: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
};
}
export async function fetchServiceInventoryAlertCounts(apmApiClient: ApmApiClient) {
const timerange = getTimerange();
const serviceInventoryResponse = await apmApiClient.readUser({
endpoint: 'GET /internal/apm/services',
params: {
query: {
...timerange,
environment: 'ENVIRONMENT_ALL',
kuery: '',
probability: 1,
documentType: ApmDocumentType.ServiceTransactionMetric,
rollupInterval: RollupInterval.SixtyMinutes,
},
},
});
return serviceInventoryResponse.body.items.reduce<Record<string, number>>((acc, item) => {
return { ...acc, [item.serviceName]: item.alertsCount ?? 0 };
}, {});
}
export async function fetchServiceTabAlertCount({
apmApiClient,
serviceName,
}: {
apmApiClient: ApmApiClient;
serviceName: string;
}) {
const timerange = getTimerange();
const alertsCountReponse = await apmApiClient.readUser({
endpoint: 'GET /internal/apm/services/{serviceName}/alerts_count',
params: {
path: {
serviceName,
},
query: {
...timerange,
environment: 'ENVIRONMENT_ALL',
},
},
});
return alertsCountReponse.body.alertsCount;
}

View file

@ -12,8 +12,8 @@ import { range } from 'lodash';
import { ML_ANOMALY_SEVERITY } from '@kbn/ml-anomaly-utils/anomaly_severity';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { createAndRunApmMlJobs } from '../../common/utils/create_and_run_apm_ml_jobs';
import { createApmRule } from './alerting_api_helper';
import { waitForRuleStatus } from './wait_for_rule_status';
import { createApmRule, deleteRuleById } from './helpers/alerting_api_helper';
import { waitForRuleStatus } from './helpers/wait_for_rule_status';
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
@ -37,7 +37,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const NORMAL_DURATION = 100;
const NORMAL_RATE = 1;
let ruleId: string | undefined;
let ruleId: string;
before(async () => {
const serviceA = apm
@ -69,7 +69,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
after(async () => {
await synthtraceEsClient.clean();
await supertest.delete(`/api/alerting/rule/${ruleId}`).set('kbn-xsrf', 'foo');
await deleteRuleById({ supertest, ruleId });
});
describe('with ml jobs', () => {
@ -98,12 +98,12 @@ export default function ApiTest({ getService }: FtrProviderContext) {
if (!ruleId) {
expect(ruleId).to.not.eql(undefined);
} else {
const executionStatus = await waitForRuleStatus({
id: ruleId,
const ruleStatus = await waitForRuleStatus({
ruleId,
expectedStatus: 'active',
supertest,
});
expect(executionStatus.status).to.be('active');
expect(ruleStatus).to.be('active');
}
});
});

View file

@ -5,57 +5,69 @@
* 2.0.
*/
import moment from 'moment';
import { ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types';
import { errorCountMessage } from '@kbn/apm-plugin/common/rules/default_action_message';
import { errorCountActionVariables } from '@kbn/apm-plugin/server/routes/alerts/rule_types/error_count/register_error_count_rule_type';
import { apm, timerange } from '@kbn/apm-synthtrace-client';
import { getErrorGroupingKey } from '@kbn/apm-synthtrace-client/src/lib/apm/instance';
import expect from '@kbn/expect';
import { omit } from 'lodash';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import {
createApmRule,
createIndexConnector,
deleteRuleById,
deleteAlertsByRuleId,
fetchServiceInventoryAlertCounts,
fetchServiceTabAlertCount,
} from './alerting_api_helper';
import {
waitForRuleStatus,
waitForDocumentInIndex,
waitForAlertInIndex,
} from './wait_for_rule_status';
ApmAlertFields,
createIndexConnector,
deleteActionConnector,
getIndexAction,
} from './helpers/alerting_api_helper';
import { cleanupAllState } from './helpers/cleanup_state';
import { waitForAlertsForRule } from './helpers/wait_for_alerts_for_rule';
import { waitForIndexConnectorResults } from './helpers/wait_for_index_connector_results';
import { waitForRuleStatus } from './helpers/wait_for_rule_status';
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const supertest = getService('supertest');
const es = getService('es');
const apmApiClient = getService('apmApiClient');
const esDeleteAllIndices = getService('esDeleteAllIndices');
const synthtraceEsClient = getService('synthtraceEsClient');
registry.when('error count threshold alert', { config: 'basic', archives: [] }, () => {
let ruleId1: string;
let ruleId2: string;
let alertId: string;
let startedAt: string;
let actionId1: string | undefined;
let actionId2: string | undefined;
const javaErrorMessage = 'a java error';
const phpErrorMessage = 'a php error';
const APM_ALERTS_INDEX = '.alerts-observability.apm.alerts-default';
const ALERT_ACTION_INDEX_NAME1 = 'alert-action-error-count1';
const ALERT_ACTION_INDEX_NAME2 = 'alert-action-error-count2';
const errorMessage = '[ResponseError] index_not_found_exception';
const errorGroupingKey = getErrorGroupingKey(errorMessage);
const ruleParams = {
environment: 'production',
threshold: 1,
windowSize: 1,
windowUnit: 'h',
groupBy: [
'service.name',
'service.environment',
'transaction.name',
'error.grouping_key',
'error.grouping_name',
],
};
before(async () => {
cleanupAllState({ es, supertest });
const opbeansJava = apm
.service({ name: 'opbeans-java', environment: 'production', agentName: 'java' })
.instance('instance');
const opbeansPhp = apm
.service({ name: 'opbeans-php', environment: 'production', agentName: 'php' })
.instance('instance');
const opbeansNode = apm
.service({ name: 'opbeans-node', environment: 'production', agentName: 'node' })
.instance('instance');
const events = timerange('now-15m', 'now')
.ratePerMinute(1)
.generator((timestamp) => {
@ -65,7 +77,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
.timestamp(timestamp)
.duration(100)
.failure()
.errors(opbeansJava.error({ message: errorMessage }).timestamp(timestamp + 50)),
.errors(opbeansJava.error({ message: javaErrorMessage }).timestamp(timestamp + 50)),
opbeansNode
.transaction({ transactionName: 'tx-node' })
.timestamp(timestamp)
@ -73,140 +86,166 @@ export default function ApiTest({ getService }: FtrProviderContext) {
.success(),
];
});
await synthtraceEsClient.index(events);
const phpEvents = timerange('now-15m', 'now')
.ratePerMinute(2)
.generator((timestamp) => {
return [
opbeansPhp
.transaction({ transactionName: 'tx-php' })
.timestamp(timestamp)
.duration(100)
.failure()
.errors(opbeansPhp.error({ message: phpErrorMessage }).timestamp(timestamp + 50)),
];
});
await Promise.all([synthtraceEsClient.index(events), synthtraceEsClient.index(phpEvents)]);
});
after(async () => {
await synthtraceEsClient.clean();
await supertest.delete(`/api/alerting/rule/${ruleId1}`).set('kbn-xsrf', 'foo');
await supertest.delete(`/api/actions/connector/${actionId1}`).set('kbn-xsrf', 'foo');
await supertest.delete(`/api/alerting/rule/${ruleId2}`).set('kbn-xsrf', 'foo');
await supertest.delete(`/api/actions/connector/${actionId2}`).set('kbn-xsrf', 'foo');
await esDeleteAllIndices([ALERT_ACTION_INDEX_NAME1, ALERT_ACTION_INDEX_NAME2]);
await es.deleteByQuery({
index: APM_ALERTS_INDEX,
query: { term: { 'kibana.alert.rule.uuid': ruleId1 } },
});
await es.deleteByQuery({
index: APM_ALERTS_INDEX,
query: { term: { 'kibana.alert.rule.uuid': ruleId2 } },
});
await es.deleteByQuery({
index: '.kibana-event-log-*',
query: { term: { 'kibana.alert.rule.consumer': 'apm' } },
});
});
describe('create alert without filter query', () => {
describe('create rule without kql filter', () => {
let ruleId: string;
let alerts: ApmAlertFields[];
let actionId: string;
before(async () => {
actionId1 = await createIndexConnector({
supertest,
name: 'Error count without filter query',
indexName: ALERT_ACTION_INDEX_NAME1,
actionId = await createIndexConnector({ supertest, name: 'Transation error count' });
const indexAction = getIndexAction({
actionId,
actionVariables: errorCountActionVariables,
});
const createdRule = await createApmRule({
supertest,
ruleTypeId: ApmRuleType.ErrorCount,
name: 'Apm error count without filter query',
name: 'Apm error count without kql query',
params: {
environment: 'production',
threshold: 1,
windowSize: 1,
windowUnit: 'h',
kqlFilter: '',
groupBy: [
'service.name',
'service.environment',
'transaction.name',
'error.grouping_key',
'error.grouping_name',
],
...ruleParams,
},
actions: [
{
group: 'threshold_met',
id: actionId1,
params: {
documents: [
{
message: `${errorCountMessage}
- Transaction name: {{context.transactionName}}
- Error grouping key: {{context.errorGroupingKey}}
- Error grouping name: {{context.errorGroupingName}}`,
},
],
},
frequency: {
notify_when: 'onActionGroupChange',
throttle: null,
summary: false,
},
},
],
actions: [indexAction],
});
expect(createdRule.id).to.not.eql(undefined);
ruleId1 = createdRule.id;
ruleId = createdRule.id;
alerts = await waitForAlertsForRule({ es, ruleId, minimumAlertCount: 2 });
});
after(async () => {
await deleteActionConnector({ supertest, es, actionId });
await deleteRuleById({ supertest, ruleId });
await deleteAlertsByRuleId({ es, ruleId });
});
it('checks if rule is active', async () => {
const executionStatus = await waitForRuleStatus({
id: ruleId1,
const ruleStatus = await waitForRuleStatus({
ruleId,
expectedStatus: 'active',
supertest,
});
expect(executionStatus.status).to.be('active');
expect(ruleStatus).to.be('active');
});
describe('action variables', () => {
let results: Array<Record<string, string>>;
before(async () => {
results = await waitForIndexConnectorResults({ es, minCount: 2 });
});
it('produces a index action document for each service', async () => {
expect(results.map(({ serviceName }) => serviceName).sort()).to.eql([
'opbeans-java',
'opbeans-php',
]);
});
it('has the right keys', async () => {
const phpEntry = results.find((result) => result.serviceName === 'opbeans-php')!;
expect(Object.keys(phpEntry).sort()).to.eql([
'alertDetailsUrl',
'environment',
'errorGroupingKey',
'errorGroupingName',
'interval',
'reason',
'serviceName',
'threshold',
'transactionName',
'triggerValue',
'viewInAppUrl',
]);
});
it('has the right values', () => {
const phpEntry = results.find((result) => result.serviceName === 'opbeans-php')!;
expect(omit(phpEntry, 'alertDetailsUrl')).to.eql({
environment: 'production',
interval: '1 hr',
reason:
'Error count is 30 in the last 1 hr for service: opbeans-php, env: production, name: tx-php, error key: c85df8159a74b47b461d6ddaa6ba7da38cfc3e74019aef66257d10df74adeb99, error name: a php error. Alert when > 1.',
serviceName: 'opbeans-php',
transactionName: 'tx-php',
errorGroupingKey: 'c85df8159a74b47b461d6ddaa6ba7da38cfc3e74019aef66257d10df74adeb99',
errorGroupingName: 'a php error',
threshold: '1',
triggerValue: '30',
viewInAppUrl:
'http://mockedPublicBaseUrl/app/apm/services/opbeans-php/errors?environment=production',
});
});
});
it('produces one alert for each of the opbeans-java and opbeans-php', async () => {
const alertReasons = [alerts[0]['kibana.alert.reason'], alerts[1]['kibana.alert.reason']];
expect(alertReasons).to.eql([
'Error count is 30 in the last 1 hr for service: opbeans-php, env: production, name: tx-php, error key: c85df8159a74b47b461d6ddaa6ba7da38cfc3e74019aef66257d10df74adeb99, error name: a php error. Alert when > 1.',
'Error count is 15 in the last 1 hr for service: opbeans-java, env: production, name: tx-java, error key: b6a4ac83620b34ae44dd98a13e144782f88698f827af7edb10690c5e6e7d8597, error name: a java error. Alert when > 1.',
]);
});
it('indexes alert document with all group-by fields', async () => {
const resp = await waitForAlertInIndex({
es,
indexName: APM_ALERTS_INDEX,
ruleId: ruleId1,
});
alertId = (resp.hits.hits[0]._source as any)['kibana.alert.uuid'];
startedAt = (resp.hits.hits[0]._source as any)['kibana.alert.start'];
expect(resp.hits.hits[0]._source).property('service.name', 'opbeans-java');
expect(resp.hits.hits[0]._source).property('service.environment', 'production');
expect(resp.hits.hits[0]._source).property('transaction.name', 'tx-java');
expect(resp.hits.hits[0]._source).property('error.grouping_key', errorGroupingKey);
expect(resp.hits.hits[0]._source).property('error.grouping_name', errorMessage);
});
it('returns correct message', async () => {
const rangeFrom = moment(startedAt).subtract('5', 'minute').toISOString();
const resp = await waitForDocumentInIndex<{ message: string }>({
es,
indexName: ALERT_ACTION_INDEX_NAME1,
const alertDetails = [alerts[0], alerts[1]].map((alert) => {
return {
serviceName: alert!['service.name'],
environment: alert!['service.environment'],
transactionName: alert!['transaction.name'],
errorGroupingKey: alert!['error.grouping_key'],
errorGroupingName: alert!['error.grouping_name'],
};
});
expect(resp.hits.hits[0]._source?.message).eql(
`Error count is 15 in the last 1 hr for service: opbeans-java, env: production, name: tx-java, error key: ${errorGroupingKey}, error name: ${errorMessage}. Alert when > 1.
Apm error count without filter query is active with the following conditions:
- Service name: opbeans-java
- Environment: production
- Error count: 15 errors over the last 1 hr
- Threshold: 1
[View alert details](http://mockedpublicbaseurl/app/observability/alerts?_a=(kuery:%27kibana.alert.uuid:%20%22${alertId}%22%27%2CrangeFrom:%27${rangeFrom}%27%2CrangeTo:now%2Cstatus:all))
- Transaction name: tx-java
- Error grouping key: ${errorGroupingKey}
- Error grouping name: ${errorMessage}`
);
expect(alertDetails).to.eql([
{
serviceName: 'opbeans-php',
environment: 'production',
transactionName: 'tx-php',
errorGroupingKey: getErrorGroupingKey(phpErrorMessage),
errorGroupingName: phpErrorMessage,
},
{
serviceName: 'opbeans-java',
environment: 'production',
transactionName: 'tx-java',
errorGroupingKey: getErrorGroupingKey(javaErrorMessage),
errorGroupingName: javaErrorMessage,
},
]);
});
it('shows the correct alert count for each service on service inventory', async () => {
it('shows the a single alert for opbeans-java and opbeans-php on the service inventory', async () => {
const serviceInventoryAlertCounts = await fetchServiceInventoryAlertCounts(apmApiClient);
expect(serviceInventoryAlertCounts).to.eql({
'opbeans-node': 0,
'opbeans-java': 1,
'opbeans-php': 1,
});
});
it('shows the correct alert count in opbeans-java service', async () => {
it('shows 1 alert for opbeans-java in the tab', async () => {
const serviceTabAlertCount = await fetchServiceTabAlertCount({
apmApiClient,
serviceName: 'opbeans-java',
@ -214,7 +253,7 @@ Apm error count without filter query is active with the following conditions:
expect(serviceTabAlertCount).to.be(1);
});
it('shows the correct alert count in opbeans-node service', async () => {
it('shows no alerts for opbeans-node in the tab', async () => {
const serviceTabAlertCount = await fetchServiceTabAlertCount({
apmApiClient,
serviceName: 'opbeans-node',
@ -223,131 +262,34 @@ Apm error count without filter query is active with the following conditions:
});
});
describe('create alert with filter query', () => {
describe('create rule with kql filter for opbeans-php', () => {
let ruleId: string;
before(async () => {
actionId2 = await createIndexConnector({
supertest,
name: 'Error count with filter query',
indexName: ALERT_ACTION_INDEX_NAME2,
});
const createdRule = await createApmRule({
supertest,
ruleTypeId: ApmRuleType.ErrorCount,
name: 'Apm error count with filter query',
name: 'Apm error count with kql query',
params: {
environment: 'ENVIRONMENT_ALL',
threshold: 1,
windowSize: 1,
windowUnit: 'h',
serviceName: undefined,
kqlFilter: 'service.name: opbeans-java and service.environment: production',
groupBy: [
'service.name',
'service.environment',
'transaction.name',
'error.grouping_key',
'error.grouping_name',
],
kqlFilter: 'service.name: opbeans-php',
...ruleParams,
},
actions: [
{
group: 'threshold_met',
id: actionId2,
params: {
documents: [
{
message: `${errorCountMessage}
- Transaction name: {{context.transactionName}}
- Error grouping key: {{context.errorGroupingKey}}
- Error grouping name: {{context.errorGroupingName}}`,
},
],
},
frequency: {
notify_when: 'onActionGroupChange',
throttle: null,
summary: false,
},
},
],
actions: [],
});
expect(createdRule.id).to.not.eql(undefined);
ruleId2 = createdRule.id;
ruleId = createdRule.id;
});
it('checks if rule is active', async () => {
const executionStatus = await waitForRuleStatus({
id: ruleId2,
expectedStatus: 'active',
supertest,
});
expect(executionStatus.status).to.be('active');
after(async () => {
await deleteRuleById({ supertest, ruleId });
await deleteAlertsByRuleId({ es, ruleId });
});
it('indexes alert document with all group-by fields', async () => {
const resp = await waitForAlertInIndex({
es,
indexName: APM_ALERTS_INDEX,
ruleId: ruleId2,
});
alertId = (resp.hits.hits[0]._source as any)['kibana.alert.uuid'];
startedAt = (resp.hits.hits[0]._source as any)['kibana.alert.start'];
expect(resp.hits.hits[0]._source).property('service.name', 'opbeans-java');
expect(resp.hits.hits[0]._source).property('service.environment', 'production');
expect(resp.hits.hits[0]._source).property('transaction.name', 'tx-java');
expect(resp.hits.hits[0]._source).property('error.grouping_key', errorGroupingKey);
expect(resp.hits.hits[0]._source).property('error.grouping_name', errorMessage);
});
it('returns correct message', async () => {
const rangeFrom = moment(startedAt).subtract('5', 'minute').toISOString();
const resp = await waitForDocumentInIndex<{ message: string }>({
es,
indexName: ALERT_ACTION_INDEX_NAME2,
});
expect(resp.hits.hits[0]._source?.message).eql(
`Error count is 15 in the last 1 hr for service: opbeans-java, env: production, name: tx-java, error key: ${errorGroupingKey}, error name: ${errorMessage}. Alert when > 1.
Apm error count with filter query is active with the following conditions:
- Service name: opbeans-java
- Environment: production
- Error count: 15 errors over the last 1 hr
- Threshold: 1
[View alert details](http://mockedpublicbaseurl/app/observability/alerts?_a=(kuery:%27kibana.alert.uuid:%20%22${alertId}%22%27%2CrangeFrom:%27${rangeFrom}%27%2CrangeTo:now%2Cstatus:all))
- Transaction name: tx-java
- Error grouping key: ${errorGroupingKey}
- Error grouping name: ${errorMessage}`
it('produces one alert for the opbeans-php service', async () => {
const alerts = await waitForAlertsForRule({ es, ruleId });
expect(alerts[0]['kibana.alert.reason']).to.be(
'Error count is 30 in the last 1 hr for service: opbeans-php, env: production, name: tx-php, error key: c85df8159a74b47b461d6ddaa6ba7da38cfc3e74019aef66257d10df74adeb99, error name: a php error. Alert when > 1.'
);
});
it('shows the correct alert count for each service on service inventory', async () => {
const serviceInventoryAlertCounts = await fetchServiceInventoryAlertCounts(apmApiClient);
expect(serviceInventoryAlertCounts).to.eql({
'opbeans-node': 0,
'opbeans-java': 2,
});
});
it('shows the correct alert count in opbeans-java service', async () => {
const serviceTabAlertCount = await fetchServiceTabAlertCount({
apmApiClient,
serviceName: 'opbeans-java',
});
expect(serviceTabAlertCount).to.be(2);
});
it('shows the correct alert count in opbeans-node service', async () => {
const serviceTabAlertCount = await fetchServiceTabAlertCount({
apmApiClient,
serviceName: 'opbeans-node',
});
expect(serviceTabAlertCount).to.be(0);
});
});
});
}

View file

@ -0,0 +1,253 @@
/*
* 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, errors } from '@elastic/elasticsearch';
import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common';
import pRetry from 'p-retry';
import type { SuperTest, Test } from 'supertest';
import { ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types';
import { ApmRuleParamsType } from '@kbn/apm-plugin/common/rules/schema';
import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type';
import { RollupInterval } from '@kbn/apm-plugin/common/rollup';
import { ApmApiClient } from '../../../common/config';
export const APM_ALERTS_INDEX = '.alerts-observability.apm.alerts-*';
export const APM_ACTION_VARIABLE_INDEX = 'apm-index-connector-test';
export async function createApmRule<T extends ApmRuleType>({
supertest,
name,
ruleTypeId,
params,
actions = [],
}: {
supertest: SuperTest<Test>;
ruleTypeId: T;
name: string;
params: ApmRuleParamsType[T];
actions?: any[];
}) {
try {
const { body } = await supertest
.post(`/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send({
params,
consumer: 'apm',
schedule: {
interval: '1m',
},
tags: ['apm'],
name,
rule_type_id: ruleTypeId,
actions,
});
return body;
} catch (error: any) {
throw new Error(`[Rule] Creating a rule failed: ${error}`);
}
}
function getTimerange() {
return {
start: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
end: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
};
}
export async function fetchServiceInventoryAlertCounts(apmApiClient: ApmApiClient) {
const timerange = getTimerange();
const serviceInventoryResponse = await apmApiClient.readUser({
endpoint: 'GET /internal/apm/services',
params: {
query: {
...timerange,
environment: 'ENVIRONMENT_ALL',
kuery: '',
probability: 1,
documentType: ApmDocumentType.ServiceTransactionMetric,
rollupInterval: RollupInterval.SixtyMinutes,
},
},
});
return serviceInventoryResponse.body.items.reduce<Record<string, number>>((acc, item) => {
return { ...acc, [item.serviceName]: item.alertsCount ?? 0 };
}, {});
}
export async function fetchServiceTabAlertCount({
apmApiClient,
serviceName,
}: {
apmApiClient: ApmApiClient;
serviceName: string;
}) {
const timerange = getTimerange();
const alertsCountReponse = await apmApiClient.readUser({
endpoint: 'GET /internal/apm/services/{serviceName}/alerts_count',
params: {
path: {
serviceName,
},
query: {
...timerange,
environment: 'ENVIRONMENT_ALL',
},
},
});
return alertsCountReponse.body.alertsCount;
}
export async function runRuleSoon({
ruleId,
supertest,
}: {
ruleId: string;
supertest: SuperTest<Test>;
}): Promise<Record<string, any>> {
return pRetry(
async () => {
try {
const response = await supertest
.post(`/internal/alerting/rule/${ruleId}/_run_soon`)
.set('kbn-xsrf', 'foo');
// Sometimes the rule may already be running, which returns a 200. Try until it isn't
if (response.status !== 204) {
throw new Error(`runRuleSoon got ${response.status} status`);
}
return response;
} catch (error) {
throw new Error(`[Rule] Running a rule ${ruleId} failed: ${error}`);
}
},
{ retries: 10 }
);
}
export async function deleteAlertsByRuleId({ es, ruleId }: { es: Client; ruleId: string }) {
await es.deleteByQuery({
index: APM_ALERTS_INDEX,
query: { term: { 'kibana.alert.rule.uuid': ruleId } },
});
}
export async function deleteRuleById({
supertest,
ruleId,
}: {
supertest: SuperTest<Test>;
ruleId: string;
}) {
await supertest.delete(`/api/alerting/rule/${ruleId}`).set('kbn-xsrf', 'foo');
}
export async function deleteApmRules(supertest: SuperTest<Test>) {
const res = await supertest.get(
`/api/alerting/rules/_find?filter=alert.attributes.consumer:apm&per_page=10000`
);
return Promise.all(
res.body.data.map(async (rule: any) => {
await supertest.delete(`/api/alerting/rule/${rule.id}`).set('kbn-xsrf', 'foo');
})
);
}
export function deleteApmAlerts(es: Client) {
return es.deleteByQuery({ index: APM_ALERTS_INDEX, query: { match_all: {} } });
}
export async function clearKibanaApmEventLog(es: Client) {
return es.deleteByQuery({
index: '.kibana-event-log-*',
query: { term: { 'kibana.alert.rule.consumer': 'apm' } },
});
}
export type ApmAlertFields = ParsedTechnicalFields & {
'service.name': string;
'service.environment': string;
'transaction.name': string;
'error.grouping_key': string;
'error.grouping_name': string;
};
export async function createIndexConnector({
supertest,
name,
}: {
supertest: SuperTest<Test>;
name: string;
}) {
const { body } = await supertest
.post(`/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.send({
name,
config: {
index: APM_ACTION_VARIABLE_INDEX,
refresh: true,
},
connector_type_id: '.index',
});
return body.id as string;
}
export function getIndexAction({
actionId,
actionVariables,
}: {
actionId: string;
actionVariables: Array<{ name: string }>;
}) {
return {
group: 'threshold_met',
id: actionId,
params: {
documents: [
actionVariables.reduce<Record<string, string>>((acc, actionVariable) => {
acc[actionVariable.name] = `{{context.${actionVariable.name}}}`;
return acc;
}, {}),
],
},
frequency: {
notify_when: 'onActionGroupChange',
throttle: null,
summary: false,
},
};
}
export async function deleteActionConnector({
supertest,
es,
actionId,
}: {
supertest: SuperTest<Test>;
es: Client;
actionId: string;
}) {
return Promise.all([
await supertest.delete(`/api/actions/connector/${actionId}`).set('kbn-xsrf', 'foo'),
await deleteActionConnectorIndex(es),
]);
}
export async function deleteActionConnectorIndex(es: Client) {
try {
await es.indices.delete({ index: APM_ACTION_VARIABLE_INDEX });
} catch (e) {
if (e instanceof errors.ResponseError && e.statusCode === 404) {
return;
}
throw e;
}
}

View file

@ -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.
*/
import { Client } from '@elastic/elasticsearch';
import type { SuperTest, Test } from 'supertest';
import {
clearKibanaApmEventLog,
deleteApmRules,
deleteApmAlerts,
deleteActionConnectorIndex,
} from './alerting_api_helper';
export async function cleanupAllState({
es,
supertest,
}: {
es: Client;
supertest: SuperTest<Test>;
}) {
try {
await Promise.all([
await deleteActionConnectorIndex(es),
await deleteApmRules(supertest),
await deleteApmAlerts(es),
await clearKibanaApmEventLog(es),
]);
} catch (e) {
// eslint-disable-next-line no-console
console.error(`An error occured while cleaning up the state: ${e}`);
}
}

View file

@ -4,22 +4,21 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { Client } from '@elastic/elasticsearch';
import pRetry from 'p-retry';
import { ToolingLog } from '@kbn/tooling-log';
import { Client } from '@elastic/elasticsearch';
import { APM_ALERTS_INDEX } from './alerting_api_helper';
async function getActiveAlert({
export async function getActiveApmAlerts({
ruleId,
esClient,
log,
}: {
ruleId: string;
waitMillis?: number;
esClient: Client;
log: ToolingLog;
}): Promise<Record<string, any>> {
const searchParams = {
index: '.alerts-observability.apm.alerts-*',
index: APM_ALERTS_INDEX,
size: 1,
query: {
bool: {
@ -44,16 +43,10 @@ async function getActiveAlert({
},
};
const response = await esClient.search(searchParams);
const firstHit = response.hits.hits[0];
if (!firstHit) {
log.debug(`No active alert found for rule ${ruleId}`);
throw new Error(`No active alert found for rule ${ruleId}`);
}
log.debug(`Get active alert for the rule ${ruleId}`);
return firstHit;
return response.hits.hits.map((hit) => hit._source);
}
export function waitForActiveAlert({
export function waitForActiveApmAlert({
ruleId,
esClient,
log,
@ -64,8 +57,21 @@ export function waitForActiveAlert({
log: ToolingLog;
}): Promise<Record<string, any>> {
log.debug(`Wait for the rule ${ruleId} to be active`);
return pRetry(() => getActiveAlert({ ruleId, esClient, log }), {
retries: 10,
factor: 1.5,
});
return pRetry(
async () => {
const activeApmAlerts = await getActiveApmAlerts({ ruleId, esClient });
if (activeApmAlerts.length === 0) {
log.debug(`No active alert found for rule ${ruleId}`);
throw new Error(`No active alert found for rule ${ruleId}`);
}
log.debug(`Get active alert for the rule ${ruleId}`);
return activeApmAlerts[0];
},
{
retries: 10,
factor: 1.5,
}
);
}

View file

@ -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 type { Client } from '@elastic/elasticsearch';
import type {
AggregationsAggregate,
SearchResponse,
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import pRetry from 'p-retry';
import { ApmAlertFields, APM_ALERTS_INDEX } from './alerting_api_helper';
async function getAlertByRuleId({ es, ruleId }: { es: Client; ruleId: string }) {
const response = (await es.search({
index: APM_ALERTS_INDEX,
body: {
query: {
term: {
'kibana.alert.rule.uuid': ruleId,
},
},
},
})) as SearchResponse<ApmAlertFields, Record<string, AggregationsAggregate>>;
return response.hits.hits.map((hit) => hit._source) as ApmAlertFields[];
}
export async function waitForAlertsForRule({
es,
ruleId,
minimumAlertCount = 1,
}: {
es: Client;
ruleId: string;
minimumAlertCount?: number;
}) {
return pRetry(
async () => {
const alerts = await getAlertByRuleId({ es, ruleId });
const actualAlertCount = alerts.length;
if (actualAlertCount < minimumAlertCount) {
throw new Error(`Expected ${minimumAlertCount} but got ${actualAlertCount} alerts`);
}
return alerts;
},
{ retries: 5 }
);
}

View file

@ -0,0 +1,31 @@
/*
* 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 pRetry from 'p-retry';
import { APM_ACTION_VARIABLE_INDEX } from './alerting_api_helper';
async function getIndexConnectorResults(es: Client) {
const res = await es.search({ index: APM_ACTION_VARIABLE_INDEX });
return res.hits.hits.map((hit) => hit._source) as Array<Record<string, string>>;
}
export async function waitForIndexConnectorResults({
es,
minCount = 1,
}: {
es: Client;
minCount?: number;
}) {
return pRetry(async () => {
const results = await getIndexConnectorResults(es);
if (results.length < minCount) {
throw new Error(`Expected ${minCount} but got ${results.length} results`);
}
return results;
});
}

View file

@ -0,0 +1,58 @@
/*
* 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 { isEqual } from 'lodash';
import pRetry from 'p-retry';
import type SuperTest from 'supertest';
export async function waitForRuleStatus({
ruleId,
expectedStatus,
supertest,
}: {
ruleId: string;
expectedStatus: string;
supertest: SuperTest.SuperTest<SuperTest.Test>;
}): Promise<Record<string, any>> {
return pRetry(
async () => {
const response = await supertest.get(`/api/alerting/rule/${ruleId}`);
const status = response.body?.execution_status?.status;
if (status !== expectedStatus) {
throw new Error(`waitForStatus(${expectedStatus}): got ${status}`);
}
return status;
},
{ retries: 10 }
);
}
export function waitFor<T>({
expectation,
fn,
debug = false,
}: {
expectation: T;
fn: () => Promise<T>;
debug?: boolean;
}) {
return pRetry(
async () => {
const actual = await fn();
if (debug) {
// eslint-disable-next-line no-console
console.log('Waiting for', expectation, 'got', actual);
}
if (!isEqual(actual, expectation)) {
throw new Error(`Expected ${actual} to be ${expectation}`);
}
},
{ retries: 5 }
);
}

View file

@ -6,41 +6,50 @@
*/
import { AggregationType, ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types';
import { transactionDurationActionVariables } from '@kbn/apm-plugin/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type';
import { apm, timerange } from '@kbn/apm-synthtrace-client';
import expect from '@kbn/expect';
import { omit } from 'lodash';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import {
createApmRule,
createIndexConnector,
fetchServiceInventoryAlertCounts,
fetchServiceTabAlertCount,
} from './alerting_api_helper';
import {
waitForRuleStatus,
waitForDocumentInIndex,
waitForAlertInIndex,
} from './wait_for_rule_status';
deleteAlertsByRuleId,
deleteRuleById,
clearKibanaApmEventLog,
ApmAlertFields,
createIndexConnector,
getIndexAction,
deleteActionConnector,
} from './helpers/alerting_api_helper';
import { cleanupAllState } from './helpers/cleanup_state';
import { waitForAlertsForRule } from './helpers/wait_for_alerts_for_rule';
import { waitForRuleStatus } from './helpers/wait_for_rule_status';
import { waitForIndexConnectorResults } from './helpers/wait_for_index_connector_results';
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const supertest = getService('supertest');
const es = getService('es');
const apmApiClient = getService('apmApiClient');
const esDeleteAllIndices = getService('esDeleteAllIndices');
const synthtraceEsClient = getService('synthtraceEsClient');
registry.when('transaction duration alert', { config: 'basic', archives: [] }, () => {
let ruleId1: string;
let actionId1: string | undefined;
let ruleId2: string;
let actionId2: string | undefined;
const APM_ALERTS_INDEX = '.alerts-observability.apm.alerts-default';
const ALERT_ACTION_INDEX_NAME1 = 'alert-action-transaction-duration1';
const ALERT_ACTION_INDEX_NAME2 = 'alert-action-transaction-duration2';
const ruleParams = {
threshold: 3000,
windowSize: 5,
windowUnit: 'm',
transactionType: 'request',
serviceName: 'opbeans-java',
environment: 'production',
aggregationType: AggregationType.Avg,
groupBy: ['service.name', 'service.environment', 'transaction.type', 'transaction.name'],
};
registry.when('transaction duration alert', { config: 'basic', archives: [] }, () => {
before(async () => {
cleanupAllState({ es, supertest });
const opbeansJava = apm
.service({ name: 'opbeans-java', environment: 'production', agentName: 'java' })
.instance('instance');
@ -68,100 +77,101 @@ export default function ApiTest({ getService }: FtrProviderContext) {
after(async () => {
await synthtraceEsClient.clean();
await supertest.delete(`/api/alerting/rule/${ruleId1}`).set('kbn-xsrf', 'foo');
await supertest.delete(`/api/actions/connector/${actionId1}`).set('kbn-xsrf', 'foo');
await supertest.delete(`/api/alerting/rule/${ruleId2}`).set('kbn-xsrf', 'foo');
await supertest.delete(`/api/actions/connector/${actionId2}`).set('kbn-xsrf', 'foo');
await esDeleteAllIndices([ALERT_ACTION_INDEX_NAME1, ALERT_ACTION_INDEX_NAME2]);
await es.deleteByQuery({
index: APM_ALERTS_INDEX,
query: { term: { 'kibana.alert.rule.uuid': ruleId1 } },
});
await es.deleteByQuery({
index: APM_ALERTS_INDEX,
query: { term: { 'kibana.alert.rule.uuid': ruleId2 } },
});
await es.deleteByQuery({
index: '.kibana-event-log-*',
query: { term: { 'kibana.alert.rule.consumer': 'apm' } },
});
await clearKibanaApmEventLog(es);
});
describe('create rule without filter query', () => {
describe('create rule for opbeans-java without kql filter', () => {
let ruleId: string;
let actionId: string;
let alerts: ApmAlertFields[];
before(async () => {
actionId1 = await createIndexConnector({
supertest,
name: 'Transation duration without filter query',
indexName: ALERT_ACTION_INDEX_NAME1,
actionId = await createIndexConnector({ supertest, name: 'Transation duration' });
const indexAction = getIndexAction({
actionId,
actionVariables: transactionDurationActionVariables,
});
const createdRule = await createApmRule({
supertest,
ruleTypeId: ApmRuleType.TransactionDuration,
name: 'Apm transaction duration without filter query',
name: 'Apm transaction duration without kql filter',
params: {
threshold: 3000,
windowSize: 5,
windowUnit: 'm',
transactionType: 'request',
serviceName: 'opbeans-java',
environment: 'production',
aggregationType: AggregationType.Avg,
kqlFilter: '',
groupBy: [
'service.name',
'service.environment',
'transaction.type',
'transaction.name',
],
...ruleParams,
},
actions: [
{
group: 'threshold_met',
id: actionId1,
params: {
documents: [{ message: 'Transaction Name: {{context.transactionName}}' }],
},
frequency: {
notify_when: 'onActionGroupChange',
throttle: null,
summary: false,
},
},
],
actions: [indexAction],
});
expect(createdRule.id).to.not.eql(undefined);
ruleId1 = createdRule.id;
ruleId = createdRule.id;
alerts = await waitForAlertsForRule({ es, ruleId });
});
after(async () => {
await deleteActionConnector({ supertest, es, actionId });
await deleteAlertsByRuleId({ es, ruleId });
await deleteRuleById({ supertest, ruleId });
});
it('checks if rule is active', async () => {
const executionStatus = await waitForRuleStatus({
id: ruleId1,
const ruleStatus = await waitForRuleStatus({
ruleId,
expectedStatus: 'active',
supertest,
});
expect(executionStatus.status).to.be('active');
expect(ruleStatus).to.be('active');
});
it('returns correct message', async () => {
const resp = await waitForDocumentInIndex<{ message: string }>({
es,
indexName: ALERT_ACTION_INDEX_NAME1,
describe('action variables', () => {
let results: Array<Record<string, string>>;
before(async () => {
results = await waitForIndexConnectorResults({ es });
});
expect(resp.hits.hits[0]._source?.message).eql(`Transaction Name: tx-java`);
it('populates the action connector index with every action variable', async () => {
expect(results.length).to.be(1);
expect(Object.keys(results[0]).sort()).to.eql([
'alertDetailsUrl',
'environment',
'interval',
'reason',
'serviceName',
'threshold',
'transactionName',
'transactionType',
'triggerValue',
'viewInAppUrl',
]);
});
it('populates the document with the correct values', async () => {
expect(omit(results[0], 'alertDetailsUrl')).to.eql({
environment: 'production',
interval: '5 mins',
reason:
'Avg. latency is 5.0 s in the last 5 mins for service: opbeans-java, env: production, type: request, name: tx-java. Alert when > 3.0 s.',
serviceName: 'opbeans-java',
transactionType: 'request',
transactionName: 'tx-java',
threshold: '3000',
triggerValue: '5,000 ms',
viewInAppUrl:
'http://mockedPublicBaseUrl/app/apm/services/opbeans-java?transactionType=request&environment=production',
});
});
});
it('produces an alert for opbeans-java with the correct reason', async () => {
expect(alerts[0]['kibana.alert.reason']).to.be(
'Avg. latency is 5.0 s in the last 5 mins for service: opbeans-java, env: production, type: request, name: tx-java. Alert when > 3.0 s.'
);
});
it('indexes alert document with all group-by fields', async () => {
const resp = await waitForAlertInIndex({
es,
indexName: APM_ALERTS_INDEX,
ruleId: ruleId1,
});
expect(resp.hits.hits[0]._source).property('service.name', 'opbeans-java');
expect(resp.hits.hits[0]._source).property('service.environment', 'production');
expect(resp.hits.hits[0]._source).property('transaction.type', 'request');
expect(resp.hits.hits[0]._source).property('transaction.name', 'tx-java');
expect(alerts[0]).property('service.name', 'opbeans-java');
expect(alerts[0]).property('service.environment', 'production');
expect(alerts[0]).property('transaction.type', 'request');
expect(alerts[0]).property('transaction.name', 'tx-java');
});
it('shows the correct alert count for each service on service inventory', async () => {
@ -189,101 +199,70 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
});
describe('create rule with filter query', () => {
describe('create rule for opbeans-node using kql filter', () => {
let ruleId: string;
let alerts: ApmAlertFields[];
before(async () => {
actionId2 = await createIndexConnector({
supertest,
name: 'Transation duration with filter query',
indexName: ALERT_ACTION_INDEX_NAME2,
});
const createdRule = await createApmRule({
supertest,
ruleTypeId: ApmRuleType.TransactionDuration,
name: 'Apm transaction duration with filter query',
name: 'Apm transaction duration with kql filter',
params: {
threshold: 3000,
windowSize: 5,
windowUnit: 'm',
transactionType: undefined,
serviceName: undefined,
environment: 'ENVIRONMENT_ALL',
aggregationType: AggregationType.Avg,
kqlFilter:
'service.name: opbeans-node and transaction.type: request and service.environment: production',
groupBy: [
'service.name',
'service.environment',
'transaction.type',
'transaction.name',
],
...ruleParams,
},
actions: [
{
group: 'threshold_met',
id: actionId2,
params: {
documents: [{ message: 'Transaction Name: {{context.transactionName}}' }],
},
frequency: {
notify_when: 'onActionGroupChange',
throttle: null,
summary: false,
},
},
],
actions: [],
});
expect(createdRule.id).to.not.eql(undefined);
ruleId2 = createdRule.id;
ruleId = createdRule.id;
alerts = await waitForAlertsForRule({ es, ruleId });
});
after(async () => {
await deleteAlertsByRuleId({ es, ruleId });
await deleteRuleById({ supertest, ruleId });
});
it('checks if rule is active', async () => {
const executionStatus = await waitForRuleStatus({
id: ruleId2,
const ruleStatus = await waitForRuleStatus({
ruleId,
expectedStatus: 'active',
supertest,
});
expect(executionStatus.status).to.be('active');
expect(ruleStatus).to.be('active');
});
it('returns correct message', async () => {
const resp = await waitForDocumentInIndex<{ message: string }>({
es,
indexName: ALERT_ACTION_INDEX_NAME2,
});
expect(resp.hits.hits[0]._source?.message).eql(`Transaction Name: tx-node`);
it('produces an alert for opbeans-node with the correct reason', async () => {
expect(alerts[0]['kibana.alert.reason']).to.be(
'Avg. latency is 4.0 s in the last 5 mins for service: opbeans-node, env: production, type: request, name: tx-node. Alert when > 3.0 s.'
);
});
it('indexes alert document with all group-by fields', async () => {
const resp = await waitForAlertInIndex({
es,
indexName: APM_ALERTS_INDEX,
ruleId: ruleId2,
});
expect(resp.hits.hits[0]._source).property('service.name', 'opbeans-node');
expect(resp.hits.hits[0]._source).property('service.environment', 'production');
expect(resp.hits.hits[0]._source).property('transaction.type', 'request');
expect(resp.hits.hits[0]._source).property('transaction.name', 'tx-node');
expect(alerts[0]).property('service.name', 'opbeans-node');
expect(alerts[0]).property('service.environment', 'production');
expect(alerts[0]).property('transaction.type', 'request');
expect(alerts[0]).property('transaction.name', 'tx-node');
});
it('shows the correct alert count for each service on service inventory', async () => {
it('shows alert count=1 for opbeans-node on service inventory', async () => {
const serviceInventoryAlertCounts = await fetchServiceInventoryAlertCounts(apmApiClient);
expect(serviceInventoryAlertCounts).to.eql({
'opbeans-node': 1,
'opbeans-java': 1,
'opbeans-java': 0,
});
});
it('shows the correct alert count in opbeans-java service', async () => {
it('shows alert count=0 in opbeans-java service', async () => {
const serviceTabAlertCount = await fetchServiceTabAlertCount({
apmApiClient,
serviceName: 'opbeans-java',
});
expect(serviceTabAlertCount).to.be(1);
expect(serviceTabAlertCount).to.be(0);
});
it('shows the correct alert count in opbeans-node service', async () => {
it('shows alert count=1 in opbeans-node service', async () => {
const serviceTabAlertCount = await fetchServiceTabAlertCount({
apmApiClient,
serviceName: 'opbeans-node',

View file

@ -6,45 +6,39 @@
*/
import { ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types';
import { transactionErrorRateActionVariables } from '@kbn/apm-plugin/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type';
import { apm, timerange } from '@kbn/apm-synthtrace-client';
import expect from '@kbn/expect';
import moment from 'moment';
import { omit } from 'lodash';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import {
createApmRule,
createIndexConnector,
fetchServiceInventoryAlertCounts,
fetchServiceTabAlertCount,
} from './alerting_api_helper';
import {
waitForRuleStatus,
waitForDocumentInIndex,
waitForAlertInIndex,
} from './wait_for_rule_status';
deleteAlertsByRuleId,
clearKibanaApmEventLog,
deleteRuleById,
ApmAlertFields,
getIndexAction,
createIndexConnector,
deleteActionConnector,
} from './helpers/alerting_api_helper';
import { cleanupAllState } from './helpers/cleanup_state';
import { waitForAlertsForRule } from './helpers/wait_for_alerts_for_rule';
import { waitForRuleStatus } from './helpers/wait_for_rule_status';
import { waitForIndexConnectorResults } from './helpers/wait_for_index_connector_results';
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const supertest = getService('supertest');
const es = getService('es');
const apmApiClient = getService('apmApiClient');
const esDeleteAllIndices = getService('esDeleteAllIndices');
const synthtraceEsClient = getService('synthtraceEsClient');
registry.when('transaction error rate alert', { config: 'basic', archives: [] }, () => {
let ruleId1: string;
let ruleId2: string;
let alertId: string;
let startedAt: string;
let actionId1: string | undefined;
let actionId2: string | undefined;
const APM_ALERTS_INDEX = '.alerts-observability.apm.alerts-default';
const ALERT_ACTION_INDEX_NAME1 = 'alert-action-transaction-error-rate1';
const ALERT_ACTION_INDEX_NAME2 = 'alert-action-transaction-error-rate2';
before(async () => {
cleanupAllState({ es, supertest });
const opbeansJava = apm
.service({ name: 'opbeans-java', environment: 'production', agentName: 'java' })
.instance('instance');
@ -82,38 +76,27 @@ export default function ApiTest({ getService }: FtrProviderContext) {
after(async () => {
await synthtraceEsClient.clean();
await supertest.delete(`/api/alerting/rule/${ruleId1}`).set('kbn-xsrf', 'foo');
await supertest.delete(`/api/actions/connector/${actionId1}`).set('kbn-xsrf', 'foo');
await supertest.delete(`/api/alerting/rule/${ruleId2}`).set('kbn-xsrf', 'foo');
await supertest.delete(`/api/actions/connector/${actionId2}`).set('kbn-xsrf', 'foo');
await esDeleteAllIndices([ALERT_ACTION_INDEX_NAME1, ALERT_ACTION_INDEX_NAME2]);
await es.deleteByQuery({
index: APM_ALERTS_INDEX,
query: { term: { 'kibana.alert.rule.uuid': ruleId1 } },
});
await es.deleteByQuery({
index: APM_ALERTS_INDEX,
query: { term: { 'kibana.alert.rule.uuid': ruleId2 } },
});
await es.deleteByQuery({
index: '.kibana-event-log-*',
query: { term: { 'kibana.alert.rule.consumer': 'apm' } },
});
await clearKibanaApmEventLog(es);
});
describe('create alert without filter query', () => {
describe('create rule without kql query', () => {
let ruleId: string;
let actionId: string;
let alerts: ApmAlertFields[];
before(async () => {
actionId1 = await createIndexConnector({
supertest,
name: 'Transation error rate without filter query',
indexName: ALERT_ACTION_INDEX_NAME1,
actionId = await createIndexConnector({ supertest, name: 'Transation error rate' });
const indexAction = getIndexAction({
actionId,
actionVariables: transactionErrorRateActionVariables,
});
const createdRule = await createApmRule({
supertest,
ruleTypeId: ApmRuleType.TransactionErrorRate,
name: 'Apm transaction error rate without filter query',
name: 'Apm transaction error rate without kql query',
params: {
threshold: 50,
threshold: 40,
windowSize: 5,
windowUnit: 'm',
transactionType: 'request',
@ -127,66 +110,81 @@ export default function ApiTest({ getService }: FtrProviderContext) {
'transaction.name',
],
},
actions: [
{
group: 'threshold_met',
id: actionId1,
params: {
documents: [
{
message: `Transaction Name: {{context.transactionName}}
- Alert URL: {{context.alertDetailsUrl}}`,
},
],
},
frequency: {
notify_when: 'onActionGroupChange',
throttle: null,
summary: false,
},
},
],
actions: [indexAction],
});
expect(createdRule.id).to.not.eql(undefined);
ruleId1 = createdRule.id;
ruleId = createdRule.id;
alerts = await waitForAlertsForRule({ es, ruleId });
});
after(async () => {
await deleteActionConnector({ supertest, es, actionId });
await deleteRuleById({ supertest, ruleId });
await deleteAlertsByRuleId({ es, ruleId });
});
it('checks if rule is active', async () => {
const executionStatus = await waitForRuleStatus({
id: ruleId1,
const ruleStatus = await waitForRuleStatus({
ruleId,
expectedStatus: 'active',
supertest,
});
expect(executionStatus.status).to.be('active');
expect(ruleStatus).to.be('active');
});
describe('action variables', () => {
let results: Array<Record<string, string>>;
before(async () => {
results = await waitForIndexConnectorResults({ es });
});
it('has the right keys', async () => {
expect(results.length).to.be(1);
expect(Object.keys(results[0]).sort()).to.eql([
'alertDetailsUrl',
'environment',
'interval',
'reason',
'serviceName',
'threshold',
'transactionName',
'transactionType',
'triggerValue',
'viewInAppUrl',
]);
});
it('has the right values', () => {
expect(omit(results[0], 'alertDetailsUrl')).to.eql({
environment: 'production',
interval: '5 mins',
reason:
'Failed transactions is 50% in the last 5 mins for service: opbeans-java, env: production, type: request, name: tx-java. Alert when > 40%.',
serviceName: 'opbeans-java',
transactionName: 'tx-java',
threshold: '40',
transactionType: 'request',
triggerValue: '50',
viewInAppUrl:
'http://mockedPublicBaseUrl/app/apm/services/opbeans-java?transactionType=request&environment=production',
});
});
});
it('indexes alert document with all group-by fields', async () => {
const resp = await waitForAlertInIndex({
es,
indexName: APM_ALERTS_INDEX,
ruleId: ruleId1,
});
alertId = (resp.hits.hits[0]._source as any)['kibana.alert.uuid'];
startedAt = (resp.hits.hits[0]._source as any)['kibana.alert.start'];
expect(resp.hits.hits[0]._source).property('service.name', 'opbeans-java');
expect(resp.hits.hits[0]._source).property('service.environment', 'production');
expect(resp.hits.hits[0]._source).property('transaction.type', 'request');
expect(resp.hits.hits[0]._source).property('transaction.name', 'tx-java');
expect(alerts[0]).property('service.name', 'opbeans-java');
expect(alerts[0]).property('service.environment', 'production');
expect(alerts[0]).property('transaction.type', 'request');
expect(alerts[0]).property('transaction.name', 'tx-java');
});
it('returns correct message', async () => {
const rangeFrom = moment(startedAt).subtract('5', 'minute').toISOString();
const resp = await waitForDocumentInIndex<{ message: string }>({
es,
indexName: ALERT_ACTION_INDEX_NAME1,
});
expect(resp.hits.hits[0]._source?.message).eql(`Transaction Name: tx-java
- Alert URL: http://mockedpublicbaseurl/app/observability/alerts?_a=(kuery:%27kibana.alert.uuid:%20%22${alertId}%22%27%2CrangeFrom:%27${rangeFrom}%27%2CrangeTo:now%2Cstatus:all)`);
it('produces an alert for opbeans-java with the correct reason', async () => {
expect(alerts[0]!['kibana.alert.reason']).to.be(
'Failed transactions is 50% in the last 5 mins for service: opbeans-java, env: production, type: request, name: tx-java. Alert when > 40%.'
);
});
it('shows the correct alert count for each service on service inventory', async () => {
it('shows the 1 alert count for opbeans-java in service inventory', async () => {
const serviceInventoryAlertCounts = await fetchServiceInventoryAlertCounts(apmApiClient);
expect(serviceInventoryAlertCounts).to.eql({
'opbeans-node': 0,
@ -211,19 +209,17 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
});
describe('create alert with filter query', () => {
describe('create rule with kql query', () => {
let ruleId: string;
let alerts: ApmAlertFields[];
before(async () => {
actionId2 = await createIndexConnector({
supertest,
name: 'Transation error rate without filter query',
indexName: ALERT_ACTION_INDEX_NAME2,
});
const createdRule = await createApmRule({
supertest,
ruleTypeId: ApmRuleType.TransactionErrorRate,
name: 'Apm transaction error rate without filter query',
name: 'Apm transaction error rate without kql query',
params: {
threshold: 50,
threshold: 40,
windowSize: 5,
windowUnit: 'm',
transactionType: undefined,
@ -238,82 +234,47 @@ export default function ApiTest({ getService }: FtrProviderContext) {
'transaction.name',
],
},
actions: [
{
group: 'threshold_met',
id: actionId2,
params: {
documents: [
{
message: `Transaction Name: {{context.transactionName}}
- Alert URL: {{context.alertDetailsUrl}}`,
},
],
},
frequency: {
notify_when: 'onActionGroupChange',
throttle: null,
summary: false,
},
},
],
actions: [],
});
expect(createdRule.id).to.not.eql(undefined);
ruleId2 = createdRule.id;
ruleId = createdRule.id;
alerts = await waitForAlertsForRule({ es, ruleId });
});
it('checks if rule is active', async () => {
const executionStatus = await waitForRuleStatus({
id: ruleId2,
expectedStatus: 'active',
supertest,
});
expect(executionStatus.status).to.be('active');
after(async () => {
await deleteRuleById({ supertest, ruleId });
await deleteAlertsByRuleId({ es, ruleId });
});
it('indexes alert document with all group-by fields', async () => {
const resp = await waitForAlertInIndex({
es,
indexName: APM_ALERTS_INDEX,
ruleId: ruleId2,
});
alertId = (resp.hits.hits[0]._source as any)['kibana.alert.uuid'];
startedAt = (resp.hits.hits[0]._source as any)['kibana.alert.start'];
expect(resp.hits.hits[0]._source).property('service.name', 'opbeans-node');
expect(resp.hits.hits[0]._source).property('service.environment', 'production');
expect(resp.hits.hits[0]._source).property('transaction.type', 'request');
expect(resp.hits.hits[0]._source).property('transaction.name', 'tx-node');
expect(alerts[0]).property('service.name', 'opbeans-node');
expect(alerts[0]).property('service.environment', 'production');
expect(alerts[0]).property('transaction.type', 'request');
expect(alerts[0]).property('transaction.name', 'tx-node');
});
it('returns correct message', async () => {
const rangeFrom = moment(startedAt).subtract('5', 'minute').toISOString();
const resp = await waitForDocumentInIndex<{ message: string }>({
es,
indexName: ALERT_ACTION_INDEX_NAME2,
});
expect(resp.hits.hits[0]._source?.message).eql(`Transaction Name: tx-node
- Alert URL: http://mockedpublicbaseurl/app/observability/alerts?_a=(kuery:%27kibana.alert.uuid:%20%22${alertId}%22%27%2CrangeFrom:%27${rangeFrom}%27%2CrangeTo:now%2Cstatus:all)`);
it('produces an alert for opbeans-node with the correct reason', async () => {
expect(alerts[0]!['kibana.alert.reason']).to.be(
'Failed transactions is 50% in the last 5 mins for service: opbeans-node, env: production, type: request, name: tx-node. Alert when > 40%.'
);
});
it('shows the correct alert count for each service on service inventory', async () => {
it('shows alert count=1 for opbeans-node on service inventory', async () => {
const serviceInventoryAlertCounts = await fetchServiceInventoryAlertCounts(apmApiClient);
expect(serviceInventoryAlertCounts).to.eql({
'opbeans-node': 1,
'opbeans-java': 1,
'opbeans-java': 0,
});
});
it('shows the correct alert count in opbeans-java service', async () => {
it('shows alert count=0 in opbeans-java service', async () => {
const serviceTabAlertCount = await fetchServiceTabAlertCount({
apmApiClient,
serviceName: 'opbeans-java',
});
expect(serviceTabAlertCount).to.be(1);
expect(serviceTabAlertCount).to.be(0);
});
it('shows the correct alert count in opbeans-node service', async () => {
it('shows alert count=1 in opbeans-node service', async () => {
const serviceTabAlertCount = await fetchServiceTabAlertCount({
apmApiClient,
serviceName: 'opbeans-node',

View file

@ -1,111 +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 type { Client } from '@elastic/elasticsearch';
import type {
AggregationsAggregate,
SearchResponse,
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import pRetry from 'p-retry';
import type SuperTest from 'supertest';
export async function waitForRuleStatus({
id,
expectedStatus,
supertest,
}: {
id: string;
expectedStatus: string;
supertest: SuperTest.SuperTest<SuperTest.Test>;
}): Promise<Record<string, any>> {
return pRetry(
async () => {
const response = await supertest.get(`/api/alerting/rule/${id}`);
const { execution_status: executionStatus } = response.body || {};
const { status } = executionStatus || {};
if (status !== expectedStatus) {
throw new Error(`waitForStatus(${expectedStatus}): got ${status}`);
}
return executionStatus;
},
{ retries: 10 }
);
}
export async function runRuleSoon({
ruleId,
supertest,
}: {
ruleId: string;
supertest: SuperTest.SuperTest<SuperTest.Test>;
}): Promise<Record<string, any>> {
return pRetry(
async () => {
try {
const response = await supertest
.post(`/internal/alerting/rule/${ruleId}/_run_soon`)
.set('kbn-xsrf', 'foo');
// Sometimes the rule may already be running, which returns a 200. Try until it isn't
if (response.status !== 204) {
throw new Error(`runRuleSoon got ${response.status} status`);
}
return response;
} catch (error) {
throw new Error(`[Rule] Running a rule ${ruleId} failed: ${error}`);
}
},
{ retries: 10 }
);
}
export async function waitForDocumentInIndex<T>({
es,
indexName,
}: {
es: Client;
indexName: string;
}): Promise<SearchResponse<T, Record<string, AggregationsAggregate>>> {
return pRetry(
async () => {
const response = await es.search<T>({ index: indexName });
if (response.hits.hits.length === 0) {
throw new Error('No hits found');
}
return response;
},
{ retries: 10 }
);
}
export async function waitForAlertInIndex<T>({
es,
indexName,
ruleId,
}: {
es: Client;
indexName: string;
ruleId: string;
}): Promise<SearchResponse<T, Record<string, AggregationsAggregate>>> {
return pRetry(
async () => {
const response = await es.search<T>({
index: indexName,
body: {
query: {
term: {
'kibana.alert.rule.uuid': ruleId,
},
},
},
});
if (response.hits.hits.length === 0) {
throw new Error('No hits found');
}
return response;
},
{ retries: 10 }
);
}

View file

@ -12,10 +12,10 @@ import { FtrProviderContext } from '../../common/ftr_provider_context';
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const apmApiClient = getService('apmApiClient');
const esClient = getService('es');
const es = getService('es');
async function getLastDocId(processorEvent: ProcessorEvent) {
const response = await esClient.search<{
const response = await es.search<{
[key: string]: { id: string };
}>({
index: ['apm-*'],

View file

@ -14,7 +14,9 @@ const envGrepFiles = process.env.APM_TEST_GREP_FILES as string;
function getGlobPattern() {
try {
const envGrepFilesParsed = JSON.parse(envGrepFiles as string) as string[];
return envGrepFilesParsed.map((pattern) => `**/${pattern}**`);
return envGrepFilesParsed.map((pattern) => {
return pattern.includes('spec') ? `**/${pattern}**` : `**/${pattern}**.spec.ts`;
});
} catch (e) {
// ignore
}

View file

@ -7,8 +7,12 @@
import { AggregationType, ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types';
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
import { waitForActiveAlert } from '../../../common/utils/wait_for_active_alert';
import { createApmRule } from '../../alerts/alerting_api_helper';
import {
createApmRule,
deleteRuleById,
deleteApmAlerts,
} from '../../alerts/helpers/alerting_api_helper';
import { waitForActiveApmAlert } from '../../alerts/helpers/wait_for_active_apm_alerts';
import {
createServiceGroupApi,
deleteAllServiceGroups,
@ -21,7 +25,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const apmApiClient = getService('apmApiClient');
const supertest = getService('supertest');
const synthtraceEsClient = getService('synthtraceEsClient');
const esClient = getService('es');
const es = getService('es');
const log = getService('log');
const start = Date.now() - 24 * 60 * 60 * 1000;
const end = Date.now();
@ -84,12 +88,12 @@ export default function ApiTest({ getService }: FtrProviderContext) {
before(async () => {
const createdRule = await createRule();
ruleId = createdRule.id;
await waitForActiveAlert({ ruleId, esClient, log });
await waitForActiveApmAlert({ ruleId, esClient: es, log });
});
after(async () => {
await supertest.delete(`/api/alerting/rule/${ruleId}`).set('kbn-xsrf', 'true');
await esClient.deleteByQuery({ index: '.alerts*', query: { match_all: {} } });
await deleteRuleById({ supertest, ruleId });
await deleteApmAlerts(es);
});
it('returns the correct number of alerts', async () => {

View file

@ -8,26 +8,27 @@ import expect from '@kbn/expect';
import { AggregationType, ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types';
import { apm, timerange } from '@kbn/apm-synthtrace-client';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { createApmRule } from '../alerts/alerting_api_helper';
import {
waitForRuleStatus,
createApmRule,
deleteApmAlerts,
runRuleSoon,
waitForAlertInIndex,
} from '../alerts/wait_for_rule_status';
deleteRuleById,
ApmAlertFields,
} from '../alerts/helpers/alerting_api_helper';
import { waitForRuleStatus } from '../alerts/helpers/wait_for_rule_status';
import { waitForAlertsForRule } from '../alerts/helpers/wait_for_alerts_for_rule';
export default function ServiceAlerts({ getService }: FtrProviderContext) {
const registry = getService('registry');
const apmApiClient = getService('apmApiClient');
const supertest = getService('supertest');
const synthtraceEsClient = getService('synthtraceEsClient');
const esClient = getService('es');
const es = getService('es');
const dayInMs = 24 * 60 * 60 * 1000;
const start = Date.now() - dayInMs;
const end = Date.now() + dayInMs;
const goService = 'synth-go';
const APM_ALERTS_INDEX = '.alerts-observability.apm.alerts-default';
async function getServiceAlerts({
serviceName,
environment,
@ -120,24 +121,26 @@ export default function ServiceAlerts({ getService }: FtrProviderContext) {
describe('with alerts', () => {
let ruleId: string;
let alerts: ApmAlertFields[];
before(async () => {
const createdRule = await createRule();
ruleId = createdRule.id;
expect(createdRule.id).to.not.eql(undefined);
alerts = await waitForAlertsForRule({ es, ruleId });
});
after(async () => {
await supertest.delete(`/api/alerting/rule/${ruleId}`).set('kbn-xsrf', 'true');
await esClient.deleteByQuery({ index: '.alerts*', query: { match_all: {} } });
await deleteRuleById({ supertest, ruleId });
await deleteApmAlerts(es);
});
it('checks if rule is active', async () => {
const executionStatus = await waitForRuleStatus({
id: ruleId,
const ruleStatus = await waitForRuleStatus({
ruleId,
expectedStatus: 'active',
supertest,
});
expect(executionStatus.status).to.be('active');
expect(ruleStatus).to.be('active');
});
it('should successfully run the rule', async () => {
@ -148,14 +151,8 @@ export default function ServiceAlerts({ getService }: FtrProviderContext) {
expect(response.status).to.be(204);
});
it('indexes alert document', async () => {
const resp = await waitForAlertInIndex({
es: esClient,
indexName: APM_ALERTS_INDEX,
ruleId,
});
expect(resp.hits.hits.length).to.be(1);
it('produces 1 alert', async () => {
expect(alerts.length).to.be(1);
});
it('returns the correct number of alerts', async () => {

View file

@ -15,7 +15,7 @@ import { expectToReject } from '../../../common/utils/expect_to_reject';
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const apmApiClient = getService('apmApiClient');
const esClient = getService('es');
const es = getService('es');
const agentKeyName = 'test';
const allApplicationPrivileges = [PrivilegeType.AGENT_CONFIG, PrivilegeType.EVENT];
@ -100,7 +100,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
{ config: 'basic', archives: [] },
() => {
afterEach(async () => {
await esClient.security.invalidateApiKey({
await es.security.invalidateApiKey({
username: ApmUsername.apmManageOwnAndCreateAgentKeys,
});
});
@ -114,7 +114,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(body.agentKey).to.have.property('encoded');
expect(body.agentKey.name).to.be(agentKeyName);
const { api_keys: apiKeys } = await esClient.security.getApiKey({});
const { api_keys: apiKeys } = await es.security.getApiKey({});
expect(
apiKeys.filter((key) => !key.invalidated && key.metadata?.application === 'apm')
).to.have.length(1);
@ -139,7 +139,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(body.invalidatedAgentKeys).to.eql([id]);
// Get
const { api_keys: apiKeys } = await esClient.security.getApiKey({});
const { api_keys: apiKeys } = await es.security.getApiKey({});
expect(
apiKeys.filter((key) => !key.invalidated && key.metadata?.application === 'apm')
).to.be.empty();

View file

@ -40,11 +40,11 @@ a1.TableInfo$ForeignKey$$ExternalSyntheticOutline0 -> a1.e:
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const apmApiClient = getService('apmApiClient');
const esClient = getService('es');
const es = getService('es');
function waitForSourceMapCount(expectedCount: number) {
const getSourceMapCount = async () => {
const res = await esClient.search({
const res = await es.search({
index: '.apm-source-map',
size: 0,
track_total_hits: true,
@ -64,7 +64,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
}
async function deleteAllApmSourceMaps() {
await esClient.deleteByQuery({
await es.deleteByQuery({
index: '.apm-source-map*',
refresh: true,
query: { match_all: {} },
@ -72,7 +72,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
}
async function deleteAllFleetSourceMaps() {
return esClient.deleteByQuery({
return es.deleteByQuery({
index: '.fleet-artifacts*',
refresh: true,
query: {
@ -195,7 +195,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
it('is uploaded as a fleet artifact', async () => {
const res = await esClient.search({
const res = await es.search({
index: '.fleet-artifacts',
size: 1,
query: {
@ -210,7 +210,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
it('is added to .apm-source-map index', async () => {
const res = await esClient.search<ApmSourceMap>({
const res = await es.search<ApmSourceMap>({
index: '.apm-source-map',
});
@ -231,8 +231,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
before(async () => {
async function getSourceMapDocFromApmIndex() {
await esClient.indices.refresh({ index: '.apm-source-map' });
return await esClient.get<ApmSourceMap>({
await es.indices.refresh({ index: '.apm-source-map' });
return await es.get<ApmSourceMap>({
index: '.apm-source-map',
id: 'uploading-test-1.0.0-bar',
});
@ -256,7 +256,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
it('creates one document in the .apm-source-map index', async () => {
const res = await esClient.search<ApmSourceMap>({ index: '.apm-source-map', size: 0 });
const res = await es.search<ApmSourceMap>({ index: '.apm-source-map', size: 0 });
// @ts-expect-error
expect(res.hits.total.value).to.be(1);
@ -301,7 +301,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
it('is uploaded as a fleet artifact', async () => {
const res = await esClient.search({
const res = await es.search({
index: '.fleet-artifacts',
size: 1,
query: {
@ -316,7 +316,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
it('is added to .apm-source-map index', async () => {
const res = await esClient.search<ApmSourceMap>({
const res = await es.search<ApmSourceMap>({
index: '.apm-source-map',
});
@ -337,8 +337,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
before(async () => {
async function getSourceMapDocFromApmIndex() {
await esClient.indices.refresh({ index: '.apm-source-map' });
return await esClient.get<ApmSourceMap>({
await es.indices.refresh({ index: '.apm-source-map' });
return await es.get<ApmSourceMap>({
index: '.apm-source-map',
id: 'uploading-test-1.0.0-android',
});
@ -361,7 +361,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
it('creates one document in the .apm-source-map index', async () => {
const res = await esClient.search<ApmSourceMap>({ index: '.apm-source-map', size: 0 });
const res = await es.search<ApmSourceMap>({ index: '.apm-source-map', size: 0 });
// @ts-expect-error
expect(res.hits.total.value).to.be(1);
@ -475,7 +475,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
it('can delete an apm source map', async () => {
// check that the sourcemap is deleted from .apm-source-map index
const res = await esClient.search({ index: '.apm-source-map' });
const res = await es.search({ index: '.apm-source-map' });
// @ts-expect-error
expect(res.hits.total.value).to.be(0);
});

View file

@ -22,8 +22,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const apmApiClient = getService('apmApiClient');
const synthtraceEsClient = getService('synthtraceEsClient');
const esClient = getService('es');
const es = getService('es');
const start = moment('2022-01-01T00:00:00.000Z');
const end = moment('2022-01-02T00:00:00.000Z').subtract(1, 'millisecond');
@ -386,7 +385,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
describe('when service metrics are only available in the current time range', () => {
before(async () => {
await esClient.deleteByQuery({
await es.deleteByQuery({
index: 'metrics-apm*',
query: {
bool: {
@ -436,7 +435,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
describe('after deleting a specific data set', () => {
before(async () => {
await esClient.deleteByQuery({
await es.deleteByQuery({
index: 'metrics-apm*',
query: {
bool: {

View file

@ -13,12 +13,15 @@ import { RollupInterval } from '@kbn/apm-plugin/common/rollup';
import { apm, timerange } from '@kbn/apm-synthtrace-client';
import { AggregationType, ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { createApmRule } from '../alerts/alerting_api_helper';
import {
waitForRuleStatus,
createApmRule,
runRuleSoon,
waitForAlertInIndex,
} from '../alerts/wait_for_rule_status';
deleteApmAlerts,
deleteRuleById,
ApmAlertFields,
} from '../alerts/helpers/alerting_api_helper';
import { waitForRuleStatus } from '../alerts/helpers/wait_for_rule_status';
import { waitForAlertsForRule } from '../alerts/helpers/wait_for_alerts_for_rule';
type TransactionsGroupsMainStatistics =
APIReturnType<'GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics'>;
@ -28,12 +31,11 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const apmApiClient = getService('apmApiClient');
const synthtraceEsClient = getService('synthtraceEsClient');
const supertest = getService('supertest');
const esClient = getService('es');
const es = getService('es');
const serviceName = 'synth-go';
const dayInMs = 24 * 60 * 60 * 1000;
const start = Date.now() - dayInMs;
const end = Date.now() + dayInMs;
const APM_ALERTS_INDEX = '.alerts-observability.apm.alerts-default';
async function getTransactionGroups(overrides?: {
path?: {
@ -139,6 +141,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
describe('Transaction groups with avg transaction duration alerts', () => {
let ruleId: string;
let alerts: ApmAlertFields[];
before(async () => {
const createdRule = await createApmRule({
@ -162,22 +165,22 @@ export default function ApiTest({ getService }: FtrProviderContext) {
},
ruleTypeId: ApmRuleType.TransactionDuration,
});
expect(createdRule.id).to.not.eql(undefined);
ruleId = createdRule.id;
alerts = await waitForAlertsForRule({ es, ruleId });
});
after(async () => {
await supertest.delete(`/api/alerting/rule/${ruleId}`).set('kbn-xsrf', 'true');
await esClient.deleteByQuery({ index: '.alerts*', query: { match_all: {} } });
await deleteRuleById({ supertest, ruleId });
await deleteApmAlerts(es);
});
it('checks if rule is active', async () => {
const executionStatus = await waitForRuleStatus({
id: ruleId,
const ruleStatus = await waitForRuleStatus({
ruleId,
expectedStatus: 'active',
supertest,
});
expect(executionStatus.status).to.be('active');
expect(ruleStatus).to.be('active');
});
it('should successfully run the rule', async () => {
@ -189,13 +192,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
it('indexes alert document', async () => {
const resp = await waitForAlertInIndex({
es: esClient,
indexName: APM_ALERTS_INDEX,
ruleId,
});
expect(resp.hits.hits.length).to.be(1);
expect(alerts.length).to.be(1);
});
it('returns the correct number of alert counts', async () => {
@ -221,6 +218,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
describe('Transaction groups with p99 transaction duration alerts', () => {
let ruleId: string;
let alerts: ApmAlertFields[];
before(async () => {
const createdRule = await createApmRule({
@ -244,22 +242,23 @@ export default function ApiTest({ getService }: FtrProviderContext) {
},
ruleTypeId: ApmRuleType.TransactionDuration,
});
expect(createdRule.id).to.not.eql(undefined);
ruleId = createdRule.id;
alerts = await waitForAlertsForRule({ es, ruleId });
});
after(async () => {
await supertest.delete(`/api/alerting/rule/${ruleId}`).set('kbn-xsrf', 'true');
await esClient.deleteByQuery({ index: '.alerts*', query: { match_all: {} } });
await deleteRuleById({ supertest, ruleId });
await deleteApmAlerts(es);
});
it('checks if rule is active', async () => {
const executionStatus = await waitForRuleStatus({
id: ruleId,
const ruleStatus = await waitForRuleStatus({
ruleId,
expectedStatus: 'active',
supertest,
});
expect(executionStatus.status).to.be('active');
expect(ruleStatus).to.be('active');
});
it('should successfully run the rule', async () => {
@ -271,13 +270,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
it('indexes alert document', async () => {
const resp = await waitForAlertInIndex({
es: esClient,
indexName: APM_ALERTS_INDEX,
ruleId,
});
expect(resp.hits.hits.length).to.be(1);
expect(alerts.length).to.be(1);
});
it('returns the correct number of alert counts', async () => {
@ -306,6 +299,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
describe('Transaction groups with error rate alerts', () => {
let ruleId: string;
let alerts: ApmAlertFields[];
before(async () => {
const createdRule = await createApmRule({
@ -328,22 +322,22 @@ export default function ApiTest({ getService }: FtrProviderContext) {
},
ruleTypeId: ApmRuleType.TransactionErrorRate,
});
expect(createdRule.id).to.not.eql(undefined);
ruleId = createdRule.id;
alerts = await waitForAlertsForRule({ es, ruleId });
});
after(async () => {
await supertest.delete(`/api/alerting/rule/${ruleId}`).set('kbn-xsrf', 'true');
await esClient.deleteByQuery({ index: '.alerts*', query: { match_all: {} } });
await deleteRuleById({ supertest, ruleId });
await deleteApmAlerts(es);
});
it('checks if rule is active', async () => {
const executionStatus = await waitForRuleStatus({
id: ruleId,
const ruleStatus = await waitForRuleStatus({
ruleId,
expectedStatus: 'active',
supertest,
});
expect(executionStatus.status).to.be('active');
expect(ruleStatus).to.be('active');
});
it('should successfully run the rule', async () => {
@ -355,13 +349,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
it('indexes alert document', async () => {
const resp = await waitForAlertInIndex({
es: esClient,
indexName: APM_ALERTS_INDEX,
ruleId,
});
expect(resp.hits.hits.length).to.be(1);
expect(alerts.length).to.be(1);
});
it('returns the correct number of alert counts', async () => {