mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[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:
parent
950ac6ea25
commit
d4402886a1
23 changed files with 1012 additions and 932 deletions
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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}`);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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 }
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
|
@ -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 }
|
||||
);
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 }
|
||||
);
|
||||
}
|
|
@ -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-*'],
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue