Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Infra] Create service in apm_data_access to fetch APM colleced hosts #189046

Closed
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
apmIndicesSavedObjectDefinition,
getApmIndicesSavedObject,
} from './saved_objects/apm_indices';
import { registerServices } from './services/register_services';
import { getTypedSearch } from '../utils/create_typed_es_client';

export class ApmDataAccessPlugin
implements Plugin<ApmDataAccessPluginSetup, ApmDataAccessPluginStart>
Expand All @@ -32,16 +34,32 @@ export class ApmDataAccessPlugin
const apmDataAccessConfig = this.initContext.config.get<APMDataAccessConfig>();
const apmIndicesFromConfigFile = apmDataAccessConfig.indices;

const getApmIndices = async (savedObjectsClient: SavedObjectsClientContract) => {
const apmIndicesFromSavedObject = await getApmIndicesSavedObject(savedObjectsClient);
return { ...apmIndicesFromConfigFile, ...apmIndicesFromSavedObject };
};

// register saved object
core.savedObjects.registerType(apmIndicesSavedObjectDefinition);

// expose
const getCoreStart = () => core.getStartServices().then(([coreStart]) => coreStart);
const getResourcesForServices = async () => {
const coreStart = await getCoreStart();
const soClient = coreStart.savedObjects.createInternalRepository();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tried changing the APM index patterns on the APM settings page and see if the correct indices are returned with this createInternalRepository?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just tested it and it worked. APM also uses it here here. It could be due to the fact that when APM indices are changed on the Settings page, Kibana does a full page refresh.

const apmIndices = await getApmIndices(soClient);
return {
apmIndices,
esClient: getTypedSearch(coreStart.elasticsearch.client.asInternalUser),
};
};

const services = registerServices({ getResourcesForServices });

