[Alerting] replace watcher http APIs used by index threshold Alerting (#59475)

Prior to this PR, the alerting UI used two HTTP endpoints provided by the
Kibana watcher plugin, to list index and field names.  There are now two HTTP
endpoints in the alerting_builtins plugin which will be used instead.

The code for the new endpoints was largely copied from the existing watcher
endpoints, and the HTTP request/response bodies kept pretty much the same.

resolves https://github.com/elastic/kibana/issues/53041
This commit is contained in:
Patrick Mueller 2020-03-09 19:30:20 -04:00 committed by GitHub
parent 6395dac297
commit 3f365a82f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 685 additions and 27 deletions

View file

@ -125,8 +125,77 @@ server log [17:32:10.060] [warning][actions][actions][plugins] \
## http endpoints
An HTTP endpoint is provided to return the values the alertType would calculate,
over a series of time. This is intended to be used in the alerting UI to
The following endpoints are provided for this alert type:
- `POST /api/alerting_builtins/index_threshold/_indices`
- `POST /api/alerting_builtins/index_threshold/_fields`
- `POST /api/alerting_builtins/index_threshold/_time_series_query`
### `POST .../_indices`
This HTTP endpoint is provided for the alerting ui to list the available
"index names" for the user to select to use with the alert. This API also
returns aliases which match the supplied pattern.
The request body is expected to be a JSON object in the following form, where the
`pattern` value may include comma-separated names and wildcards.
```js
{
pattern: "index-name-pattern"
}
```
The response body is a JSON object in the following form, where each element
of the `indices` array is the name of an index or alias. The number of elements
returned is limited, as this API is intended to be used to help narrow down
index names to use with the alert, and not support pagination, etc.
```js
{
indices: ["index-name-1", "alias-name-1", ...]
}
```
### `POST .../_fields`
This HTTP endpoint is provided for the alerting ui to list the available
fields for the user to select to use with the alert.
The request body is expected to be a JSON object in the following form, where the
`indexPatterns` array elements may include comma-separated names and wildcards.
```js
{
indexPatterns: ["index-pattern-1", "index-pattern-2"]
}
```
The response body is a JSON object in the following form, where each element
fields array is a field object.
```js
{
fields: [fieldObject1, fieldObject2, ...]
}
```
A field object is the following shape:
```typescript
{
name: string, // field name
type: string, // field type - eg 'keyword', 'date', 'long', etc
normalizedType: string, // for numeric types, this will be 'number'
aggregatable: true, // value from elasticsearch field capabilities
searchable: true, // value from elasticsearch field capabilities
}
```
### `POST .../_time_series_query`
This HTTP endpoint is provided to return the values the alertType would calculate,
over a series of time. It is intended to be used in the alerting UI to
provide a "preview" of the alert during creation/editing based on recent data,
and could be used to show a "simulation" of the the alert over an arbitrary
range of time.

View file

@ -7,7 +7,7 @@
import { Service, AlertingSetup, IRouter } from '../../types';
import { timeSeriesQuery } from './lib/time_series_query';
import { getAlertType } from './alert_type';
import { createTimeSeriesQueryRoute } from './routes';
import { registerRoutes } from './routes';
// future enhancement: make these configurable?
export const MAX_INTERVALS = 1000;
@ -32,6 +32,6 @@ export function register(params: RegisterParams) {
alerting.registerType(getAlertType(service));
const alertTypeBaseRoute = `${baseRoute}/index_threshold`;
createTimeSeriesQueryRoute(service, router, alertTypeBaseRoute);
const baseBuiltInRoute = `${baseRoute}/index_threshold`;
registerRoutes({ service, router, baseRoute: baseBuiltInRoute });
}

View file

@ -0,0 +1,142 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// the business logic of this code is from watcher, in:
// x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts
import { schema, TypeOf } from '@kbn/config-schema';
import {
IRouter,
RequestHandlerContext,
KibanaRequest,
IKibanaResponse,
KibanaResponseFactory,
IScopedClusterClient,
} from 'kibana/server';
import { Service } from '../../../types';
const bodySchema = schema.object({
indexPatterns: schema.arrayOf(schema.string()),
});
type RequestBody = TypeOf<typeof bodySchema>;
export function createFieldsRoute(service: Service, router: IRouter, baseRoute: string) {
const path = `${baseRoute}/_fields`;
service.logger.debug(`registering indexThreshold route POST ${path}`);
router.post(
{
path,
validate: {
body: bodySchema,
},
},
handler
);
async function handler(
ctx: RequestHandlerContext,
req: KibanaRequest<any, any, RequestBody, any>,
res: KibanaResponseFactory
): Promise<IKibanaResponse> {
service.logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`);
let rawFields: RawFields;
// special test for no patterns, otherwise all are returned!
if (req.body.indexPatterns.length === 0) {
return res.ok({ body: { fields: [] } });
}
try {
rawFields = await getRawFields(ctx.core.elasticsearch.dataClient, req.body.indexPatterns);
} catch (err) {
service.logger.debug(`route ${path} error: ${err.message}`);
return res.internalError({ body: 'error getting field data' });
}
const result = { fields: getFieldsFromRawFields(rawFields) };
service.logger.debug(`route ${path} response: ${JSON.stringify(result)}`);
return res.ok({ body: result });
}
}
// RawFields is a structure with the following shape:
// {
// "fields": {
// "_routing": { "_routing": { "type": "_routing", "searchable": true, "aggregatable": false}},
// "host": { "keyword": { "type": "keyword", "searchable": true, "aggregatable": true}},
// ...
// }
interface RawFields {
fields: Record<string, Record<string, RawField>>;
}
interface RawField {
type: string;
searchable: boolean;
aggregatable: boolean;
}
interface Field {
name: string;
type: string;
normalizedType: string;
searchable: boolean;
aggregatable: boolean;
}
async function getRawFields(
dataClient: IScopedClusterClient,
indexes: string[]
): Promise<RawFields> {
const params = {
index: indexes,
fields: ['*'],
ignoreUnavailable: true,
allowNoIndices: true,
ignore: 404,
};
const result = await dataClient.callAsCurrentUser('fieldCaps', params);
return result as RawFields;
}
function getFieldsFromRawFields(rawFields: RawFields): Field[] {
const result: Field[] = [];
if (!rawFields || !rawFields.fields) {
return [];
}
for (const name of Object.keys(rawFields.fields)) {
const rawField = rawFields.fields[name];
const type = Object.keys(rawField)[0];
const values = rawField[type];
if (!type || type.startsWith('_')) continue;
if (!values) continue;
const normalizedType = normalizedFieldTypes[type] || type;
const aggregatable = values.aggregatable;
const searchable = values.searchable;
result.push({ name, type, normalizedType, aggregatable, searchable });
}
result.sort((a, b) => a.name.localeCompare(b.name));
return result;
}
const normalizedFieldTypes: Record<string, string> = {
long: 'number',
integer: 'number',
short: 'number',
byte: 'number',
double: 'number',
float: 'number',
half_float: 'number',
scaled_float: 'number',
};

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;
* you may not use this file except in compliance with the Elastic License.
*/
import { Service, IRouter } from '../../../types';
import { createTimeSeriesQueryRoute } from './time_series_query';
import { createFieldsRoute } from './fields';
import { createIndicesRoute } from './indices';
interface RegisterRoutesParams {
service: Service;
router: IRouter;
baseRoute: string;
}
export function registerRoutes(params: RegisterRoutesParams) {
const { service, router, baseRoute } = params;
createTimeSeriesQueryRoute(service, router, baseRoute);
createFieldsRoute(service, router, baseRoute);
createIndicesRoute(service, router, baseRoute);
}

View file

@ -0,0 +1,136 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// the business logic of this code is from watcher, in:
// x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts
const MAX_INDICES = 20;
import { schema, TypeOf } from '@kbn/config-schema';
import {
IRouter,
RequestHandlerContext,
KibanaRequest,
IKibanaResponse,
KibanaResponseFactory,
IScopedClusterClient,
} from 'kibana/server';
import { Service } from '../../../types';
const bodySchema = schema.object({
pattern: schema.string(),
});
type RequestBody = TypeOf<typeof bodySchema>;
export function createIndicesRoute(service: Service, router: IRouter, baseRoute: string) {
const path = `${baseRoute}/_indices`;
service.logger.debug(`registering indexThreshold route POST ${path}`);
router.post(
{
path,
validate: {
body: bodySchema,
},
},
handler
);
async function handler(
ctx: RequestHandlerContext,
req: KibanaRequest<any, any, RequestBody, any>,
res: KibanaResponseFactory
): Promise<IKibanaResponse> {
const pattern = req.body.pattern;
service.logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`);
if (pattern.trim() === '') {
return res.ok({ body: { indices: [] } });
}
let aliases: string[] = [];
try {
aliases = await getAliasesFromPattern(ctx.core.elasticsearch.dataClient, pattern);
} catch (err) {
service.logger.debug(`route ${path} error: ${err.message}`);
return res.internalError({ body: 'error getting alias data' });
}
let indices: string[] = [];
try {
indices = await getIndicesFromPattern(ctx.core.elasticsearch.dataClient, pattern);
} catch (err) {
service.logger.debug(`route ${path} error: ${err.message}`);
return res.internalError({ body: 'error getting index data' });
}
const result = { indices: uniqueCombined(aliases, indices, MAX_INDICES) };
service.logger.debug(`route ${path} response: ${JSON.stringify(result)}`);
return res.ok({ body: result });
}
}
function uniqueCombined(list1: string[], list2: string[], limit: number) {
const set = new Set(list1.concat(list2));
const result = Array.from(set);
result.sort((string1, string2) => string1.localeCompare(string2));
return result.slice(0, limit);
}
async function getIndicesFromPattern(
dataClient: IScopedClusterClient,
pattern: string
): Promise<string[]> {
const params = {
index: pattern,
ignore: [404],
ignoreUnavailable: true,
body: {
size: 0, // no hits
aggs: {
indices: {
terms: {
field: '_index',
size: MAX_INDICES,
},
},
},
},
};
const response = await dataClient.callAsCurrentUser('search', params);
if (response.status === 404 || !response.aggregations) {
return [];
}
return response.aggregations.indices.buckets.map((bucket: any) => bucket.key);
}
async function getAliasesFromPattern(
dataClient: IScopedClusterClient,
pattern: string
): Promise<string[]> {
const params = {
index: pattern,
ignoreUnavailable: true,
ignore: [404],
};
const result: string[] = [];
const response = await dataClient.callAsCurrentUser('indices.getAlias', params);
if (response.status === 404) {
return result;
}
for (const index of Object.keys(response)) {
const aliasRecord = response[index];
if (aliasRecord.aliases) {
const aliases = Object.keys(aliasRecord.aliases);
result.push(...aliases);
}
}
return result;
}

