diff --git a/x-pack/plugins/infra/common/http_api/metrics_api.ts b/x-pack/plugins/infra/common/http_api/metrics_api.ts index 41657fdce215..14b7c24e5769 100644 --- a/x-pack/plugins/infra/common/http_api/metrics_api.ts +++ b/x-pack/plugins/infra/common/http_api/metrics_api.ts @@ -30,6 +30,7 @@ export const MetricsAPIRequestRT = rt.intersection([ }), rt.partial({ groupBy: rt.array(groupByRT), + modules: rt.array(rt.string), afterKey: rt.union([rt.null, afterKeyObjectRT]), limit: rt.union([rt.number, rt.null, rt.undefined]), filters: rt.array(rt.object), diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index 1d89b7be4329..49fe55e3dee0 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -25,6 +25,7 @@ import { import { initGetK8sAnomaliesRoute } from './routes/infra_ml'; import { initGetHostsAnomaliesRoute } from './routes/infra_ml'; import { initMetricExplorerRoute } from './routes/metrics_explorer'; +import { initMetricsAPIRoute } from './routes/metrics_api'; import { initMetadataRoute } from './routes/metadata'; import { initSnapshotRoute } from './routes/snapshot'; import { initNodeDetailsRoute } from './routes/node_details'; @@ -74,6 +75,7 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initLogEntriesSummaryHighlightsRoute(libs); initLogEntriesItemRoute(libs); initMetricExplorerRoute(libs); + initMetricsAPIRoute(libs); initMetadataRoute(libs); initInventoryMetaRoute(libs); initLogSourceConfigurationRoutes(libs); diff --git a/x-pack/plugins/infra/server/lib/metrics/index.ts b/x-pack/plugins/infra/server/lib/metrics/index.ts index 183254a0486a..9401d34ca62f 100644 --- a/x-pack/plugins/infra/server/lib/metrics/index.ts +++ b/x-pack/plugins/infra/server/lib/metrics/index.ts @@ -17,11 +17,20 @@ import { EMPTY_RESPONSE } from './constants'; import { createAggregations } from './lib/create_aggregations'; import { convertHistogramBucketsToTimeseries } from './lib/convert_histogram_buckets_to_timeseries'; import { calculateBucketSize } from './lib/calculate_bucket_size'; +import { calculatedInterval } from './lib/calculate_interval'; export const query = async ( search: ESSearchClient, - options: MetricsAPIRequest + rawOptions: MetricsAPIRequest ): Promise => { + const interval = await calculatedInterval(search, rawOptions); + const options = { + ...rawOptions, + timerange: { + ...rawOptions.timerange, + interval, + }, + }; const hasGroupBy = Array.isArray(options.groupBy) && options.groupBy.length > 0; const filter: Array> = [ { @@ -35,6 +44,7 @@ export const query = async ( }, ...(options.groupBy?.map((field) => ({ exists: { field } })) ?? []), ]; + const params = { allowNoIndices: true, ignoreUnavailable: true, @@ -70,7 +80,7 @@ export const query = async ( throw new Error('Aggregations should be present.'); } - const { bucketSize } = calculateBucketSize(options.timerange); + const { bucketSize } = calculateBucketSize({ ...options.timerange, interval }); if (hasGroupBy && GroupingResponseRT.is(response.aggregations)) { const { groupings } = response.aggregations; diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/calculate_interval.ts b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_interval.ts new file mode 100644 index 000000000000..46682e2213a3 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_interval.ts @@ -0,0 +1,34 @@ +/* + * 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 { isArray, isNumber } from 'lodash'; +import { MetricsAPIRequest } from '../../../../common/http_api'; +import { ESSearchClient } from '../types'; +import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; + +export const calculatedInterval = async (search: ESSearchClient, options: MetricsAPIRequest) => { + const useModuleInterval = + options.timerange.interval === 'modules' && + isArray(options.modules) && + options.modules.length > 0; + + const calcualatedInterval = useModuleInterval + ? await calculateMetricInterval( + search, + { + indexPattern: options.indexPattern, + timestampField: options.timerange.field, + timerange: { from: options.timerange.from, to: options.timerange.to }, + }, + options.modules + ) + : false; + + const defaultInterval = + options.timerange.interval === 'modules' ? 'auto' : options.timerange.interval; + + return isNumber(calcualatedInterval) ? `>=${calcualatedInterval}s` : defaultInterval; +}; diff --git a/x-pack/plugins/infra/server/routes/metrics_api/index.ts b/x-pack/plugins/infra/server/routes/metrics_api/index.ts new file mode 100644 index 000000000000..f3dcdeeb70cc --- /dev/null +++ b/x-pack/plugins/infra/server/routes/metrics_api/index.ts @@ -0,0 +1,50 @@ +/* + * 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 Boom from 'boom'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { schema } from '@kbn/config-schema'; +import { InfraBackendLibs } from '../../lib/infra_types'; +import { throwErrors } from '../../../common/runtime_types'; +import { createSearchClient } from '../../lib/create_search_client'; +import { query } from '../../lib/metrics'; +import { MetricsAPIRequestRT, MetricsAPIResponseRT } from '../../../common/http_api'; + +const escapeHatch = schema.object({}, { unknowns: 'allow' }); + +export const initMetricsAPIRoute = (libs: InfraBackendLibs) => { + const { framework } = libs; + framework.registerRoute( + { + method: 'post', + path: '/api/infra/metrics_api', + validate: { + body: escapeHatch, + }, + }, + async (requestContext, request, response) => { + try { + const options = pipe( + MetricsAPIRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + + const client = createSearchClient(requestContext, framework); + const metricsApiResponse = await query(client, options); + + return response.ok({ + body: MetricsAPIResponseRT.encode(metricsApiResponse), + }); + } catch (error) { + return response.internalError({ + body: error.message, + }); + } + } + ); +};