return {
apmIndicesFromConfigFile,
getApmIndices: async (savedObjectsClient: SavedObjectsClientContract) => {
const apmIndicesFromSavedObject = await getApmIndicesSavedObject(savedObjectsClient);
return { ...apmIndicesFromConfigFile, ...apmIndicesFromSavedObject };
},
getApmIndices,
services,
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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 { estypes } from '@elastic/elasticsearch';
import { rangeQuery } from '@kbn/observability-plugin/server';
import { RegisterParams } from '../register_services';

export interface ServicesHostNamesParams {
query: estypes.QueryDslQueryContainer;
from: number;
to: number;
limit: number;
}

export function createGetHostNames(params: RegisterParams) {
return async ({ from: timeFrom, to: timeTo, query, limit }: ServicesHostNamesParams) => {
const { apmIndices, esClient } = await params.getResourcesForServices();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should stay away from patterns like this, and expect the consumer to pass in asynchronously resolved dependencies, such as getting the APM indices. If we don't do that, we'll end up with a ton of small requests - I see this happen with e.g. the ML plugin that does a ton of small requests because it encapsulates privilege and other checks in each service call. For instance: #161229

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was just having second thoughts about this approach.

Copy link
Contributor Author

@crespocarlos crespocarlos Jul 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

though it's kind of weird asking the consumer to pass the APM indices to a service in the apm_data_acess plugin.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is a little weird, and there might be a better way (e.g. to have a per-request service that does per-request caching, but that also might create some risks).

Copy link
Contributor Author

@crespocarlos crespocarlos Jul 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't really see the risks of getting the APM indices when calling a service in the apm_data_access, what could happen? if we just get the APM indices there, do you think it would still end up in the same situation from ML plugin?

I'm more concerned about the fact I'm using the internal user to get both apm indices and typed esClient.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One downside is potentially requesting APM indices twice.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One downside is potentially requesting APM indices twice.

This is my (only) concern. It might end up being more than twice however. Especially in the context of e.g. rule executors this becomes more important. My experience is that if we abstract away async operations, people lose awareness of what is happening as part of the service call, and it leads to performance bottlenecks. I'd be less concerned about this if we have really good insight in production bottlenecks, but we don't.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the lack of better alternatives, I'll leave it up to the consumer the send the APM indices for now. It's bad DX but for the time being it's better than end up in situation where it might lead to performance problems.


const esResponse = await esClient.search({
index: [apmIndices.metric, apmIndices.transaction, apmIndices.span, apmIndices.error],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be slow, because you're aggregating over all data. Even if it's just a single service, you'll run into performance issues. What would be more efficient here is if you figure out what data source to query first (tx metrics or raw events, 1m vs 10m and 60m), and then execute it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that would be the same thing that the get_document_sources does. We'd have to move that to apm_data_access. Is that your idea?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, how involved would that be?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a relatively big change. A few things would have to be moved (could be more):

document_type, get_document_sources and the types they depend on.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, sounds fine to do it in a follow-up but I'd like to make it happen, to set a good first example

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, makes sense. I'm going to get a sense of how much effort it would be. it might as well be doable in this PR.

track_total_hits: false,
ignore_unavailable: true,
size: 0,
query: {
bool: {
filter: [query, ...rangeQuery(timeFrom, timeTo)],
},
},
aggs: {
hostNames: {
terms: {
field: 'host.name',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is a constant for this somewhere (HOST_NAME).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not in the apm_data_access. we need to either move the constants from apm plugin here or create a new constants file.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, let's move the constants then

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do that in a follow up PR, over 250 files will be changed after this

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

package might be better at that point, WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah. When doing this #189046 (comment), some types could also be moved to this new package.

size: limit,
},
},
},
});

return esResponse.aggregations?.hostNames.buckets.map((bucket) => bucket.key as string) ?? [];
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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 { APMIndices } from '..';
import { getTypedSearch } from '../../utils/create_typed_es_client';
import { createGetHostNames } from './get_host_names';

interface Resources {
apmIndices: APMIndices;
esClient: ReturnType<typeof getTypedSearch>;
}
export interface RegisterParams {
getResourcesForServices: () => Promise<Resources>;
}

export function registerServices(params: RegisterParams) {
return {
getHostNames: createGetHostNames(params),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@

import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import { APMIndices } from '.';
import { registerServices } from './services/register_services';

export interface ApmDataAccessPluginSetup {
apmIndicesFromConfigFile: APMIndices;
getApmIndices: (soClient: SavedObjectsClientContract) => Promise<APMIndices>;
services: ReturnType<typeof registerServices>;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ApmDataAccessPluginStart {}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,21 @@
"compilerOptions": {
"outDir": "target/types"
},
"include": ["../../../typings/**/*", "common/**/*", "server/**/*", "jest.config.js"],
"include": [
"../../../typings/**/*",
"common/**/*",
"server/**/*",
"utils/**/*",
"jest.config.js"
],
"exclude": ["target/**/*"],
"kbn_references": [
"@kbn/config-schema",
"@kbn/core",
"@kbn/i18n",
"@kbn/core-saved-objects-api-server"
"@kbn/core-saved-objects-api-server",
"@kbn/observability-plugin",
"@kbn/core-elasticsearch-server",
"@kbn/es-types"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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 { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types';

type RequiredParams = ESSearchRequest & {
size: number;
track_total_hits: boolean | number;
};

export type TypedSearch = ReturnType<typeof getTypedSearch>;
export function getTypedSearch(esClient: ElasticsearchClient) {
async function search<TDocument, TParams extends RequiredParams>(
opts: TParams
): Promise<InferSearchResponseOf<TDocument, TParams>> {
return esClient.search<TDocument>(opts) as Promise<any>;
}

return { search };
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@ export const GetInfraMetricsRequestBodyPayloadRT = rt.intersection([
}),
rt.type({
type: rt.literal('host'),
limit: rt.union([inRangeRt(1, 500), createLiteralValueFromUndefinedRT(20)]),
metrics: rt.array(rt.type({ type: InfraMetricTypeRT })),
limit: rt.union([inRangeRt(1, 500), createLiteralValueFromUndefinedRT(500)]),
metrics: rt.array(InfraMetricTypeRT),
sourceId: rt.string,
range: RangeRT,
from: dateRt,
to: dateRt,
}),
]);

Expand Down Expand Up @@ -77,12 +78,10 @@ export type InfraAssetMetricsItem = rt.TypeOf<typeof InfraAssetMetricsItemRT>;

export type GetInfraMetricsRequestBodyPayload = Omit<
rt.TypeOf<typeof GetInfraMetricsRequestBodyPayloadRT>,
'limit' | 'range'
'limit' | 'from' | 'to'
> & {
limit?: number;
range: {
from: string;
to: string;
};
limit: number;
from: string;
to: string;
};
export type GetInfraMetricsResponsePayload = rt.TypeOf<typeof GetInfraMetricsResponsePayloadRT>;
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ import {
} from '../../../../../common/http_api';
import { StringDateRange } from './use_unified_search_url_state';

const HOST_TABLE_METRICS: Array<{ type: InfraAssetMetricType }> = [
{ type: 'cpu' },
{ type: 'diskSpaceUsage' },
{ type: 'memory' },
{ type: 'memoryFree' },
{ type: 'normalizedLoad1m' },
{ type: 'rx' },
{ type: 'tx' },
const HOST_TABLE_METRICS: InfraAssetMetricType[] = [
'cpu',
'diskSpaceUsage',
'memory',
'memoryFree',
'normalizedLoad1m',
'rx',
'tx',
];

const BASE_INFRA_METRICS_PATH = '/api/metrics/infra';
Expand Down Expand Up @@ -105,10 +105,8 @@ const createInfraMetricsRequest = ({
}): GetInfraMetricsRequestBodyPayload => ({
type: 'host',
query: esQuery,
range: {
from: dateRange.from,
to: dateRange.to,
},
from: dateRange.from,
to: dateRange.to,
metrics: HOST_TABLE_METRICS,
limit,
sourceId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ import type { AlertsLocatorParams } from '@kbn/observability-plugin/common';
import { ObservabilityConfig } from '@kbn/observability-plugin/server';
import type { LocatorPublic } from '@kbn/share-plugin/common';
import type { ILogsSharedLogEntriesDomain } from '@kbn/logs-shared-plugin/server';
import type { MetricsDataClient } from '@kbn/metrics-data-access-plugin/server';
import { APMDataAccessConfig } from '@kbn/apm-data-access-plugin/server';
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import type { MetricsDataPluginSetup } from '@kbn/metrics-data-access-plugin/server';
import { ApmDataAccessPluginSetup } from '@kbn/apm-data-access-plugin/server';
import { RulesServiceSetup } from '../services/rules';
import { InfraConfig, InfraPluginStartServicesAccessor } from '../types';
import { KibanaFramework } from './adapters/framework/kibana_framework_adapter';
Expand Down Expand Up @@ -42,6 +41,6 @@ export interface InfraBackendLibs extends InfraDomainLibs {
handleEsError: typeof handleEsError;
logger: Logger;
alertsLocator?: LocatorPublic<AlertsLocatorParams>;
metricsClient: MetricsDataClient;
getApmIndices: (soClient: SavedObjectsClientContract) => Promise<APMDataAccessConfig['indices']>;
metricsDataAccess: MetricsDataPluginSetup;
apmDataAccess: ApmDataAccessPluginSetup;
}
27 changes: 15 additions & 12 deletions x-pack/plugins/observability_solution/infra/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,18 +176,21 @@ export class InfraServerPlugin

setup(core: InfraPluginCoreSetup, plugins: InfraServerPluginSetupDeps) {
const framework = new KibanaFramework(core, this.config, plugins);
const metricsClient = plugins.metricsDataAccess.client;
const getApmIndices = plugins.apmDataAccess.getApmIndices;
metricsClient.setDefaultMetricIndicesHandler(async (options: GetMetricIndicesOptions) => {
const sourceConfiguration = await sources.getInfraSourceConfiguration(
options.savedObjectsClient,
'default'
);
return sourceConfiguration.configuration.metricAlias;
});
const apmDataAccess = plugins.apmDataAccess;
const metricsDataAccess = plugins.metricsDataAccess;

metricsDataAccess.client.setDefaultMetricIndicesHandler(
async (options: GetMetricIndicesOptions) => {
const sourceConfiguration = await sources.getInfraSourceConfiguration(
options.savedObjectsClient,
'default'
);
return sourceConfiguration.configuration.metricAlias;
}
);
const sources = new InfraSources({
config: this.config,
metricsClient,
metricsClient: metricsDataAccess.client,
});
const sourceStatus = new InfraSourceStatus(
new InfraElasticsearchSourceStatusAdapter(framework),
Expand Down Expand Up @@ -222,8 +225,8 @@ export class InfraServerPlugin
framework,
sources,
sourceStatus,
metricsClient,
getApmIndices,
metricsDataAccess,
apmDataAccess,
...domainLibs,
handleEsError,
logsRules: this.logsRules.setup(core, plugins),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,7 @@ curl --location -u elastic:changeme 'http://0.0.0.0:5601/ftw/api/metrics/infra'
--data '{
"type": 'host',
"limit": 100,
"metrics": [
{
"type": "rx"
},
{
"type": "tx"
},
{
"type": "memory"
},
{
"type": "cpu"
},
{
type: 'diskSpaceUsage',
},
{
type: 'memoryFree',
},
],
"metrics": ["rx","tx","memory","diskSpaceUsage","memoryFree"],
"query": {
"bool": {
"must": [],
Expand All @@ -58,10 +39,8 @@ curl --location -u elastic:changeme 'http://0.0.0.0:5601/ftw/api/metrics/infra'
"must_not": []
}
},
"range": {
"from": "2023-04-18T11:15:31.407Z",
"to": "2023-04-18T11:30:31.407Z"
},
"from": "2023-04-18T11:15:31.407Z",
"to": "2023-04-18T11:30:31.407Z"
"sourceId": "default"
}'
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ export const initInfraAssetRoutes = (libs: InfraBackendLibs) => {
const hosts = await getHosts({
infraMetricsClient,
alertsClient,
params,
apmDataAccess: libs.apmDataAccess,
...params,
});

return response.ok({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,3 @@
export const BUCKET_KEY = 'host.name';
export const METADATA_AGGREGATION_NAME = 'metadata';
export const FILTER_AGGREGATION_SUB_AGG_NAME = 'result';

export const MAX_SIZE = 500;
Loading