Skip to content

Commit 021da34

Browse files
committed
[Metrics UI] Fix p95/p99 charts and alerting error (elastic#65579)
* [Metrics UI] Fix p95/p99 charts and alerting error - Fixes elastic#65561 * Fixing open in visualize for percentiles * Adding test for P95; refactoring to use first consitently
1 parent a1bfedf commit 021da34

File tree

10 files changed

+144
-18
lines changed

10 files changed

+144
-18
lines changed

x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,7 @@ export const ExpressionChart: React.FC<Props> = ({
118118
const series = {
119119
...firstSeries,
120120
rows: firstSeries.rows.map(row => {
121-
const newRow: MetricsExplorerRow = {
122-
timestamp: row.timestamp,
123-
metric_0: row.metric_0 || null,
124-
};
121+
const newRow: MetricsExplorerRow = { ...row };
125122
thresholds.forEach((thresholdValue, index) => {
126123
newRow[`metric_threshold_${index}`] = thresholdValue;
127124
});

x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,20 @@ export const aggregationType: { [key: string]: any } = {
234234
value: AGGREGATION_TYPES.SUM,
235235
validNormalizedTypes: ['number'],
236236
},
237+
p95: {
238+
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.p95', {
239+
defaultMessage: '95th Percentile',
240+
}),
241+
fieldRequired: false,
242+
value: AGGREGATION_TYPES.P95,
243+
validNormalizedTypes: ['number'],
244+
},
245+
p99: {
246+
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.p99', {
247+
defaultMessage: '99th Percentile',
248+
}),
249+
fieldRequired: false,
250+
value: AGGREGATION_TYPES.P99,
251+
validNormalizedTypes: ['number'],
252+
},
237253
};

x-pack/plugins/infra/public/alerting/metric_threshold/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export enum AGGREGATION_TYPES {
2929
MAX = 'max',
3030
RATE = 'rate',
3131
CARDINALITY = 'cardinality',
32+
P95 = 'p95',
33+
P99 = 'p99',
3234
}
3335

3436
export interface MetricThresholdAlertParams {

x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,24 @@ export const metricsExplorerMetricToTSVBMetric = (metric: MetricsExplorerOptions
4646
field: derivativeId,
4747
},
4848
];
49+
} else if (metric.aggregation === 'p95' || metric.aggregation === 'p99') {
50+
const percentileValue = metric.aggregation === 'p95' ? '95' : '99';
51+
return [
52+
{
53+
id: uuid.v1(),
54+
type: 'percentile',
55+
field: metric.field,
56+
percentiles: [
57+
{
58+
id: uuid.v1(),
59+
value: percentileValue,
60+
mode: 'line',
61+
percentile: '',
62+
shade: 0.2,
63+
},
64+
],
65+
},
66+
];
4967
} else {
5068
return [
5169
{

x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
MetricsExplorerOptionsMetric,
2121
MetricsExplorerChartType,
2222
} from '../hooks/use_metrics_explorer_options';
23+
import { getMetricId } from './helpers/get_metric_id';
2324

2425
type NumberOrString = string | number;
2526

@@ -45,10 +46,12 @@ export const MetricsExplorerAreaChart = ({ metric, id, series, type, stack, opac
4546
colorTransformer(MetricsExplorerColor.color0);
4647

4748
const yAccessors = Array.isArray(id)
48-
? id.map(i => `metric_${i}`).slice(id.length - 1, id.length)
49-
: [`metric_${id}`];
49+
? id.map(i => getMetricId(metric, i)).slice(id.length - 1, id.length)
50+
: [getMetricId(metric, id)];
5051
const y0Accessors =
51-
Array.isArray(id) && id.length > 1 ? id.map(i => `metric_${i}`).slice(0, 1) : undefined;
52+
Array.isArray(id) && id.length > 1
53+
? id.map(i => getMetricId(metric, i)).slice(0, 1)
54+
: undefined;
5255
const chartId = `series-${series.id}-${yAccessors.join('-')}`;
5356

5457
const seriesAreaStyle: RecursivePartial<AreaSeriesStyle> = {
@@ -85,8 +88,10 @@ export const MetricsExplorerBarChart = ({ metric, id, series, stack }: Props) =>
8588
(metric.color && colorTransformer(metric.color)) ||
8689
colorTransformer(MetricsExplorerColor.color0);
8790

88-
const yAccessor = `metric_${id}`;
89-
const chartId = `series-${series.id}-${yAccessor}`;
91+
const yAccessors = Array.isArray(id)
92+
? id.map(i => getMetricId(metric, i)).slice(id.length - 1, id.length)
93+
: [getMetricId(metric, id)];
94+
const chartId = `series-${series.id}-${yAccessors.join('-')}`;
9095

9196
const seriesBarStyle: RecursivePartial<BarSeriesStyle> = {
9297
rectBorder: {
@@ -100,13 +105,13 @@ export const MetricsExplorerBarChart = ({ metric, id, series, stack }: Props) =>
100105
};
101106
return (
102107
<BarSeries
103-
id={yAccessor}
108+
id={chartId}
104109
key={chartId}
105110
name={createMetricLabel(metric)}
106111
xScaleType={ScaleType.Time}
107112
yScaleType={ScaleType.Linear}
108113
xAccessor="timestamp"
109-
yAccessors={[yAccessor]}
114+
yAccessors={yAccessors}
110115
data={series.rows}
111116
stackAccessors={stack ? ['timestamp'] : void 0}
112117
barSeriesStyle={seriesBarStyle}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { Aggregators } from './types';
8+
export const createPercentileAggregation = (
9+
type: Aggregators.P95 | Aggregators.P99,
10+
field: string
11+
) => {
12+
const value = type === Aggregators.P95 ? 95 : 99;
13+
return {
14+
aggregatedValue: {
15+
percentiles: {
16+
field,
17+
percents: [value],
18+
keyed: false,
19+
},
20+
},
21+
};
22+
};

x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,58 @@ describe('The metric threshold alert type', () => {
233233
expect(getState(instanceID).alertState).toBe(AlertStates.OK);
234234
});
235235
});
236+
describe('querying with the p99 aggregator', () => {
237+
const instanceID = 'test-*';
238+
const execute = (comparator: Comparator, threshold: number[]) =>
239+
executor({
240+
services,
241+
params: {
242+
criteria: [
243+
{
244+
...baseCriterion,
245+
comparator,
246+
threshold,
247+
aggType: 'p99',
248+
metric: 'test.metric.2',
249+
},
250+
],
251+
},
252+
});
253+
test('alerts based on the p99 values', async () => {
254+
await execute(Comparator.GT, [1]);
255+
expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id);
256+
expect(getState(instanceID).alertState).toBe(AlertStates.ALERT);
257+
await execute(Comparator.LT, [1]);
258+
expect(mostRecentAction(instanceID)).toBe(undefined);
259+
expect(getState(instanceID).alertState).toBe(AlertStates.OK);
260+
});
261+
});
262+
describe('querying with the p95 aggregator', () => {
263+
const instanceID = 'test-*';
264+
const execute = (comparator: Comparator, threshold: number[]) =>
265+
executor({
266+
services,
267+
params: {
268+
criteria: [
269+
{
270+
...baseCriterion,
271+
comparator,
272+
threshold,
273+
aggType: 'p95',
274+
metric: 'test.metric.1',
275+
},
276+
],
277+
},
278+
});
279+
test('alerts based on the p95 values', async () => {
280+
await execute(Comparator.GT, [0.25]);
281+
expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id);
282+
expect(getState(instanceID).alertState).toBe(AlertStates.ALERT);
283+
await execute(Comparator.LT, [0.95]);
284+
expect(mostRecentAction(instanceID)).toBe(undefined);
285+
expect(getState(instanceID).alertState).toBe(AlertStates.OK);
286+
});
287+
});
236288
describe("querying a metric that hasn't reported data", () => {
237289
const instanceID = 'test-*';
238290
const execute = (alertOnNoData: boolean) =>

x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* or more contributor license agreements. Licensed under the Elastic License;
44
* you may not use this file except in compliance with the Elastic License.
55
*/
6-
import { mapValues } from 'lodash';
6+
import { mapValues, first } from 'lodash';
77
import { i18n } from '@kbn/i18n';
88
import { InfraDatabaseSearchResponse } from '../../adapters/framework/adapter_types';
99
import { createAfterKeyHandler } from '../../../utils/create_afterkey_handler';
@@ -21,12 +21,16 @@ import { AlertServices, AlertExecutorOptions } from '../../../../../alerting/ser
2121
import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds';
2222
import { getDateHistogramOffset } from '../../snapshot/query_helpers';
2323
import { InfraBackendLibs } from '../../infra_types';
24+
import { createPercentileAggregation } from './create_percentile_aggregation';
2425

2526
const TOTAL_BUCKETS = 5;
2627

2728
interface Aggregation {
2829
aggregatedIntervals: {
29-
buckets: Array<{ aggregatedValue: { value: number }; doc_count: number }>;
30+
buckets: Array<{
31+
aggregatedValue: { value: number; values?: Array<{ key: number; value: number }> };
32+
doc_count: number;
33+
}>;
3034
};
3135
}
3236

@@ -47,6 +51,12 @@ const getCurrentValueFromAggregations = (
4751
if (aggType === Aggregators.COUNT) {
4852
return mostRecentBucket.doc_count;
4953
}
54+
if (aggType === Aggregators.P95 || aggType === Aggregators.P99) {
55+
const values = mostRecentBucket.aggregatedValue?.values || [];
56+
const firstValue = first(values);
57+
if (!firstValue) return null;
58+
return firstValue.value;
59+
}
5060
const { value } = mostRecentBucket.aggregatedValue;
5161
return value;
5262
} catch (e) {
@@ -86,6 +96,8 @@ export const getElasticsearchMetricQuery = (
8696
? {}
8797
: aggType === Aggregators.RATE
8898
? networkTraffic('aggregatedValue', metric)
99+
: aggType === Aggregators.P95 || aggType === Aggregators.P99
100+
? createPercentileAggregation(aggType, metric)
89101
: {
90102
aggregatedValue: {
91103
[aggType]: {
@@ -275,7 +287,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs, alertId: s
275287
);
276288

277289
// Because each alert result has the same group definitions, just grap the groups from the first one.
278-
const groups = Object.keys(alertResults[0]);
290+
const groups = Object.keys(first(alertResults));
279291
for (const group of groups) {
280292
const alertInstance = services.alertInstanceFactory(`${alertId}-${group}`);
281293

x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,22 @@
77
const bucketsA = [
88
{
99
doc_count: 2,
10-
aggregatedValue: { value: 0.5 },
10+
aggregatedValue: { value: 0.5, values: [{ key: 95.0, value: 0.5 }] },
1111
},
1212
{
1313
doc_count: 3,
14-
aggregatedValue: { value: 1.0 },
14+
aggregatedValue: { value: 1.0, values: [{ key: 95.0, value: 1.0 }] },
1515
},
1616
];
1717

1818
const bucketsB = [
1919
{
2020
doc_count: 4,
21-
aggregatedValue: { value: 2.5 },
21+
aggregatedValue: { value: 2.5, values: [{ key: 99.0, value: 2.5 }] },
2222
},
2323
{
2424
doc_count: 5,
25-
aggregatedValue: { value: 3.5 },
25+
aggregatedValue: { value: 3.5, values: [{ key: 99.0, value: 3.5 }] },
2626
},
2727
];
2828

x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export enum Aggregators {
2323
MAX = 'max',
2424
RATE = 'rate',
2525
CARDINALITY = 'cardinality',
26+
P95 = 'p95',
27+
P99 = 'p99',
2628
}
2729

2830
export enum AlertStates {

0 commit comments

Comments
 (0)