[Rule Registry] Rewrite APM registry rules for Observability (#117740) (#120557)

* [Actionable Observability] Rewrite APM registry rules for Observability

* removing apm's rule registry test

* Moving everything under observability_api_integration

* removing old observability directory under api_integrations since it's not being used

* removing observability from api_integration tests

* renaming file

* moving test to x-pack/test/rule_registry

* Adding error handing to cleanupTargetIndices

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Chris Cowan <chris@elastic.co>
This commit is contained in:
Kibana Machine 2021-12-06 19:20:40 -05:00 committed by GitHub
parent ad3660f3ac
commit 99f26c912e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 834 additions and 591 deletions

View file

@ -45,6 +45,7 @@ const onlyNotInCoverageTests = [
require.resolve('../test/plugin_api_integration/config.ts'),
require.resolve('../test/rule_registry/security_and_spaces/config_basic.ts'),
require.resolve('../test/rule_registry/security_and_spaces/config_trial.ts'),
require.resolve('../test/rule_registry/spaces_only/config_basic.ts'),
require.resolve('../test/rule_registry/spaces_only/config_trial.ts'),
require.resolve('../test/security_api_integration/saml.config.ts'),
require.resolve('../test/security_api_integration/session_idle.config.ts'),

View file

@ -1,14 +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 { FtrProviderContext } from '../../ftr_provider_context';
export default function observabilityApiIntegrationTests({ loadTestFile }: FtrProviderContext) {
describe('Observability specs', () => {
loadTestFile(require.resolve('./annotations'));
});
}

View file

@ -1,576 +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 expect from '@kbn/expect';
import {
ALERT_DURATION,
ALERT_END,
ALERT_RULE_UUID,
ALERT_START,
ALERT_STATUS,
ALERT_UUID,
EVENT_KIND,
VERSION,
} from '@kbn/rule-data-utils';
import { merge, omit } from 'lodash';
import { FtrProviderContext } from '../../common/ftr_provider_context';
interface Alert {
schedule: {
interval: string;
};
updatedAt: string;
executionStatus: {
lastExecutionDate: string;
status: string;
};
updatedBy: string;
id: string;
params: Record<string, unknown>;
scheduledTaskId: string;
}
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const supertest = getService('legacySupertestAsApmWriteUser');
const es = getService('es');
const MAX_POLLS = 10;
const BULK_INDEX_DELAY = 1000;
const INDEXING_DELAY = 5000;
const getAlertsTargetIndicesUrl =
'/api/observability/rules/alerts/dynamic_index_pattern?namespace=default&registrationContexts=observability.apm&registrationContexts=';
const getAlertsTargetIndices = async () =>
supertest.get(getAlertsTargetIndicesUrl).send().set('kbn-xsrf', 'foo');
const APM_METRIC_INDEX_NAME = 'apm-8.0.0-transaction';
const createTransactionMetric = (override: Record<string, any>) => {
const now = Date.now();
const time = now - INDEXING_DELAY;
return merge(
{
'@timestamp': new Date(time).toISOString(),
service: {
name: 'opbeans-go',
},
event: {
outcome: 'success',
},
transaction: {
duration: {
histogram: {
values: [1000000],
counts: [1],
},
},
type: 'request',
},
processor: {
event: 'metric',
},
observer: {
version_major: 7,
},
},
override
);
};
async function waitUntilNextExecution(
alert: Alert,
intervalInSeconds: number = 1,
count: number = 0
): Promise<Alert> {
await new Promise((resolve) => {
setTimeout(resolve, intervalInSeconds * 1000);
});
const { body, status } = await supertest
.get(`/api/alerts/alert/${alert.id}`)
.set('kbn-xsrf', 'foo');
const { body: targetIndices, status: targetIndicesStatus } = await getAlertsTargetIndices();
if (targetIndices.length === 0) {
const error = new Error('Error getting alert');
Object.assign(error, { response: { body: targetIndices, status: targetIndicesStatus } });
throw error;
}
if (status >= 300) {
const error = new Error('Error getting alert');
Object.assign(error, { response: { body, status } });
throw error;
}
const nextAlert = body as Alert;
if (nextAlert.executionStatus.lastExecutionDate !== alert.executionStatus.lastExecutionDate) {
await new Promise((resolve) => {
setTimeout(resolve, BULK_INDEX_DELAY);
});
/**
* When calling refresh on an index pattern .alerts-observability.apm.alerts* (as was originally the hard-coded string in this test)
* The response from Elasticsearch is a 200, even if no indices which match that index pattern have been created.
* When calling refresh on a concrete index alias .alerts-observability.apm.alerts-default for instance,
* we receive a 404 error index_not_found_exception when no indices have been created which match that alias (obviously).
* Since we are receiving a concrete index alias from the observability api instead of a kibana index pattern
* and we understand / expect that this index does not exist at certain points of the test, we can try-catch at certain points without caring if the call fails.
* There are points in the code where we do want to ensure we get the appropriate error message back
*/
try {
await es.indices.refresh({
index: targetIndices[0],
});
// eslint-disable-next-line no-empty
} catch (exc) {}
return nextAlert;
}
if (count >= MAX_POLLS) {
throw new Error('Maximum number of polls exceeded');
}
return waitUntilNextExecution(alert, intervalInSeconds, count + 1);
}
registry.when('Rule registry with write enabled', { config: 'rules', archives: [] }, () => {
it('does not bootstrap indices on plugin startup', async () => {
const { body: targetIndices } = await getAlertsTargetIndices();
try {
const res = await es.indices.get({
index: targetIndices[0],
expand_wildcards: 'open',
allow_no_indices: true,
});
expect(res).to.be.empty();
} catch (exc) {
expect(exc.statusCode).to.eql(404);
}
});
describe('when creating a rule', () => {
let createResponse: {
alert: Alert;
status: number;
};
before(async () => {
await es.indices.create({
index: APM_METRIC_INDEX_NAME,
body: {
mappings: {
dynamic: 'strict',
properties: {
event: {
properties: {
outcome: {
type: 'keyword',
},
},
},
processor: {
properties: {
event: {
type: 'keyword',
},
},
},
observer: {
properties: {
version_major: {
type: 'byte',
},
},
},
service: {
properties: {
name: {
type: 'keyword',
},
environment: {
type: 'keyword',
},
},
},
transaction: {
properties: {
type: {
type: 'keyword',
},
duration: {
properties: {
histogram: {
type: 'histogram',
},
},
},
},
},
'@timestamp': {
type: 'date',
},
},
},
},
});
const body = {
params: {
threshold: 30,
windowSize: 5,
windowUnit: 'm',
transactionType: 'request',
environment: 'ENVIRONMENT_ALL',
serviceName: 'opbeans-go',
},
consumer: 'apm',
alertTypeId: 'apm.transaction_error_rate',
schedule: { interval: '5s' },
actions: [],
tags: ['apm', 'service.name:opbeans-go'],
notifyWhen: 'onActionGroupChange',
name: 'Failed transaction rate threshold | opbeans-go',
};
const { body: response, status } = await supertest
.post('/api/alerts/alert')
.send(body)
.set('kbn-xsrf', 'foo');
createResponse = {
alert: response,
status,
};
});
after(async () => {
const { body: targetIndices } = await getAlertsTargetIndices();
if (createResponse.alert) {
const { body, status } = await supertest
.delete(`/api/alerts/alert/${createResponse.alert.id}`)
.set('kbn-xsrf', 'foo');
if (status >= 300) {
const error = new Error('Error deleting alert');
Object.assign(error, { response: { body, status } });
throw error;
}
}
await es.deleteByQuery({
index: targetIndices[0],
body: {
query: {
match_all: {},
},
},
refresh: true,
});
await es.indices.delete({
index: APM_METRIC_INDEX_NAME,
});
});
it('writes alerts data to the alert indices', async () => {
expect(createResponse.status).to.be.below(299);
expect(createResponse.alert).not.to.be(undefined);
let alert = await waitUntilNextExecution(createResponse.alert);
const { body: targetIndices } = await getAlertsTargetIndices();
try {
const res = await es.search({
index: targetIndices[0],
body: {
query: {
term: {
[EVENT_KIND]: 'signal',
},
},
size: 1,
sort: {
'@timestamp': 'desc',
},
},
});
expect(res).to.be.empty();
} catch (exc) {
expect(exc.message).contain('index_not_found_exception');
}
await es.index({
index: APM_METRIC_INDEX_NAME,
body: createTransactionMetric({
event: {
outcome: 'success',
},
}),
refresh: true,
});
alert = await waitUntilNextExecution(alert);
try {
const res = await es.search({
index: targetIndices[0],
body: {
query: {
term: {
[EVENT_KIND]: 'signal',
},
},
size: 1,
sort: {
'@timestamp': 'desc',
},
},
});
expect(res).to.be.empty();
} catch (exc) {
expect(exc.message).contain('index_not_found_exception');
}
await es.index({
index: APM_METRIC_INDEX_NAME,
body: createTransactionMetric({
event: {
outcome: 'failure',
},
}),
refresh: true,
});
alert = await waitUntilNextExecution(alert);
const afterViolatingDataResponse = await es.search({
index: targetIndices[0],
body: {
query: {
term: {
[EVENT_KIND]: 'signal',
},
},
size: 1,
sort: {
'@timestamp': 'desc',
},
_source: false,
fields: [{ field: '*', include_unmapped: true }],
},
});
expect(afterViolatingDataResponse.hits.hits.length).to.be(1);
const alertEvent = afterViolatingDataResponse.hits.hits[0].fields as Record<string, any>;
const exclude = ['@timestamp', ALERT_START, ALERT_UUID, ALERT_RULE_UUID, VERSION];
const toCompare = omit(alertEvent, exclude);
expectSnapshot(toCompare).toMatchInline(`
Object {
"event.action": Array [
"open",
],
"event.kind": Array [
"signal",
],
"kibana.alert.duration.us": Array [
0,
],
"kibana.alert.evaluation.threshold": Array [
30,
],
"kibana.alert.evaluation.value": Array [
50,
],
"kibana.alert.instance.id": Array [
"apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED",
],
"kibana.alert.reason": Array [
"Failed transactions rate is greater than 30% (current value is 50%) for opbeans-go",
],
"kibana.alert.rule.category": Array [
"Failed transaction rate threshold",
],
"kibana.alert.rule.consumer": Array [
"apm",
],
"kibana.alert.rule.name": Array [
"Failed transaction rate threshold | opbeans-go",
],
"kibana.alert.rule.producer": Array [
"apm",
],
"kibana.alert.rule.rule_type_id": Array [
"apm.transaction_error_rate",
],
"kibana.alert.status": Array [
"active",
],
"kibana.alert.workflow_status": Array [
"open",
],
"kibana.space_ids": Array [
"default",
],
"processor.event": Array [
"transaction",
],
"service.name": Array [
"opbeans-go",
],
"tags": Array [
"apm",
"service.name:opbeans-go",
],
"transaction.type": Array [
"request",
],
}
`);
await es.bulk({
index: APM_METRIC_INDEX_NAME,
body: [
{ index: {} },
createTransactionMetric({
event: {
outcome: 'success',
},
}),
{ index: {} },
createTransactionMetric({
event: {
outcome: 'success',
},
}),
],
refresh: true,
});
alert = await waitUntilNextExecution(alert);
const afterRecoveryResponse = await es.search({
index: targetIndices[0],
body: {
query: {
term: {
[EVENT_KIND]: 'signal',
},
},
size: 1,
sort: {
'@timestamp': 'desc',
},
_source: false,
fields: [{ field: '*', include_unmapped: true }],
},
});
expect(afterRecoveryResponse.hits.hits.length).to.be(1);
const recoveredAlertEvent = afterRecoveryResponse.hits.hits[0].fields as Record<
string,
any
>;
expect(recoveredAlertEvent[ALERT_STATUS]?.[0]).to.eql('recovered');
expect(recoveredAlertEvent[ALERT_DURATION]?.[0]).to.be.greaterThan(0);
expect(new Date(recoveredAlertEvent[ALERT_END]?.[0]).getTime()).to.be.greaterThan(0);
expectSnapshot(omit(recoveredAlertEvent, exclude.concat([ALERT_DURATION, ALERT_END])))
.toMatchInline(`
Object {
"event.action": Array [
"close",
],
"event.kind": Array [
"signal",
],
"kibana.alert.evaluation.threshold": Array [
30,
],
"kibana.alert.evaluation.value": Array [
50,
],
"kibana.alert.instance.id": Array [
"apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED",
],
"kibana.alert.reason": Array [
"Failed transactions rate is greater than 30% (current value is 50%) for opbeans-go",
],
"kibana.alert.rule.category": Array [
"Failed transaction rate threshold",
],
"kibana.alert.rule.consumer": Array [
"apm",
],
"kibana.alert.rule.name": Array [
"Failed transaction rate threshold | opbeans-go",
],
"kibana.alert.rule.producer": Array [
"apm",
],
"kibana.alert.rule.rule_type_id": Array [
"apm.transaction_error_rate",
],
"kibana.alert.status": Array [
"recovered",
],
"kibana.alert.workflow_status": Array [
"open",
],
"kibana.space_ids": Array [
"default",
],
"processor.event": Array [
"transaction",
],
"service.name": Array [
"opbeans-go",
],
"tags": Array [
"apm",
"service.name:opbeans-go",
],
"transaction.type": Array [
"request",
],
}
`);
});
});
});
registry.when('Rule registry with write not enabled', { config: 'basic', archives: [] }, () => {
it('does not bootstrap the apm rule indices', async () => {
const { body: targetIndices } = await getAlertsTargetIndices();
const errorOrUndefined = await es.indices
.get({
index: targetIndices[0],
expand_wildcards: 'open',
allow_no_indices: false,
})
.then(() => {})
.catch((error) => {
return error.toString();
});
expect(errorOrUndefined).not.to.be(undefined);
expect(errorOrUndefined).to.contain('index_not_found_exception');
});
});
}

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
export const APM_METRIC_INDEX_NAME = 'apm-8.0.0-transaction';
export const MAX_POLLS = 10;
export const BULK_INDEX_DELAY = 1000;
export const INDEXING_DELAY = 5000;
export const ALERTS_TARGET_INDICES_URL =
'/api/observability/rules/alerts/dynamic_index_pattern?namespace=default&registrationContexts=observability.apm&registrationContexts=';

View file

@ -0,0 +1,30 @@
/*
* 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 expect from '@kbn/expect';
import { GetService } from '../../types';
import { User } from '../authentication/types';
import { getAlertsTargetIndices } from './get_alerts_target_indices';
export const cleanupTargetIndices = async (getService: GetService, user: User, spaceId: string) => {
const es = getService('es');
try {
const { body: targetIndices } = await getAlertsTargetIndices(getService, user, spaceId);
const aliasMap = await es.indices.getAlias({
name: targetIndices,
allow_no_indices: true,
expand_wildcards: 'open',
});
const indices = Object.keys(aliasMap);
expect(indices.length > 0).to.be(true);
return es.indices.delete({ index: indices }, { ignore: [404] });
} catch (error) {
if (error.meta.statusCode !== 404) {
throw error;
}
}
};

View file

@ -0,0 +1,25 @@
/*
* 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 { User } from '../authentication/types';
import { GetService, AlertDef } from '../../types';
import { getSpaceUrlPrefix } from '../authentication/spaces';
export const createAlert = async (
getService: GetService,
user: User,
spaceId: string,
alertDef: AlertDef
) => {
const supertest = getService('supertestWithoutAuth');
const { body: response, status } = await supertest
.post(`${getSpaceUrlPrefix(spaceId)}/api/alerts/alert`)
.auth(user.username, user.password)
.send(alertDef)
.set('kbn-xsrf', 'foo');
return { alert: response, status };
};

View file

@ -0,0 +1,70 @@
/*
* 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 { APM_METRIC_INDEX_NAME } from '../../constants';
import { GetService } from '../../types';
export const createApmMetricIndex = async (getService: GetService) => {
const es = getService('es');
await es.indices.create({
index: APM_METRIC_INDEX_NAME,
body: {
mappings: {
dynamic: 'strict',
properties: {
event: {
properties: {
outcome: {
type: 'keyword',
},
},
},
processor: {
properties: {
event: {
type: 'keyword',
},
},
},
observer: {
properties: {
version_major: {
type: 'byte',
},
},
},
service: {
properties: {
name: {
type: 'keyword',
},
environment: {
type: 'keyword',
},
},
},
transaction: {
properties: {
type: {
type: 'keyword',
},
duration: {
properties: {
histogram: {
type: 'histogram',
},
},
},
},
},
'@timestamp': {
type: 'date',
},
},
},
},
});
};

View file

@ -0,0 +1,42 @@
/*
* 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 { merge } from 'lodash';
import { INDEXING_DELAY } from '../../constants';
export const createTransactionMetric = (override: Record<string, any>) => {
const now = Date.now();
const time = now - INDEXING_DELAY;
return merge(
{
'@timestamp': new Date(time).toISOString(),
service: {
name: 'opbeans-go',
},
event: {
outcome: 'success',
},
transaction: {
duration: {
histogram: {
values: [1000000],
counts: [1],
},
},
type: 'request',
},
processor: {
event: 'metric',
},
observer: {
version_major: 7,
},
},
override
);
};

View file

@ -0,0 +1,49 @@
/*
* 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 { APM_METRIC_INDEX_NAME } from '../../constants';
import { GetService } from '../../types';
import { getSpaceUrlPrefix } from '../authentication/spaces';
import { User } from '../authentication/types';
import { getAlertsTargetIndices } from './get_alerts_target_indices';
export const deleteAlert = async (
getService: GetService,
user: User,
spaceId: string,
id: string | undefined
) => {
const es = getService('es');
const supertest = getService('supertestWithoutAuth');
const { body: targetIndices } = await getAlertsTargetIndices(getService, user, spaceId);
if (id) {
const { body, status } = await supertest
.delete(`${getSpaceUrlPrefix(spaceId)}/api/alerts/alert/${id}`)
.auth(user.username, user.password)
.set('kbn-xsrf', 'foo');
if (status >= 300) {
const error = new Error('Error deleting alert');
Object.assign(error, { response: { body, status } });
throw error;
}
}
await es.deleteByQuery({
index: targetIndices[0],
body: {
query: {
match_all: {},
},
},
refresh: true,
});
await es.indices.delete({
index: APM_METRIC_INDEX_NAME,
});
};

View file

@ -0,0 +1,23 @@
/*
* 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 { ALERTS_TARGET_INDICES_URL } from '../../constants';
import { GetService } from '../../types';
import { User } from '../authentication/types';
import { getSpaceUrlPrefix } from '../authentication/spaces';
export const getAlertsTargetIndices = async (
getService: GetService,
user: User,
spaceId: string
) => {
const supertest = getService('supertestWithoutAuth');
return supertest
.get(`${getSpaceUrlPrefix(spaceId)}${ALERTS_TARGET_INDICES_URL}`)
.auth(user.username, user.password)
.send()
.set('kbn-xsrf', 'foo');
};

View file

@ -0,0 +1,14 @@
/*
* 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.
*/
export * from './create_alert';
export * from './create_apm_metric_index';
export * from './create_transaction_metric';
export * from './get_alerts_target_indices';
export * from './wait_until_next_execution';
export * from './cleanup_target_indices';
export * from './delete_alert';

View file

@ -0,0 +1,81 @@
/*
* 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 { GetService } from '../../types';
import { getAlertsTargetIndices } from './get_alerts_target_indices';
import { BULK_INDEX_DELAY, MAX_POLLS } from '../../constants';
import { Alert } from '../../../../../plugins/alerting/common';
import { getSpaceUrlPrefix } from '../authentication/spaces';
import { User } from '../authentication/types';
export async function waitUntilNextExecution(
getService: GetService,
user: User,
alert: Alert,
spaceId: string,
intervalInSeconds: number = 1,
count: number = 0
): Promise<Alert> {
const supertest = getService('supertestWithoutAuth');
const es = getService('es');
await new Promise((resolve) => {
setTimeout(resolve, intervalInSeconds * 1000);
});
const { body, status } = await supertest
.get(`${getSpaceUrlPrefix(spaceId)}/api/alerts/alert/${alert.id}`)
.auth(user.username, user.password)
.set('kbn-xsrf', 'foo');
const { body: targetIndices, status: targetIndicesStatus } = await getAlertsTargetIndices(
getService,
user,
spaceId
);
if (targetIndices.length === 0) {
const error = new Error('Error getting target indices');
Object.assign(error, { response: { body: targetIndices, status: targetIndicesStatus } });
throw error;
}
if (status >= 300) {
const error = new Error('Error getting alert');
Object.assign(error, { response: { body, status } });
throw error;
}
const nextAlert = body as Alert;
if (nextAlert.executionStatus.lastExecutionDate !== alert.executionStatus.lastExecutionDate) {
await new Promise((resolve) => {
setTimeout(resolve, BULK_INDEX_DELAY);
});
/**
* When calling refresh on an index pattern .alerts-observability.apm.alerts* (as was originally the hard-coded string in this test)
* The response from Elasticsearch is a 200, even if no indices which match that index pattern have been created.
* When calling refresh on a concrete index alias .alerts-observability.apm.alerts-default for instance,
* we receive a 404 error index_not_found_exception when no indices have been created which match that alias (obviously).
* Since we are receiving a concrete index alias from the observability api instead of a kibana index pattern
* and we understand / expect that this index does not exist at certain points of the test, we can try-catch at certain points without caring if the call fails.
* There are points in the code where we do want to ensure we get the appropriate error message back
*/
try {
await es.indices.refresh({
index: targetIndices[0],
});
// eslint-disable-next-line no-empty
} catch (exc) {}
return nextAlert;
}
if (count >= MAX_POLLS) {
throw new Error('Maximum number of polls exceeded');
}
return waitUntilNextExecution(getService, user, alert, spaceId, intervalInSeconds, count + 1);
}

