Skip to content

Commit

Permalink
[OBS-UX-MNGMT] - Add P95, P99, and RATE Aggs to the Custom Threshold …
Browse files Browse the repository at this point in the history
…rule (elastic#173728)

## Summary

Fixes elastic#172945 by adding P95, P99, and RATE Aggs to the Custom Threshold
rule

<img width="575" alt="Screenshot 2023-12-20 at 12 31 50"
src="https://github.com/elastic/kibana/assets/6838659/fd9f33fc-97c5-4cf8-b15e-3134048c861a">
<img width="565" alt="Screenshot 2023-12-20 at 12 31 57"
src="https://github.com/elastic/kibana/assets/6838659/56047c84-a6e5-4a8a-a05f-b6c1358a7297">
<img width="601" alt="Screenshot 2023-12-20 at 12 36 16"
src="https://github.com/elastic/kibana/assets/6838659/af7c291d-320f-4cb1-b62f-46e29f596bcd">
  • Loading branch information
fkanout committed Mar 4, 2024
1 parent f20c680 commit 93503ff
Show file tree
Hide file tree
Showing 25 changed files with 1,201 additions and 95 deletions.
2 changes: 2 additions & 0 deletions x-pack/plugins/observability/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,5 @@ export const observabilityRuleCreationValidConsumers: RuleCreationValidConsumer[
AlertConsumers.LOGS,
AlertConsumers.OBSERVABILITY,
];

export const EventsAsUnit = 'events';
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ export const metricValueFormatter = (value: number | null, metric: string = '')
}
);

const formatter = metric.endsWith('.pct')
? createFormatter('percent')
: createFormatter('highPrecision');
let formatter = createFormatter('highPrecision');
if (metric.endsWith('.pct')) formatter = createFormatter('percent');
if (metric.endsWith('.bytes')) formatter = createFormatter('bytes');

return value == null ? noDataValue : formatter(value);
};
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export enum Aggregators {
MIN = 'min',
MAX = 'max',
CARDINALITY = 'cardinality',
RATE = 'rate',
P95 = 'p95',
P99 = 'p99',
}
export const aggType = fromEnum('Aggregators', Aggregators);
export type AggType = rt.TypeOf<typeof aggType>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,4 +307,31 @@ export const aggregationType: { [key: string]: AggregationType } = {
value: Aggregators.SUM,
validNormalizedTypes: ['number', 'histogram'],
},
p95: {
text: i18n.translate(
'xpack.observability.customThreshold.rule.alertFlyout.aggregationText.p95',
{ defaultMessage: '95th Percentile' }
),
fieldRequired: false,
value: Aggregators.P95,
validNormalizedTypes: ['number', 'histogram'],
},
p99: {
text: i18n.translate(
'xpack.observability.customThreshold.rule.alertFlyout.aggregationText.p99',
{ defaultMessage: '99th Percentile' }
),
fieldRequired: false,
value: Aggregators.P99,
validNormalizedTypes: ['number', 'histogram'],
},
rate: {
text: i18n.translate(
'xpack.observability..customThreshold.rule.alertFlyout.aggregationText.rate',
{ defaultMessage: 'Rate' }
),
fieldRequired: false,
value: Aggregators.RATE,
validNormalizedTypes: ['number'],
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
Aggregators,
CustomThresholdExpressionMetric,
} from '../../../../../common/custom_threshold_rule/types';
import { getBufferThreshold, getLensOperationFromRuleMetric, lensFieldFormatter } from './helpers';
const useCases = [
[
{
aggType: Aggregators.SUM,
field: 'system.cpu.user.pct',
filter: '',
name: '',
},
'sum(system.cpu.user.pct)',
],
[
{
aggType: Aggregators.MAX,
field: 'system.cpu.user.pct',
filter: '',
name: '',
},
'max(system.cpu.user.pct)',
],
[
{
aggType: Aggregators.MIN,
field: 'system.cpu.user.pct',
filter: '',
name: '',
},
'min(system.cpu.user.pct)',
],
[
{
aggType: Aggregators.AVERAGE,
field: 'system.cpu.user.pct',
filter: '',
name: '',
},
'average(system.cpu.user.pct)',
],
[
{
aggType: Aggregators.COUNT,
field: 'system.cpu.user.pct',
filter: '',
name: '',
},
'count(___records___)',
],
[
{
aggType: Aggregators.CARDINALITY,
field: 'system.cpu.user.pct',
filter: '',
name: '',
},
'unique_count(system.cpu.user.pct)',
],
[
{
aggType: Aggregators.P95,
field: 'system.cpu.user.pct',
filter: '',
name: '',
},
'percentile(system.cpu.user.pct, percentile=95)',
],
[
{
aggType: Aggregators.P99,
field: 'system.cpu.user.pct',
filter: '',
name: '',
},
'percentile(system.cpu.user.pct, percentile=99)',
],
[
{
aggType: Aggregators.RATE,
field: 'system.network.in.bytes',
filter: '',
name: '',
},
`counter_rate(max(system.network.in.bytes), kql='')`,
],
[
{
aggType: Aggregators.RATE,
field: 'system.network.in.bytes',
filter: 'host.name : "foo"',
name: '',
},
`counter_rate(max(system.network.in.bytes), kql='host.name : foo')`,
],
];

