Skip to content

Commit

Permalink
[Alerting] replace last watcher APIs used in alerting ui
Browse files Browse the repository at this point in the history
  • Loading branch information
pmuellr committed Mar 5, 2020
1 parent c42bd33 commit 5861190
Show file tree
Hide file tree
Showing 6 changed files with 287 additions and 11 deletions.
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,123 @@
/*
* 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({
indexes: 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;
try {
rawFields = await getRawFields(ctx.core.elasticsearch.dataClient, req.body.indexes);
} 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 });
}
}

interface RawField {
type: string;
searchable: boolean;
aggregatable: boolean;
}

interface RawFields {
fields: Record<string, Record<string, RawField>>;
}

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[] = [];

for (const name of Object.keys(rawFields.fields)) {
const rawField = rawFields.fields[name];
const type = Object.keys(rawField)[0];
if (type.startsWith('_')) continue;

const values = rawField[type];
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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* 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 = 10;

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> {
service.logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`);

let aliases: string[] = [];
try {
aliases = await getAliasesFromPattern(ctx.core.elasticsearch.dataClient, req.body.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, req.body.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(a: string[], b: string[], limit: number) {
const set = new Set(a.concat(b));
const result = Array.from(set);
result.sort((x, y) => x.localeCompare(y));
return result.slice(0, limit);
}

async function getIndicesFromPattern(
dataClient: IScopedClusterClient,
pattern: string
): Promise<string[]> {
const params = {
index: pattern,
ignore: [404],
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,
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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -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 });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { HttpSetup } from 'kibana/public';

const WATCHER_API_ROOT = '/api/watcher';
const INDEX_THRESHOLD_API_ROOT = '/api/alerting_builtins/index_threshold';

// TODO: replace watcher api with the proper from alerts

Expand All @@ -22,7 +23,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;
Expand All @@ -35,7 +36,7 @@ export async function getThresholdAlertTypeFields({
indexes: string[];
http: HttpSetup;
}): Promise<Record<string, any>> {
const { fields } = await http.post(`${WATCHER_API_ROOT}/fields`, {
const { fields } = await http.post(`${INDEX_THRESHOLD_API_ROOT}/_fields`, {
body: JSON.stringify({ indexes }),
});
return fields;
Expand Down

0 comments on commit 5861190

Please sign in to comment.