mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
49b39dff2a
commit
b93dc5f8c2
18 changed files with 886 additions and 44 deletions
|
@ -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.
|
||||
+
|
||||
|
|
|
@ -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"`;
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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<
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
};
|
||||
}) ?? []
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 };
|
||||
});
|
||||
}
|
|
@ -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 };
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ describe('services queries', () => {
|
|||
getServicesItems({
|
||||
setup,
|
||||
searchAggregatedTransactions: false,
|
||||
searchAggregatedServiceMetrics: false,
|
||||
logger: {} as any,
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
kuery: '',
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue