Skip to content

Commit

Permalink
[Alerting] replace watcher http APIs used by index threshold Alerting (
Browse files Browse the repository at this point in the history
…elastic#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 elastic#53041
  • Loading branch information
pmuellr authored Mar 9, 2020
1 parent 6395dac commit 3f365a8
Show file tree
Hide file tree
Showing 12 changed files with 685 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 });
}
Original file line number Diff line number Diff line change
@@ -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',
};
Original file line number Diff line number Diff line change
@@ -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);
}
Loading

0 comments on commit 3f365a8

Please sign in to comment.