test.each(useCases)('returns the correct operation from %p. => %p', (metric, expectedValue) => {
return expect(getLensOperationFromRuleMetric(metric as CustomThresholdExpressionMetric)).toEqual(
expectedValue
);
});

describe('getBufferThreshold', () => {
const testData = [
{ threshold: undefined, buffer: '0.00' },
{ threshold: 0.1, buffer: '0.12' },
{ threshold: 0.01, buffer: '0.02' },
{ threshold: 0.001, buffer: '0.01' },
{ threshold: 0.00098, buffer: '0.01' },
{ threshold: 130, buffer: '143.00' },
];

it.each(testData)('getBufferThreshold($threshold) = $buffer', ({ threshold, buffer }) => {
expect(getBufferThreshold(threshold)).toBe(buffer);
});
});

describe('lensFieldFormatter', () => {
const testData = [
{ metrics: [{ field: 'system.bytes' }], format: 'bits' },
{ metrics: [{ field: 'system.pct' }], format: 'percent' },
{ metrics: [{ field: 'system.host.cores' }], format: 'number' },
{ metrics: [{ field: undefined }], format: 'number' },
];
it.each(testData)('getBufferThreshold($threshold) = $buffer', ({ metrics, format }) => {
expect(lensFieldFormatter(metrics as unknown as CustomThresholdExpressionMetric[])).toBe(
format
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import {
Aggregators,
CustomThresholdExpressionMetric,
} from '../../../../../common/custom_threshold_rule/types';

export const getLensOperationFromRuleMetric = (metric: CustomThresholdExpressionMetric): string => {
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}')`;
}

if (aggType === Aggregators.AVERAGE) operation = 'average';
if (aggType === Aggregators.CARDINALITY) operation = 'unique_count';
if (aggType === Aggregators.P95 || aggType === Aggregators.P99) operation = 'percentile';
if (aggType === Aggregators.COUNT) operation = 'count';

let sourceField = field;

if (aggType === Aggregators.COUNT) {
sourceField = '___records___';
}

operationArgs.push(sourceField || '');

if (aggType === Aggregators.P95) {
operationArgs.push('percentile=95');
}

if (aggType === Aggregators.P99) {
operationArgs.push('percentile=99');
}

if (aggFilter) operationArgs.push(`kql='${aggFilter}'`);

return operation + '(' + operationArgs.join(', ') + ')';
};

export const getBufferThreshold = (threshold?: number): string =>
(Math.ceil((threshold || 0) * 1.1 * 100) / 100).toFixed(2).toString();

export const LensFieldFormat = {
NUMBER: 'number',
PERCENT: 'percent',
BITS: 'bits',
} as const;

export const lensFieldFormatter = (
metrics: CustomThresholdExpressionMetric[]
): typeof LensFieldFormat[keyof typeof LensFieldFormat] => {
if (metrics.length < 1 || !metrics[0].field) return LensFieldFormat.NUMBER;
const firstMetricField = metrics[0].field;
if (firstMetricField.endsWith('.pct')) return LensFieldFormat.PERCENT;
if (firstMetricField.endsWith('.bytes')) return LensFieldFormat.BITS;
return LensFieldFormat.NUMBER;
};

export const isRate = (metrics: CustomThresholdExpressionMetric[]): boolean =>
Boolean(metrics.length > 0 && metrics[0].aggType === Aggregators.RATE);
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Comparator, Aggregators } from '../../../../../common/custom_threshold_
import { useKibana } from '../../../../utils/kibana_react';
import { kibanaStartMock } from '../../../../utils/kibana_react.mock';
import { MetricExpression } from '../../types';
import { getBufferThreshold, RuleConditionChart } from './rule_condition_chart';
import { RuleConditionChart } from './rule_condition_chart';

jest.mock('../../../../utils/kibana_react');

Expand Down Expand Up @@ -71,18 +71,3 @@ describe('Rule condition chart', () => {
expect(wrapper.find('[data-test-subj="thresholdRuleNoChartData"]').exists()).toBeTruthy();
});
});

describe('getBufferThreshold', () => {
const testData = [
{ threshold: undefined, buffer: '0.00' },
{ threshold: 0.1, buffer: '0.12' },
{ threshold: 0.01, buffer: '0.02' },
{ threshold: 0.001, buffer: '0.01' },
{ threshold: 0.00098, buffer: '0.01' },
{ threshold: 130, buffer: '143.00' },
];

it.each(testData)('getBufferThreshold($threshold) = $buffer', ({ threshold, buffer }) => {
expect(getBufferThreshold(threshold)).toBe(buffer);
});
});
Loading

0 comments on commit 93503ff

Please sign in to comment.