[Metrics UI] Add support for multiple groupings to Metrics Explorer (and Alerts) (#66503)

* [Metrics UI] Adding support for multiple groupings to Metrics Explorer

* Adding keys to title parts

* removing commented line

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Chris Cowan 2020-05-19 14:42:49 -07:00 committed by GitHub
parent a7c2db73c5
commit 59ae529322
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 256 additions and 85 deletions

View file

@ -52,9 +52,12 @@ export const metricsExplorerRequestBodyRequiredFieldsRT = rt.type({
metrics: rt.array(metricsExplorerMetricRT),
});
const groupByRT = rt.union([rt.string, rt.null, rt.undefined]);
export const afterKeyObjectRT = rt.record(rt.string, rt.union([rt.string, rt.null]));
export const metricsExplorerRequestBodyOptionalFieldsRT = rt.partial({
groupBy: rt.union([rt.string, rt.null, rt.undefined]),
afterKey: rt.union([rt.string, rt.null, rt.undefined]),
groupBy: rt.union([groupByRT, rt.array(groupByRT)]),
afterKey: rt.union([rt.string, rt.null, rt.undefined, afterKeyObjectRT]),
limit: rt.union([rt.number, rt.null, rt.undefined]),
filterQuery: rt.union([rt.string, rt.null, rt.undefined]),
forceInterval: rt.boolean,
@ -68,7 +71,7 @@ export const metricsExplorerRequestBodyRT = rt.intersection([
export const metricsExplorerPageInfoRT = rt.type({
total: rt.number,
afterKey: rt.union([rt.string, rt.null]),
afterKey: rt.union([rt.string, rt.null, afterKeyObjectRT]),
});
export const metricsExplorerColumnTypeRT = rt.keyof({
@ -89,11 +92,16 @@ export const metricsExplorerRowRT = rt.intersection([
rt.record(rt.string, rt.union([rt.string, rt.number, rt.null, rt.undefined])),
]);
export const metricsExplorerSeriesRT = rt.type({
id: rt.string,
columns: rt.array(metricsExplorerColumnRT),
rows: rt.array(metricsExplorerRowRT),
});
export const metricsExplorerSeriesRT = rt.intersection([
rt.type({
id: rt.string,
columns: rt.array(metricsExplorerColumnRT),
rows: rt.array(metricsExplorerRowRT),
}),
rt.partial({
keys: rt.array(rt.string),
}),
]);
export const metricsExplorerResponseRT = rt.type({
series: rt.array(metricsExplorerSeriesRT),

View file

@ -138,7 +138,7 @@ export const Expressions: React.FC<Props> = props => {
]);
const onGroupByChange = useCallback(
(group: string | null) => {
(group: string | null | string[]) => {
setAlertParams('groupBy', group || '');
},
[setAlertParams]
@ -206,7 +206,10 @@ export const Expressions: React.FC<Props> = props => {
convertKueryToElasticSearchQuery(md.currentOptions.filterQuery, derivedIndexPattern) || ''
);
} else if (md && md.currentOptions?.groupBy && md.series) {
const filter = `${md.currentOptions?.groupBy}: "${md.series.id}"`;
const { groupBy } = md.currentOptions;
const filter = Array.isArray(groupBy)
? groupBy.map((field, index) => `${field}: "${md.series?.keys?.[index]}"`).join(' and ')
: `${groupBy}: "${md.series.id}"`;
setAlertParams('filterQueryText', filter);
setAlertParams(
'filterQuery',

View file

@ -35,7 +35,7 @@ export enum AGGREGATION_TYPES {
export interface MetricThresholdAlertParams {
criteria?: MetricExpression[];
groupBy?: string;
groupBy?: string | string[];
filterQuery?: string;
sourceId?: string;
}

View file

@ -36,6 +36,7 @@ import { getChartTheme } from './helpers/get_chart_theme';
import { useKibanaUiSetting } from '../../../../utils/use_kibana_ui_setting';
import { calculateDomain } from './helpers/calculate_domain';
import { useKibana, useUiSetting } from '../../../../../../../../src/plugins/kibana_react/public';
import { ChartTitle } from './chart_title';
interface Props {
title?: string | null;
@ -92,16 +93,17 @@ export const MetricsExplorerChart = ({
chartOptions.yAxisMode === MetricsExplorerYAxisMode.fromZero
? { ...dataDomain, min: 0 }
: dataDomain;
return (
<div style={{ padding: 24 }}>
{options.groupBy ? (
<EuiTitle size="xs">
<EuiFlexGroup alignItems="center">
<ChartTitle>
<ChartTitleContainer>
<EuiToolTip content={title} anchorClassName="metricsExplorerTitleAnchor">
<span>{title}</span>
<ChartTitle series={series} />
</EuiToolTip>
</ChartTitle>
</ChartTitleContainer>
<EuiFlexItem grow={false}>
<MetricsExplorerChartContextMenu
timeRange={timeRange}
@ -170,7 +172,7 @@ export const MetricsExplorerChart = ({
);
};
const ChartTitle = euiStyled.div`
const ChartTitleContainer = euiStyled.div`
width: 100%;
overflow: hidden;
text-overflow: ellipsis;

View file

@ -39,15 +39,16 @@ export interface Props {
const fieldToNodeType = (
source: SourceConfiguration,
field: string
groupBy: string | string[]
): InventoryItemType | undefined => {
if (source.fields.host === field) {
const fields = Array.isArray(groupBy) ? groupBy : [groupBy];
if (fields.includes(source.fields.host)) {
return 'host';
}
if (source.fields.pod === field) {
if (fields.includes(source.fields.pod)) {
return 'pod';
}
if (source.fields.container === field) {
if (fields.includes(source.fields.container)) {
return 'container';
}
};
@ -88,10 +89,16 @@ export const MetricsExplorerChartContextMenu: React.FC<Props> = ({
// onFilter needs check for Typescript even though it's
// covered by supportFiltering variable
if (supportFiltering && onFilter) {
onFilter(`${options.groupBy}: "${series.id}"`);
if (Array.isArray(options.groupBy)) {
onFilter(
options.groupBy.map((field, index) => `${field}: "${series.keys?.[index]}"`).join(' and ')
);
} else {
onFilter(`${options.groupBy}: "${series.id}"`);
}
}
setPopoverState(false);
}, [supportFiltering, options.groupBy, series.id, onFilter]);
}, [supportFiltering, onFilter, options, series.keys, series.id]);
// Only display the "Add Filter" option if it's supported
const filterByItem = supportFiltering

View file

@ -0,0 +1,40 @@
/*
* 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 React, { Fragment } from 'react';
import { EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { MetricsExplorerSeries } from '../../../../../common/http_api';
interface Props {
series: MetricsExplorerSeries;
}
export const ChartTitle = ({ series }: Props) => {
if (series.keys != null) {
const { keys } = series;
return (
<EuiFlexGroup gutterSize="xs">
{keys.map((name, i) => (
<Fragment key={name}>
<EuiFlexItem grow={false}>
<EuiText size="m" color={keys.length - 1 > i ? 'subdued' : 'default'}>
<strong>{name}</strong>
</EuiText>
</EuiFlexItem>
{keys.length - 1 > i && (
<EuiFlexItem grow={false}>
<EuiText size="m" color="subdued">
<span>/</span>
</EuiText>
</EuiFlexItem>
)}
</Fragment>
))}
</EuiFlexGroup>
);
}
return <span>{series.id}</span>;
};

View file

@ -19,11 +19,13 @@ import { NoData } from '../../../../components/empty_states/no_data';
import { MetricsExplorerChart } from './chart';
import { SourceQuery } from '../../../../graphql/types';
type stringOrNull = string | null;
interface Props {
loading: boolean;
options: MetricsExplorerOptions;
chartOptions: MetricsExplorerChartOptions;
onLoadMore: (afterKey: string | null) => void;
onLoadMore: (afterKey: stringOrNull | Record<string, stringOrNull>) => void;
onRefetch: () => void;
onFilter: (filter: string) => void;
onTimeChange: (start: string, end: string) => void;
@ -74,6 +76,8 @@ export const MetricsExplorerCharts = ({
);
}
const and = i18n.translate('xpack.infra.metricsExplorer.andLabel', { defaultMessage: '" and "' });
return (
<div style={{ width: '100%' }}>
<EuiFlexGrid gutterSize="s" columns={data.series.length === 1 ? 1 : 3}>
@ -105,7 +109,9 @@ export const MetricsExplorerCharts = ({
values={{
length: data.series.length,
total: data.pageInfo.total,
groupBy: options.groupBy,
groupBy: Array.isArray(options.groupBy)
? options.groupBy.join(and)
: options.groupBy,
}}
/>
</p>

View file

@ -13,19 +13,25 @@ import { MetricsExplorerOptions } from '../hooks/use_metrics_explorer_options';
interface Props {
options: MetricsExplorerOptions;
onChange: (groupBy: string | null) => void;
onChange: (groupBy: string | null | string[]) => void;
fields: IFieldType[];
}
export const MetricsExplorerGroupBy = ({ options, onChange, fields }: Props) => {
const handleChange = useCallback(
selectedOptions => {
const groupBy = (selectedOptions.length === 1 && selectedOptions[0].label) || null;
(selectedOptions: Array<{ label: string }>) => {
const groupBy = selectedOptions.map(option => option.label);
onChange(groupBy);
},
[onChange]
);
const selectedOptions = Array.isArray(options.groupBy)
? options.groupBy.map(field => ({ label: field }))
: options.groupBy
? [{ label: options.groupBy }]
: [];
return (
<EuiComboBox
placeholder={i18n.translate('xpack.infra.metricsExplorer.groupByLabel', {
@ -35,8 +41,8 @@ export const MetricsExplorerGroupBy = ({ options, onChange, fields }: Props) =>
defaultMessage: 'Graph per',
})}
fullWidth
singleSelection={true}
selectedOptions={(options.groupBy && [{ label: options.groupBy }]) || []}
singleSelection={false}
selectedOptions={selectedOptions}
options={fields
.filter(f => f.aggregatable && f.type === 'string')
.map(f => ({ label: f.name }))}

View file

@ -109,7 +109,21 @@ export const createFilterFromOptions = (
}
if (options.groupBy) {
const id = series.id.replace('"', '\\"');
filters.push(`${options.groupBy} : "${id}"`);
const groupByFilters = Array.isArray(options.groupBy)
? options.groupBy
.map((field, index) => {
if (!series.keys) {
return null;
}
const value = series.keys[index];
if (!value) {
return null;
}
return `${field}: "${value.replace('"', '\\"')}"`;
})
.join(' and ')
: `${options.groupBy} : "${id}"`;
filters.push(groupByFilters);
}
return { language: 'kuery', query: filters.join(' and ') };
};

View file

@ -37,7 +37,7 @@ interface Props {
defaultViewState: MetricExplorerViewState;
onRefresh: () => void;
onTimeChange: (start: string, end: string) => void;
onGroupByChange: (groupBy: string | null) => void;
onGroupByChange: (groupBy: string | null | string[]) => void;
onFilterQuerySubmit: (query: string) => void;
onMetricsChange: (metrics: MetricsExplorerMetric[]) => void;
onAggregationChange: (aggregation: MetricsExplorerAggregation) => void;

View file

@ -30,7 +30,7 @@ export const useMetricsExplorerState = (
derivedIndexPattern: IIndexPattern
) => {
const [refreshSignal, setRefreshSignal] = useState(0);
const [afterKey, setAfterKey] = useState<string | null>(null);
const [afterKey, setAfterKey] = useState<string | null | Record<string, string | null>>(null);
const {
defaultViewState,
options,
@ -63,7 +63,7 @@ export const useMetricsExplorerState = (
);
const handleGroupByChange = useCallback(
(groupBy: string | null) => {
(groupBy: string | null | string[]) => {
setAfterKey(null);
setOptions({
...options,

View file

@ -46,7 +46,7 @@ const renderUseMetricsExplorerDataHook = () => {
source,
derivedIndexPattern,
timeRange,
afterKey: null as string | null,
afterKey: null as string | null | Record<string, string | null>,
signal: 1,
},
wrapper,

View file

@ -28,7 +28,7 @@ export function useMetricsExplorerData(
source: SourceQuery.Query['source']['configuration'] | undefined,
derivedIndexPattern: IIndexPattern,
timerange: MetricsExplorerTimeOptions,
afterKey: string | null,
afterKey: string | null | Record<string, string | null>,
signal: any,
fetch?: HttpHandler
) {

View file

@ -37,7 +37,7 @@ export interface MetricsExplorerChartOptions {
export interface MetricsExplorerOptions {
metrics: MetricsExplorerOptionsMetric[];
limit?: number;
groupBy?: string;
groupBy?: string | string[];
filterQuery?: string;
aggregation: MetricsExplorerAggregation;
forceInterval?: boolean;

View file

@ -74,7 +74,7 @@ const getParsedFilterQuery: (
export const getElasticsearchMetricQuery = (
{ metric, aggType, timeUnit, timeSize }: MetricExpressionParams,
timefield: string,
groupBy?: string,
groupBy?: string | string[],
filterQuery?: string
) => {
if (aggType === Aggregators.COUNT && metric) {
@ -126,15 +126,21 @@ export const getElasticsearchMetricQuery = (
groupings: {
composite: {
size: 10,
sources: [
{
groupBy: {
terms: {
field: groupBy,
sources: Array.isArray(groupBy)
? groupBy.map((field, index) => ({
[`groupBy${index}`]: {
terms: { field },
},
},
},
],
}))
: [
{
groupBy0: {
terms: {
field: groupBy,
},
},
},
],
},
aggs: baseAggs,
},
@ -186,7 +192,7 @@ const getMetric: (
params: MetricExpressionParams,
index: string,
timefield: string,
groupBy: string | undefined,
groupBy: string | undefined | string[],
filterQuery: string | undefined
) => Promise<Record<string, number>> = async function(
{ callCluster },
@ -213,11 +219,13 @@ const getMetric: (
searchBody,
bucketSelector,
afterKeyHandler
)) as Array<Aggregation & { key: { groupBy: string } }>;
)) as Array<Aggregation & { key: Record<string, string> }>;
return compositeBuckets.reduce(
(result, bucket) => ({
...result,
[bucket.key.groupBy]: getCurrentValueFromAggregations(bucket, aggType),
[Object.values(bucket.key)
.map(value => value)
.join(', ')]: getCurrentValueFromAggregations(bucket, aggType),
}),
{}
);
@ -249,7 +257,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs, alertId: s
async function({ services, params }: AlertExecutorOptions) {
const { criteria, groupBy, filterQuery, sourceId, alertOnNoData } = params as {
criteria: MetricExpressionParams[];
groupBy: string | undefined;
groupBy: string | undefined | string[];
filterQuery: string | undefined;
sourceId?: string;
alertOnNoData: boolean;

View file

@ -62,7 +62,7 @@ export function registerMetricThresholdAlertType(libs: InfraBackendLibs) {
params: schema.object(
{
criteria: schema.arrayOf(schema.oneOf([countCriterion, nonCountCriterion])),
groupBy: schema.maybe(schema.string()),
groupBy: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])),
filterQuery: schema.maybe(
schema.string({
validate: validateIsStringElasticsearchJSONFilter,

View file

@ -64,11 +64,11 @@ export const emptyMetricResponse = {
export const basicCompositeResponse = {
aggregations: {
groupings: {
after_key: 'foo',
after_key: { groupBy0: 'foo' },
buckets: [
{
key: {
groupBy: 'a',
groupBy0: 'a',
},
aggregatedIntervals: {
buckets: bucketsA,
@ -76,7 +76,7 @@ export const basicCompositeResponse = {
},
{
key: {
groupBy: 'b',
groupBy0: 'b',
},
aggregatedIntervals: {
buckets: bucketsB,
@ -95,11 +95,11 @@ export const basicCompositeResponse = {
export const alternateCompositeResponse = {
aggregations: {
groupings: {
after_key: 'foo',
after_key: { groupBy0: 'foo' },
buckets: [
{
key: {
groupBy: 'a',
groupBy0: 'a',
},
aggregatedIntervals: {
buckets: bucketsB,
@ -107,7 +107,7 @@ export const alternateCompositeResponse = {
},
{
key: {
groupBy: 'b',
groupBy0: 'b',
},
aggregatedIntervals: {
buckets: bucketsA,

View file

@ -5,10 +5,12 @@
*/
import { isObject, set } from 'lodash';
import { i18n } from '@kbn/i18n';
import { InfraDatabaseSearchResponse } from '../../../lib/adapters/framework';
import {
MetricsExplorerRequestBody,
MetricsExplorerResponse,
afterKeyObjectRT,
} from '../../../../common/http_api/metrics_explorer';
interface GroupingAggregation {
@ -24,7 +26,13 @@ interface GroupingAggregation {
}
const EMPTY_RESPONSE = {
series: [{ id: 'ALL', columns: [], rows: [] }],
series: [
{
id: i18n.translate('xpack.infra.metricsExploer.everything', { defaultMessage: 'Everything' }),
columns: [],
rows: [],
},
],
pageInfo: { total: 0, afterKey: null },
};
@ -35,7 +43,25 @@ export const getGroupings = async (
if (!options.groupBy) {
return EMPTY_RESPONSE;
}
if (Array.isArray(options.groupBy) && options.groupBy.length === 0) {
return EMPTY_RESPONSE;
}
const limit = options.limit || 9;
const groupBy = Array.isArray(options.groupBy) ? options.groupBy : [options.groupBy];
const filter: Array<Record<string, any>> = [
{
range: {
[options.timerange.field]: {
gte: options.timerange.from,
lte: options.timerange.to,
format: 'epoch_millis',
},
},
},
...groupBy.map(field => ({ exists: { field } })),
];
const params = {
allowNoIndices: true,
ignoreUnavailable: true,
@ -51,27 +77,21 @@ export const getGroupings = async (
exists: { field: m.field },
})),
],
filter: [
{
range: {
[options.timerange.field]: {
gte: options.timerange.from,
lte: options.timerange.to,
format: 'epoch_millis',
},
},
},
] as object[],
filter,
},
},
aggs: {
groupingsCount: {
cardinality: { field: options.groupBy },
cardinality: {
script: { source: groupBy.map(field => `doc['${field}'].value`).join('+') },
},
},
groupings: {
composite: {
size: limit,
sources: [{ groupBy: { terms: { field: options.groupBy, order: 'asc' } } }],
sources: groupBy.map((field, index) => ({
[`groupBy${index}`]: { terms: { field, order: 'asc' } },
})),
},
},
},
@ -83,7 +103,11 @@ export const getGroupings = async (
}
if (options.afterKey) {
set(params, 'body.aggs.groupings.composite.after', { groupBy: options.afterKey });
if (afterKeyObjectRT.is(options.afterKey)) {
set(params, 'body.aggs.groupings.composite.after', options.afterKey);
} else {
set(params, 'body.aggs.groupings.composite.after', { groupBy0: options.afterKey });
}
}
if (options.filterQuery) {
@ -113,11 +137,13 @@ export const getGroupings = async (
const { after_key: afterKey } = groupings;
return {
series: groupings.buckets.map(bucket => {
return { id: bucket.key.groupBy, rows: [], columns: [] };
const keys = Object.values(bucket.key);
const id = keys.join(' / ');
return { id, keys, rows: [], columns: [] };
}),
pageInfo: {
total: groupingsCount.value,
afterKey: afterKey && groupings.buckets.length === limit ? afterKey.groupBy : null,
afterKey: afterKey && groupings.buckets.length === limit ? afterKey : null,
},
};
};

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { union, uniq } from 'lodash';
import { union, uniq, isArray, isString } from 'lodash';
import { KibanaRequest, RequestHandlerContext } from 'src/core/server';
import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter';
import {
@ -38,9 +38,21 @@ export const populateSeriesWithTSVBData = (
}
// Set the filter for the group by or match everything
const filters: JsonObject[] = options.groupBy
? [{ match: { [options.groupBy]: series.id } }]
const isGroupBySet =
Array.isArray(options.groupBy) && options.groupBy.length
? true
: isString(options.groupBy)
? true
: false;
const filters: JsonObject[] = isGroupBySet
? isArray(options.groupBy)
? options.groupBy
.filter(f => f)
.map((field, index) => ({ match: { [field as string]: series.keys?.[index] || '' } }))
: [{ match: { [options.groupBy as string]: series.id } }]
: [];
if (options.filterQuery) {
try {
const filterQuery = JSON.parse(options.filterQuery);

View file

@ -36,11 +36,9 @@ export default function({ getService }: FtrProviderContext) {
{
aggregation: 'avg',
field: 'system.cpu.user.pct',
rate: false,
},
{
aggregation: 'count',
rate: false,
},
],
};
@ -52,7 +50,7 @@ export default function({ getService }: FtrProviderContext) {
const body = decodeOrThrow(metricsExplorerResponseRT)(response.body);
expect(body.series).length(1);
const firstSeries = first(body.series);
expect(firstSeries).to.have.property('id', 'ALL');
expect(firstSeries).to.have.property('id', 'Everything');
expect(firstSeries.columns).to.eql([
{ name: 'timestamp', type: 'date' },
{ name: 'metric_0', type: 'number' },
@ -81,7 +79,6 @@ export default function({ getService }: FtrProviderContext) {
{
aggregation: 'avg',
field: 'system.cpu.user.pct',
rate: false,
},
],
};
@ -93,7 +90,7 @@ export default function({ getService }: FtrProviderContext) {
const body = decodeOrThrow(metricsExplorerResponseRT)(response.body);
expect(body.series).length(1);
const firstSeries = first(body.series);
expect(firstSeries).to.have.property('id', 'ALL');
expect(firstSeries).to.have.property('id', 'Everything');
expect(firstSeries.columns).to.eql([
{ name: 'timestamp', type: 'date' },
{ name: 'metric_0', type: 'number' },
@ -124,7 +121,7 @@ export default function({ getService }: FtrProviderContext) {
const body = decodeOrThrow(metricsExplorerResponseRT)(response.body);
expect(body.series).length(1);
const firstSeries = first(body.series);
expect(firstSeries).to.have.property('id', 'ALL');
expect(firstSeries).to.have.property('id', 'Everything');
expect(firstSeries.columns).to.eql([]);
expect(firstSeries.rows).to.have.length(0);
});
@ -144,7 +141,6 @@ export default function({ getService }: FtrProviderContext) {
metrics: [
{
aggregation: 'count',
rate: false,
},
],
};
@ -169,10 +165,55 @@ export default function({ getService }: FtrProviderContext) {
timestamp: 1547571300000,
});
expect(body.pageInfo).to.eql({
afterKey: 'system.fsstat',
afterKey: { groupBy0: 'system.fsstat' },
total: 12,
});
});
it('should work with multiple groupBy', async () => {
const postBody = {
timerange: {
field: '@timestamp',
to: max,
from: min,
interval: '>=1m',
},
indexPattern: 'metricbeat-*',
groupBy: ['host.name', 'system.network.name'],
limit: 3,
afterKey: null,
metrics: [
{
aggregation: 'rate',
field: 'system.network.out.bytes',
},
],
};
const response = await supertest
.post('/api/infra/metrics_explorer')
.set('kbn-xsrf', 'xxx')
.send(postBody)
.expect(200);
const body = decodeOrThrow(metricsExplorerResponseRT)(response.body);
expect(body.series).length(3);
const firstSeries = first(body.series);
expect(firstSeries).to.have.property('id', 'demo-stack-mysql-01 / eth0');
expect(firstSeries.columns).to.eql([
{ name: 'timestamp', type: 'date' },
{ name: 'metric_0', type: 'number' },
{ name: 'groupBy', type: 'string' },
]);
expect(firstSeries.rows).to.have.length(9);
expect(firstSeries.rows![1]).to.eql({
groupBy: 'demo-stack-mysql-01 / eth0',
metric_0: 53577.683333333334,
timestamp: 1547571300000,
});
expect(body.pageInfo).to.eql({
afterKey: { groupBy0: 'demo-stack-mysql-01', groupBy1: 'eth2' },
total: 4,
});
});
});
describe('without data', () => {
@ -191,7 +232,6 @@ export default function({ getService }: FtrProviderContext) {
{
aggregation: 'avg',
field: 'system.cpu.user.pct',
rate: false,
},
],
};
@ -225,7 +265,6 @@ export default function({ getService }: FtrProviderContext) {
{
aggregation: 'avg',
field: 'system.cpu.user.pct',
rate: false,
},
],
};