From 43d1c586d2d84a5f2da527a0ac080e2d56804d6c Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 9 Oct 2019 13:02:54 +0200 Subject: [PATCH] [APM] Garbage collection metrics charts (#47023) * [APM] Garbage collection metrics charts Closes #36320. * Review feedback * Display average of delta in gc chart --- .../elasticsearch_fieldnames.test.ts.snap | 18 ++ .../apm/common/elasticsearch_fieldnames.ts | 4 + .../app/ServiceNodeMetrics/index.tsx | 3 +- .../shared/charts/MetricsChart/index.tsx | 18 +- .../java/gc/fetchAndTransformGcMetrics.ts | 159 ++++++++++++++++++ .../by_agent/java/gc/getGcRateChart.ts | 47 ++++++ .../by_agent/java/gc/getGcTimeChart.ts | 47 ++++++ .../server/lib/metrics/by_agent/java/index.ts | 6 +- .../plugins/apm/typings/elasticsearch.ts | 11 +- .../legacy/plugins/apm/typings/timeseries.ts | 2 +- 10 files changed, 309 insertions(+), 6 deletions(-) create mode 100644 x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/fetchAndTransformGcMetrics.ts create mode 100644 x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcRateChart.ts create mode 100644 x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcTimeChart.ts diff --git a/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index ca1029db12d527..7409e64d396e8b 100644 --- a/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -20,6 +20,12 @@ exports[`Error HOST_NAME 1`] = `"my hostname"`; exports[`Error HTTP_REQUEST_METHOD 1`] = `undefined`; +exports[`Error LABEL_NAME 1`] = `undefined`; + +exports[`Error METRIC_JAVA_GC_COUNT 1`] = `undefined`; + +exports[`Error METRIC_JAVA_GC_TIME 1`] = `undefined`; + exports[`Error METRIC_JAVA_HEAP_MEMORY_COMMITTED 1`] = `undefined`; exports[`Error METRIC_JAVA_HEAP_MEMORY_MAX 1`] = `undefined`; @@ -116,6 +122,12 @@ exports[`Span HOST_NAME 1`] = `undefined`; exports[`Span HTTP_REQUEST_METHOD 1`] = `undefined`; +exports[`Span LABEL_NAME 1`] = `undefined`; + +exports[`Span METRIC_JAVA_GC_COUNT 1`] = `undefined`; + +exports[`Span METRIC_JAVA_GC_TIME 1`] = `undefined`; + exports[`Span METRIC_JAVA_HEAP_MEMORY_COMMITTED 1`] = `undefined`; exports[`Span METRIC_JAVA_HEAP_MEMORY_MAX 1`] = `undefined`; @@ -212,6 +224,12 @@ exports[`Transaction HOST_NAME 1`] = `"my hostname"`; exports[`Transaction HTTP_REQUEST_METHOD 1`] = `"GET"`; +exports[`Transaction LABEL_NAME 1`] = `undefined`; + +exports[`Transaction METRIC_JAVA_GC_COUNT 1`] = `undefined`; + +exports[`Transaction METRIC_JAVA_GC_TIME 1`] = `undefined`; + exports[`Transaction METRIC_JAVA_HEAP_MEMORY_COMMITTED 1`] = `undefined`; exports[`Transaction METRIC_JAVA_HEAP_MEMORY_MAX 1`] = `undefined`; diff --git a/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts index 4787b54d8dd5a2..f36bccc5868cb5 100644 --- a/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts @@ -58,6 +58,10 @@ export const METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED = 'jvm.memory.non_heap.committed'; export const METRIC_JAVA_NON_HEAP_MEMORY_USED = 'jvm.memory.non_heap.used'; export const METRIC_JAVA_THREAD_COUNT = 'jvm.thread.count'; +export const METRIC_JAVA_GC_COUNT = 'jvm.gc.count'; +export const METRIC_JAVA_GC_TIME = 'jvm.gc.time'; + +export const LABEL_NAME = 'labels.name'; export const HOST_NAME = 'host.hostname'; export const CONTAINER_ID = 'container.id'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx index 562fd5add83c5b..083ec939c9adcb 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx @@ -61,8 +61,7 @@ export function ServiceNodeMetrics() { } }, [serviceName, serviceNodeName]); - const isLoading = - status === FETCH_STATUS.LOADING || status === FETCH_STATUS.PENDING; + const isLoading = status === FETCH_STATUS.LOADING; return (
diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx index 9a12a704af9684..51aa4a40fb9234 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx @@ -12,7 +12,9 @@ import { asDynamicBytes, asPercent, getFixedByteFormatter, - asDecimal + asDecimal, + asTime, + asInteger } from '../../../../utils/formatters'; import { Coordinate } from '../../../../../typings/timeseries'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; @@ -64,6 +66,13 @@ function getYTickFormatter(chart: GenericMetricsChart) { case 'percent': { return (y: number | null | undefined) => asPercent(y || 0, 1); } + case 'time': { + return (y: number | null | undefined) => asTime(y); + } + case 'integer': { + return (y: number | null | undefined) => + isValidCoordinateValue(y) ? asInteger(y) : y; + } default: { return (y: number | null | undefined) => isValidCoordinateValue(y) ? asDecimal(y) : y; @@ -79,6 +88,13 @@ function getTooltipFormatter({ yUnit }: GenericMetricsChart) { case 'percent': { return (c: Coordinate) => asPercent(c.y || 0, 1); } + case 'time': { + return (c: Coordinate) => asTime(c.y); + } + case 'integer': { + return (c: Coordinate) => + isValidCoordinateValue(c.y) ? asInteger(c.y) : c.y; + } default: { return (c: Coordinate) => isValidCoordinateValue(c.y) ? asDecimal(c.y) : c.y; diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/fetchAndTransformGcMetrics.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/fetchAndTransformGcMetrics.ts new file mode 100644 index 00000000000000..a43d8cd0dc2ea4 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/fetchAndTransformGcMetrics.ts @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { idx } from '@kbn/elastic-idx'; +import { sum } from 'lodash'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { Setup } from '../../../../helpers/setup_request'; +import { getMetricsDateHistogramParams } from '../../../../helpers/metrics'; +import { ChartBase } from '../../../types'; +import { getMetricsProjection } from '../../../../../../common/projections/metrics'; +import { mergeProjection } from '../../../../../../common/projections/util/merge_projection'; +import { + SERVICE_AGENT_NAME, + LABEL_NAME, + METRIC_JAVA_GC_COUNT, + METRIC_JAVA_GC_TIME +} from '../../../../../../common/elasticsearch_fieldnames'; + +const colors = [ + theme.euiColorVis0, + theme.euiColorVis1, + theme.euiColorVis2, + theme.euiColorVis3, + theme.euiColorVis4, + theme.euiColorVis5, + theme.euiColorVis6 +]; + +export async function fetchAndTransformGcMetrics({ + setup, + serviceName, + serviceNodeName, + chartBase, + fieldName +}: { + setup: Setup; + serviceName: string; + serviceNodeName?: string; + chartBase: ChartBase; + fieldName: typeof METRIC_JAVA_GC_COUNT | typeof METRIC_JAVA_GC_TIME; +}) { + const { start, end, client } = setup; + + const projection = getMetricsProjection({ + setup, + serviceName, + serviceNodeName + }); + + // GC rate and time are reported by the agents as monotonically + // increasing counters, which means that we have to calculate + // the delta in an es query. In the future agent might start + // reporting deltas. + const params = mergeProjection(projection, { + body: { + size: 0, + query: { + bool: { + filter: [ + ...projection.body.query.bool.filter, + { exists: { field: fieldName } }, + { term: { [SERVICE_AGENT_NAME]: 'java' } } + ] + } + }, + aggs: { + per_pool: { + terms: { + field: `${LABEL_NAME}` + }, + aggs: { + over_time: { + date_histogram: getMetricsDateHistogramParams(start, end), + aggs: { + // get the max value + max: { + max: { + field: fieldName + } + }, + // get the derivative, which is the delta y + derivative: { + derivative: { + buckets_path: 'max' + } + }, + // if a gc counter is reset, the delta will be >0 and + // needs to be excluded + value: { + bucket_script: { + buckets_path: { value: 'derivative' }, + script: 'params.value > 0.0 ? params.value : 0.0' + } + } + } + } + } + } + } + } + }); + + const response = await client.search(params); + + const aggregations = idx(response, _ => _.aggregations); + + if (!aggregations) { + return { + ...chartBase, + totalHits: 0, + series: [] + }; + } + + const series = aggregations.per_pool.buckets.map((poolBucket, i) => { + const label = poolBucket.key; + const timeseriesData = poolBucket.over_time; + + const data = (idx(timeseriesData, _ => _.buckets) || []).map(bucket => { + // derivative/value will be undefined for the first hit and if the `max` value is null + const y = + 'value' in bucket && bucket.value.value !== null + ? bucket.value.value + : null; + + return { + y, + x: bucket.key + }; + }); + + const values = data.map(coordinate => coordinate.y).filter(y => y !== null); + + const overallValue = sum(values) / values.length; + + return { + title: label, + key: label, + type: chartBase.type, + color: colors[i], + overallValue, + data + }; + }); + + return { + ...chartBase, + totalHits: response.hits.total, + series + }; +} diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcRateChart.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcRateChart.ts new file mode 100644 index 00000000000000..7374171a360141 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcRateChart.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { i18n } from '@kbn/i18n'; +import { METRIC_JAVA_GC_COUNT } from '../../../../../../common/elasticsearch_fieldnames'; +import { Setup } from '../../../../helpers/setup_request'; +import { fetchAndTransformGcMetrics } from './fetchAndTransformGcMetrics'; +import { ChartBase } from '../../../types'; + +const series = { + [METRIC_JAVA_GC_COUNT]: { + title: i18n.translate('xpack.apm.agentMetrics.java.gcRate', { + defaultMessage: 'GC count' + }), + color: theme.euiColorVis0 + } +}; + +const chartBase: ChartBase = { + title: i18n.translate('xpack.apm.agentMetrics.java.gcCountChartTitle', { + defaultMessage: 'Garbage collection count' + }), + key: 'gc_count_line_chart', + type: 'linemark', + yUnit: 'integer', + series +}; + +const getGcRateChart = ( + setup: Setup, + serviceName: string, + serviceNodeName?: string +) => { + return fetchAndTransformGcMetrics({ + setup, + serviceName, + serviceNodeName, + chartBase, + fieldName: METRIC_JAVA_GC_COUNT + }); +}; + +export { getGcRateChart }; diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcTimeChart.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcTimeChart.ts new file mode 100644 index 00000000000000..43c10f5b97c54e --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcTimeChart.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { i18n } from '@kbn/i18n'; +import { METRIC_JAVA_GC_TIME } from '../../../../../../common/elasticsearch_fieldnames'; +import { Setup } from '../../../../helpers/setup_request'; +import { fetchAndTransformGcMetrics } from './fetchAndTransformGcMetrics'; +import { ChartBase } from '../../../types'; + +const series = { + [METRIC_JAVA_GC_TIME]: { + title: i18n.translate('xpack.apm.agentMetrics.java.gcTime', { + defaultMessage: 'GC time' + }), + color: theme.euiColorVis0 + } +}; + +const chartBase: ChartBase = { + title: i18n.translate('xpack.apm.agentMetrics.java.gcTimeChartTitle', { + defaultMessage: 'Garbage collection time' + }), + key: 'gc_time_line_chart', + type: 'linemark', + yUnit: 'time', + series +}; + +const getGcTimeChart = ( + setup: Setup, + serviceName: string, + serviceNodeName?: string +) => { + return fetchAndTransformGcMetrics({ + setup, + serviceName, + serviceNodeName, + chartBase, + fieldName: METRIC_JAVA_GC_TIME + }); +}; + +export { getGcTimeChart }; diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/index.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/index.ts index 55fcdd1c284ecd..e8f9e4345d06c0 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/index.ts @@ -10,6 +10,8 @@ import { getNonHeapMemoryChart } from './non_heap_memory'; import { getThreadCountChart } from './thread_count'; import { getCPUChartData } from '../shared/cpu'; import { getMemoryChartData } from '../shared/memory'; +import { getGcRateChart } from './gc/getGcRateChart'; +import { getGcTimeChart } from './gc/getGcTimeChart'; export async function getJavaMetricsCharts( setup: Setup, @@ -21,7 +23,9 @@ export async function getJavaMetricsCharts( getMemoryChartData(setup, serviceName, serviceNodeName), getHeapMemoryChart(setup, serviceName, serviceNodeName), getNonHeapMemoryChart(setup, serviceName, serviceNodeName), - getThreadCountChart(setup, serviceName, serviceNodeName) + getThreadCountChart(setup, serviceName, serviceNodeName), + getGcRateChart(setup, serviceName, serviceNodeName), + getGcTimeChart(setup, serviceName, serviceNodeName) ]); return { charts }; diff --git a/x-pack/legacy/plugins/apm/typings/elasticsearch.ts b/x-pack/legacy/plugins/apm/typings/elasticsearch.ts index 7d63d1ede20227..f2e84be6661177 100644 --- a/x-pack/legacy/plugins/apm/typings/elasticsearch.ts +++ b/x-pack/legacy/plugins/apm/typings/elasticsearch.ts @@ -30,7 +30,9 @@ declare module 'elasticsearch' { | 'filters' | 'cardinality' | 'sampler' - | 'value_count'; + | 'value_count' + | 'derivative' + | 'bucket_script'; type AggOptions = AggregationOptionMap & { [key: string]: any; @@ -139,6 +141,13 @@ declare module 'elasticsearch' { value: number; }; sampler: SamplerAggregation; + derivative: BucketAggregation< + AggregationOption[AggregationName], + number + >; + bucket_script: { + value: number | null; + }; }[AggregationType & keyof AggregationOption[AggregationName]]; } >; diff --git a/x-pack/legacy/plugins/apm/typings/timeseries.ts b/x-pack/legacy/plugins/apm/typings/timeseries.ts index e165c3b7af47b8..9b9f7dcc2c8207 100644 --- a/x-pack/legacy/plugins/apm/typings/timeseries.ts +++ b/x-pack/legacy/plugins/apm/typings/timeseries.ts @@ -27,4 +27,4 @@ export interface TimeSeries { } export type ChartType = 'area' | 'linemark'; -export type YUnit = 'percent' | 'bytes' | 'number'; +export type YUnit = 'percent' | 'bytes' | 'number' | 'time' | 'integer';