Skip to content

Commit

Permalink
[Infra] Create typed api client and use it in hosts endpoint (#178214)
Browse files Browse the repository at this point in the history
closes #175717
closes #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 <[email protected]>
  • Loading branch information
crespocarlos and kibanamachine authored Mar 28, 2024
1 parent 4c4a28e commit fc7d417
Show file tree
Hide file tree
Showing 13 changed files with 206 additions and 479 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ESSearchRequest, 'index'> & {
body: {
size: number;
track_total_hits: boolean | number;
};
};

export type InfraMetricsClient = Awaited<ReturnType<typeof getInfraMetricsClient>>;

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<TDocument, TParams extends RequiredParams>(
searchParams: TParams
): Promise<InferSearchResponseOf<TDocument, TParams>> {
return framework.callWithRequest(
requestContext,
'search',
{
...searchParams,
index: source.configuration.metricAlias,
},
request
) as Promise<any>;
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, estypes.AggregationsAggregationContainer> = {
[METADATA_AGGREGATION_NAME]: {
top_metrics: {
metrics: [
{
field: 'host.os.name',
},
{
field: 'cloud.provider',
},
{
field: 'host.ip',
},
],
size: 1,
sort: {
'@timestamp': 'desc',
},
},
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 = <T>(
serchClient: ISearchClient,
queryRequest: ESSearchRequest,
decoder: (aggregation: Record<string, estypes.AggregationsAggregate> | undefined) => T | undefined
): Observable<T | undefined> => {
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<string, estypes.AggregationsAggregationContainer> => {
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,
{}
);
};
Loading

0 comments on commit fc7d417

Please sign in to comment.