[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:
parent
6395dac297
commit
3f365a82f8
|
@ -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.
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
};
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
Loading…
Reference in a new issue