Skip to content

Commit

Permalink
[7.x] [APM] Include services with only metrics (#92378) (#93177)
Browse files Browse the repository at this point in the history
* [APM] Include services with only metric documents

Closes #92075.

* Explain as_mutable_array

* Use kuery instead of uiFilters for API tests
  • Loading branch information
dgieselaar authored Mar 2, 2021
1 parent 4eeac3a commit 30e4689
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 20 deletions.
41 changes: 41 additions & 0 deletions x-pack/plugins/apm/common/utils/as_mutable_array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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.
*/

// Sometimes we use `as const` to have a more specific type,
// because TypeScript by default will widen the value type of an
// array literal. Consider the following example:
//
// const filter = [
// { term: { 'agent.name': 'nodejs' } },
// { range: { '@timestamp': { gte: 'now-15m ' }}
// ];

// The result value type will be:

// const filter: ({
// term: {
// 'agent.name'?: string
// };
// range?: undefined
// } | {
// term?: undefined;
// range: {
// '@timestamp': {
// gte: string
// }
// }
// })[];

// This can sometimes leads to issues. In those cases, we can
// use `as const`. However, the Readonly<any> type is not compatible
// with Array<any>. This function returns a mutable version of a type.

export function asMutableArray<T extends Readonly<any>>(
arr: T
): T extends Readonly<[...infer U]> ? U : unknown[] {
return arr as any;
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ interface AggregationParams {
kuery?: string;
setup: ServicesItemsSetup;
searchAggregatedTransactions: boolean;
maxNumServices: number;
}

const MAX_NUMBER_OF_SERVICES = 500;

export async function getServiceTransactionStats({
environment,
kuery,
setup,
searchAggregatedTransactions,
maxNumServices,
}: AggregationParams) {
return withApmSpan('get_service_transaction_stats', async () => {
const { apmEventClient, start, end } = setup;
Expand Down Expand Up @@ -92,7 +92,7 @@ export async function getServiceTransactionStats({
services: {
terms: {
field: SERVICE_NAME,
size: MAX_NUMBER_OF_SERVICES,
size: maxNumServices,
},
aggs: {
transactionType: {
Expand All @@ -104,7 +104,6 @@ export async function getServiceTransactionStats({
environments: {
terms: {
field: SERVICE_ENVIRONMENT,
missing: '',
},
},
sample: {
Expand Down Expand Up @@ -147,9 +146,9 @@ export async function getServiceTransactionStats({
return {
serviceName: bucket.key as string,
transactionType: topTransactionTypeBucket.key as string,
environments: topTransactionTypeBucket.environments.buckets
.map((environmentBucket) => environmentBucket.key as string)
.filter(Boolean),
environments: topTransactionTypeBucket.environments.buckets.map(
(environmentBucket) => environmentBucket.key as string
),
agentName: topTransactionTypeBucket.sample.top[0].metrics[
AGENT_NAME
] as AgentName,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* 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 { AgentName } from '../../../../typings/es_schemas/ui/fields/agent';
import {
AGENT_NAME,
SERVICE_ENVIRONMENT,
SERVICE_NAME,
} from '../../../../common/elasticsearch_fieldnames';
import { environmentQuery, kqlQuery, rangeQuery } from '../../../utils/queries';
import { ProcessorEvent } from '../../../../common/processor_event';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
import { withApmSpan } from '../../../utils/with_apm_span';

export function getServicesFromMetricDocuments({
environment,
setup,
maxNumServices,
kuery,
}: {
setup: Setup & SetupTimeRange;
environment?: string;
maxNumServices: number;
kuery?: string;
}) {
return withApmSpan('get_services_from_metric_documents', async () => {
const { apmEventClient, start, end } = setup;

const response = await apmEventClient.search({
apm: {
events: [ProcessorEvent.metric],
},
body: {
size: 0,
query: {
bool: {
filter: [
...rangeQuery(start, end),
...environmentQuery(environment),
...kqlQuery(kuery),
],
},
},
aggs: {
services: {
terms: {
field: SERVICE_NAME,
size: maxNumServices,
},
aggs: {
environments: {
terms: {
field: SERVICE_ENVIRONMENT,
},
},
latest: {
top_metrics: {
metrics: { field: AGENT_NAME } as const,
sort: { '@timestamp': 'desc' },
},
},
},
},
},
},
});

return (
response.aggregations?.services.buckets.map((bucket) => {
return {
serviceName: bucket.key as string,
environments: bucket.environments.buckets.map(
(envBucket) => envBucket.key as string
),
agentName: bucket.latest.top[0].metrics[AGENT_NAME] as AgentName,
};
}) ?? []
);
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@
*/

import { Logger } from '@kbn/logging';
import { asMutableArray } from '../../../../common/utils/as_mutable_array';
import { joinByKey } from '../../../../common/utils/join_by_key';
import { getServicesProjection } from '../../../projections/services';
import { withApmSpan } from '../../../utils/with_apm_span';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
import { getHealthStatuses } from './get_health_statuses';
import { getServicesFromMetricDocuments } from './get_services_from_metric_documents';
import { getServiceTransactionStats } from './get_service_transaction_stats';

export type ServicesItemsSetup = Setup & SetupTimeRange;

const MAX_NUMBER_OF_SERVICES = 500;

export async function getServicesItems({
environment,
kuery,
Expand All @@ -32,33 +35,49 @@ export async function getServicesItems({
const params = {
environment,
kuery,
projection: getServicesProjection({
kuery,
setup,
searchAggregatedTransactions,
}),
setup,
searchAggregatedTransactions,
maxNumServices: MAX_NUMBER_OF_SERVICES,
};

const [transactionStats, healthStatuses] = await Promise.all([
const [
transactionStats,
servicesFromMetricDocuments,
healthStatuses,
] = await Promise.all([
getServiceTransactionStats(params),
getServicesFromMetricDocuments(params),
getHealthStatuses(params).catch((err) => {
logger.error(err);
return [];
}),
]);

const apmServices = transactionStats.map(({ serviceName }) => serviceName);
const foundServiceNames = transactionStats.map(
({ serviceName }) => serviceName
);

const servicesWithOnlyMetricDocuments = servicesFromMetricDocuments.filter(
({ serviceName }) => !foundServiceNames.includes(serviceName)
);

const allServiceNames = foundServiceNames.concat(
servicesWithOnlyMetricDocuments.map(({ serviceName }) => serviceName)
);

// make sure to exclude health statuses from services
// that are not found in APM data
const matchedHealthStatuses = healthStatuses.filter(({ serviceName }) =>
apmServices.includes(serviceName)
allServiceNames.includes(serviceName)
);

const allMetrics = [...transactionStats, ...matchedHealthStatuses];

return joinByKey(allMetrics, 'serviceName');
return joinByKey(
asMutableArray([
...transactionStats,
...servicesWithOnlyMetricDocuments,
...matchedHealthStatuses,
] as const),
'serviceName'
);
});
}
43 changes: 43 additions & 0 deletions x-pack/test/apm_api_integration/tests/services/top_services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,49 @@ export default function ApiTest({ getService }: FtrProviderContext) {
}
);

registry.when(
'APM Services Overview with a basic license when data is loaded excluding transaction events',
{ config: 'basic', archives: [archiveName] },
() => {
it('includes services that only report metric data', async () => {
interface Response {
status: number;
body: APIReturnType<'GET /api/apm/services'>;
}

const [unfilteredResponse, filteredResponse] = await Promise.all([
supertest.get(`/api/apm/services?start=${start}&end=${end}`) as Promise<Response>,
supertest.get(
`/api/apm/services?start=${start}&end=${end}&kuery=${encodeURIComponent(
'not (processor.event:transaction)'
)}`
) as Promise<Response>,
]);

expect(unfilteredResponse.body.items.length).to.be.greaterThan(0);

const unfilteredServiceNames = unfilteredResponse.body.items
.map((item) => item.serviceName)
.sort();

const filteredServiceNames = filteredResponse.body.items
.map((item) => item.serviceName)
.sort();

expect(unfilteredServiceNames).to.eql(filteredServiceNames);

expect(
filteredResponse.body.items.every((item) => {
// make sure it did not query transaction data
return isEmpty(item.avgResponseTime);
})
).to.be(true);

expect(filteredResponse.body.items.every((item) => !!item.agentName)).to.be(true);
});
}
);

registry.when(
'APM Services overview with a trial license when data is loaded',
{ config: 'trial', archives: [archiveName] },
Expand Down

0 comments on commit 30e4689

Please sign in to comment.