View file

@ -12,13 +12,13 @@ import {
KibanaResponseFactory,
} from 'kibana/server';
import { Service } from '../../types';
import { TimeSeriesQuery, TimeSeriesQuerySchema, TimeSeriesResult } from './lib/time_series_types';
export { TimeSeriesQuery, TimeSeriesResult } from './lib/time_series_types';
import { Service } from '../../../types';
import { TimeSeriesQuery, TimeSeriesQuerySchema, TimeSeriesResult } from '../lib/time_series_types';
export { TimeSeriesQuery, TimeSeriesResult } from '../lib/time_series_types';
export function createTimeSeriesQueryRoute(service: Service, router: IRouter, baseRoute: string) {
const path = `${baseRoute}/_time_series_query`;
service.logger.debug(`registering indexThreshold timeSeriesQuery route POST ${path}`);
service.logger.debug(`registering indexThreshold route POST ${path}`);
router.post(
{
path,
@ -33,7 +33,7 @@ export function createTimeSeriesQueryRoute(service: Service, router: IRouter, ba
req: KibanaRequest<any, any, TimeSeriesQuery, any>,
res: KibanaResponseFactory
): Promise<IKibanaResponse> {
service.logger.debug(`route query_data request: ${JSON.stringify(req.body, null, 4)}`);
service.logger.debug(`route query_data request: ${JSON.stringify(req.body)}`);
let result: TimeSeriesResult;
try {
@ -47,7 +47,7 @@ export function createTimeSeriesQueryRoute(service: Service, router: IRouter, ba
return res.internalError({ body: 'error running time series query' });
}
service.logger.debug(`route query_data response: ${JSON.stringify(result, null, 4)}`);
service.logger.debug(`route query_data response: ${JSON.stringify(result)}`);
return res.ok({ body: result });
}
}

View file

@ -7,9 +7,7 @@ import { HttpSetup } from 'kibana/public';
import { TimeSeriesResult } from '../types';
export { TimeSeriesResult } from '../types';
const WATCHER_API_ROOT = '/api/watcher';
// TODO: replace watcher api with the proper from alerts
const INDEX_THRESHOLD_API_ROOT = '/api/alerting_builtins/index_threshold';
export async function getMatchingIndicesForThresholdAlertType({
pattern,
@ -24,7 +22,7 @@ export async function getMatchingIndicesForThresholdAlertType({
if (!pattern.endsWith('*')) {
pattern = `${pattern}*`;
}
const { indices } = await http.post(`${WATCHER_API_ROOT}/indices`, {
const { indices } = await http.post(`${INDEX_THRESHOLD_API_ROOT}/_indices`, {
body: JSON.stringify({ pattern }),
});
return indices;
@ -37,8 +35,8 @@ export async function getThresholdAlertTypeFields({
indexes: string[];
http: HttpSetup;
}): Promise<Record<string, any>> {
const { fields } = await http.post(`${WATCHER_API_ROOT}/fields`, {
body: JSON.stringify({ indexes }),
const { fields } = await http.post(`${INDEX_THRESHOLD_API_ROOT}/_fields`, {
body: JSON.stringify({ indexPatterns: indexes }),
});
return fields;
}
@ -62,8 +60,6 @@ export const loadIndexPatterns = async () => {
return savedObjects;
};
const TimeSeriesQueryRoute = '/api/alerting_builtins/index_threshold/_time_series_query';
interface GetThresholdAlertVisualizationDataParams {
model: any;
visualizeOptions: any;
@ -90,7 +86,7 @@ export async function getThresholdAlertVisualizationData({
interval: visualizeOptions.interval,
};
return await http.post<TimeSeriesResult>(TimeSeriesQueryRoute, {
return await http.post<TimeSeriesResult>(`${INDEX_THRESHOLD_API_ROOT}/_time_series_query`, {
body: JSON.stringify(timeSeriesQueryParams),
});
}

View file

@ -24,9 +24,9 @@ const DOCUMENT_SOURCE = 'queryDataEndpointTests';
export async function createEsDocuments(
es: any,
esTestIndexTool: ESTestIndexTool,
startDate: string,
intervals: number,
intervalMillis: number
startDate: string = START_DATE,
intervals: number = 1,
intervalMillis: number = 1000
) {
const totalDocuments = intervals * 2;
const startDateMillis = Date.parse(startDate) - intervalMillis / 2;

View file

@ -0,0 +1,166 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect';
import { Spaces } from '../../../../scenarios';
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
import { ESTestIndexTool, ES_TEST_INDEX_NAME, getUrlPrefix } from '../../../../../common/lib';
const API_URI = 'api/alerting_builtins/index_threshold/_fields';
// eslint-disable-next-line import/no-default-export
export default function fieldsEndpointTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const retry = getService('retry');
const es = getService('legacyEs');
const esTestIndexTool = new ESTestIndexTool(es, retry);
describe('fields endpoint', () => {
before(async () => {
await esTestIndexTool.destroy();
await esTestIndexTool.setup();
});
after(async () => {
await esTestIndexTool.destroy();
});
// this test will start failing if the fields/mappings of
// the ES_TEST_INDEX changes
it('should return fields from the test index', async () => {
const query = { indexPatterns: [ES_TEST_INDEX_NAME] };
const result = await runQueryExpect(query, 200);
expect(result.fields).to.be.an('array');
let field = getFieldNamed(result.fields, 'source');
expect(field).to.eql({
name: 'source',
type: 'keyword',
normalizedType: 'keyword',
aggregatable: true,
searchable: true,
});
field = getFieldNamed(result.fields, 'date');
expect(field).to.eql({
name: 'date',
type: 'date',
normalizedType: 'date',
aggregatable: true,
searchable: true,
});
field = getFieldNamed(result.fields, 'testedValue');
expect(field).to.eql({
name: 'testedValue',
type: 'long',
normalizedType: 'number',
aggregatable: true,
searchable: true,
});
});
it('should return errors when expected', async () => {
expect(await runQueryExpect(null, 400)).to.eql(
bodyWithMessage('[request body]: expected a plain object value, but found [null] instead.')
);
expect(await runQueryExpect({}, 400)).to.eql(
bodyWithMessage(
'[request body.indexPatterns]: expected value of type [array] but got [undefined]'
)
);
expect(await runQueryExpect({ indices: ['*'] }, 400)).to.eql(
bodyWithMessage(
'[request body.indexPatterns]: expected value of type [array] but got [undefined]'
)
);
expect(await runQueryExpect({ indexPatterns: 'foo' }, 400)).to.eql(
bodyWithMessage('[request body.indexPatterns]: could not parse array value from json input')
);
expect(await runQueryExpect({ indexPatterns: [1] }, 400)).to.eql(
bodyWithMessage(
'[request body.indexPatterns.0]: expected value of type [string] but got [number]'
)
);
function bodyWithMessage(message: string): any {
return {
error: 'Bad Request',
message,
statusCode: 400,
};
}
});
it('should return an empty array for empty input', async () => {
const result = await runQueryExpect({ indexPatterns: [] }, 200);
expect(result.fields).to.be.an('array');
expect(result.fields.length).to.be(0);
});
it('should handle indices that do not exist', async () => {
const NON_EXISTANT_INDEX_NAME = 'non-existent-index-name-foo';
const exactResult = await runQueryExpect({ indexPatterns: [ES_TEST_INDEX_NAME] }, 200);
let indexPatterns = [NON_EXISTANT_INDEX_NAME];
let testResult = await runQueryExpect({ indexPatterns }, 200);
expect(testResult.fields.length).to.be(0);
indexPatterns = [ES_TEST_INDEX_NAME, NON_EXISTANT_INDEX_NAME];
testResult = await runQueryExpect({ indexPatterns }, 200);
expect(testResult).to.eql(exactResult);
indexPatterns = [NON_EXISTANT_INDEX_NAME, ES_TEST_INDEX_NAME];
testResult = await runQueryExpect({ indexPatterns }, 200);
expect(testResult).to.eql(exactResult);
});
it('should handle wildcards', async () => {
const exactResult = await runQueryExpect({ indexPatterns: [ES_TEST_INDEX_NAME] }, 200);
let indexPatterns = [`*${ES_TEST_INDEX_NAME}`];
let testResult = await runQueryExpect({ indexPatterns }, 200);
expect(testResult).to.eql(exactResult);
indexPatterns = [`${ES_TEST_INDEX_NAME}*`];
testResult = await runQueryExpect({ indexPatterns }, 200);
expect(testResult).to.eql(exactResult);
});
it('should handle aliases', async () => {
const result = await runQueryExpect({ indexPatterns: ['.kibana'] }, 200);
const field = getFieldNamed(result.fields, 'updated_at');
expect(field).to.be.ok();
expect(field.name).to.eql('updated_at');
expect(field.type).to.eql('date');
});
});
function getFieldNamed(fields: any[], fieldName: string): any | undefined {
const matching = fields.filter(field => field.name === fieldName);
if (matching.length === 0) return;
if (matching.length === 1) return matching[0];
throw new Error(`multiple fields named ${fieldName}`);
}
async function runQueryExpect(requestBody: any, status: number): Promise<any> {
const url = `${getUrlPrefix(Spaces.space1.id)}/${API_URI}`;
const res = await supertest
.post(url)
.set('kbn-xsrf', 'foo')
.send(requestBody);
if (res.status !== status) {
// good place to put a console log for debugging unexpected results
// console.log(res.body)
throw new Error(`expected status ${status}, but got ${res.status}`);
}
return res.body;
}
}

View file

@ -9,6 +9,8 @@ import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function alertingTests({ loadTestFile }: FtrProviderContext) {
describe('index_threshold', () => {
loadTestFile(require.resolve('./query_data_endpoint'));
loadTestFile(require.resolve('./time_series_query_endpoint'));
loadTestFile(require.resolve('./fields_endpoint'));
loadTestFile(require.resolve('./indices_endpoint'));
});
}

View file

@ -0,0 +1,125 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect';
import { Spaces } from '../../../../scenarios';
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
import { ESTestIndexTool, ES_TEST_INDEX_NAME, getUrlPrefix } from '../../../../../common/lib';
import { createEsDocuments } from './create_test_data';
const API_URI = 'api/alerting_builtins/index_threshold/_indices';
// eslint-disable-next-line import/no-default-export
export default function indicesEndpointTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const retry = getService('retry');
const es = getService('legacyEs');
const esTestIndexTool = new ESTestIndexTool(es, retry);
describe('indices endpoint', () => {
before(async () => {
await esTestIndexTool.destroy();
await esTestIndexTool.setup();
await createEsDocuments(es, esTestIndexTool);
});
after(async () => {
await esTestIndexTool.destroy();
});
it('should return the test index', async () => {
const query = { pattern: ES_TEST_INDEX_NAME };
const result = await runQueryExpect(query, 200);
expect(result.indices).to.eql([ES_TEST_INDEX_NAME]);
});
it('should return errors when expected', async () => {
expect(await runQueryExpect(null, 400)).to.eql(
bodyWithMessage('[request body]: expected a plain object value, but found [null] instead.')
);
expect(await runQueryExpect({}, 400)).to.eql(
bodyWithMessage(
'[request body.pattern]: expected value of type [string] but got [undefined]'
)
);
expect(await runQueryExpect({ pattern: null }, 400)).to.eql(
bodyWithMessage('[request body.pattern]: expected value of type [string] but got [null]')
);
expect(await runQueryExpect({ pattern: 1 }, 400)).to.eql(
bodyWithMessage('[request body.pattern]: expected value of type [string] but got [number]')
);
function bodyWithMessage(message: string): any {
return {
error: 'Bad Request',
message,
statusCode: 400,
};
}
});
it('should return an empty array for empty input', async () => {
const result = await runQueryExpect({ pattern: '' }, 200);
expect(result.indices).to.be.an('array');
expect(result.indices.length).to.be(0);
});
it('should handle indices that do not exist', async () => {
const NON_EXISTANT_INDEX_NAME = 'non-existent-index-name-foo';
const exactResult = await runQueryExpect({ pattern: ES_TEST_INDEX_NAME }, 200);
expect(exactResult.indices).to.be.an('array');
expect(exactResult.indices.length).to.be(1);
let pattern = NON_EXISTANT_INDEX_NAME;
let testResult = await runQueryExpect({ pattern }, 200);
expect(testResult.indices.length).to.be(0);
pattern = `${ES_TEST_INDEX_NAME},${NON_EXISTANT_INDEX_NAME}`;
testResult = await runQueryExpect({ pattern }, 200);
expect(testResult).to.eql(exactResult);
pattern = `${NON_EXISTANT_INDEX_NAME},${ES_TEST_INDEX_NAME}`;
testResult = await runQueryExpect({ pattern }, 200);
expect(testResult).to.eql(exactResult);
});
it('should handle wildcards', async () => {
const exactResult = await runQueryExpect({ pattern: ES_TEST_INDEX_NAME }, 200);
let pattern = `*${ES_TEST_INDEX_NAME}`;
let testResult = await runQueryExpect({ pattern }, 200);
expect(testResult).to.eql(exactResult);
pattern = `${ES_TEST_INDEX_NAME}*`;
testResult = await runQueryExpect({ pattern }, 200);
expect(testResult).to.eql(exactResult);
});
it('should handle aliases', async () => {
const result = await runQueryExpect({ pattern: '.kibana' }, 200);
expect(result.indices).to.be.an('array');
expect(result.indices.includes('.kibana')).to.be(true);
});
});
async function runQueryExpect(requestBody: any, status: number): Promise<any> {
const url = `${getUrlPrefix(Spaces.space1.id)}/${API_URI}`;
const res = await supertest
.post(url)
.set('kbn-xsrf', 'foo')
.send(requestBody);
if (res.status !== status) {
// good place to put a console log for debugging unexpected results
// console.log(res.body)
throw new Error(`expected status ${status}, but got ${res.status}`);
}
return res.body;
}
}

View file

@ -9,7 +9,7 @@ import expect from '@kbn/expect';
import { Spaces } from '../../../../scenarios';
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
import { ESTestIndexTool, ES_TEST_INDEX_NAME, getUrlPrefix } from '../../../../../common/lib';
import { TimeSeriesQuery } from '../../../../../../../plugins/alerting_builtins/server/alert_types/index_threshold/routes';
import { TimeSeriesQuery } from '../../../../../../../plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_query';
import { createEsDocuments } from './create_test_data';
@ -48,13 +48,13 @@ const START_DATE_MINUS_2INTERVALS = getStartDate(-2 * INTERVAL_MILLIS);
*/
// eslint-disable-next-line import/no-default-export
export default function queryDataEndpointTests({ getService }: FtrProviderContext) {
export default function timeSeriesQueryEndpointTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const retry = getService('retry');
const es = getService('legacyEs');
const esTestIndexTool = new ESTestIndexTool(es, retry);
describe('query_data endpoint', () => {
describe('time_series_query endpoint', () => {
before(async () => {
await esTestIndexTool.destroy();
await esTestIndexTool.setup();