Skip to content

Commit

Permalink
[ObsUX][Infra] Move getServices to apm_data_access plugin (#192565)
Browse files Browse the repository at this point in the history
Closes #190338

### What was done

- Creates a service in `apm_data_access` that returns services for a
specific host
- Remove all getServices() from infra plugin

#### How to test

In Infrastructure/Hosts, filter by `service.name: *`, when clicking on
one of the hosts we expect to see in the host details, and also the
flyout, a list of services with their proper agent icon

<img width="1041" alt="image"
src="https://github.com/user-attachments/assets/6fbd0b67-4345-48e7-9ad4-5a354218246a">
  • Loading branch information
MiriamAparicio authored Sep 12, 2024
1 parent 6f7bc21 commit 7256c6c
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 185 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*
* 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 { rangeQuery, termQuery } from '@kbn/observability-plugin/server';
import {
AGENT_NAME,
HOST_HOSTNAME,
HOST_NAME,
METRICSET_NAME,
SERVICE_NAME,
} from '@kbn/apm-types/es_fields';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
RollupInterval,
TimeRangeMetadata,
getBucketSize,
getPreferredBucketSizeAndDataSource,
} from '../../../common';
import { ApmDocumentType } from '../../../common/document_type';
import type { ApmDataAccessServicesParams } from '../get_services';

const MAX_SIZE = 1000;

export interface HostServicesRequest {
filters: Record<string, string>;
start: number;
end: number;
size?: number;
documentSources: TimeRangeMetadata['sources'];
}

const suitableTypes = [ApmDocumentType.TransactionMetric, ApmDocumentType.ErrorEvent];

export function createGetHostServices({ apmEventClient }: ApmDataAccessServicesParams) {
return async ({ start, end, size = MAX_SIZE, filters, documentSources }: HostServicesRequest) => {
const sourcesToUse = getPreferredBucketSizeAndDataSource({
sources: documentSources.filter((s) => suitableTypes.includes(s.documentType)),
bucketSizeInSeconds: getBucketSize({ start, end, numBuckets: 50 }).bucketSize,
});

const commonFiltersList: QueryDslQueryContainer[] = [
...rangeQuery(start, end),
{
exists: {
field: SERVICE_NAME,
},
},
];

if (filters[HOST_NAME]) {
commonFiltersList.push({
bool: {
should: [
...termQuery(HOST_NAME, filters[HOST_NAME]),
...termQuery(HOST_HOSTNAME, filters[HOST_HOSTNAME]),
],
minimum_should_match: 1,
},
});
}
// get services from transaction metrics
const metricsQuery = await apmEventClient.search('get_apm_host_services_from_metrics', {
apm: {
sources: [
{
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.OneMinute,
},
],
},
body: {
track_total_hits: false,
size: 0,
query: {
bool: {
filter: [
{
bool: {
should: [
...termQuery(METRICSET_NAME, 'app'),
{
bool: {
must: [...termQuery(METRICSET_NAME, 'transaction')],
},
},
],
minimum_should_match: 1,
},
},
...commonFiltersList,
],
},
},
aggs: {
services: {
terms: {
field: SERVICE_NAME,
size,
},
aggs: {
latestAgent: {
top_metrics: {
metrics: [{ field: AGENT_NAME }],
sort: {
'@timestamp': 'desc',
},
size: 1,
},
},
},
},
},
},
});

// get services from logs
const logsQuery = await apmEventClient.search('get_apm_host_services_from_logs', {
apm: {
sources: [
{
documentType: ApmDocumentType.ErrorEvent,
rollupInterval: sourcesToUse.source.rollupInterval,
},
],
},
body: {
track_total_hits: false,
size: 0,
query: {
bool: {
filter: commonFiltersList,
},
},
aggs: {
services: {
terms: {
field: SERVICE_NAME,
size,
},
aggs: {
latestAgent: {
top_metrics: {
metrics: [{ field: AGENT_NAME }],
sort: {
'@timestamp': 'desc',
},
size: 1,
},
},
},
},
},
},
});

const servicesListBucketsFromMetrics = metricsQuery.aggregations?.services.buckets || [];
const servicesListBucketsFromLogs = logsQuery.aggregations?.services.buckets || [];
const serviceMap = [...servicesListBucketsFromMetrics, ...servicesListBucketsFromLogs].reduce(
(acc, bucket) => {
const serviceName = bucket.key as string;
const latestAgentEntry = bucket.latestAgent.top[0];
const latestTimestamp = latestAgentEntry.sort[0] as string;
const agentName = latestAgentEntry.metrics[AGENT_NAME] as string | null;
// dedup and get the latest timestamp
const existingService = acc.get(serviceName);
if (!existingService || existingService.latestTimestamp < latestTimestamp) {
acc.set(serviceName, { latestTimestamp, agentName });
}
return acc;
},
new Map<string, { latestTimestamp: string; agentName: string | null }>()
);
const services = Array.from(serviceMap)
.slice(0, size)
.map(([serviceName, { agentName }]) => ({
serviceName,
agentName,
}));
return { services };
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { APMEventClient } from '../lib/helpers/create_es_client/create_apm_event
import { createGetDocumentSources } from './get_document_sources';
import { getDocumentTypeConfig } from './get_document_type_config';
import { createGetHostNames } from './get_host_names';
import { createGetHostServices } from './get_host_services';

export interface ApmDataAccessServicesParams {
apmEventClient: APMEventClient;
Expand All @@ -19,5 +20,6 @@ export function getServices(params: ApmDataAccessServicesParams) {
getDocumentSources: createGetDocumentSources(params),
getHostNames: createGetHostNames(params),
getDocumentTypeConfig,
getHostServices: createGetHostServices(params),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,14 @@
import {
createLiteralValueFromUndefinedRT,
inRangeFromStringRt,
dateRt,
datemathStringRt,
isoToEpochRt,
} from '@kbn/io-ts-utils';
import * as rt from 'io-ts';

export const sizeRT = rt.union([
inRangeFromStringRt(1, 100),
createLiteralValueFromUndefinedRT(10),
]);
export const assetDateRT = rt.union([dateRt, datemathStringRt]);

export const servicesFiltersRT = rt.strict({
['host.name']: rt.string,
Expand All @@ -26,7 +24,7 @@ export const servicesFiltersRT = rt.strict({
export type ServicesFilter = rt.TypeOf<typeof servicesFiltersRT>;

export const GetServicesRequestQueryRT = rt.intersection([
rt.strict({ from: assetDateRT, to: assetDateRT, filters: rt.string }),
rt.strict({ from: isoToEpochRt, to: isoToEpochRt, filters: rt.string }),
rt.partial({
size: sizeRT,
validatedFilters: servicesFiltersRT,
Expand All @@ -37,8 +35,8 @@ export type GetServicesRequestQuery = rt.TypeOf<typeof GetServicesRequestQueryRT

export interface ServicesAPIRequest {
filters: ServicesFilter;
from: string;
to: string;
from: number;
to: number;
size?: number;
}

Expand Down
Loading

0 comments on commit 7256c6c

Please sign in to comment.