View file

@ -0,0 +1,22 @@
/*
* 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 { GenericFtrProviderContext } from '@kbn/test';
import { Alert, AlertTypeParams } from '../../../plugins/alerting/common';
import { services } from './services';
export type GetService = GenericFtrProviderContext<typeof services, {}>['getService'];
export interface AlertParams extends AlertTypeParams {
windowSize?: number;
windowUnit?: string;
threshold?: number;
serviceName?: string;
transactionType?: string;
environment?: string;
}
export type AlertDef<Params extends AlertTypeParams = {}> = Partial<Alert<Params>>;

View file

@ -0,0 +1,16 @@
/*
* 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 { createTestConfig } from '../common/config';
// eslint-disable-next-line import/no-default-export
export default createTestConfig('spaces_only', {
license: 'basic',
disabledPlugins: ['security'],
ssl: false,
testFiles: [require.resolve('./tests/basic')],
});

View file

@ -0,0 +1,42 @@
/*
* 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 expect from '@kbn/expect';
import type { FtrProviderContext } from '../../../common/ftr_provider_context';
import { obsOnlyRead } from '../../../common/lib/authentication/users';
import { getAlertsTargetIndices } from '../../../common/lib/helpers';
// eslint-disable-next-line import/no-default-export
export default function registryRulesApiTest({ getService }: FtrProviderContext) {
const es = getService('es');
describe('Rule Registry API', () => {
describe('with read permissions', () => {
it('does not bootstrap the apm rule indices', async () => {
const { body: targetIndices } = await getAlertsTargetIndices(
getService,
obsOnlyRead,
'space1'
);
const errorOrUndefined = await es.indices
.get({
index: targetIndices[0],
expand_wildcards: 'open',
allow_no_indices: false,
})
.then(() => {})
.catch((error) => {
return error.toString();
});
expect(errorOrUndefined).not.to.be(undefined);
expect(errorOrUndefined).to.contain('index_not_found_exception');
});
});
});
}

View file

@ -0,0 +1,28 @@
/*
* 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 { FtrProviderContext } from '../../../common/ftr_provider_context';
import { createSpaces, deleteSpaces } from '../../../common/lib/authentication';
// eslint-disable-next-line import/no-default-export
export default ({ loadTestFile, getService }: FtrProviderContext): void => {
describe('rule registry spaces only: trial', function () {
// Fastest ciGroup for the moment.
this.tags('ciGroup5');
before(async () => {
await createSpaces(getService);
});
after(async () => {
await deleteSpaces(getService);
});
// Basic
loadTestFile(require.resolve('./bootstrap'));
});
};

View file

@ -0,0 +1,124 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rule registry spaces only: trial Rule Registry API with write permissions when creating a rule writes alerts data to the alert indices 1`] = `
Object {
"event.action": Array [
"open",
],
"event.kind": Array [
"signal",
],
"kibana.alert.duration.us": Array [
0,
],
"kibana.alert.evaluation.threshold": Array [
30,
],
"kibana.alert.evaluation.value": Array [
50,
],
"kibana.alert.instance.id": Array [
"apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED",
],
"kibana.alert.reason": Array [
"Failed transactions rate is greater than 30% (current value is 50%) for opbeans-go",
],
"kibana.alert.rule.category": Array [
"Failed transaction rate threshold",
],
"kibana.alert.rule.consumer": Array [
"apm",
],
"kibana.alert.rule.name": Array [
"Failed transaction rate threshold | opbeans-go",
],
"kibana.alert.rule.producer": Array [
"apm",
],
"kibana.alert.rule.rule_type_id": Array [
"apm.transaction_error_rate",
],
"kibana.alert.status": Array [
"active",
],
"kibana.alert.workflow_status": Array [
"open",
],
"kibana.space_ids": Array [
"space1",
],
"processor.event": Array [
"transaction",
],
"service.name": Array [
"opbeans-go",
],
"tags": Array [
"apm",
"service.name:opbeans-go",
],
"transaction.type": Array [
"request",
],
}
`;
exports[`rule registry spaces only: trial Rule Registry API with write permissions when creating a rule writes alerts data to the alert indices 2`] = `
Object {
"event.action": Array [
"close",
],
"event.kind": Array [
"signal",
],
"kibana.alert.evaluation.threshold": Array [
30,
],
"kibana.alert.evaluation.value": Array [
50,
],
"kibana.alert.instance.id": Array [
"apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED",
],
"kibana.alert.reason": Array [
"Failed transactions rate is greater than 30% (current value is 50%) for opbeans-go",
],
"kibana.alert.rule.category": Array [
"Failed transaction rate threshold",
],
"kibana.alert.rule.consumer": Array [
"apm",
],
"kibana.alert.rule.name": Array [
"Failed transaction rate threshold | opbeans-go",
],
"kibana.alert.rule.producer": Array [
"apm",
],
"kibana.alert.rule.rule_type_id": Array [
"apm.transaction_error_rate",
],
"kibana.alert.status": Array [
"recovered",
],
"kibana.alert.workflow_status": Array [
"open",
],
"kibana.space_ids": Array [
"space1",
],
"processor.event": Array [
"transaction",
],
"service.name": Array [
"opbeans-go",
],
"tags": Array [
"apm",
"service.name:opbeans-go",
],
"transaction.type": Array [
"request",
],
}
`;

View file

@ -0,0 +1,252 @@
/*
* 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 expect from '@kbn/expect';
import {
ALERT_DURATION,
ALERT_END,
ALERT_RULE_UUID,
ALERT_START,
ALERT_STATUS,
ALERT_UUID,
EVENT_KIND,
VERSION,
} from '@kbn/rule-data-utils';
import { omit } from 'lodash';
import type { FtrProviderContext } from '../../../common/ftr_provider_context';
import {
getAlertsTargetIndices,
createApmMetricIndex,
createAlert,
waitUntilNextExecution,
createTransactionMetric,
cleanupTargetIndices,
deleteAlert,
} from '../../../common/lib/helpers';
import { AlertDef, AlertParams } from '../../../common/types';
import { Alert } from '../../../../../plugins/alerting/common';
import { APM_METRIC_INDEX_NAME } from '../../../common/constants';
import { obsOnly } from '../../../common/lib/authentication/users';
const SPACE_ID = 'space1';
// eslint-disable-next-line import/no-default-export
export default function registryRulesApiTest({ getService }: FtrProviderContext) {
const es = getService('es');
describe('Rule Registry API', () => {
describe('with write permissions', () => {
it('does not bootstrap indices on plugin startup', async () => {
const { body: targetIndices } = await getAlertsTargetIndices(getService, obsOnly, SPACE_ID);
try {
const res = await es.indices.get({
index: targetIndices[0],
expand_wildcards: 'open',
allow_no_indices: true,
});
expect(res).to.be.empty();
} catch (exc) {
expect(exc.statusCode).to.eql(404);
}
});
describe('when creating a rule', () => {
let createResponse: {
alert: Alert;
status: number;
};
before(async () => {
await createApmMetricIndex(getService);
const alertDef: AlertDef<AlertParams> = {
params: {
threshold: 30,
windowSize: 5,
windowUnit: 'm',
transactionType: 'request',
environment: 'ENVIRONMENT_ALL',
serviceName: 'opbeans-go',
},
consumer: 'apm',
alertTypeId: 'apm.transaction_error_rate',
schedule: { interval: '5s' },
actions: [],
tags: ['apm', 'service.name:opbeans-go'],
notifyWhen: 'onActionGroupChange',
name: 'Failed transaction rate threshold | opbeans-go',
};
createResponse = await createAlert(getService, obsOnly, SPACE_ID, alertDef);
});
after(async () => {
await deleteAlert(getService, obsOnly, SPACE_ID, createResponse.alert.id);
await cleanupTargetIndices(getService, obsOnly, SPACE_ID);
});
it('writes alerts data to the alert indices', async () => {
expect(createResponse.status).to.be.below(299);
expect(createResponse.alert).not.to.be(undefined);
let alert = await waitUntilNextExecution(
getService,
obsOnly,
createResponse.alert,
SPACE_ID
);
const { body: targetIndices } = await getAlertsTargetIndices(
getService,
obsOnly,
SPACE_ID
);
try {
const res = await es.search({
index: targetIndices[0],
body: {
query: {
term: {
[EVENT_KIND]: 'signal',
},
},
size: 1,
sort: {
'@timestamp': 'desc',
},
},
});
expect(res).to.be.empty();
} catch (exc) {
expect(exc.message).contain('index_not_found_exception');
}
await es.index({
index: APM_METRIC_INDEX_NAME,
body: createTransactionMetric({
event: {
outcome: 'success',
},
}),
refresh: true,
});
alert = await waitUntilNextExecution(getService, obsOnly, alert, SPACE_ID);
try {
const res = await es.search({
index: targetIndices[0],
body: {
query: {
term: {
[EVENT_KIND]: 'signal',
},
},
size: 1,
sort: {
'@timestamp': 'desc',
},
},
});
expect(res).to.be.empty();
} catch (exc) {
expect(exc.message).contain('index_not_found_exception');
}
await es.index({
index: APM_METRIC_INDEX_NAME,
body: createTransactionMetric({
event: {
outcome: 'failure',
},
}),
refresh: true,
});
alert = await waitUntilNextExecution(getService, obsOnly, alert, SPACE_ID);
const afterViolatingDataResponse = await es.search({
index: targetIndices[0],
body: {
query: {
term: {
[EVENT_KIND]: 'signal',
},
},
size: 1,
sort: {
'@timestamp': 'desc',
},
_source: false,
fields: [{ field: '*', include_unmapped: true }],
},
});
expect(afterViolatingDataResponse.hits.hits.length).to.be(1);
const alertEvent = afterViolatingDataResponse.hits.hits[0].fields as Record<string, any>;
const exclude = ['@timestamp', ALERT_START, ALERT_UUID, ALERT_RULE_UUID, VERSION];
const toCompare = omit(alertEvent, exclude);
expectSnapshot(toCompare).toMatch();
await es.bulk({
index: APM_METRIC_INDEX_NAME,
body: [
{ index: {} },
createTransactionMetric({
event: {
outcome: 'success',
},
}),
{ index: {} },
createTransactionMetric({
event: {
outcome: 'success',
},
}),
],
refresh: true,
});
alert = await waitUntilNextExecution(getService, obsOnly, alert, SPACE_ID);
const afterRecoveryResponse = await es.search({
index: targetIndices[0],
body: {
query: {
term: {
[EVENT_KIND]: 'signal',
},
},
size: 1,
sort: {
'@timestamp': 'desc',
},
_source: false,
fields: [{ field: '*', include_unmapped: true }],
},
});
expect(afterRecoveryResponse.hits.hits.length).to.be(1);
const recoveredAlertEvent = afterRecoveryResponse.hits.hits[0].fields as Record<
string,
any
>;
expect(recoveredAlertEvent[ALERT_STATUS]?.[0]).to.eql('recovered');
expect(recoveredAlertEvent[ALERT_DURATION]?.[0]).to.be.greaterThan(0);
expect(new Date(recoveredAlertEvent[ALERT_END]?.[0]).getTime()).to.be.greaterThan(0);
expectSnapshot(
omit(recoveredAlertEvent, exclude.concat([ALERT_DURATION, ALERT_END]))
).toMatch();
});
});
});
});
}

View file

@ -22,8 +22,9 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => {
await deleteSpaces(getService);
});
// Basic
// Trial
loadTestFile(require.resolve('./get_alert_by_id'));
loadTestFile(require.resolve('./update_alert'));
loadTestFile(require.resolve('./create_rule'));
});
};