[APM] [experimental] Service metrics on inventory page (#140868)

* Service metrics on inventory page

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

* Create seperate query for service aggregegated transaction stats

* Fix typo and eslint errors

* Fix failed transaction rate function

* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'

* Use service metrics for details stats

* Fix throughput calculation

* Fix types

* Update snapshot

* Clean up code

* Create a helper to get aggregated metrics

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

* Add test for configuration

* Rename getAggregatedMetrics to getServiceInventorySearchSource

* query on metricset.name

* Address PR feedback

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

* Sort servcice metrics bucker by `value_count`

* fix broken path

* fix types

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Katerina Patticha 2022-09-20 17:17:33 +02:00 committed by GitHub
parent 49b39dff2a
commit b93dc5f8c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 886 additions and 44 deletions

View file

@ -77,6 +77,9 @@ Maximum number of child items displayed when viewing trace details. Defaults to
`xpack.observability.annotations.index` {ess-icon}::
Index name where Observability annotations are stored. Defaults to `observability-annotations`.
`xpack.apm.searchAggregatedServiceMetrics` {ess-icon}::
Enables Service metrics. Defaults to `false`. When set to `true`, additional configuration in APM Server is required.
`xpack.apm.searchAggregatedTransactions` {ess-icon}::
Enables Transaction histogram metrics. Defaults to `auto` so the UI will use metric indices over transaction indices for transactions if aggregated transactions are found. When set to `always`, additional configuration in APM Server is required. When set to `never` and aggregated transactions are not used.
+

View file

@ -221,6 +221,10 @@ exports[`Error TRANSACTION_DURATION 1`] = `undefined`;
exports[`Error TRANSACTION_DURATION_HISTOGRAM 1`] = `undefined`;
exports[`Error TRANSACTION_DURATION_SUMMARY 1`] = `undefined`;
exports[`Error TRANSACTION_FAILURE_COUNT 1`] = `undefined`;
exports[`Error TRANSACTION_ID 1`] = `"transaction id"`;
exports[`Error TRANSACTION_NAME 1`] = `undefined`;
@ -233,6 +237,8 @@ exports[`Error TRANSACTION_ROOT 1`] = `undefined`;
exports[`Error TRANSACTION_SAMPLED 1`] = `undefined`;
exports[`Error TRANSACTION_SUCCESS_COUNT 1`] = `undefined`;
exports[`Error TRANSACTION_TYPE 1`] = `"request"`;
exports[`Error TRANSACTION_URL 1`] = `undefined`;
@ -462,6 +468,10 @@ exports[`Span TRANSACTION_DURATION 1`] = `undefined`;
exports[`Span TRANSACTION_DURATION_HISTOGRAM 1`] = `undefined`;
exports[`Span TRANSACTION_DURATION_SUMMARY 1`] = `undefined`;
exports[`Span TRANSACTION_FAILURE_COUNT 1`] = `undefined`;
exports[`Span TRANSACTION_ID 1`] = `"transaction id"`;
exports[`Span TRANSACTION_NAME 1`] = `undefined`;
@ -474,6 +484,8 @@ exports[`Span TRANSACTION_ROOT 1`] = `undefined`;
exports[`Span TRANSACTION_SAMPLED 1`] = `undefined`;
exports[`Span TRANSACTION_SUCCESS_COUNT 1`] = `undefined`;
exports[`Span TRANSACTION_TYPE 1`] = `undefined`;
exports[`Span TRANSACTION_URL 1`] = `undefined`;
@ -721,6 +733,10 @@ exports[`Transaction TRANSACTION_DURATION 1`] = `1337`;
exports[`Transaction TRANSACTION_DURATION_HISTOGRAM 1`] = `undefined`;
exports[`Transaction TRANSACTION_DURATION_SUMMARY 1`] = `undefined`;
exports[`Transaction TRANSACTION_FAILURE_COUNT 1`] = `undefined`;
exports[`Transaction TRANSACTION_ID 1`] = `"transaction id"`;
exports[`Transaction TRANSACTION_NAME 1`] = `"transaction name"`;
@ -733,6 +749,8 @@ exports[`Transaction TRANSACTION_ROOT 1`] = `undefined`;
exports[`Transaction TRANSACTION_SAMPLED 1`] = `true`;
exports[`Transaction TRANSACTION_SUCCESS_COUNT 1`] = `undefined`;
exports[`Transaction TRANSACTION_TYPE 1`] = `"transaction type"`;
exports[`Transaction TRANSACTION_URL 1`] = `"http://www.elastic.co"`;

View file

@ -46,12 +46,15 @@ export const PROCESSOR_EVENT = 'processor.event';
export const TRANSACTION_DURATION = 'transaction.duration.us';
export const TRANSACTION_DURATION_HISTOGRAM = 'transaction.duration.histogram';
export const TRANSACTION_DURATION_SUMMARY = 'transaction.duration.summary';
export const TRANSACTION_TYPE = 'transaction.type';
export const TRANSACTION_RESULT = 'transaction.result';
export const TRANSACTION_NAME = 'transaction.name';
export const TRANSACTION_ID = 'transaction.id';
export const TRANSACTION_SAMPLED = 'transaction.sampled';
export const TRANSACTION_PAGE_URL = 'transaction.page.url';
export const TRANSACTION_FAILURE_COUNT = 'transaction.failure_count';
export const TRANSACTION_SUCCESS_COUNT = 'transaction.success_count';
// for transaction metrics
export const TRANSACTION_ROOT = 'transaction.root';

View file

@ -31,6 +31,9 @@ const configSchema = schema.object({
transactionGroupBucketSize: schema.number({ defaultValue: 1000 }),
maxTraceItems: schema.number({ defaultValue: 1000 }),
}),
searchAggregatedServiceMetrics: schema.boolean({
defaultValue: false,
}),
searchAggregatedTransactions: schema.oneOf(
[
schema.literal(SearchAggregatedTransactionSetting.auto),

View file

@ -12,12 +12,17 @@ export function getBucketSizeForAggregatedTransactions({
end,
numBuckets = 50,
searchAggregatedTransactions,
searchAggregatedServiceMetrics,
}: {
start: number;
end: number;
numBuckets?: number;
searchAggregatedTransactions?: boolean;
searchAggregatedServiceMetrics?: boolean;
}) {
const minBucketSize = searchAggregatedTransactions ? 60 : undefined;
const minBucketSize =
searchAggregatedTransactions || searchAggregatedServiceMetrics
? 60
: undefined;
return getBucketSize({ start, end, numBuckets, minBucketSize });
}

View file

@ -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 { APMEventClient } from './create_es_client/create_apm_event_client';
import { getSearchAggregatedTransactions } from './transactions';
import { getSearchAggregatedServiceMetrics } from './service_metrics';
import { APMConfig } from '../..';
export async function getServiceInventorySearchSource({
config,
apmEventClient,
start,
end,
kuery,
}: {
config: APMConfig;
apmEventClient: APMEventClient;
start: number;
end: number;
kuery: string;
}): Promise<{
searchAggregatedTransactions: boolean;
searchAggregatedServiceMetrics: boolean;
}> {
const commonProps = {
config,
apmEventClient,
kuery,
start,
end,
};
const [searchAggregatedTransactions, searchAggregatedServiceMetrics] =
await Promise.all([
getSearchAggregatedTransactions(commonProps),
getSearchAggregatedServiceMetrics(commonProps),
]);
return {
searchAggregatedTransactions,
searchAggregatedServiceMetrics,
};
}

View file

@ -0,0 +1,124 @@
/*
* 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 { getSearchAggregatedServiceMetrics } from '.';
import {
SearchParamsMock,
inspectSearchParams,
} from '../../../utils/test_helpers';
import { Setup } from '../setup_request';
const mockResponseWithServiceMetricsHits = {
took: 398,
timed_out: false,
_shards: {
total: 1,
successful: 1,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 1,
relation: 'gte' as const,
},
hits: [],
},
};
const mockResponseWithServiceMetricsNoHits = {
took: 398,
timed_out: false,
_shards: {
total: 1,
successful: 1,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 0,
relation: 'gte' as const,
},
hits: [],
},
};
describe('get default configuration for aggregated service metrics', () => {
it('should be false by default', async () => {
const mockSetup = {
apmEventClient: { search: () => Promise.resolve(response) },
config: {},
} as unknown as Setup;
const response = await getSearchAggregatedServiceMetrics({
apmEventClient: mockSetup.apmEventClient,
config: mockSetup.config,
kuery: '',
});
expect(response).toBeFalsy();
});
});
describe('get has aggregated', () => {
it('should be false when xpack.apm.searchAggregatedServiceMetrics=false ', async () => {
const mockSetup = {
apmEventClient: { search: () => Promise.resolve(response) },
config: { 'xpack.apm.searchAggregatedServiceMetrics': false },
} as unknown as Setup;
const response = await getSearchAggregatedServiceMetrics({
apmEventClient: mockSetup.apmEventClient,
config: mockSetup.config,
kuery: '',
});
expect(response).toBeFalsy();
});
describe('with xpack.apm.searchAggregatedServiceMetrics=true', () => {
let mock: SearchParamsMock;
const config = {
searchAggregatedServiceMetrics: true,
};
afterEach(() => {
mock.teardown();
});
it('should be true when service metrics data are found', async () => {
mock = await inspectSearchParams(
(setup) =>
getSearchAggregatedServiceMetrics({
apmEventClient: setup.apmEventClient,
config: setup.config,
kuery: '',
}),
{
config,
mockResponse: () => mockResponseWithServiceMetricsHits,
}
);
expect(mock.response).toBeTruthy();
});
it('should be false when service metrics data are not found', async () => {
mock = await inspectSearchParams(
(setup) =>
getSearchAggregatedServiceMetrics({
apmEventClient: setup.apmEventClient,
config: setup.config,
kuery: '',
}),
{
config,
mockResponse: () => mockResponseWithServiceMetricsNoHits,
}
);
expect(mock.response).toBeFalsy();
});
});
});

View file

@ -0,0 +1,83 @@
/*
* 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 { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server';
import { ProcessorEvent } from '@kbn/observability-plugin/common';
import { METRICSET_NAME } from '../../../../common/elasticsearch_fieldnames';
import { APMConfig } from '../../..';
import { APMEventClient } from '../create_es_client/create_apm_event_client';
export async function getSearchAggregatedServiceMetrics({
config,
start,
end,
apmEventClient,
kuery,
}: {
config: APMConfig;
start?: number;
end?: number;
apmEventClient: APMEventClient;
kuery: string;
}): Promise<boolean> {
if (config.searchAggregatedServiceMetrics) {
return getHasAggregatedServicesMetrics({
start,
end,
apmEventClient,
kuery,
});
}
return false;
}
export async function getHasAggregatedServicesMetrics({
start,
end,
apmEventClient,
kuery,
}: {
start?: number;
end?: number;
apmEventClient: APMEventClient;
kuery: string;
}) {
const response = await apmEventClient.search(
'get_has_aggregated_service_metrics',
{
apm: {
events: [ProcessorEvent.metric],
},
body: {
size: 1,
query: {
bool: {
filter: [
...getDocumentTypeFilterForServiceMetrics(),
...(start && end ? rangeQuery(start, end) : []),
...kqlQuery(kuery),
],
},
},
},
terminate_after: 1,
}
);
return response.hits.total.value > 0;
}
export function getDocumentTypeFilterForServiceMetrics() {
return [
{
term: {
[METRICSET_NAME]: 'service',
},
},
];
}

View file

@ -0,0 +1,54 @@
/*
* 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 { calculateFailedTransactionRateFromServiceMetrics } from './transaction_error_rate';
describe('calculateFailedTransactionRateFromServiceMetrics', () => {
it('should return 0 when all params are null', () => {
expect(
calculateFailedTransactionRateFromServiceMetrics({
failedTransactions: null,
successfulTransactions: null,
})
).toBe(0);
});
it('should return 9 when failedTransactions:null', () => {
expect(
calculateFailedTransactionRateFromServiceMetrics({
failedTransactions: null,
successfulTransactions: 2,
})
).toBe(0);
});
it('should return 0 when failedTransactions:0', () => {
expect(
calculateFailedTransactionRateFromServiceMetrics({
failedTransactions: 0,
successfulTransactions: null,
})
).toBe(0);
});
it('should return 1 when failedTransactions:10 and successfulTransactions:0', () => {
expect(
calculateFailedTransactionRateFromServiceMetrics({
failedTransactions: 10,
successfulTransactions: 0,
})
).toBe(1);
});
it('should return 0,5 when failedTransactions:10 and successfulTransactions:10', () => {
expect(
calculateFailedTransactionRateFromServiceMetrics({
failedTransactions: 10,
successfulTransactions: 10,
})
).toBe(0.5);
});
});

View file

@ -9,15 +9,18 @@ import type {
AggregationOptionsByType,
AggregationResultOf,
} from '@kbn/es-types';
import { isNull } from 'lodash';
import { EVENT_OUTCOME } from '../../../common/elasticsearch_fieldnames';
import { EventOutcome } from '../../../common/event_outcome';
export const getOutcomeAggregation = () => ({
terms: {
field: EVENT_OUTCOME,
include: [EventOutcome.failure, EventOutcome.success],
},
});
export const getOutcomeAggregation = () => {
return {
terms: {
field: EVENT_OUTCOME,
include: [EventOutcome.failure, EventOutcome.success],
},
};
};
type OutcomeAggregation = ReturnType<typeof getOutcomeAggregation>;
@ -48,6 +51,21 @@ export function calculateFailedTransactionRate(
return failedTransactions / (successfulTransactions + failedTransactions);
}
export function calculateFailedTransactionRateFromServiceMetrics({
failedTransactions,
successfulTransactions,
}: {
failedTransactions: number | null;
successfulTransactions: number | null;
}) {
if (isNull(failedTransactions) || failedTransactions === 0) {
return 0;
}
successfulTransactions = successfulTransactions ?? 0;
return failedTransactions / (successfulTransactions + failedTransactions);
}
export function getFailedTransactionRateTimeSeries(
buckets: AggregationResultOf<
{

View file

@ -0,0 +1,174 @@
/*
* 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 { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server';
import { ProcessorEvent } from '@kbn/observability-plugin/common';
import {
AGENT_NAME,
SERVICE_ENVIRONMENT,
SERVICE_NAME,
TRANSACTION_TYPE,
TRANSACTION_DURATION_SUMMARY,
TRANSACTION_FAILURE_COUNT,
TRANSACTION_SUCCESS_COUNT,
} from '../../../../common/elasticsearch_fieldnames';
import {
TRANSACTION_PAGE_LOAD,
TRANSACTION_REQUEST,
} from '../../../../common/transaction_types';
import { environmentQuery } from '../../../../common/utils/environment_query';
import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent';
import { calculateThroughputWithRange } from '../../../lib/helpers/calculate_throughput';
import { calculateFailedTransactionRateFromServiceMetrics } from '../../../lib/helpers/transaction_error_rate';
import { ServicesItemsSetup } from './get_services_items';
import { serviceGroupQuery } from '../../../lib/service_group_query';
import { ServiceGroup } from '../../../../common/service_groups';
import { RandomSampler } from '../../../lib/helpers/get_random_sampler';
import { getDocumentTypeFilterForServiceMetrics } from '../../../lib/helpers/service_metrics';
interface AggregationParams {
environment: string;
kuery: string;
setup: ServicesItemsSetup;
maxNumServices: number;
start: number;
end: number;
serviceGroup: ServiceGroup | null;
randomSampler: RandomSampler;
}
export async function getServiceAggregatedTransactionStats({
environment,
kuery,
setup,
maxNumServices,
start,
end,
serviceGroup,
randomSampler,
}: AggregationParams) {
const { apmEventClient } = setup;
const response = await apmEventClient.search(
'get_service_aggregated_transaction_stats',
{
apm: {
events: [ProcessorEvent.metric],
},
body: {
size: 0,
query: {
bool: {
filter: [
...getDocumentTypeFilterForServiceMetrics(),
...rangeQuery(start, end),
...environmentQuery(environment),
...kqlQuery(kuery),
...serviceGroupQuery(serviceGroup),
],
},
},
aggs: {
sample: {
random_sampler: randomSampler,
aggs: {
services: {
terms: {
field: SERVICE_NAME,
size: maxNumServices,
},
aggs: {
transactionType: {
terms: {
field: TRANSACTION_TYPE,
},
aggs: {
avg_duration: {
avg: {
field: TRANSACTION_DURATION_SUMMARY,
},
},
total_doc: {
value_count: {
field: TRANSACTION_DURATION_SUMMARY,
},
},
failure_count: {
sum: {
field: TRANSACTION_FAILURE_COUNT,
},
},
success_count: {
sum: {
field: TRANSACTION_SUCCESS_COUNT,
},
},
environments: {
terms: {
field: SERVICE_ENVIRONMENT,
},
},
sample: {
top_metrics: {
metrics: [{ field: AGENT_NAME } as const],
sort: {
'@timestamp': 'desc' as const,
},
},
},
bucket_sort: {
bucket_sort: {
sort: [
{
total_doc: {
order: 'desc',
},
},
],
},
},
},
},
},
},
},
},
},
},
}
);
return (
response.aggregations?.sample.services.buckets.map((bucket) => {
const topTransactionTypeBucket =
bucket.transactionType.buckets.find(
({ key }) =>
key === TRANSACTION_REQUEST || key === TRANSACTION_PAGE_LOAD
) ?? bucket.transactionType.buckets[0];
return {
serviceName: bucket.key as string,
transactionType: topTransactionTypeBucket.key as string,
environments: topTransactionTypeBucket.environments.buckets.map(
(environmentBucket) => environmentBucket.key as string
),
agentName: topTransactionTypeBucket.sample.top[0].metrics[
AGENT_NAME
] as AgentName,
latency: topTransactionTypeBucket.avg_duration.value,
transactionErrorRate: calculateFailedTransactionRateFromServiceMetrics({
failedTransactions: topTransactionTypeBucket.failure_count.value,
successfulTransactions: topTransactionTypeBucket.success_count.value,
}),
throughput: calculateThroughputWithRange({
start,
end,
value: topTransactionTypeBucket.total_doc.value,
}),
};
}) ?? []
);
}

View file

@ -11,6 +11,7 @@ import { Setup } from '../../../lib/helpers/setup_request';
import { getHealthStatuses } from './get_health_statuses';
import { getServicesFromErrorAndMetricDocuments } from './get_services_from_error_and_metric_documents';
import { getServiceTransactionStats } from './get_service_transaction_stats';
import { getServiceAggregatedTransactionStats } from './get_service_aggregated_transaction_stats';
import { mergeServiceStats } from './merge_service_stats';
import { ServiceGroup } from '../../../../common/service_groups';
import { RandomSampler } from '../../../lib/helpers/get_random_sampler';
@ -24,6 +25,7 @@ export async function getServicesItems({
kuery,
setup,
searchAggregatedTransactions,
searchAggregatedServiceMetrics,
logger,
start,
end,
@ -34,6 +36,7 @@ export async function getServicesItems({
kuery: string;
setup: ServicesItemsSetup;
searchAggregatedTransactions: boolean;
searchAggregatedServiceMetrics: boolean;
logger: Logger;
start: number;
end: number;
@ -46,6 +49,7 @@ export async function getServicesItems({
kuery,
setup,
searchAggregatedTransactions,
searchAggregatedServiceMetrics,
maxNumServices: MAX_NUMBER_OF_SERVICES,
start,
end,
@ -58,7 +62,9 @@ export async function getServicesItems({
servicesFromErrorAndMetricDocuments,
healthStatuses,
] = await Promise.all([
getServiceTransactionStats(params),
searchAggregatedServiceMetrics
? getServiceAggregatedTransactionStats(params)
: getServiceTransactionStats(params),
getServicesFromErrorAndMetricDocuments(params),
getHealthStatuses(params).catch((err) => {
logger.error(err);

View file

@ -17,6 +17,7 @@ export async function getServices({
kuery,
setup,
searchAggregatedTransactions,
searchAggregatedServiceMetrics,
logger,
start,
end,
@ -27,6 +28,7 @@ export async function getServices({
kuery: string;
setup: Setup;
searchAggregatedTransactions: boolean;
searchAggregatedServiceMetrics: boolean;
logger: Logger;
start: number;
end: number;
@ -39,6 +41,7 @@ export async function getServices({
kuery,
setup,
searchAggregatedTransactions,
searchAggregatedServiceMetrics,
logger,
start,
end,

View file

@ -0,0 +1,244 @@
/*
* 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 { keyBy } from 'lodash';
import { ProcessorEvent } from '@kbn/observability-plugin/common';
import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server';
import {
SERVICE_NAME,
TRANSACTION_TYPE,
TRANSACTION_DURATION_SUMMARY,
TRANSACTION_FAILURE_COUNT,
TRANSACTION_SUCCESS_COUNT,
} from '../../../../common/elasticsearch_fieldnames';
import { withApmSpan } from '../../../utils/with_apm_span';
import {
TRANSACTION_PAGE_LOAD,
TRANSACTION_REQUEST,
} from '../../../../common/transaction_types';
import { environmentQuery } from '../../../../common/utils/environment_query';
import { getOffsetInMs } from '../../../../common/utils/get_offset_in_ms';
import { calculateThroughputWithRange } from '../../../lib/helpers/calculate_throughput';
import { getBucketSizeForAggregatedTransactions } from '../../../lib/helpers/get_bucket_size_for_aggregated_transactions';
import { Setup } from '../../../lib/helpers/setup_request';
import { calculateFailedTransactionRateFromServiceMetrics } from '../../../lib/helpers/transaction_error_rate';
import { RandomSampler } from '../../../lib/helpers/get_random_sampler';
import { getDocumentTypeFilterForServiceMetrics } from '../../../lib/helpers/service_metrics';
export async function getServiceAggregatedTransactionDetailedStats({
serviceNames,
environment,
kuery,
setup,
searchAggregatedServiceMetrics,
offset,
start,
end,
randomSampler,
}: {
serviceNames: string[];
environment: string;
kuery: string;
setup: Setup;
searchAggregatedServiceMetrics: boolean;
offset?: string;
start: number;
end: number;
randomSampler: RandomSampler;
}) {
const { apmEventClient } = setup;
const { offsetInMs, startWithOffset, endWithOffset } = getOffsetInMs({
start,
end,
offset,
});
const metrics = {
avg_duration: {
avg: {
field: TRANSACTION_DURATION_SUMMARY,
},
},
total_doc: {
value_count: {
field: TRANSACTION_DURATION_SUMMARY,
},
},
failure_count: {
sum: {
field: TRANSACTION_FAILURE_COUNT,
},
},
success_count: {
sum: {
field: TRANSACTION_SUCCESS_COUNT,
},
},
};
const response = await apmEventClient.search(
'get_service_aggregated_transaction_detail_stats',
{
apm: {
events: [ProcessorEvent.metric],
},
body: {
size: 0,
query: {
bool: {
filter: [
{ terms: { [SERVICE_NAME]: serviceNames } },
...getDocumentTypeFilterForServiceMetrics(),
...rangeQuery(startWithOffset, endWithOffset),
...environmentQuery(environment),
...kqlQuery(kuery),
],
},
},
aggs: {
sample: {
random_sampler: randomSampler,
aggs: {
services: {
terms: {
field: SERVICE_NAME,
size: serviceNames.length,
},
aggs: {
transactionType: {
terms: {
field: TRANSACTION_TYPE,
},
aggs: {
...metrics,
timeseries: {
date_histogram: {
field: '@timestamp',
fixed_interval:
getBucketSizeForAggregatedTransactions({
start: startWithOffset,
end: endWithOffset,
numBuckets: 20,
searchAggregatedServiceMetrics,
}).intervalString,
min_doc_count: 0,
extended_bounds: {
min: startWithOffset,
max: endWithOffset,
},
},
aggs: metrics,
},
bucket_sort: {
bucket_sort: {
sort: [
{
total_doc: {
order: 'desc',
},
},
],
},
},
},
},
},
},
},
},
},
},
}
);
return keyBy(
response.aggregations?.sample.services.buckets.map((bucket) => {
const topTransactionTypeBucket =
bucket.transactionType.buckets.find(
({ key }) =>
key === TRANSACTION_REQUEST || key === TRANSACTION_PAGE_LOAD
) ?? bucket.transactionType.buckets[0];
return {
serviceName: bucket.key as string,
latency: topTransactionTypeBucket.timeseries.buckets.map(
(dateBucket) => ({
x: dateBucket.key + offsetInMs,
y: dateBucket.avg_duration.value,
})
),
transactionErrorRate: topTransactionTypeBucket.timeseries.buckets.map(
(dateBucket) => ({
x: dateBucket.key + offsetInMs,
y: calculateFailedTransactionRateFromServiceMetrics({
failedTransactions: dateBucket.failure_count.value,
successfulTransactions: dateBucket.success_count.value,
}),
})
),
throughput: topTransactionTypeBucket.timeseries.buckets.map(
(dateBucket) => ({
x: dateBucket.key + offsetInMs,
y: calculateThroughputWithRange({
start,
end,
value: dateBucket.total_doc.value,
}),
})
),
};
}) ?? [],
'serviceName'
);
}
export async function getServiceAggregatedDetailedStatsPeriods({
serviceNames,
environment,
kuery,
setup,
searchAggregatedServiceMetrics,
offset,
start,
end,
randomSampler,
}: {
serviceNames: string[];
environment: string;
kuery: string;
setup: Setup;
searchAggregatedServiceMetrics: boolean;
offset?: string;
start: number;
end: number;
randomSampler: RandomSampler;
}) {
return withApmSpan('get_service_aggregated_detailed_stats', async () => {
const commonProps = {
serviceNames,
environment,
kuery,
setup,
searchAggregatedServiceMetrics,
start,
end,
randomSampler,
};
const [currentPeriod, previousPeriod] = await Promise.all([
getServiceAggregatedTransactionDetailedStats(commonProps),
offset
? getServiceAggregatedTransactionDetailedStats({
...commonProps,
offset,
})
: Promise.resolve({}),
]);
return { currentPeriod, previousPeriod };
});
}

View file

@ -11,6 +11,7 @@ import {
SERVICE_NAME,
TRANSACTION_TYPE,
} from '../../../../common/elasticsearch_fieldnames';
import { withApmSpan } from '../../../utils/with_apm_span';
import {
TRANSACTION_PAGE_LOAD,
TRANSACTION_REQUEST,
@ -31,7 +32,7 @@ import {
} from '../../../lib/helpers/transaction_error_rate';
import { RandomSampler } from '../../../lib/helpers/get_random_sampler';
export async function getServiceTransactionDetailedStatistics({
export async function getServiceTransactionDetailedStats({
serviceNames,
environment,
kuery,
@ -175,3 +176,50 @@ export async function getServiceTransactionDetailedStatistics({
'serviceName'
);
}
export async function getServiceDetailedStatsPeriods({
serviceNames,
environment,
kuery,
setup,
searchAggregatedTransactions,
offset,
start,
end,
randomSampler,
}: {
serviceNames: string[];
environment: string;
kuery: string;
setup: Setup;
searchAggregatedTransactions: boolean;
offset?: string;
start: number;
end: number;
randomSampler: RandomSampler;
}) {
return withApmSpan('get_service_detailed_statistics', async () => {
const commonProps = {
serviceNames,
environment,
kuery,
setup,
searchAggregatedTransactions,
start,
end,
randomSampler,
};
const [currentPeriod, previousPeriod] = await Promise.all([
getServiceTransactionDetailedStats(commonProps),
offset
? getServiceTransactionDetailedStats({
...commonProps,
offset,
})
: Promise.resolve({}),
]);
return { currentPeriod, previousPeriod };
});
}

View file

@ -5,9 +5,9 @@
* 2.0.
*/
import { withApmSpan } from '../../../utils/with_apm_span';
import { Setup } from '../../../lib/helpers/setup_request';
import { getServiceTransactionDetailedStatistics } from './get_service_transaction_detailed_statistics';
import { getServiceDetailedStatsPeriods } from './get_service_transaction_detailed_statistics';
import { getServiceAggregatedDetailedStatsPeriods } from './get_service_aggregated_transaction_detailed_statistics';
import { RandomSampler } from '../../../lib/helpers/get_random_sampler';
export async function getServicesDetailedStatistics({
@ -16,6 +16,7 @@ export async function getServicesDetailedStatistics({
kuery,
setup,
searchAggregatedTransactions,
searchAggregatedServiceMetrics,
offset,
start,
end,
@ -26,30 +27,29 @@ export async function getServicesDetailedStatistics({
kuery: string;
setup: Setup;
searchAggregatedTransactions: boolean;
searchAggregatedServiceMetrics: boolean;
offset?: string;
start: number;
end: number;
randomSampler: RandomSampler;
}) {
return withApmSpan('get_service_detailed_statistics', async () => {
const commonProps = {
serviceNames,
environment,
kuery,
setup,
searchAggregatedTransactions,
start,
end,
randomSampler,
};
const [currentPeriod, previousPeriod] = await Promise.all([
getServiceTransactionDetailedStatistics(commonProps),
offset
? getServiceTransactionDetailedStatistics({ ...commonProps, offset })
: Promise.resolve({}),
]);
return { currentPeriod, previousPeriod };
});
const commonProps = {
serviceNames,
environment,
kuery,
setup,
start,
end,
randomSampler,
offset,
};
return searchAggregatedServiceMetrics
? getServiceAggregatedDetailedStatsPeriods({
...commonProps,
searchAggregatedServiceMetrics,
})
: getServiceDetailedStatsPeriods({
...commonProps,
searchAggregatedTransactions,
});
}

View file

@ -54,6 +54,7 @@ describe('services queries', () => {
getServicesItems({
setup,
searchAggregatedTransactions: false,
searchAggregatedServiceMetrics: false,
logger: {} as any,
environment: ENVIRONMENT_ALL.value,
kuery: '',

View file

@ -19,6 +19,7 @@ import { Annotation } from '@kbn/observability-plugin/common/annotations';
import { apmServiceGroupMaxNumberOfServices } from '@kbn/observability-plugin/common';
import { latencyAggregationTypeRt } from '../../../common/latency_aggregation_types';
import { getSearchAggregatedTransactions } from '../../lib/helpers/transactions';
import { getServiceInventorySearchSource } from '../../lib/helpers/get_service_inventory_search_source';
import { setupRequest } from '../../lib/helpers/setup_request';
import { getServiceAnnotations } from './annotations';
import { getServices } from './get_services';
@ -129,18 +130,23 @@ const servicesRoute = createApmServerRoute({
: Promise.resolve(null),
getRandomSampler({ security, request, probability }),
]);
const searchAggregatedTransactions = await getSearchAggregatedTransactions({
...setup,
kuery,
start,
end,
});
const { apmEventClient, config } = setup;
const { searchAggregatedTransactions, searchAggregatedServiceMetrics } =
await getServiceInventorySearchSource({
config,
apmEventClient,
kuery,
start,
end,
});
return getServices({
environment,
kuery,
setup,
searchAggregatedTransactions,
searchAggregatedServiceMetrics,
logger,
start,
end,
@ -213,12 +219,15 @@ const servicesDetailedStatisticsRoute = createApmServerRoute({
getRandomSampler({ security, request, probability }),
]);
const searchAggregatedTransactions = await getSearchAggregatedTransactions({
...setup,
start,
end,
kuery,
});
const { apmEventClient, config } = setup;
const { searchAggregatedTransactions, searchAggregatedServiceMetrics } =
await getServiceInventorySearchSource({
config,
apmEventClient,
kuery,
start,
end,
});
if (!serviceNames.length) {
throw Boom.badRequest(`serviceNames cannot be empty`);
@ -229,6 +238,7 @@ const servicesDetailedStatisticsRoute = createApmServerRoute({
kuery,
setup,
searchAggregatedTransactions,
searchAggregatedServiceMetrics,
offset,
serviceNames,
start,