[Custom threshold/Metric threshold] [Preview chart] Sort groups by first metric and aggType combination (#199643)

Resolves https://github.com/elastic/kibana/issues/183491

This PR sorts the data in Preview chart of Custom threshold and Metric
threshold rules based on first metric and aggType combination used in
the rule equation. For `rate`, `percentile` and `last_value`
aggregations, I have used `max` aggregation as those aggregations
require additional params to pass to `LensAttributesBuilder` which are
not supported currently. Also, sorting based on equation is not
supported right now.

| Before | After |
| --- | --- |
| <img width="601" alt="Screenshot 2024-11-11 at 17 07 47"
src="https://github.com/user-attachments/assets/0f22991f-fa82-4dcf-8f44-7c88d7f85d8e">
| <img width="596" alt="Screenshot 2024-11-12 at 13 37 06"
src="https://github.com/user-attachments/assets/a4a53bac-ecd0-4cc7-9ba5-11c733cc8f88">
|

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Bena Kansara 2024-11-18 11:45:35 +01:00 committed by GitHub
parent 5d00a0a098
commit b87e47bdce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 288 additions and 63 deletions

View file

@ -28,7 +28,7 @@ export type DateHistogramColumnParams = DateHistogramIndexPatternColumn['params'
export type TopValuesColumnParams = Pick<
TermsIndexPatternColumn['params'],
'size' | 'orderDirection' | 'orderBy' | 'secondaryFields' | 'accuracyMode'
'size' | 'orderDirection' | 'orderBy' | 'secondaryFields' | 'accuracyMode' | 'orderAgg'
>;
export const getHistogramColumn = ({

View file

@ -17,7 +17,11 @@ const useCases = [
filter: '',
name: '',
},
'sum(system.cpu.user.pct)',
{
operation: 'sum',
operationWithField: 'sum(system.cpu.user.pct)',
sourceField: 'system.cpu.user.pct',
},
],
[
{
@ -26,7 +30,11 @@ const useCases = [
filter: '',
name: '',
},
'max(system.cpu.user.pct)',
{
operation: 'max',
operationWithField: 'max(system.cpu.user.pct)',
sourceField: 'system.cpu.user.pct',
},
],
[
{
@ -35,7 +43,11 @@ const useCases = [
filter: '',
name: '',
},
'min(system.cpu.user.pct)',
{
operation: 'min',
operationWithField: 'min(system.cpu.user.pct)',
sourceField: 'system.cpu.user.pct',
},
],
[
{
@ -44,16 +56,24 @@ const useCases = [
filter: '',
name: '',
},
'average(system.cpu.user.pct)',
{
operation: 'average',
operationWithField: 'average(system.cpu.user.pct)',
sourceField: 'system.cpu.user.pct',
},
],
[
{
aggType: Aggregators.COUNT,
field: 'system.cpu.user.pct',
filter: '',
field: '',
filter: 'system.cpu.user.pct: *',
name: '',
},
'count(___records___)',
{
operation: 'count',
operationWithField: `count(kql='system.cpu.user.pct: *')`,
sourceField: '',
},
],
[
{
@ -62,7 +82,11 @@ const useCases = [
filter: '',
name: '',
},
'unique_count(system.cpu.user.pct)',
{
operation: 'unique_count',
operationWithField: 'unique_count(system.cpu.user.pct)',
sourceField: 'system.cpu.user.pct',
},
],
[
{
@ -71,7 +95,11 @@ const useCases = [
filter: '',
name: '',
},
'percentile(system.cpu.user.pct, percentile=95)',
{
operation: 'percentile',
operationWithField: 'percentile(system.cpu.user.pct, percentile=95)',
sourceField: 'system.cpu.user.pct',
},
],
[
{
@ -80,7 +108,11 @@ const useCases = [
filter: '',
name: '',
},
'percentile(system.cpu.user.pct, percentile=99)',
{
operation: 'percentile',
operationWithField: 'percentile(system.cpu.user.pct, percentile=99)',
sourceField: 'system.cpu.user.pct',
},
],
[
{
@ -89,7 +121,11 @@ const useCases = [
filter: '',
name: '',
},
`counter_rate(max(system.network.in.bytes), kql='')`,
{
operation: 'counter_rate',
operationWithField: `counter_rate(max(system.network.in.bytes), kql='')`,
sourceField: 'system.network.in.bytes',
},
],
[
{
@ -98,7 +134,11 @@ const useCases = [
filter: 'host.name : "foo"',
name: '',
},
`counter_rate(max(system.network.in.bytes), kql='host.name : foo')`,
{
operation: 'counter_rate',
operationWithField: `counter_rate(max(system.network.in.bytes), kql='host.name : foo')`,
sourceField: 'system.network.in.bytes',
},
],
];

View file

@ -8,14 +8,24 @@
import { Aggregators } from '../../../common/custom_threshold_rule/types';
import { GenericMetric } from './rule_condition_chart';
export const getLensOperationFromRuleMetric = (metric: GenericMetric): string => {
export interface LensOperation {
operation: string;
operationWithField: string;
sourceField: string;
}
export const getLensOperationFromRuleMetric = (metric: GenericMetric): LensOperation => {
const { aggType, field, filter } = metric;
let operation: string = aggType;
const operationArgs: string[] = [];
const aggFilter = JSON.stringify(filter || '').replace(/"|\\/g, '');
if (aggType === Aggregators.RATE) {
return `counter_rate(max(${field}), kql='${aggFilter}')`;
return {
operation: 'counter_rate',
operationWithField: `counter_rate(max(${field}), kql='${aggFilter}')`,
sourceField: field || '',
};
}
if (aggType === Aggregators.AVERAGE) operation = 'average';
@ -23,14 +33,10 @@ export const getLensOperationFromRuleMetric = (metric: GenericMetric): string =>
if (aggType === Aggregators.P95 || aggType === Aggregators.P99) operation = 'percentile';
if (aggType === Aggregators.COUNT) operation = 'count';
let sourceField = field;
if (aggType === Aggregators.COUNT) {
sourceField = '___records___';
if (field) {
operationArgs.push(field);
}
operationArgs.push(sourceField || '');
if (aggType === Aggregators.P95) {
operationArgs.push('percentile=95');
}
@ -41,7 +47,11 @@ export const getLensOperationFromRuleMetric = (metric: GenericMetric): string =>
if (aggFilter) operationArgs.push(`kql='${aggFilter}'`);
return operation + '(' + operationArgs.join(', ') + ')';
return {
operation,
operationWithField: `${operation}(${operationArgs.join(', ')})`,
sourceField: field || '',
};
};
export const getBufferThreshold = (threshold?: number): string =>

View file

@ -17,7 +17,11 @@ describe('PainlessTinyMathParser', () => {
const parser = new PainlessTinyMathParser({
equation,
aggMap: {
A: 'average(system.cpu.system.pct)',
A: {
operationWithField: 'average(system.cpu.system.pct)',
operation: 'average',
sourceField: 'system.cpu.system.pct',
},
},
});
// ✅ checked with Lens Formula editor
@ -28,7 +32,11 @@ describe('PainlessTinyMathParser', () => {
const parser = new PainlessTinyMathParser({
equation,
aggMap: {
'ABC-abc': 'average(system.cpu.system.pct)',
'ABC-abc': {
operationWithField: 'average(system.cpu.system.pct)',
operation: 'average',
sourceField: 'system.cpu.system.pct',
},
},
});
expect(parser.parse()).toEqual('100*average(system.cpu.system.pct)');
@ -38,8 +46,16 @@ describe('PainlessTinyMathParser', () => {
const parser = new PainlessTinyMathParser({
equation,
aggMap: {
A: 'average(system.cpu.system.pct)',
B: 'average(system.cpu.user.pct)',
A: {
operationWithField: 'average(system.cpu.system.pct)',
operation: 'average',
sourceField: 'system.cpu.system.pct',
},
B: {
operationWithField: 'average(system.cpu.user.pct)',
operation: 'average',
sourceField: 'system.cpu.user.pct',
},
},
});
// ✅ checked with Lens Formula editor
@ -52,9 +68,21 @@ describe('PainlessTinyMathParser', () => {
const parser = new PainlessTinyMathParser({
equation,
aggMap: {
A: 'average(system.cpu.system.pct)',
B: 'average(system.cpu.user.pct)',
C: 'average(system.cpu.cores)',
A: {
operationWithField: 'average(system.cpu.system.pct)',
operation: 'average',
sourceField: 'system.cpu.system.pct',
},
B: {
operationWithField: 'average(system.cpu.user.pct)',
operation: 'average',
sourceField: 'system.cpu.user.pct',
},
C: {
operationWithField: 'average(system.cpu.cores)',
operation: 'average',
sourceField: 'system.cpu.cores',
},
},
});
// ✅ checked with Lens Formula editor
@ -67,7 +95,11 @@ describe('PainlessTinyMathParser', () => {
const parser = new PainlessTinyMathParser({
equation,
aggMap: {
A: 'average(system.cpu.system.pct)',
A: {
operationWithField: 'average(system.cpu.system.pct)',
operation: 'average',
sourceField: 'system.cpu.system.pct',
},
},
});
// ✅ checked with Lens Formula editor
@ -79,8 +111,16 @@ describe('PainlessTinyMathParser', () => {
const parser = new PainlessTinyMathParser({
equation,
aggMap: {
A: 'average(system.cpu.system.pct)',
B: 'average(system.cpu.user.pct)',
A: {
operationWithField: 'average(system.cpu.system.pct)',
operation: 'average',
sourceField: 'system.cpu.system.pct',
},
B: {
operationWithField: 'average(system.cpu.user.pct)',
operation: 'average',
sourceField: 'system.cpu.user.pct',
},
},
});
// ✅ checked with Lens Formula editor
@ -93,8 +133,16 @@ describe('PainlessTinyMathParser', () => {
const parser = new PainlessTinyMathParser({
equation,
aggMap: {
A: 'average(system.cpu.system.pct)',
B: 'average(system.cpu.user.pct)',
A: {
operationWithField: 'average(system.cpu.system.pct)',
operation: 'average',
sourceField: 'system.cpu.system.pct',
},
B: {
operationWithField: 'average(system.cpu.user.pct)',
operation: 'average',
sourceField: 'system.cpu.user.pct',
},
},
});
// ✅ checked with Lens Formula editor
@ -107,8 +155,16 @@ describe('PainlessTinyMathParser', () => {
const parser = new PainlessTinyMathParser({
equation,
aggMap: {
A: 'average(system.cpu.system.pct)',
B: 'average(system.cpu.user.pct)',
A: {
operationWithField: 'average(system.cpu.system.pct)',
operation: 'average',
sourceField: 'system.cpu.system.pct',
},
B: {
operationWithField: 'average(system.cpu.user.pct)',
operation: 'average',
sourceField: 'system.cpu.user.pct',
},
},
});
// ✅ checked with Lens Formula editor
@ -121,8 +177,16 @@ describe('PainlessTinyMathParser', () => {
const parser = new PainlessTinyMathParser({
equation,
aggMap: {
A: 'average(system.cpu.system.pct)',
B: 'average(system.cpu.user.pct)',
A: {
operationWithField: 'average(system.cpu.system.pct)',
operation: 'average',
sourceField: 'system.cpu.system.pct',
},
B: {
operationWithField: 'average(system.cpu.user.pct)',
operation: 'average',
sourceField: 'system.cpu.user.pct',
},
},
});
// ✅ checked with Lens Formula editor
@ -135,8 +199,16 @@ describe('PainlessTinyMathParser', () => {
const parser = new PainlessTinyMathParser({
equation,
aggMap: {
A: 'average(system.cpu.system.pct)',
B: 'average(system.cpu.user.pct)',
A: {
operationWithField: 'average(system.cpu.system.pct)',
operation: 'average',
sourceField: 'system.cpu.system.pct',
},
B: {
operationWithField: 'average(system.cpu.user.pct)',
operation: 'average',
sourceField: 'system.cpu.user.pct',
},
},
});
// ✅ checked with Lens Formula editor
@ -149,8 +221,16 @@ describe('PainlessTinyMathParser', () => {
const parser = new PainlessTinyMathParser({
equation,
aggMap: {
A: 'average(system.cpu.system.pct)',
B: 'average(system.cpu.user.pct)',
A: {
operationWithField: 'average(system.cpu.system.pct)',
operation: 'average',
sourceField: 'system.cpu.system.pct',
},
B: {
operationWithField: 'average(system.cpu.user.pct)',
operation: 'average',
sourceField: 'system.cpu.user.pct',
},
},
});
// ✅ checked with Lens Formula editor
@ -163,8 +243,16 @@ describe('PainlessTinyMathParser', () => {
const parser = new PainlessTinyMathParser({
equation,
aggMap: {
A: 'average(system.cpu.system.pct)',
B: 'average(system.cpu.user.pct)',
A: {
operationWithField: 'average(system.cpu.system.pct)',
operation: 'average',
sourceField: 'system.cpu.system.pct',
},
B: {
operationWithField: 'average(system.cpu.user.pct)',
operation: 'average',
sourceField: 'system.cpu.user.pct',
},
},
});
// ✅ checked with Lens Formula editor
@ -178,8 +266,16 @@ describe('PainlessTinyMathParser', () => {
const parser = new PainlessTinyMathParser({
equation,
aggMap: {
aa: 'average(system.cpu.system.pct)',
baa: 'average(system.cpu.user.pct)',
aa: {
operationWithField: 'average(system.cpu.system.pct)',
operation: 'average',
sourceField: 'system.cpu.system.pct',
},
baa: {
operationWithField: 'average(system.cpu.user.pct)',
operation: 'average',
sourceField: 'system.cpu.user.pct',
},
},
});
expect(parser.parse()).toEqual(
@ -193,12 +289,36 @@ describe('PainlessTinyMathParser', () => {
const parser = new PainlessTinyMathParser({
equation,
aggMap: {
A: 'average(system.cpu.system.pct)',
B: 'average(system.cpu.user.pct)',
C: 'average(system.cpu.total.pct)',
D: 'average(system.cpu.cores)',
E: 'count()',
F: 'sum(system.cpu.total.pct)',
A: {
operationWithField: 'average(system.cpu.system.pct)',
operation: 'average',
sourceField: 'system.cpu.system.pct',
},
B: {
operationWithField: 'average(system.cpu.user.pct)',
operation: 'average',
sourceField: 'system.cpu.user.pct',
},
C: {
operationWithField: 'average(system.cpu.total.pct)',
operation: 'average',
sourceField: 'system.cpu.total.pct',
},
D: {
operationWithField: 'average(system.cpu.cores)',
operation: 'average',
sourceField: 'system.cpu.cores',
},
E: {
operationWithField: 'count()',
operation: 'count',
sourceField: '',
},
F: {
operationWithField: 'sum(system.cpu.total.pct)',
operation: 'sum',
sourceField: 'system.cpu.total.pct',
},
},
});
// ✅ checked with Lens Formula editor
@ -213,12 +333,36 @@ describe('PainlessTinyMathParser', () => {
const parser = new PainlessTinyMathParser({
equation,
aggMap: {
A: 'average(system.cpu.system.pct)',
B: 'average(system.cpu.user.pct)',
C: 'average(system.cpu.total.pct)',
D: 'average(system.cpu.cores)',
E: 'count()',
F: 'sum(system.cpu.total.pct)',
A: {
operationWithField: 'average(system.cpu.system.pct)',
operation: 'average',
sourceField: 'system.cpu.system.pct',
},
B: {
operationWithField: 'average(system.cpu.user.pct)',
operation: 'average',
sourceField: 'system.cpu.user.pct',
},
C: {
operationWithField: 'average(system.cpu.total.pct)',
operation: 'average',
sourceField: 'system.cpu.total.pct',
},
D: {
operationWithField: 'average(system.cpu.cores)',
operation: 'average',
sourceField: 'system.cpu.cores',
},
E: {
operationWithField: 'count()',
operation: 'count',
sourceField: '',
},
F: {
operationWithField: 'sum(system.cpu.total.pct)',
operation: 'sum',
sourceField: 'system.cpu.total.pct',
},
},
});
// ✅ checked with Lens Formula editor

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import { LensOperation } from './helpers';
// This is a parser of a subset operations/expression/statement of Painless A-Z, +, -, /, *, (, ), ?, !, &, :, |, >, <, = to be used in Lens formula editor that uses TinyMath
// The goal is to parse painless expressions to a format that can be used in Lens formula editor
// The parser will also replace the characters A-Z with the values from aggMap
@ -13,7 +15,7 @@
// This parser is using a simple recursive function to parse the expression and replace the characters with the values from aggMap
export interface AggMap {
[key: string]: string;
[key: string]: LensOperation;
}
interface PainlessTinyMathParserProps {
equation: string;
@ -81,7 +83,10 @@ export class PainlessTinyMathParser {
.sort()
.reverse()
.forEach((metricName) => {
parsedInputString = parsedInputString.replaceAll(metricName, aggMap[metricName]);
parsedInputString = parsedInputString.replaceAll(
metricName,
aggMap[metricName].operationWithField
);
});
return parsedInputString;

View file

@ -8,7 +8,7 @@
import React, { useState, useEffect } from 'react';
import { EuiEmptyPrompt, useEuiTheme } from '@elastic/eui';
import { Query, Filter } from '@kbn/es-query';
import { FillStyle, SeriesType } from '@kbn/lens-plugin/public';
import { FillStyle, SeriesType, TermsIndexPatternColumn } from '@kbn/lens-plugin/public';
import { DataView } from '@kbn/data-views-plugin/common';
import { FormattedMessage } from '@kbn/i18n-react';
import useAsync from 'react-use/lib/useAsync';
@ -82,6 +82,10 @@ export interface RuleConditionChartProps {
additionalFilters?: Filter[];
}
export type TopValuesOrderParams =
| Pick<TermsIndexPatternColumn['params'], 'orderDirection' | 'orderBy' | 'orderAgg'>
| undefined;
const defaultQuery: Query = {
language: 'kuery',
query: '',
@ -299,10 +303,10 @@ export function RuleConditionChart({
return;
}
const aggMapFromMetrics = metrics.reduce((acc, metric) => {
const operationField = getLensOperationFromRuleMetric(metric);
const { operation, operationWithField, sourceField } = getLensOperationFromRuleMetric(metric);
return {
...acc,
[metric.name]: operationField,
[metric.name]: { operation, operationWithField, sourceField },
};
}, {} as AggMap);
@ -354,6 +358,26 @@ export function RuleConditionChart({
seriesType: seriesType ? seriesType : 'bar',
};
const firstMetricAggMap = aggMap && metrics.length > 0 ? aggMap[metrics[0].name] : undefined;
const convertToMaxOperation = ['counter_rate', 'last_value', 'percentile'];
const orderParams: TopValuesOrderParams = firstMetricAggMap
? {
orderDirection: 'desc',
orderBy: { type: 'custom' },
orderAgg: {
label: firstMetricAggMap.operationWithField,
dataType: 'number',
operationType: convertToMaxOperation.includes(firstMetricAggMap.operation)
? 'max'
: firstMetricAggMap.operation,
sourceField: firstMetricAggMap.sourceField,
isBucketed: false,
scale: 'ratio',
},
}
: undefined;
if (groupBy && groupBy?.length) {
xYDataLayerOptions.breakdown = {
type: 'top_values',
@ -362,6 +386,7 @@ export function RuleConditionChart({
size: 3,
secondaryFields: (groupBy as string[]).slice(1),
accuracyMode: false,
...orderParams,
},
};
}
@ -425,6 +450,7 @@ export function RuleConditionChart({
timeUnit,
seriesType,
warningThresholdReferenceLine,
aggMap,
]);
if (