[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:
parent
a7c2db73c5
commit
59ae529322
|
@ -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({
|
||||
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),
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -35,7 +35,7 @@ export enum AGGREGATION_TYPES {
|
|||
|
||||
export interface MetricThresholdAlertParams {
|
||||
criteria?: MetricExpression[];
|
||||
groupBy?: string;
|
||||
groupBy?: string | string[];
|
||||
filterQuery?: string;
|
||||
sourceId?: string;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
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
|
||||
|
|
|
@ -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>;
|
||||
};
|
|
@ -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>
|
||||
|
|
|
@ -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 }))}
|
||||
|
|
|
@ -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 ') };
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,9 +126,15 @@ export const getElasticsearchMetricQuery = (
|
|||
groupings: {
|
||||
composite: {
|
||||
size: 10,
|
||||
sources: [
|
||||
sources: Array.isArray(groupBy)
|
||||
? groupBy.map((field, index) => ({
|
||||
[`groupBy${index}`]: {
|
||||
terms: { field },
|
||||
},
|
||||
}))
|
||||
: [
|
||||
{
|
||||
groupBy: {
|
||||
groupBy0: {
|
||||
terms: {
|
||||
field: groupBy,
|
||||
},
|
||||
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue