From fc7d41702af2c39c6fcb4561ee7841a6bea4c5d4 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Thu, 28 Mar 2024 15:43:34 +0100 Subject: [PATCH] [Infra] Create typed api client and use it in hosts endpoint (#178214) closes https://github.com/elastic/kibana/issues/175717 closes https://github.com/elastic/kibana/issues/175268 ## Summary This PR creates a client to wrap searches against metrics indices. With this change, we are able remove most of the manual ES query typing that uses io-ts in favor of the `InferSearchResponseOf` type. As part of this PR, I refactored the hosts endpoint to use the new client. Other routes will need to be refactored as well. ### How to test - Start a local Kibana instance - Navigate to Infrastructure > Hosts - See if the page loads correctly and sorts by CPU usage desc --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../http_api/infra/get_infra_metrics.ts | 2 +- .../lib/helpers/get_infra_alerts_client.ts | 2 +- .../lib/helpers/get_infra_metrics_client.ts | 53 +++++++ .../infra/server/routes/infra/index.ts | 20 +-- .../server/routes/infra/lib/constants.ts | 25 ---- .../server/routes/infra/lib/helpers/query.ts | 73 +++------- .../routes/infra/lib/host/get_all_hosts.ts | 131 ++++++++++++------ .../infra/lib/host/get_filtered_hosts.ts | 36 +---- .../server/routes/infra/lib/host/get_hosts.ts | 26 +++- .../infra/lib/host/get_hosts_alerts_count.ts | 12 +- .../server/routes/infra/lib/mapper.test.ts | 117 ---------------- .../infra/server/routes/infra/lib/mapper.ts | 104 -------------- .../infra/server/routes/infra/lib/types.ts | 84 +---------- 13 files changed, 206 insertions(+), 479 deletions(-) rename x-pack/plugins/observability_solution/infra/server/{routes/infra => }/lib/helpers/get_infra_alerts_client.ts (95%) create mode 100644 x-pack/plugins/observability_solution/infra/server/lib/helpers/get_infra_metrics_client.ts delete mode 100644 x-pack/plugins/observability_solution/infra/server/routes/infra/lib/mapper.test.ts delete mode 100644 x-pack/plugins/observability_solution/infra/server/routes/infra/lib/mapper.ts diff --git a/x-pack/plugins/observability_solution/infra/common/http_api/infra/get_infra_metrics.ts b/x-pack/plugins/observability_solution/infra/common/http_api/infra/get_infra_metrics.ts index 1c6076b277a55..24d27a2394570 100644 --- a/x-pack/plugins/observability_solution/infra/common/http_api/infra/get_infra_metrics.ts +++ b/x-pack/plugins/observability_solution/infra/common/http_api/infra/get_infra_metrics.ts @@ -37,7 +37,7 @@ export const InfraAssetMetricsRT = rt.type({ export const InfraAssetMetadataRT = rt.type({ // keep the actual field name from the index mappings name: InfraAssetMetadataTypeRT, - value: rt.union([rt.string, rt.null]), + value: rt.union([rt.number, rt.string, rt.null]), }); export const GetInfraMetricsRequestBodyPayloadRT = rt.intersection([ diff --git a/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/helpers/get_infra_alerts_client.ts b/x-pack/plugins/observability_solution/infra/server/lib/helpers/get_infra_alerts_client.ts similarity index 95% rename from x-pack/plugins/observability_solution/infra/server/routes/infra/lib/helpers/get_infra_alerts_client.ts rename to x-pack/plugins/observability_solution/infra/server/lib/helpers/get_infra_alerts_client.ts index 70941ae17f1e3..5a9c016c7c81f 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/helpers/get_infra_alerts_client.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/helpers/get_infra_alerts_client.ts @@ -8,7 +8,7 @@ import { isEmpty } from 'lodash'; import { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; import { KibanaRequest } from '@kbn/core/server'; -import type { InfraPluginStartServicesAccessor } from '../../../../types'; +import type { InfraPluginStartServicesAccessor } from '../../types'; type RequiredParams = ESSearchRequest & { size: number; diff --git a/x-pack/plugins/observability_solution/infra/server/lib/helpers/get_infra_metrics_client.ts b/x-pack/plugins/observability_solution/infra/server/lib/helpers/get_infra_metrics_client.ts new file mode 100644 index 0000000000000..8e8934fb7b3f0 --- /dev/null +++ b/x-pack/plugins/observability_solution/infra/server/lib/helpers/get_infra_metrics_client.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; +import type { KibanaRequest } from '@kbn/core/server'; +import type { InfraPluginRequestHandlerContext } from '../../types'; +import { InfraSources } from '../sources'; +import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; + +type RequiredParams = Omit & { + body: { + size: number; + track_total_hits: boolean | number; + }; +}; + +export type InfraMetricsClient = Awaited>; + +export async function getInfraMetricsClient({ + sourceId, + framework, + infraSources, + requestContext, + request, +}: { + sourceId: string; + framework: KibanaFramework; + infraSources: InfraSources; + requestContext: InfraPluginRequestHandlerContext; + request?: KibanaRequest; +}) { + const soClient = (await requestContext.core).savedObjects.getClient(); + const source = await infraSources.getSourceConfiguration(soClient, sourceId); + + return { + search( + searchParams: TParams + ): Promise> { + return framework.callWithRequest( + requestContext, + 'search', + { + ...searchParams, + index: source.configuration.metricAlias, + }, + request + ) as Promise; + }, + }; +} diff --git a/x-pack/plugins/observability_solution/infra/server/routes/infra/index.ts b/x-pack/plugins/observability_solution/infra/server/routes/infra/index.ts index 4d8fd931082c1..6ea6354b14853 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/infra/index.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/infra/index.ts @@ -14,8 +14,9 @@ import { GetInfraMetricsResponsePayloadRT, } from '../../../common/http_api/infra'; import { InfraBackendLibs } from '../../lib/infra_types'; -import { getInfraAlertsClient } from './lib/helpers/get_infra_alerts_client'; +import { getInfraAlertsClient } from '../../lib/helpers/get_infra_alerts_client'; import { getHosts } from './lib/host/get_hosts'; +import { getInfraMetricsClient } from '../../lib/helpers/get_infra_metrics_client'; export const initInfraMetricsRoute = (libs: InfraBackendLibs) => { const validateBody = createRouteValidationFunction(GetInfraMetricsRequestBodyPayloadRT); @@ -30,23 +31,26 @@ export const initInfraMetricsRoute = (libs: InfraBackendLibs) => { body: validateBody, }, }, - async (_, request, response) => { - const [{ savedObjects }, { data }] = await libs.getStartServices(); + async (requestContext, request, response) => { const params: GetInfraMetricsRequestBodyPayload = request.body; try { - const searchClient = data.search.asScoped(request); + const infraMetricsClient = await getInfraMetricsClient({ + framework, + request, + infraSources: libs.sources, + requestContext, + sourceId: params.sourceId, + }); + const alertsClient = await getInfraAlertsClient({ getStartServices: libs.getStartServices, request, }); - const soClient = savedObjects.getScopedClient(request); - const source = await libs.sources.getSourceConfiguration(soClient, params.sourceId); const hosts = await getHosts({ - searchClient, + infraMetricsClient, alertsClient, - sourceConfig: source.configuration, params, }); diff --git a/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/constants.ts b/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/constants.ts index f39eaafdd039e..d47b443fc5d14 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/constants.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/constants.ts @@ -5,33 +5,8 @@ * 2.0. */ -import { estypes } from '@elastic/elasticsearch'; - export const BUCKET_KEY = 'host.name'; export const METADATA_AGGREGATION_NAME = 'metadata'; export const FILTER_AGGREGATION_SUB_AGG_NAME = 'result'; -export const INVENTORY_MODEL_NODE_TYPE = 'host'; export const MAX_SIZE = 500; - -export const METADATA_AGGREGATION: Record = { - [METADATA_AGGREGATION_NAME]: { - top_metrics: { - metrics: [ - { - field: 'host.os.name', - }, - { - field: 'cloud.provider', - }, - { - field: 'host.ip', - }, - ], - size: 1, - sort: { - '@timestamp': 'desc', - }, - }, - }, -}; diff --git a/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/helpers/query.ts b/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/helpers/query.ts index 18507cc0b53a9..495bacc2d7227 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/helpers/query.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/helpers/query.ts @@ -6,15 +6,13 @@ */ import { estypes } from '@elastic/elasticsearch'; -import { ISearchClient } from '@kbn/data-plugin/common'; -import { ESSearchRequest } from '@kbn/es-types'; -import { catchError, map, Observable } from 'rxjs'; import { findInventoryModel } from '@kbn/metrics-data-access-plugin/common'; +import { termsQuery, rangeQuery } from '@kbn/observability-plugin/server'; import { GetInfraMetricsRequestBodyPayload, InfraAssetMetricType, } from '../../../../../common/http_api/infra'; -import { INVENTORY_MODEL_NODE_TYPE } from '../constants'; +import { BUCKET_KEY } from '../constants'; export const createFilters = ({ params, @@ -26,71 +24,42 @@ export const createFilters = ({ extraFilter?: estypes.QueryDslQueryContainer; }) => { const extrafilterClause = extraFilter?.bool?.filter; + const extraFilterList = !!extrafilterClause ? Array.isArray(extrafilterClause) ? extrafilterClause : [extrafilterClause] : []; - const hostNamesFilter = - hostNamesShortList.length > 0 - ? [ - { - terms: { - 'host.name': hostNamesShortList, - }, - }, - ] - : []; return [ - ...hostNamesFilter, ...extraFilterList, - { - range: { - '@timestamp': { - gte: new Date(params.range.from).getTime(), - lte: new Date(params.range.to).getTime(), - format: 'epoch_millis', - }, - }, - }, + ...termsQuery(BUCKET_KEY, ...hostNamesShortList), + ...rangeQuery(new Date(params.range.from).getTime(), new Date(params.range.to).getTime()), { exists: { - field: 'host.name', + field: BUCKET_KEY, }, }, ]; }; -export const runQuery = ( - serchClient: ISearchClient, - queryRequest: ESSearchRequest, - decoder: (aggregation: Record | undefined) => T | undefined -): Observable => { - return serchClient - .search({ - params: queryRequest, - }) - .pipe( - map((res) => decoder(res.rawResponse.aggregations)), - catchError((err) => { - const error = { - message: err.message, - statusCode: err.statusCode, - attributes: err.errBody?.error, - }; - - throw error; - }) - ); -}; - export const getInventoryModelAggregations = ( + assetType: 'host', metrics: InfraAssetMetricType[] -): Record => { - const inventoryModel = findInventoryModel(INVENTORY_MODEL_NODE_TYPE); - return metrics.reduce( - (acc, metric) => Object.assign(acc, inventoryModel.metrics.snapshot?.[metric]), +) => { + const inventoryModel = findInventoryModel(assetType); + return metrics.reduce< + Partial< + Record< + InfraAssetMetricType, + typeof inventoryModel.metrics.snapshot[keyof typeof inventoryModel.metrics.snapshot] + > + > + >( + (acc, metric) => + inventoryModel.metrics.snapshot?.[metric] + ? Object.assign(acc, inventoryModel.metrics.snapshot[metric]) + : acc, {} ); }; diff --git a/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/host/get_all_hosts.ts b/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/host/get_all_hosts.ts index e63f0d78b367d..7172a0c3da4d4 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/host/get_all_hosts.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/host/get_all_hosts.ts @@ -5,43 +5,63 @@ * 2.0. */ -import { estypes } from '@elastic/elasticsearch'; -import { lastValueFrom } from 'rxjs'; -import { ESSearchRequest } from '@kbn/es-types'; -import { InfraStaticSourceConfiguration } from '../../../../lib/sources'; -import { decodeOrThrow } from '../../../../../common/runtime_types'; -import { GetInfraMetricsRequestBodyPayload } from '../../../../../common/http_api/infra'; -import { BUCKET_KEY, MAX_SIZE, METADATA_AGGREGATION } from '../constants'; -import { - GetHostsArgs, - HostsMetricsSearchAggregationResponse, - HostsMetricsSearchAggregationResponseRT, -} from '../types'; -import { createFilters, getInventoryModelAggregations, runQuery } from '../helpers/query'; +import type { + GetInfraMetricsRequestBodyPayload, + InfraAssetMetadataType, +} from '../../../../../common/http_api/infra'; +import { BUCKET_KEY, MAX_SIZE, METADATA_AGGREGATION_NAME } from '../constants'; +import type { GetHostsArgs } from '../types'; +import { createFilters, getInventoryModelAggregations } from '../helpers/query'; +import { BasicMetricValueRT } from '../../../../lib/metrics/types'; export const getAllHosts = async ( - { searchClient, sourceConfig, params }: GetHostsArgs, + { infraMetricsClient, params }: GetHostsArgs, hostNamesShortList: string[] = [] -): Promise => { - const query = createQuery(params, sourceConfig, hostNamesShortList); - return lastValueFrom( - runQuery(searchClient, query, decodeOrThrow(HostsMetricsSearchAggregationResponseRT)) - ); +) => { + const query = createQuery(params, hostNamesShortList); + const response = await infraMetricsClient.search(query); + + const result = (response.aggregations?.nodes.buckets ?? []) + .sort((a, b) => { + const aValue = getMetricValue(a?.cpu) ?? 0; + const bValue = getMetricValue(b?.cpu) ?? 0; + return bValue - aValue; + }) + .map((bucket) => { + const metadata = (bucket?.metadata.top ?? []) + .flatMap((top) => Object.entries(top.metrics)) + .map(([key, value]) => ({ + name: key as InfraAssetMetadataType, + value: typeof value === 'string' && value.trim().length === 0 ? null : value, + })); + + const metrics = params.metrics.map((metric) => ({ + name: metric.type, + value: metric.type in bucket ? getMetricValue(bucket[metric.type]) ?? 0 : null, + })); + + return { + name: bucket?.key as string, + metadata, + metrics, + }; + }); + + return result; }; -const createQuery = ( - params: GetInfraMetricsRequestBodyPayload, - sourceConfig: InfraStaticSourceConfiguration, - hostNamesShortList: string[] -): ESSearchRequest => { - const metricAggregations = getInventoryModelAggregations(params.metrics.map((p) => p.type)); +const createQuery = (params: GetInfraMetricsRequestBodyPayload, hostNamesShortList: string[]) => { + const metricAggregations = getInventoryModelAggregations( + params.type, + params.metrics.map((p) => p.type) + ); return { allow_no_indices: true, ignore_unavailable: true, - index: sourceConfig.metricAlias, body: { size: 0, + track_total_hits: false, query: { bool: { filter: createFilters({ @@ -50,28 +70,47 @@ const createQuery = ( }), }, }, - aggs: createAggregations(params, metricAggregations), - }, - }; -}; - -const createAggregations = ( - { limit }: GetInfraMetricsRequestBodyPayload, - metricAggregations: Record -): Record => { - return { - nodes: { - terms: { - field: BUCKET_KEY, - size: limit ?? MAX_SIZE, - order: { - _key: 'asc', - }, - }, aggs: { - ...metricAggregations, - ...METADATA_AGGREGATION, + nodes: { + terms: { + field: BUCKET_KEY, + size: params.limit ?? MAX_SIZE, + order: { + _key: 'asc' as const, + }, + }, + aggs: { + ...metricAggregations, + [METADATA_AGGREGATION_NAME]: { + top_metrics: { + metrics: [ + { + field: 'host.os.name', + }, + { + field: 'cloud.provider', + }, + { + field: 'host.ip', + }, + ], + size: 1, + sort: { + '@timestamp': 'desc' as const, + }, + }, + }, + }, + }, }, }, }; }; + +const getMetricValue = (valueObject: unknown): number | null => { + if (BasicMetricValueRT.is(valueObject)) { + return valueObject.value; + } + + return valueObject as number | null; +}; diff --git a/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/host/get_filtered_hosts.ts b/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/host/get_filtered_hosts.ts index 95b8753487cf7..ed55b5c6d47a0 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/host/get_filtered_hosts.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/host/get_filtered_hosts.ts @@ -5,44 +5,20 @@ * 2.0. */ -import { ESSearchRequest } from '@kbn/es-types'; -import { lastValueFrom } from 'rxjs'; - -import { InfraStaticSourceConfiguration } from '../../../../lib/sources'; -import { decodeOrThrow } from '../../../../../common/runtime_types'; -import { GetInfraMetricsRequestBodyPayload } from '../../../../../common/http_api/infra'; -import { - FilteredHostsSearchAggregationResponseRT, - FilteredHostsSearchAggregationResponse, - GetHostsArgs, -} from '../types'; +import { GetHostsArgs } from '../types'; import { BUCKET_KEY, MAX_SIZE } from '../constants'; import { assertQueryStructure } from '../utils'; -import { createFilters, runQuery } from '../helpers/query'; - -export const getFilteredHosts = async ({ - searchClient, - sourceConfig, - params, -}: GetHostsArgs): Promise => { - const query = createQuery(params, sourceConfig); - return lastValueFrom( - runQuery(searchClient, query, decodeOrThrow(FilteredHostsSearchAggregationResponseRT)) - ); -}; +import { createFilters } from '../helpers/query'; -const createQuery = ( - params: GetInfraMetricsRequestBodyPayload, - sourceConfig: InfraStaticSourceConfiguration -): ESSearchRequest => { +export const getFilteredHosts = async ({ infraMetricsClient, params }: GetHostsArgs) => { assertQueryStructure(params.query); - return { + return infraMetricsClient.search({ allow_no_indices: true, ignore_unavailable: true, - index: sourceConfig.metricAlias, body: { size: 0, + track_total_hits: false, query: { bool: { ...params.query.bool, @@ -61,5 +37,5 @@ const createQuery = ( }, }, }, - }; + }); }; diff --git a/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/host/get_hosts.ts b/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/host/get_hosts.ts index 6eab207f7fbae..5b988fca8b4c5 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/host/get_hosts.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/host/get_hosts.ts @@ -5,11 +5,10 @@ * 2.0. */ -import { GetInfraMetricsResponsePayload } from '../../../../../common/http_api/infra'; +import type { GetInfraMetricsResponsePayload } from '../../../../../common/http_api/infra'; import { getFilteredHosts } from './get_filtered_hosts'; -import { mapToApiResponse } from '../mapper'; import { hasFilters } from '../utils'; -import { GetHostsArgs } from '../types'; +import type { GetHostsArgs } from '../types'; import { getAllHosts } from './get_all_hosts'; import { getHostsAlertsCount } from './get_hosts_alerts_count'; @@ -29,7 +28,7 @@ export const getHosts = async (args: GetHostsArgs): Promise { + acc[name] = { alertsCount }; + return acc; + }, {} as Record); + + const hosts = hostMetrics.map(({ name, metrics, metadata }) => { + const { alertsCount } = alertsByHostName[name] ?? {}; + return { name, metrics, metadata, alertsCount }; + }); + + return { + type: args.params.type, + nodes: hosts, + }; }; const getFilteredHostNames = async (args: GetHostsArgs) => { const filteredHosts = await getFilteredHosts(args); - const { nodes } = filteredHosts ?? {}; - return nodes?.buckets.map((p) => p.key) ?? []; + const { nodes } = filteredHosts.aggregations ?? {}; + return nodes?.buckets.map((p) => p.key as string) ?? []; }; diff --git a/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/host/get_hosts_alerts_count.ts b/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/host/get_hosts_alerts_count.ts index d1e17e8d133ee..5a32423d62ee1 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/host/get_hosts_alerts_count.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/host/get_hosts_alerts_count.ts @@ -14,12 +14,7 @@ import { } from '@kbn/rule-data-utils'; import { INFRA_ALERT_FEATURE_ID } from '../../../../../common/constants'; import { BUCKET_KEY, MAX_SIZE } from '../constants'; -import { InfraAlertsClient } from '../helpers/get_infra_alerts_client'; - -export type HostAlertsResponse = Array<{ - name: string; - alertsCount: number; -}>; +import { InfraAlertsClient } from '../../../../lib/helpers/get_infra_alerts_client'; export async function getHostsAlertsCount({ alertsClient, @@ -35,7 +30,7 @@ export async function getHostsAlertsCount({ from: string; to: string; maxNumHosts?: number; -}): Promise { +}) { const rangeQuery = [ { range: { @@ -66,6 +61,9 @@ export async function getHostsAlertsCount({ terms: { field: BUCKET_KEY, size: maxNumHosts, + order: { + _key: 'asc', + }, }, aggs: { alerts_count: { diff --git a/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/mapper.test.ts b/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/mapper.test.ts deleted file mode 100644 index 40b72ecaa49e7..0000000000000 --- a/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/mapper.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mapToApiResponse } from './mapper'; -import { GetInfraMetricsRequestBodyPayload } from '../../../../common/http_api/infra'; - -const metricsApiRequest: GetInfraMetricsRequestBodyPayload = { - type: 'host', - limit: 20, - metrics: [ - { - type: 'cpu', - }, - { - type: 'diskSpaceUsage', - }, - { - type: 'memory', - }, - { - type: 'memoryFree', - }, - { - type: 'normalizedLoad1m', - }, - { - type: 'rx', - }, - { - type: 'tx', - }, - ], - range: { - from: '2023-04-18T11:15:31.407Z', - to: '2023-04-18T11:15:31.407Z', - }, - query: { bool: [{ must_not: [], filter: [], should: [], must: [] }] }, - sourceId: 'id', -}; - -describe('mapper', () => { - test('should map the aggregation object to the expected response object', () => { - const hosts = mapToApiResponse(metricsApiRequest, [ - { - key: 'host-0', - doc_count: 155, - diskSpaceUsage: { - value: 0.2040001, - }, - memory: { - value: 0.542838307852529, - }, - memoryFree: { - value: 34359.738368, - }, - normalizedLoad1m: { - value: 239.2040001, - }, - tx: { - doc_count: 155, - result: { - value: 100.26926542816672, - }, - }, - rx: { - doc_count: 155, - result: { - value: 3959.4930095127706, - }, - }, - cpu: { - doc_count: 155, - result: { - value: 0.13271302652800487, - }, - }, - metadata: { - top: [ - { - sort: ['2023-04-04T06:35:13.793Z'], - metrics: { - 'host.os.name': null, - 'cloud.provider': '', - }, - }, - ], - }, - }, - ]); - - expect(hosts).toEqual({ - type: 'host', - nodes: [ - { - metadata: [ - { name: 'host.os.name', value: null }, - { name: 'cloud.provider', value: null }, - ], - metrics: [ - { name: 'cpu', value: 0.13271302652800487 }, - { name: 'diskSpaceUsage', value: 0.2040001 }, - { name: 'memory', value: 0.542838307852529 }, - { name: 'memoryFree', value: 34359.738368 }, - { name: 'normalizedLoad1m', value: 239.2040001 }, - { name: 'rx', value: 3959.4930095127706 }, - { name: 'tx', value: 100.26926542816672 }, - ], - name: 'host-0', - }, - ], - }); - }); -}); diff --git a/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/mapper.ts b/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/mapper.ts deleted file mode 100644 index cc922d42c8991..0000000000000 --- a/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/mapper.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { BasicMetricValueRT, TopMetricsTypeRT } from '../../../lib/metrics/types'; -import { - GetInfraMetricsRequestBodyPayload, - GetInfraMetricsResponsePayload, - InfraAssetMetadata, - InfraAssetMetrics, -} from '../../../../common/http_api/infra'; - -import { - FilteredMetricsTypeRT, - HostsMetricsSearchBucket, - HostsMetricsSearchValue, - HostsMetricsSearchValueRT, -} from './types'; -import { METADATA_AGGREGATION_NAME } from './constants'; -import { HostAlertsResponse } from './host/get_hosts_alerts_count'; - -export const mapToApiResponse = ( - params: GetInfraMetricsRequestBodyPayload, - buckets?: HostsMetricsSearchBucket[] | undefined, - alertsCountResponse?: HostAlertsResponse -): GetInfraMetricsResponsePayload => { - if (!buckets) { - return { - type: params.type, - nodes: [], - }; - } - - const hosts = buckets - .map((bucket) => { - const metrics = convertMetricBucket(params, bucket); - const metadata = convertMetadataBucket(bucket); - - const cpuValue = metrics.find((metric) => metric.name === 'cpu')?.value ?? 0; - const alerts = alertsCountResponse?.find((item) => item.name === bucket.key); - - return { name: bucket.key as string, metrics, metadata, cpuValue, ...alerts }; - }) - .sort((a, b) => { - return b.cpuValue - a.cpuValue; - }) - .map(({ cpuValue, ...rest }) => rest); - - return { - type: params.type, - nodes: hosts, - }; -}; - -const normalizeValue = (value: string | number | null) => { - if (typeof value === 'string') { - return value?.trim().length === 0 ? null : value; - } - - return value; -}; - -const convertMetadataBucket = (bucket: HostsMetricsSearchBucket): InfraAssetMetadata[] => { - const metadataAggregation = bucket[METADATA_AGGREGATION_NAME]; - return TopMetricsTypeRT.is(metadataAggregation) - ? metadataAggregation.top - .flatMap((top) => Object.entries(top.metrics)) - .map( - ([key, value]) => - ({ - name: key, - value: normalizeValue(value), - } as InfraAssetMetadata) - ) - : []; -}; - -const convertMetricBucket = ( - params: GetInfraMetricsRequestBodyPayload, - bucket: HostsMetricsSearchBucket -): InfraAssetMetrics[] => { - return params.metrics.map((returnedMetric) => { - const metricBucket = bucket[returnedMetric.type]; - return { - name: returnedMetric.type, - value: HostsMetricsSearchValueRT.is(metricBucket) ? getMetricValue(metricBucket) ?? 0 : null, - } as InfraAssetMetrics; - }); -}; - -export const getMetricValue = (valueObject: HostsMetricsSearchValue) => { - if (FilteredMetricsTypeRT.is(valueObject)) { - return valueObject.result.value; - } - - if (BasicMetricValueRT.is(valueObject)) { - return valueObject.value; - } - - return valueObject; -}; diff --git a/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/types.ts b/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/types.ts index ab097e91b2b09..c4d0b6dd32996 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/types.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/infra/lib/types.ts @@ -5,90 +5,12 @@ * 2.0. */ -import { estypes } from '@elastic/elasticsearch'; -import { ISearchClient } from '@kbn/data-plugin/common'; -import * as rt from 'io-ts'; -import { InfraStaticSourceConfiguration } from '../../../../common/source_configuration/source_configuration'; - import { GetInfraMetricsRequestBodyPayload } from '../../../../common/http_api/infra'; -import { BasicMetricValueRT, TopMetricsTypeRT } from '../../../lib/metrics/types'; -import { InfraAlertsClient } from './helpers/get_infra_alerts_client'; - -export const FilteredMetricsTypeRT = rt.type({ - doc_count: rt.number, - result: BasicMetricValueRT, -}); - -export const HostsMetricsSearchValueRT = rt.union([ - BasicMetricValueRT, - FilteredMetricsTypeRT, - TopMetricsTypeRT, -]); - -export const HostsMetricsSearchBucketRT = rt.record( - rt.union([rt.string, rt.undefined]), - rt.union([ - rt.string, - rt.number, - HostsMetricsSearchValueRT, - rt.record(rt.string, rt.string), - rt.type({ doc_count: rt.number }), - ]) -); - -export const HostsNameBucketRT = rt.type({ - key: rt.string, - doc_count: rt.number, -}); - -export const HostsMetricsSearchAggregationResponseRT = rt.union([ - rt.type({ - nodes: rt.intersection([ - rt.partial({ - sum_other_doc_count: rt.number, - doc_count_error_upper_bound: rt.number, - }), - rt.type({ buckets: rt.array(HostsMetricsSearchBucketRT) }), - ]), - }), - rt.undefined, -]); - -export const FilteredHostsSearchAggregationResponseRT = rt.union([ - rt.type({ - nodes: rt.intersection([ - rt.partial({ - sum_other_doc_count: rt.number, - doc_count_error_upper_bound: rt.number, - }), - rt.type({ - buckets: rt.array(HostsNameBucketRT), - }), - ]), - }), - rt.undefined, -]); - -export interface HostsMetricsAggregationQueryConfig { - fieldName: string; - aggregation: estypes.AggregationsAggregationContainer; - runtimeField?: estypes.MappingRuntimeFields; -} +import { InfraAlertsClient } from '../../../lib/helpers/get_infra_alerts_client'; +import { InfraMetricsClient } from '../../../lib/helpers/get_infra_metrics_client'; export interface GetHostsArgs { - searchClient: ISearchClient; + infraMetricsClient: InfraMetricsClient; alertsClient: InfraAlertsClient; - sourceConfig: InfraStaticSourceConfiguration; params: GetInfraMetricsRequestBodyPayload; } - -export type HostsMetricsSearchValue = rt.TypeOf; -export type HostsMetricsSearchBucket = rt.TypeOf; - -export type FilteredHostsSearchAggregationResponse = rt.TypeOf< - typeof FilteredHostsSearchAggregationResponseRT ->; - -export type HostsMetricsSearchAggregationResponse = rt.TypeOf< - typeof HostsMetricsSearchAggregationResponseRT ->;