[7.x] [APM]: Inferred types for aggregations (#37360) (#39148)

* [APM]: Inferred types for aggregations

Previously, aggregations returned by the ESClient were 'any' by default, and the return type had to be explicitly defined by the consumer to get any type safety. This leads to both type duplication and errors because of wrong assumptions.

This change infers the aggregation return type from the parameters passed to ESClient.search.

* Fix idx error

* Safeguard against querying against non-existing indices in functional tests

* Improve metric typings

* Automatically infer params from function arguments

* Remove unnecessary type hints
This commit is contained in:
Dario Gieselaar 2019-06-18 14:56:20 +02:00 committed by GitHub
parent 3551d6a3f3
commit d3853ee9c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 598 additions and 823 deletions

View file

@ -22,14 +22,20 @@ const getRelativeImpact = (
);
function getWithRelativeImpact(items: TransactionListAPIResponse) {
const impacts = items.map(({ impact }) => impact);
const impacts = items
.map(({ impact }) => impact)
.filter(impact => impact !== null) as number[];
const impactMin = Math.min(...impacts);
const impactMax = Math.max(...impacts);
return items.map(item => {
return {
...item,
impactRelative: getRelativeImpact(item.impact, impactMin, impactMax)
impactRelative:
item.impact !== null
? getRelativeImpact(item.impact, impactMin, impactMax)
: null
};
});
}

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { BucketAgg, ESFilter } from 'elasticsearch';
import { ESFilter } from 'elasticsearch';
import {
ERROR_GROUP_ID,
PROCESSOR_EVENT,
@ -61,13 +61,8 @@ export async function getBuckets({
}
};
interface Aggs {
distribution: {
buckets: Array<BucketAgg<number>>;
};
}
const resp = await client.search(params);
const resp = await client.search<void, Aggs>(params);
const buckets = resp.aggregations.distribution.buckets.map(bucket => ({
key: bucket.key,
count: bucket.doc_count

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { SearchParams } from 'elasticsearch';
import { idx } from '@kbn/elastic-idx';
import {
ERROR_CULPRIT,
@ -37,7 +36,10 @@ export async function getErrorGroups({
}) {
const { start, end, uiFiltersES, client, config } = setup;
const params: SearchParams = {
// sort buckets by last occurrence of error
const sortByLatestOccurrence = sortField === 'latestOccurrenceAt';
const params = {
index: config.get<string>('apm_oss.errorIndices'),
body: {
size: 0,
@ -56,7 +58,11 @@ export async function getErrorGroups({
terms: {
field: ERROR_GROUP_ID,
size: 500,
order: { _count: sortDirection }
order: sortByLatestOccurrence
? {
max_timestamp: sortDirection
}
: { _count: sortDirection }
},
aggs: {
sample: {
@ -72,24 +78,22 @@ export async function getErrorGroups({
sort: [{ '@timestamp': 'desc' }],
size: 1
}
}
},
...(sortByLatestOccurrence
? {
max_timestamp: {
max: {
field: '@timestamp'
}
}
}
: {})
}
}
}
}
};
// sort buckets by last occurrence of error
if (sortField === 'latestOccurrenceAt') {
params.body.aggs.error_groups.terms.order = {
max_timestamp: sortDirection
};
params.body.aggs.error_groups.aggs.max_timestamp = {
max: { field: '@timestamp' }
};
}
interface SampleError {
'@timestamp': APMError['@timestamp'];
error: {
@ -105,44 +109,27 @@ export async function getErrorGroups({
};
}
interface Bucket {
key: string;
doc_count: number;
sample: {
hits: {
total: number;
max_score: number | null;
hits: Array<{
_source: SampleError;
}>;
const resp = await client.search(params);
// aggregations can be undefined when no matching indices are found.
// this is an exception rather than the rule so the ES type does not account for this.
const hits = (idx(resp, _ => _.aggregations.error_groups.buckets) || []).map(
bucket => {
const source = bucket.sample.hits.hits[0]._source as SampleError;
const message =
idx(source, _ => _.error.log.message) ||
idx(source, _ => _.error.exception[0].message);
return {
message,
occurrenceCount: bucket.doc_count,
culprit: idx(source, _ => _.error.culprit),
groupId: idx(source, _ => _.error.grouping_key),
latestOccurrenceAt: source['@timestamp'],
handled: idx(source, _ => _.error.exception[0].handled)
};
};
}
interface Aggs {
error_groups: {
buckets: Bucket[];
};
}
const resp = await client.search<void, Aggs>(params);
const buckets = idx(resp, _ => _.aggregations.error_groups.buckets) || [];
const hits = buckets.map(bucket => {
const source = bucket.sample.hits.hits[0]._source;
const message =
idx(source, _ => _.error.log.message) ||
idx(source, _ => _.error.exception[0].message);
return {
message,
occurrenceCount: bucket.doc_count,
culprit: idx(source, _ => _.error.culprit),
groupId: idx(source, _ => _.error.grouping_key),
latestOccurrenceAt: source['@timestamp'],
handled: idx(source, _ => _.error.exception[0].handled)
};
});
}
);
return hits;
}

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { SearchParams } from 'elasticsearch';
import {
PROCESSOR_EVENT,
TRACE_ID,
@ -17,25 +16,14 @@ export interface ErrorsPerTransaction {
[transactionId: string]: number;
}
interface TraceErrorsAggBucket {
key: string;
doc_count: number;
}
interface TraceErrorsAggResponse {
transactions: {
buckets: TraceErrorsAggBucket[];
};
}
export async function getTraceErrorsPerTransaction(
traceId: string,
setup: Setup
): Promise<ErrorsPerTransaction> {
const { start, end, client, config } = setup;
const params: SearchParams = {
index: [config.get('apm_oss.errorIndices')],
const params = {
index: config.get<string>('apm_oss.errorIndices'),
body: {
size: 0,
query: {
@ -57,7 +45,7 @@ export async function getTraceErrorsPerTransaction(
}
};
const resp = await client.search<never, TraceErrorsAggResponse>(params);
const resp = await client.search(params);
return resp.aggregations.transactions.buckets.reduce(
(acc, bucket) => ({

View file

@ -90,10 +90,10 @@ export function getESClient(req: Legacy.Request) {
const query = (req.query as unknown) as APMRequestQuery;
return {
search: async <Hits = unknown, Aggs = unknown>(
params: SearchParams,
search: async <Hits = unknown, U extends SearchParams = {}>(
params: U,
apmOptions?: APMOptions
): Promise<AggregationSearchResponse<Hits, Aggs>> => {
): Promise<AggregationSearchResponse<Hits, U>> => {
const nextParams = await getParamsForSearchRequest(
req,
params,
@ -112,7 +112,7 @@ export function getESClient(req: Legacy.Request) {
}
return cluster.callWithRequest(req, 'search', nextParams) as Promise<
AggregationSearchResponse<Hits, Aggs>
AggregationSearchResponse<Hits, U>
>;
},
index: <Body>(params: IndexDocumentParams<Body>) => {

View file

@ -1,64 +0,0 @@
/*
* 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 {
SERVICE_AGENT_NAME,
PROCESSOR_EVENT,
SERVICE_NAME,
METRIC_JAVA_HEAP_MEMORY_MAX,
METRIC_JAVA_HEAP_MEMORY_COMMITTED,
METRIC_JAVA_HEAP_MEMORY_USED
} from '../../../../../../common/elasticsearch_fieldnames';
import { Setup } from '../../../../helpers/setup_request';
import { MetricsAggs, MetricSeriesKeys, AggValue } from '../../../types';
import { getMetricsDateHistogramParams } from '../../../../helpers/metrics';
import { rangeFilter } from '../../../../helpers/range_filter';
export interface HeapMemoryMetrics extends MetricSeriesKeys {
heapMemoryMax: AggValue;
heapMemoryCommitted: AggValue;
heapMemoryUsed: AggValue;
}
export async function fetch(setup: Setup, serviceName: string) {
const { start, end, uiFiltersES, client, config } = setup;
const aggs = {
heapMemoryMax: { avg: { field: METRIC_JAVA_HEAP_MEMORY_MAX } },
heapMemoryCommitted: {
avg: { field: METRIC_JAVA_HEAP_MEMORY_COMMITTED }
},
heapMemoryUsed: { avg: { field: METRIC_JAVA_HEAP_MEMORY_USED } }
};
const params = {
index: config.get<string>('apm_oss.metricsIndices'),
body: {
size: 0,
query: {
bool: {
filter: [
{ term: { [SERVICE_NAME]: serviceName } },
{ term: { [PROCESSOR_EVENT]: 'metric' } },
{ term: { [SERVICE_AGENT_NAME]: 'java' } },
{
range: rangeFilter(start, end)
},
...uiFiltersES
]
}
},
aggs: {
timeseriesData: {
date_histogram: getMetricsDateHistogramParams(start, end),
aggs
},
...aggs
}
}
};
return client.search<void, MetricsAggs<HeapMemoryMetrics>>(params);
}

View file

@ -6,49 +6,62 @@
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { i18n } from '@kbn/i18n';
import {
METRIC_JAVA_HEAP_MEMORY_MAX,
METRIC_JAVA_HEAP_MEMORY_COMMITTED,
METRIC_JAVA_HEAP_MEMORY_USED,
SERVICE_AGENT_NAME
} from '../../../../../../common/elasticsearch_fieldnames';
import { Setup } from '../../../../helpers/setup_request';
import { fetch, HeapMemoryMetrics } from './fetcher';
import { fetchAndTransformMetrics } from '../../../fetch_and_transform_metrics';
import { ChartBase } from '../../../types';
import { transformDataToMetricsChart } from '../../../transform_metrics_chart';
// TODO: i18n for titles
const series = {
heapMemoryUsed: {
title: i18n.translate('xpack.apm.agentMetrics.java.heapMemorySeriesUsed', {
defaultMessage: 'Avg. used'
}),
color: theme.euiColorVis0
},
heapMemoryCommitted: {
title: i18n.translate(
'xpack.apm.agentMetrics.java.heapMemorySeriesCommitted',
{
defaultMessage: 'Avg. committed'
}
),
color: theme.euiColorVis1
},
heapMemoryMax: {
title: i18n.translate('xpack.apm.agentMetrics.java.heapMemorySeriesMax', {
defaultMessage: 'Avg. limit'
}),
color: theme.euiColorVis2
}
};
const chartBase: ChartBase<HeapMemoryMetrics> = {
const chartBase: ChartBase = {
title: i18n.translate('xpack.apm.agentMetrics.java.heapMemoryChartTitle', {
defaultMessage: 'Heap Memory'
}),
key: 'heap_memory_area_chart',
type: 'area',
yUnit: 'bytes',
series: {
heapMemoryUsed: {
title: i18n.translate(
'xpack.apm.agentMetrics.java.heapMemorySeriesUsed',
{
defaultMessage: 'Avg. used'
}
),
color: theme.euiColorVis0
},
heapMemoryCommitted: {
title: i18n.translate(
'xpack.apm.agentMetrics.java.heapMemorySeriesCommitted',
{
defaultMessage: 'Avg. committed'
}
),
color: theme.euiColorVis1
},
heapMemoryMax: {
title: i18n.translate('xpack.apm.agentMetrics.java.heapMemorySeriesMax', {
defaultMessage: 'Avg. limit'
}),
color: theme.euiColorVis2
}
}
series
};
export async function getHeapMemoryChart(setup: Setup, serviceName: string) {
const result = await fetch(setup, serviceName);
return transformDataToMetricsChart<HeapMemoryMetrics>(result, chartBase);
return fetchAndTransformMetrics({
setup,
serviceName,
chartBase,
aggs: {
heapMemoryMax: { avg: { field: METRIC_JAVA_HEAP_MEMORY_MAX } },
heapMemoryCommitted: {
avg: { field: METRIC_JAVA_HEAP_MEMORY_COMMITTED }
},
heapMemoryUsed: { avg: { field: METRIC_JAVA_HEAP_MEMORY_USED } }
},
additionalFilters: [{ term: { [SERVICE_AGENT_NAME]: 'java' } }]
});
}

View file

@ -1,65 +0,0 @@
/*
* 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 {
SERVICE_AGENT_NAME,
PROCESSOR_EVENT,
SERVICE_NAME,
METRIC_JAVA_NON_HEAP_MEMORY_MAX,
METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED,
METRIC_JAVA_NON_HEAP_MEMORY_USED
} from '../../../../../../common/elasticsearch_fieldnames';
import { Setup } from '../../../../helpers/setup_request';
import { MetricsAggs, MetricSeriesKeys, AggValue } from '../../../types';
import { getMetricsDateHistogramParams } from '../../../../helpers/metrics';
import { rangeFilter } from '../../../../helpers/range_filter';
export interface NonHeapMemoryMetrics extends MetricSeriesKeys {
nonHeapMemoryCommitted: AggValue;
nonHeapMemoryUsed: AggValue;
}
export async function fetch(setup: Setup, serviceName: string) {
const { start, end, uiFiltersES, client, config } = setup;
const aggs = {
nonHeapMemoryMax: { avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_MAX } },
nonHeapMemoryCommitted: {
avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED }
},
nonHeapMemoryUsed: {
avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_USED }
}
};
const params = {
index: config.get<string>('apm_oss.metricsIndices'),
body: {
size: 0,
query: {
bool: {
filter: [
{ term: { [SERVICE_NAME]: serviceName } },
{ term: { [PROCESSOR_EVENT]: 'metric' } },
{ term: { [SERVICE_AGENT_NAME]: 'java' } },
{
range: rangeFilter(start, end)
},
...uiFiltersES
]
}
},
aggs: {
timeseriesData: {
date_histogram: getMetricsDateHistogramParams(start, end),
aggs
},
...aggs
}
}
};
return client.search<void, MetricsAggs<NonHeapMemoryMetrics>>(params);
}

View file

@ -6,41 +6,61 @@
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { i18n } from '@kbn/i18n';
import {
METRIC_JAVA_NON_HEAP_MEMORY_MAX,
METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED,
METRIC_JAVA_NON_HEAP_MEMORY_USED,
SERVICE_AGENT_NAME
} from '../../../../../../common/elasticsearch_fieldnames';
import { Setup } from '../../../../helpers/setup_request';
import { fetch, NonHeapMemoryMetrics } from './fetcher';
import { ChartBase } from '../../../types';
import { transformDataToMetricsChart } from '../../../transform_metrics_chart';
import { fetchAndTransformMetrics } from '../../../fetch_and_transform_metrics';
const chartBase: ChartBase<NonHeapMemoryMetrics> = {
const series = {
nonHeapMemoryUsed: {
title: i18n.translate(
'xpack.apm.agentMetrics.java.nonHeapMemorySeriesUsed',
{
defaultMessage: 'Avg. used'
}
),
color: theme.euiColorVis0
},
nonHeapMemoryCommitted: {
title: i18n.translate(
'xpack.apm.agentMetrics.java.nonHeapMemorySeriesCommitted',
{
defaultMessage: 'Avg. committed'
}
),
color: theme.euiColorVis1
}
};
const chartBase: ChartBase = {
title: i18n.translate('xpack.apm.agentMetrics.java.nonHeapMemoryChartTitle', {
defaultMessage: 'Non-Heap Memory'
}),
key: 'non_heap_memory_area_chart',
type: 'area',
yUnit: 'bytes',
series: {
nonHeapMemoryUsed: {
title: i18n.translate(
'xpack.apm.agentMetrics.java.nonHeapMemorySeriesUsed',
{
defaultMessage: 'Avg. used'
}
),
color: theme.euiColorVis0
},
nonHeapMemoryCommitted: {
title: i18n.translate(
'xpack.apm.agentMetrics.java.nonHeapMemorySeriesCommitted',
{
defaultMessage: 'Avg. committed'
}
),
color: theme.euiColorVis1
}
}
series
};
export async function getNonHeapMemoryChart(setup: Setup, serviceName: string) {
const result = await fetch(setup, serviceName);
return transformDataToMetricsChart<NonHeapMemoryMetrics>(result, chartBase);
return fetchAndTransformMetrics({
setup,
serviceName,
chartBase,
aggs: {
nonHeapMemoryMax: { avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_MAX } },
nonHeapMemoryCommitted: {
avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED }
},
nonHeapMemoryUsed: {
avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_USED }
}
},
additionalFilters: [{ term: { [SERVICE_AGENT_NAME]: 'java' } }]
});
}

View file

@ -1,55 +0,0 @@
/*
* 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 {
SERVICE_AGENT_NAME,
PROCESSOR_EVENT,
SERVICE_NAME,
METRIC_JAVA_THREAD_COUNT
} from '../../../../../../common/elasticsearch_fieldnames';
import { Setup } from '../../../../helpers/setup_request';
import { MetricsAggs, MetricSeriesKeys, AggValue } from '../../../types';
import { getMetricsDateHistogramParams } from '../../../../helpers/metrics';
import { rangeFilter } from '../../../../helpers/range_filter';
export interface ThreadCountMetrics extends MetricSeriesKeys {
threadCount: AggValue;
}
export async function fetch(setup: Setup, serviceName: string) {
const { start, end, uiFiltersES, client, config } = setup;
const aggs = {
threadCount: { avg: { field: METRIC_JAVA_THREAD_COUNT } },
threadCountMax: { max: { field: METRIC_JAVA_THREAD_COUNT } }
};
const params = {
index: config.get<string>('apm_oss.metricsIndices'),
body: {
size: 0,
query: {
bool: {
filter: [
{ term: { [SERVICE_NAME]: serviceName } },
{ term: { [PROCESSOR_EVENT]: 'metric' } },
{ term: { [SERVICE_AGENT_NAME]: 'java' } },
{ range: rangeFilter(start, end) },
...uiFiltersES
]
}
},
aggs: {
timeseriesData: {
date_histogram: getMetricsDateHistogramParams(start, end),
aggs
},
...aggs
}
}
};
return client.search<void, MetricsAggs<ThreadCountMetrics>>(params);
}

View file

@ -6,35 +6,48 @@
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { i18n } from '@kbn/i18n';
import {
METRIC_JAVA_THREAD_COUNT,
SERVICE_AGENT_NAME
} from '../../../../../../common/elasticsearch_fieldnames';
import { Setup } from '../../../../helpers/setup_request';
import { fetch, ThreadCountMetrics } from './fetcher';
import { ChartBase } from '../../../types';
import { transformDataToMetricsChart } from '../../../transform_metrics_chart';
import { fetchAndTransformMetrics } from '../../../fetch_and_transform_metrics';
const chartBase: ChartBase<ThreadCountMetrics> = {
const series = {
threadCount: {
title: i18n.translate('xpack.apm.agentMetrics.java.threadCount', {
defaultMessage: 'Avg. count'
}),
color: theme.euiColorVis0
},
threadCountMax: {
title: i18n.translate('xpack.apm.agentMetrics.java.threadCountMax', {
defaultMessage: 'Max count'
}),
color: theme.euiColorVis1
}
};
const chartBase: ChartBase = {
title: i18n.translate('xpack.apm.agentMetrics.java.threadCountChartTitle', {
defaultMessage: 'Thread Count'
}),
key: 'thread_count_line_chart',
type: 'linemark',
yUnit: 'number',
series: {
threadCount: {
title: i18n.translate('xpack.apm.agentMetrics.java.threadCount', {
defaultMessage: 'Avg. count'
}),
color: theme.euiColorVis0
},
threadCountMax: {
title: i18n.translate('xpack.apm.agentMetrics.java.threadCountMax', {
defaultMessage: 'Max count'
}),
color: theme.euiColorVis1
}
}
series
};
export async function getThreadCountChart(setup: Setup, serviceName: string) {
const result = await fetch(setup, serviceName);
return transformDataToMetricsChart<ThreadCountMetrics>(result, chartBase);
return fetchAndTransformMetrics({
setup,
serviceName,
chartBase,
aggs: {
threadCount: { avg: { field: METRIC_JAVA_THREAD_COUNT } },
threadCountMax: { max: { field: METRIC_JAVA_THREAD_COUNT } }
},
additionalFilters: [{ term: { [SERVICE_AGENT_NAME]: 'java' } }]
});
}

View file

@ -1,62 +0,0 @@
/*
* 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 {
METRIC_PROCESS_CPU_PERCENT,
METRIC_SYSTEM_CPU_PERCENT,
PROCESSOR_EVENT,
SERVICE_NAME
} from '../../../../../../common/elasticsearch_fieldnames';
import { Setup } from '../../../../helpers/setup_request';
import { MetricsAggs, MetricSeriesKeys, AggValue } from '../../../types';
import { getMetricsDateHistogramParams } from '../../../../helpers/metrics';
import { rangeFilter } from '../../../../helpers/range_filter';
export interface CPUMetrics extends MetricSeriesKeys {
systemCPUAverage: AggValue;
systemCPUMax: AggValue;
processCPUAverage: AggValue;
processCPUMax: AggValue;
}
export async function fetch(setup: Setup, serviceName: string) {
const { start, end, uiFiltersES, client, config } = setup;
const aggs = {
systemCPUAverage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } },
systemCPUMax: { max: { field: METRIC_SYSTEM_CPU_PERCENT } },
processCPUAverage: { avg: { field: METRIC_PROCESS_CPU_PERCENT } },
processCPUMax: { max: { field: METRIC_PROCESS_CPU_PERCENT } }
};
const params = {
index: config.get<string>('apm_oss.metricsIndices'),
body: {
size: 0,
query: {
bool: {
filter: [
{ term: { [SERVICE_NAME]: serviceName } },
{ term: { [PROCESSOR_EVENT]: 'metric' } },
{
range: rangeFilter(start, end)
},
...uiFiltersES
]
}
},
aggs: {
timeseriesData: {
date_histogram: getMetricsDateHistogramParams(start, end),
aggs
},
...aggs
}
}
};
return client.search<void, MetricsAggs<CPUMetrics>>(params);
}

View file

@ -6,47 +6,63 @@
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { i18n } from '@kbn/i18n';
import {
METRIC_SYSTEM_CPU_PERCENT,
METRIC_PROCESS_CPU_PERCENT
} from '../../../../../../common/elasticsearch_fieldnames';
import { Setup } from '../../../../helpers/setup_request';
import { fetch, CPUMetrics } from './fetcher';
import { ChartBase } from '../../../types';
import { transformDataToMetricsChart } from '../../../transform_metrics_chart';
import { fetchAndTransformMetrics } from '../../../fetch_and_transform_metrics';
const chartBase: ChartBase<CPUMetrics> = {
const series = {
systemCPUMax: {
title: i18n.translate('xpack.apm.chart.cpuSeries.systemMaxLabel', {
defaultMessage: 'System max'
}),
color: theme.euiColorVis1
},
systemCPUAverage: {
title: i18n.translate('xpack.apm.chart.cpuSeries.systemAverageLabel', {
defaultMessage: 'System average'
}),
color: theme.euiColorVis0
},
processCPUMax: {
title: i18n.translate('xpack.apm.chart.cpuSeries.processMaxLabel', {
defaultMessage: 'Process max'
}),
color: theme.euiColorVis7
},
processCPUAverage: {
title: i18n.translate('xpack.apm.chart.cpuSeries.processAverageLabel', {
defaultMessage: 'Process average'
}),
color: theme.euiColorVis5
}
};
const chartBase: ChartBase = {
title: i18n.translate('xpack.apm.serviceDetails.metrics.cpuUsageChartTitle', {
defaultMessage: 'CPU usage'
}),
key: 'cpu_usage_chart',
type: 'linemark',
yUnit: 'percent',
series: {
systemCPUMax: {
title: i18n.translate('xpack.apm.chart.cpuSeries.systemMaxLabel', {
defaultMessage: 'System max'
}),
color: theme.euiColorVis1
},
systemCPUAverage: {
title: i18n.translate('xpack.apm.chart.cpuSeries.systemAverageLabel', {
defaultMessage: 'System average'
}),
color: theme.euiColorVis0
},
processCPUMax: {
title: i18n.translate('xpack.apm.chart.cpuSeries.processMaxLabel', {
defaultMessage: 'Process max'
}),
color: theme.euiColorVis7
},
processCPUAverage: {
title: i18n.translate('xpack.apm.chart.cpuSeries.processAverageLabel', {
defaultMessage: 'Process average'
}),
color: theme.euiColorVis5
}
}
series
};
export async function getCPUChartData(setup: Setup, serviceName: string) {
const result = await fetch(setup, serviceName);
return transformDataToMetricsChart<CPUMetrics>(result, chartBase);
const metricsChart = await fetchAndTransformMetrics({
setup,
serviceName,
chartBase,
aggs: {
systemCPUAverage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } },
systemCPUMax: { max: { field: METRIC_SYSTEM_CPU_PERCENT } },
processCPUAverage: { avg: { field: METRIC_PROCESS_CPU_PERCENT } },
processCPUMax: { max: { field: METRIC_PROCESS_CPU_PERCENT } }
}
});
return metricsChart;
}

View file

@ -1,73 +0,0 @@
/*
* 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 {
PROCESSOR_EVENT,
SERVICE_NAME,
METRIC_SYSTEM_FREE_MEMORY,
METRIC_SYSTEM_TOTAL_MEMORY
} from '../../../../../../common/elasticsearch_fieldnames';
import { Setup } from '../../../../helpers/setup_request';
import { MetricsAggs, MetricSeriesKeys, AggValue } from '../../../types';
import { getMetricsDateHistogramParams } from '../../../../helpers/metrics';
import { rangeFilter } from '../../../../helpers/range_filter';
export interface MemoryMetrics extends MetricSeriesKeys {
memoryUsedAvg: AggValue;
memoryUsedMax: AggValue;
}
const percentUsedScript = {
lang: 'expression',
source: `1 - doc['${METRIC_SYSTEM_FREE_MEMORY}'] / doc['${METRIC_SYSTEM_TOTAL_MEMORY}']`
};
export async function fetch(setup: Setup, serviceName: string) {
const { start, end, uiFiltersES, client, config } = setup;
const aggs = {
memoryUsedAvg: { avg: { script: percentUsedScript } },
memoryUsedMax: { max: { script: percentUsedScript } }
};
const params = {
index: config.get<string>('apm_oss.metricsIndices'),
body: {
size: 0,
query: {
bool: {
filter: [
{ term: { [SERVICE_NAME]: serviceName } },
{ term: { [PROCESSOR_EVENT]: 'metric' } },
{
range: rangeFilter(start, end)
},
{
exists: {
field: METRIC_SYSTEM_FREE_MEMORY
}
},
{
exists: {
field: METRIC_SYSTEM_TOTAL_MEMORY
}
},
...uiFiltersES
]
}
},
aggs: {
timeseriesData: {
date_histogram: getMetricsDateHistogramParams(start, end),
aggs
},
...aggs
}
}
};
return client.search<void, MetricsAggs<MemoryMetrics>>(params);
}

View file

@ -5,12 +5,28 @@
*/
import { i18n } from '@kbn/i18n';
import {
METRIC_SYSTEM_FREE_MEMORY,
METRIC_SYSTEM_TOTAL_MEMORY
} from '../../../../../../common/elasticsearch_fieldnames';
import { Setup } from '../../../../helpers/setup_request';
import { fetch, MemoryMetrics } from './fetcher';
import { ChartBase } from '../../../types';
import { transformDataToMetricsChart } from '../../../transform_metrics_chart';
import { fetchAndTransformMetrics } from '../../../fetch_and_transform_metrics';
const chartBase: ChartBase<MemoryMetrics> = {
const series = {
memoryUsedMax: {
title: i18n.translate('xpack.apm.chart.memorySeries.systemMaxLabel', {
defaultMessage: 'Max'
})
},
memoryUsedAvg: {
title: i18n.translate('xpack.apm.chart.memorySeries.systemAverageLabel', {
defaultMessage: 'Average'
})
}
};
const chartBase: ChartBase = {
title: i18n.translate(
'xpack.apm.serviceDetails.metrics.memoryUsageChartTitle',
{
@ -20,21 +36,34 @@ const chartBase: ChartBase<MemoryMetrics> = {
key: 'memory_usage_chart',
type: 'linemark',
yUnit: 'percent',
series: {
memoryUsedMax: {
title: i18n.translate('xpack.apm.chart.memorySeries.systemMaxLabel', {
defaultMessage: 'Max'
})
},
memoryUsedAvg: {
title: i18n.translate('xpack.apm.chart.memorySeries.systemAverageLabel', {
defaultMessage: 'Average'
})
}
}
series
};
const percentUsedScript = {
lang: 'expression',
source: `1 - doc['${METRIC_SYSTEM_FREE_MEMORY}'] / doc['${METRIC_SYSTEM_TOTAL_MEMORY}']`
};
export async function getMemoryChartData(setup: Setup, serviceName: string) {
const result = await fetch(setup, serviceName);
return transformDataToMetricsChart<MemoryMetrics>(result, chartBase);
return fetchAndTransformMetrics({
setup,
serviceName,
chartBase,
aggs: {
memoryUsedAvg: { avg: { script: percentUsedScript } },
memoryUsedMax: { max: { script: percentUsedScript } }
},
additionalFilters: [
{
exists: {
field: METRIC_SYSTEM_FREE_MEMORY
}
},
{
exists: {
field: METRIC_SYSTEM_TOTAL_MEMORY
}
}
]
});
}

View file

@ -0,0 +1,80 @@
/*
* 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 {
PROCESSOR_EVENT,
SERVICE_NAME
} from '../../../common/elasticsearch_fieldnames';
import { Setup } from '../helpers/setup_request';
import { getMetricsDateHistogramParams } from '../helpers/metrics';
import { rangeFilter } from '../helpers/range_filter';
import { ChartBase } from './types';
import { transformDataToMetricsChart } from './transform_metrics_chart';
interface Aggs {
[key: string]: {
min?: any;
max?: any;
sum?: any;
avg?: any;
};
}
interface Filter {
exists?: {
field: string;
};
term?: {
[key: string]: string;
};
}
export async function fetchAndTransformMetrics<T extends Aggs>({
setup,
serviceName,
chartBase,
aggs,
additionalFilters = []
}: {
setup: Setup;
serviceName: string;
chartBase: ChartBase;
aggs: T;
additionalFilters?: Filter[];
}) {
const { start, end, uiFiltersES, client, config } = setup;
const params = {
index: config.get<string>('apm_oss.metricsIndices'),
body: {
size: 0,
query: {
bool: {
filter: [
{ term: { [SERVICE_NAME]: serviceName } },
{ term: { [PROCESSOR_EVENT]: 'metric' } },
{
range: rangeFilter(start, end)
},
...additionalFilters,
...uiFiltersES
]
}
},
aggs: {
timeseriesData: {
date_histogram: getMetricsDateHistogramParams(start, end),
aggs
},
...aggs
}
}
};
const response = await client.search(params);
return transformDataToMetricsChart(response, chartBase);
}

View file

@ -3,22 +3,12 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { AggregationSearchResponse } from 'elasticsearch';
import { MetricsAggs, MetricSeriesKeys, AggValue } from './types';
import { transformDataToMetricsChart } from './transform_metrics_chart';
import { ChartType, YUnit } from '../../../typings/timeseries';
test('transformDataToMetricsChart should transform an ES result into a chart object', () => {
interface TestKeys extends MetricSeriesKeys {
a: AggValue;
b: AggValue;
c: AggValue;
}
type R = AggregationSearchResponse<void, MetricsAggs<TestKeys>>;
const response = {
hits: { total: 5000 } as R['hits'],
hits: { total: 5000 },
aggregations: {
a: { value: 1000 },
b: { value: 1000 },
@ -29,24 +19,27 @@ test('transformDataToMetricsChart should transform an ES result into a chart obj
a: { value: 10 },
b: { value: 10 },
c: { value: 10 },
key: 1
} as R['aggregations']['timeseriesData']['buckets'][0],
key: 1,
doc_count: 0
},
{
a: { value: 20 },
b: { value: 20 },
c: { value: 20 },
key: 2
} as R['aggregations']['timeseriesData']['buckets'][0],
key: 2,
doc_count: 0
},
{
a: { value: 30 },
b: { value: 30 },
c: { value: 30 },
key: 3
} as R['aggregations']['timeseriesData']['buckets'][0]
key: 3,
doc_count: 0
}
]
}
} as R['aggregations']
} as R;
}
} as any;
const chartBase = {
title: 'Test Chart Title',

View file

@ -3,9 +3,9 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { AggregationSearchResponse } from 'elasticsearch';
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { ChartBase, MetricsAggs, MetricSeriesKeys } from './types';
import { AggregationSearchResponse, AggregatedValue } from 'elasticsearch';
import { ChartBase } from './types';
const colors = [
theme.euiColorVis0,
@ -20,9 +20,33 @@ const colors = [
export type GenericMetricsChart = ReturnType<
typeof transformDataToMetricsChart
>;
export function transformDataToMetricsChart<T extends MetricSeriesKeys>(
result: AggregationSearchResponse<void, MetricsAggs<T>>,
chartBase: ChartBase<T>
interface AggregatedParams {
body: {
aggs: {
timeseriesData: {
date_histogram: any;
aggs: {
min?: any;
max?: any;
sum?: any;
avg?: any;
};
};
} & {
[key: string]: {
min?: any;
max?: any;
sum?: any;
avg?: any;
};
};
};
}
export function transformDataToMetricsChart<Params extends AggregatedParams>(
result: AggregationSearchResponse<unknown, Params>,
chartBase: ChartBase
) {
const { aggregations, hits } = result;
const { timeseriesData } = aggregations;
@ -32,20 +56,24 @@ export function transformDataToMetricsChart<T extends MetricSeriesKeys>(
key: chartBase.key,
yUnit: chartBase.yUnit,
totalHits: hits.total,
series: Object.keys(chartBase.series).map((seriesKey, i) => ({
title: chartBase.series[seriesKey].title,
key: seriesKey,
type: chartBase.type,
color: chartBase.series[seriesKey].color || colors[i],
overallValue: aggregations[seriesKey].value,
data: timeseriesData.buckets.map(bucket => {
const { value } = bucket[seriesKey];
const y = value === null || isNaN(value) ? null : value;
return {
x: bucket.key,
y
};
})
}))
series: Object.keys(chartBase.series).map((seriesKey, i) => {
const agg = aggregations[seriesKey];
return {
title: chartBase.series[seriesKey].title,
key: seriesKey,
type: chartBase.type,
color: chartBase.series[seriesKey].color || colors[i],
overallValue: agg.value,
data: timeseriesData.buckets.map(bucket => {
const { value } = bucket[seriesKey] as AggregatedValue;
const y = value === null || isNaN(value) ? null : value;
return {
x: bucket.key,
y
};
})
};
})
};
}

View file

@ -3,38 +3,17 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ChartType, YUnit } from '../../../typings/timeseries';
export interface AggValue {
value: number | null;
}
export interface MetricSeriesKeys {
[key: string]: AggValue;
}
export interface ChartBase<T extends MetricSeriesKeys> {
export interface ChartBase {
title: string;
key: string;
type: ChartType;
yUnit: YUnit;
series: {
[key in keyof T]: {
[key: string]: {
title: string;
color?: string;
}
};
};
}
export type MetricsAggs<T extends MetricSeriesKeys> = {
timeseriesData: {
buckets: Array<
{
key_as_string: string; // timestamp as string
key: number; // timestamp as epoch milliseconds
doc_count: number;
} & T
>;
};
} & T;

View file

@ -3,8 +3,6 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { BucketAgg } from 'elasticsearch';
import { idx } from '@kbn/elastic-idx';
import {
PROCESSOR_EVENT,
@ -48,19 +46,11 @@ export async function getService(serviceName: string, setup: Setup) {
}
};
interface Aggs {
types: {
buckets: BucketAgg[];
};
agents: {
buckets: BucketAgg[];
};
}
const { aggregations } = await client.search<void, Aggs>(params);
const { aggregations } = await client.search(params);
const buckets = idx(aggregations, _ => _.types.buckets) || [];
const types = buckets.map(bucket => bucket.key);
const agentName = idx(aggregations, _ => _.agents.buckets[0].key);
const agentName = idx(aggregations, _ => _.agents.buckets[0].key) || '';
return {
serviceName,
types,

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { BucketAgg } from 'elasticsearch';
import { idx } from '@kbn/elastic-idx';
import {
PROCESSOR_EVENT,
@ -65,33 +64,14 @@ export async function getServicesItems(setup: Setup) {
}
};
interface ServiceBucket extends BucketAgg {
avg: {
value: number;
};
agents: {
buckets: BucketAgg[];
};
events: {
buckets: BucketAgg[];
};
environments: {
buckets: BucketAgg[];
};
}
interface Aggs extends BucketAgg {
services: {
buckets: ServiceBucket[];
};
}
const resp = await client.search<void, Aggs>(params);
const resp = await client.search(params);
const aggs = resp.aggregations;
const serviceBuckets = idx(aggs, _ => _.services.buckets) || [];
const items = serviceBuckets.map(bucket => {
const eventTypes = bucket.events.buckets;
const transactions = eventTypes.find(e => e.key === 'transaction');
const totalTransactions = idx(transactions, _ => _.doc_count) || 0;

View file

@ -4,42 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { SearchParams } from 'elasticsearch';
import {
TRANSACTION_DURATION,
TRANSACTION_NAME
} from '../../../common/elasticsearch_fieldnames';
import { PromiseReturnType, StringMap } from '../../../typings/common';
import { Transaction } from '../../../typings/es_schemas/ui/Transaction';
import { Setup } from '../helpers/setup_request';
interface Bucket {
key: string;
doc_count: number;
avg: { value: number };
p95: { values: { '95.0': number } };
sum: { value: number };
sample: {
hits: {
total: number;
max_score: number | null;
hits: Array<{
_source: Transaction;
}>;
};
};
}
interface Aggs {
transactions: {
buckets: Bucket[];
};
}
export type ESResponse = PromiseReturnType<typeof transactionGroupsFetcher>;
export function transactionGroupsFetcher(setup: Setup, bodyQuery: StringMap) {
const { client, config } = setup;
const params: SearchParams = {
const params = {
index: config.get<string>('apm_oss.transactionIndices'),
body: {
size: 0,
@ -72,5 +47,5 @@ export function transactionGroupsFetcher(setup: Setup, bodyQuery: StringMap) {
}
};
return client.search<void, Aggs>(params);
return client.search(params);
}

View file

@ -6,16 +6,23 @@
import moment from 'moment';
import { idx } from '@kbn/elastic-idx';
import { Transaction } from '../../../typings/es_schemas/ui/Transaction';
import { ESResponse } from './fetcher';
function calculateRelativeImpacts(transactionGroups: ITransactionGroup[]) {
const values = transactionGroups.map(({ impact }) => impact);
const values = transactionGroups
.map(({ impact }) => impact)
.filter(value => value !== null) as number[];
const max = Math.max(...values);
const min = Math.min(...values);
return transactionGroups.map(bucket => ({
...bucket,
impact: ((bucket.impact - min) / (max - min)) * 100 || 0
impact:
bucket.impact !== null
? ((bucket.impact - min) / (max - min)) * 100 || 0
: 0
}));
}
@ -27,7 +34,7 @@ function getTransactionGroup(
const averageResponseTime = bucket.avg.value;
const transactionsPerMinute = bucket.doc_count / minutes;
const impact = bucket.sum.value;
const sample = bucket.sample.hits.hits[0]._source;
const sample = bucket.sample.hits.hits[0]._source as Transaction;
return {
name: bucket.key,

View file

@ -8,28 +8,11 @@ import { getMlIndex } from '../../../../../common/ml_job_constants';
import { PromiseReturnType } from '../../../../../typings/common';
import { Setup } from '../../../helpers/setup_request';
export interface ESBucket {
key_as_string: string; // timestamp as string
key: number; // timestamp
doc_count: number;
anomaly_score: {
value: number | null;
};
lower: {
value: number | null;
};
upper: {
value: number | null;
};
}
export type ESResponse = Exclude<
PromiseReturnType<typeof anomalySeriesFetcher>,
undefined
>;
interface Aggs {
ml_avg_response_times: {
buckets: ESBucket[];
};
}
export type ESResponse = PromiseReturnType<typeof anomalySeriesFetcher>;
export async function anomalySeriesFetcher({
serviceName,
transactionType,
@ -91,7 +74,8 @@ export async function anomalySeriesFetcher({
};
try {
return await client.search<void, Aggs>(params);
const response = await client.search(params);
return response;
} catch (err) {
const isHttpError = 'statusCode' in err;
if (isHttpError) {

View file

@ -55,10 +55,12 @@ export async function getAnomalySeries({
setup
});
return anomalySeriesTransform(
esResponse,
mlBucketSize,
bucketSize,
timeSeriesDates
);
return esResponse
? anomalySeriesTransform(
esResponse,
mlBucketSize,
bucketSize,
timeSeriesDates
)
: undefined;
}

View file

@ -6,7 +6,7 @@
import { ESResponse } from '../fetcher';
export const mlAnomalyResponse: ESResponse = {
export const mlAnomalyResponse: ESResponse = ({
took: 3,
timed_out: false,
_shards: {
@ -124,4 +124,4 @@ export const mlAnomalyResponse: ESResponse = {
]
}
}
};
} as unknown) as ESResponse;

View file

@ -5,7 +5,7 @@
*/
import { idx } from '@kbn/elastic-idx';
import { ESBucket, ESResponse } from './fetcher';
import { ESResponse } from './fetcher';
import { mlAnomalyResponse } from './mock-responses/mlAnomalyResponse';
import { anomalySeriesTransform, replaceFirstAndLastBucket } from './transform';
@ -46,7 +46,7 @@ describe('anomalySeriesTransform', () => {
key: 20000,
anomaly_score: { value: 90 }
}
] as ESBucket[]);
]);
const getMlBucketSize = 5;
const bucketSize = 5;
@ -72,7 +72,7 @@ describe('anomalySeriesTransform', () => {
key: 5000,
anomaly_score: { value: 90 }
}
] as ESBucket[]);
]);
const getMlBucketSize = 10;
const bucketSize = 5;
@ -112,7 +112,7 @@ describe('anomalySeriesTransform', () => {
upper: { value: 45 },
lower: { value: 40 }
}
] as ESBucket[]);
]);
const mlBucketSize = 10;
const bucketSize = 5;
@ -151,7 +151,7 @@ describe('anomalySeriesTransform', () => {
upper: { value: 25 },
lower: { value: 20 }
}
] as ESBucket[]);
]);
const getMlBucketSize = 10;
const bucketSize = 5;
@ -190,7 +190,7 @@ describe('anomalySeriesTransform', () => {
upper: { value: null },
lower: { value: null }
}
] as ESBucket[]);
]);
const getMlBucketSize = 10;
const bucketSize = 5;
@ -234,10 +234,10 @@ describe('replaceFirstAndLastBucket', () => {
lower: 30,
upper: 40
}
] as any;
];
const timeSeriesDates = [10, 15];
expect(replaceFirstAndLastBucket(buckets, timeSeriesDates)).toEqual([
expect(replaceFirstAndLastBucket(buckets as any, timeSeriesDates)).toEqual([
{ x: 10, lower: 10, upper: 20 },
{ x: 15, lower: 30, upper: 40 }
]);
@ -271,8 +271,8 @@ describe('replaceFirstAndLastBucket', () => {
});
});
function getESResponse(buckets: ESBucket[]): ESResponse {
return {
function getESResponse(buckets: any): ESResponse {
return ({
took: 3,
timed_out: false,
_shards: {
@ -288,7 +288,7 @@ function getESResponse(buckets: ESBucket[]): ESResponse {
},
aggregations: {
ml_avg_response_times: {
buckets: buckets.map(bucket => {
buckets: buckets.map((bucket: any) => {
return {
...bucket,
lower: { value: idx(bucket, _ => _.lower.value) || null },
@ -300,5 +300,5 @@ function getESResponse(buckets: ESBucket[]): ESResponse {
})
}
}
};
} as unknown) as ESResponse;
}

View file

@ -7,10 +7,12 @@
import { first, last } from 'lodash';
import { idx } from '@kbn/elastic-idx';
import { Coordinate, RectCoordinate } from '../../../../../typings/timeseries';
import { ESBucket, ESResponse } from './fetcher';
import { ESResponse } from './fetcher';
type IBucket = ReturnType<typeof getBucket>;
function getBucket(bucket: ESBucket) {
function getBucket(
bucket: ESResponse['aggregations']['ml_avg_response_times']['buckets'][0]
) {
return {
x: bucket.key,
anomalyScore: bucket.anomaly_score.value,
@ -28,10 +30,6 @@ export function anomalySeriesTransform(
bucketSize: number,
timeSeriesDates: number[]
) {
if (!response) {
return;
}
const buckets = (
idx(response, _ => _.aggregations.ml_avg_response_times.buckets) || []
).map(getBucket);

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ESFilter, SearchParams } from 'elasticsearch';
import { ESFilter } from 'elasticsearch';
import {
PROCESSOR_EVENT,
SERVICE_NAME,
@ -18,53 +18,6 @@ import { getBucketSize } from '../../../helpers/get_bucket_size';
import { rangeFilter } from '../../../helpers/range_filter';
import { Setup } from '../../../helpers/setup_request';
interface ResponseTimeBucket {
key_as_string: string;
key: number;
doc_count: number;
avg: {
value: number | null;
};
pct: {
values: {
'95.0': number | 'NaN';
'99.0': number | 'NaN';
};
};
}
interface TransactionResultBucket {
/**
* transaction result eg. 2xx
*/
key: string;
doc_count: number;
timeseries: {
buckets: Array<{
key_as_string: string;
/**
* timestamp in ms
*/
key: number;
doc_count: number;
}>;
};
}
interface Aggs {
response_times: {
buckets: ResponseTimeBucket[];
};
transaction_results: {
doc_count_error_upper_bound: number;
sum_other_doc_count: number;
buckets: TransactionResultBucket[];
};
overall_avg_duration: {
value: number;
};
}
export type ESResponse = PromiseReturnType<typeof timeseriesFetcher>;
export function timeseriesFetcher({
serviceName,
@ -96,8 +49,8 @@ export function timeseriesFetcher({
filter.push({ term: { [TRANSACTION_TYPE]: transactionType } });
}
const params: SearchParams = {
index: config.get('apm_oss.transactionIndices'),
const params = {
index: config.get<string>('apm_oss.transactionIndices'),
body: {
size: 0,
query: { bool: { filter } },
@ -134,5 +87,5 @@ export function timeseriesFetcher({
}
};
return client.search<void, Aggs>(params);
return client.search(params);
}

View file

@ -6,7 +6,7 @@
import { ESResponse } from '../fetcher';
export const timeseriesResponse: ESResponse = {
export const timeseriesResponse = ({
took: 368,
timed_out: false,
_shards: {
@ -2826,4 +2826,4 @@ export const timeseriesResponse: ESResponse = {
value: 32861.15660262639
}
}
};
} as unknown) as ESResponse;

View file

@ -96,7 +96,7 @@ describe('getTpmBuckets', () => {
}
];
const bucketSize = 10;
expect(getTpmBuckets(buckets, bucketSize)).toEqual([
expect(getTpmBuckets(buckets as any, bucketSize)).toEqual([
{
dataPoints: [
{ x: 0, y: 0 },

View file

@ -10,19 +10,7 @@ import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
import { Coordinate } from '../../../../../typings/timeseries';
import { ESResponse } from './fetcher';
export interface ApmTimeSeriesResponse {
totalHits: number;
responseTimes: {
avg: Coordinate[];
p95: Coordinate[];
p99: Coordinate[];
};
tpmBuckets: Array<{
key: string;
dataPoints: Coordinate[];
}>;
overallAvgDuration?: number;
}
export type ApmTimeSeriesResponse = ReturnType<typeof timeseriesTransformer>;
export function timeseriesTransformer({
timeseriesResponse,
@ -30,7 +18,7 @@ export function timeseriesTransformer({
}: {
timeseriesResponse: ESResponse;
bucketSize: number;
}): ApmTimeSeriesResponse {
}) {
const aggs = timeseriesResponse.aggregations;
const overallAvgDuration = idx(aggs, _ => _.overall_avg_duration.value);
const responseTimeBuckets = idx(aggs, _ => _.response_times.buckets);

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { SearchParams } from 'elasticsearch';
import {
PROCESSOR_EVENT,
SERVICE_NAME,
@ -22,8 +21,8 @@ export async function calculateBucketSize(
) {
const { start, end, uiFiltersES, client, config } = setup;
const params: SearchParams = {
index: config.get('apm_oss.transactionIndices'),
const params = {
index: config.get<string>('apm_oss.transactionIndices'),
body: {
size: 0,
query: {
@ -56,13 +55,8 @@ export async function calculateBucketSize(
}
};
interface Aggs {
stats: {
max: number;
};
}
const resp = await client.search(params);
const resp = await client.search<void, Aggs>(params);
const minBucketSize: number = config.get('xpack.apm.minimumBucketSize');
const bucketTargetCount: number = config.get('xpack.apm.bucketTargetCount');
const max = resp.aggregations.stats.max;

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { SearchResponse } from 'elasticsearch';
import {
PROCESSOR_EVENT,
SERVICE_NAME,
@ -15,29 +14,9 @@ import {
TRANSACTION_SAMPLED,
TRANSACTION_TYPE
} from '../../../../../common/elasticsearch_fieldnames';
import { PromiseReturnType } from '../../../../../typings/common';
import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction';
import { rangeFilter } from '../../../helpers/range_filter';
import { Setup } from '../../../helpers/setup_request';
interface Bucket {
key: number;
doc_count: number;
sample: SearchResponse<{
transaction: Pick<Transaction['transaction'], 'id' | 'sampled'>;
trace: {
id: string;
};
}>;
}
interface Aggs {
distribution: {
buckets: Bucket[];
};
}
export type ESResponse = PromiseReturnType<typeof bucketFetcher>;
export function bucketFetcher(
serviceName: string,
transactionName: string,
@ -95,5 +74,5 @@ export function bucketFetcher(
}
};
return client.search<void, Aggs>(params);
return client.search(params);
}

View file

@ -6,7 +6,11 @@
import { isEmpty } from 'lodash';
import { idx } from '@kbn/elastic-idx';
import { ESResponse } from './fetcher';
import { PromiseReturnType } from '../../../../../typings/common';
import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction';
import { bucketFetcher } from './fetcher';
type DistributionBucketResponse = PromiseReturnType<typeof bucketFetcher>;
function getDefaultSample(buckets: IBucket[]) {
const samples = buckets
@ -23,9 +27,12 @@ function getDefaultSample(buckets: IBucket[]) {
export type IBucket = ReturnType<typeof getBucket>;
function getBucket(
bucket: ESResponse['aggregations']['distribution']['buckets'][0]
bucket: DistributionBucketResponse['aggregations']['distribution']['buckets'][0]
) {
const sampleSource = idx(bucket, _ => _.sample.hits.hits[0]._source);
const sampleSource = idx(bucket, _ => _.sample.hits.hits[0]._source) as
| Transaction
| undefined;
const isSampled = idx(sampleSource, _ => _.transaction.sampled);
const sample = {
traceId: idx(sampleSource, _ => _.trace.id),
@ -39,7 +46,7 @@ function getBucket(
};
}
export function bucketTransformer(response: ESResponse) {
export function bucketTransformer(response: DistributionBucketResponse) {
const buckets = response.aggregations.distribution.buckets.map(getBucket);
return {

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { BucketAgg, ESFilter } from 'elasticsearch';
import { ESFilter } from 'elasticsearch';
import { idx } from '@kbn/elastic-idx';
import {
PROCESSOR_EVENT,
@ -57,13 +57,7 @@ export async function getEnvironments(setup: Setup, serviceName?: string) {
}
};
interface Aggs extends BucketAgg {
environments: {
buckets: BucketAgg[];
};
}
const resp = await client.search<void, Aggs>(params);
const resp = await client.search(params);
const aggs = resp.aggregations;
const environmentsBuckets = idx(aggs, _ => _.environments.buckets) || [];

View file

@ -20,3 +20,9 @@ export type PromiseReturnType<Func> = Func extends (
) => Promise<infer Value>
? Value
: Func;
export type IndexAsString<Map> = {
[k: string]: Map[keyof Map];
} & Map;
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

View file

@ -4,24 +4,114 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { StringMap } from './common';
import { StringMap, IndexAsString } from './common';
declare module 'elasticsearch' {
// extending SearchResponse to be able to have typed aggregations
export interface AggregationSearchResponse<Hits = unknown, Aggs = unknown>
extends SearchResponse<Hits> {
aggregations: Aggs;
type AggregationType =
| 'date_histogram'
| 'histogram'
| 'terms'
| 'avg'
| 'top_hits'
| 'max'
| 'min'
| 'percentiles'
| 'sum'
| 'extended_stats';
type AggOptions = AggregationOptionMap & {
[key: string]: any;
};
// eslint-disable-next-line @typescript-eslint/prefer-interface
export type AggregationOptionMap = {
aggs?: {
[aggregationName: string]: {
[T in AggregationType]?: AggOptions & AggregationOptionMap
};
};
};
// eslint-disable-next-line @typescript-eslint/prefer-interface
type BucketAggregation<SubAggregationMap, KeyType = string> = {
buckets: Array<
{
key: KeyType;
key_as_string: string;
doc_count: number;
} & (SubAggregationMap extends { aggs: any }
? AggregationResultMap<SubAggregationMap['aggs']>
: {})
>;
};
interface AggregatedValue {
value: number | null;
}
export interface BucketAgg<T = string> {
key: T;
doc_count: number;
}
type AggregationResultMap<AggregationOption> = IndexAsString<
{
[AggregationName in keyof AggregationOption]: {
avg: AggregatedValue;
max: AggregatedValue;
min: AggregatedValue;
sum: AggregatedValue;
terms: BucketAggregation<AggregationOption[AggregationName]>;
date_histogram: BucketAggregation<
AggregationOption[AggregationName],
number
>;
histogram: BucketAggregation<
AggregationOption[AggregationName],
number
>;
top_hits: {
hits: {
total: number;
max_score: number | null;
hits: Array<{
_source: AggregationOption[AggregationName] extends {
Mapping: any;
}
? AggregationOption[AggregationName]['Mapping']
: never;
}>;
};
};
percentiles: {
values: {
[key: string]: number;
};
};
extended_stats: {
count: number;
min: number;
max: number;
avg: number;
sum: number;
sum_of_squares: number;
variance: number;
std_deviation: number;
std_deviation_bounds: {
upper: number;
lower: number;
};
};
}[AggregationType & keyof AggregationOption[AggregationName]]
}
>;
export interface TermsAggsBucket {
key: string;
doc_count: number;
}
export type AggregationSearchResponse<HitType, SearchParams> = Pick<
SearchResponse<HitType>,
Exclude<keyof SearchResponse<HitType>, 'aggregations'>
> &
(SearchParams extends { body: Required<AggregationOptionMap> }
? {
aggregations: AggregationResultMap<SearchParams['body']['aggs']>;
}
: {});
export interface ESFilter {
[key: string]: {