mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[SLO] Add preview chart to custom metric indicator (#161597)
## Summary
This PR adds a preview chart to the custom metric indicator.
<img width="691" alt="image"
src="9c2c5fa0
-d6b0-4d93-86cd-eee38052db16">
This commit is contained in:
parent
cb853a1d9a
commit
7035adb4cc
8 changed files with 229 additions and 54 deletions
|
@ -22,6 +22,7 @@ import {
|
|||
useFetchIndexPatternFields,
|
||||
} from '../../../../hooks/slo/use_fetch_index_pattern_fields';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { DataPreviewChart } from '../common/data_preview_chart';
|
||||
import { QueryBuilder } from '../common/query_builder';
|
||||
import { IndexSelection } from '../custom_common/index_selection';
|
||||
import { MetricIndicator } from './metric_indicator';
|
||||
|
@ -221,6 +222,7 @@ export function CustomMetricIndicatorTypeForm() {
|
|||
}
|
||||
/>
|
||||
</EuiPanel>
|
||||
<DataPreviewChart />
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GetHistogramIndicatorAggregation should generate a aggregation for good events 1`] = `
|
||||
Object {
|
||||
"_good_A": Object {
|
||||
"aggs": Object {
|
||||
"sum": Object {
|
||||
"sum": Object {
|
||||
"field": "total",
|
||||
},
|
||||
},
|
||||
},
|
||||
"filter": Object {
|
||||
"match_all": Object {},
|
||||
},
|
||||
},
|
||||
"_good_B": Object {
|
||||
"aggs": Object {
|
||||
"sum": Object {
|
||||
"sum": Object {
|
||||
"field": "processed",
|
||||
},
|
||||
},
|
||||
},
|
||||
"filter": Object {
|
||||
"match_all": Object {},
|
||||
},
|
||||
},
|
||||
"goodEvents": Object {
|
||||
"bucket_script": Object {
|
||||
"buckets_path": Object {
|
||||
"A": "_good_A>sum",
|
||||
"B": "_good_B>sum",
|
||||
},
|
||||
"script": Object {
|
||||
"lang": "painless",
|
||||
"source": "params.A - params.B",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`GetHistogramIndicatorAggregation should generate a aggregation for total events 1`] = `
|
||||
Object {
|
||||
"_total_A": Object {
|
||||
"aggs": Object {
|
||||
"sum": Object {
|
||||
"sum": Object {
|
||||
"field": "total",
|
||||
},
|
||||
},
|
||||
},
|
||||
"filter": Object {
|
||||
"match_all": Object {},
|
||||
},
|
||||
},
|
||||
"totalEvents": Object {
|
||||
"bucket_script": Object {
|
||||
"buckets_path": Object {
|
||||
"A": "_total_A>sum",
|
||||
},
|
||||
"script": Object {
|
||||
"lang": "painless",
|
||||
"source": "params.A",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { GetCustomMetricIndicatorAggregation } from './get_custom_metric_indicator_aggregation';
|
||||
import { createMetricCustomIndicator } from '../fixtures/slo';
|
||||
|
||||
describe('GetHistogramIndicatorAggregation', () => {
|
||||
it('should generate a aggregation for good events', () => {
|
||||
const getCustomMetricIndicatorAggregation = new GetCustomMetricIndicatorAggregation(
|
||||
createMetricCustomIndicator()
|
||||
);
|
||||
expect(
|
||||
getCustomMetricIndicatorAggregation.execute({ type: 'good', aggregationKey: 'goodEvents' })
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate a aggregation for total events', () => {
|
||||
const getCustomMetricIndicatorAggregation = new GetCustomMetricIndicatorAggregation(
|
||||
createMetricCustomIndicator()
|
||||
);
|
||||
expect(
|
||||
getCustomMetricIndicatorAggregation.execute({ type: 'total', aggregationKey: 'totalEvents' })
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 { MetricCustomIndicator } from '@kbn/slo-schema';
|
||||
import { getElastichsearchQueryOrThrow } from '../transform_generators';
|
||||
|
||||
type MetricCustomMetricDef =
|
||||
| MetricCustomIndicator['params']['good']
|
||||
| MetricCustomIndicator['params']['total'];
|
||||
|
||||
export class GetCustomMetricIndicatorAggregation {
|
||||
constructor(private indicator: MetricCustomIndicator) {}
|
||||
|
||||
private buildMetricAggregations(type: 'good' | 'total', metricDef: MetricCustomMetricDef) {
|
||||
return metricDef.metrics.reduce((acc, metric) => {
|
||||
const filter = metric.filter
|
||||
? getElastichsearchQueryOrThrow(metric.filter)
|
||||
: { match_all: {} };
|
||||
return {
|
||||
...acc,
|
||||
[`_${type}_${metric.name}`]: {
|
||||
filter,
|
||||
aggs: {
|
||||
sum: {
|
||||
[metric.aggregation]: { field: metric.field },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
|
||||
private convertEquationToPainless(bucketsPath: Record<string, string>, equation: string) {
|
||||
const workingEquation = equation || Object.keys(bucketsPath).join(' + ');
|
||||
return Object.keys(bucketsPath).reduce((acc, key) => {
|
||||
return acc.replace(key, `params.${key}`);
|
||||
}, workingEquation);
|
||||
}
|
||||
|
||||
private buildMetricEquation(type: 'good' | 'total', metricDef: MetricCustomMetricDef) {
|
||||
const bucketsPath = metricDef.metrics.reduce(
|
||||
(acc, metric) => ({ ...acc, [metric.name]: `_${type}_${metric.name}>sum` }),
|
||||
{}
|
||||
);
|
||||
return {
|
||||
bucket_script: {
|
||||
buckets_path: bucketsPath,
|
||||
script: {
|
||||
source: this.convertEquationToPainless(bucketsPath, metricDef.equation),
|
||||
lang: 'painless',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public execute({ type, aggregationKey }: { type: 'good' | 'total'; aggregationKey: string }) {
|
||||
const indicatorDef = this.indicator.params[type];
|
||||
return {
|
||||
...this.buildMetricAggregations(type, indicatorDef),
|
||||
[aggregationKey]: this.buildMetricEquation(type, indicatorDef),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './get_custom_metric_indicator_aggregation';
|
|
@ -71,7 +71,7 @@ export const createKQLCustomIndicator = (
|
|||
|
||||
export const createMetricCustomIndicator = (
|
||||
params: Partial<MetricCustomIndicator['params']> = {}
|
||||
): Indicator => ({
|
||||
): MetricCustomIndicator => ({
|
||||
type: 'sli.metric.custom',
|
||||
params: {
|
||||
index: 'my-index*',
|
||||
|
|
|
@ -10,6 +10,7 @@ import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
|
|||
import { ALL_VALUE, GetPreviewDataParams, GetPreviewDataResponse } from '@kbn/slo-schema';
|
||||
import { computeSLI } from '../../domain/services';
|
||||
import { InvalidQueryError } from '../../errors';
|
||||
import { GetCustomMetricIndicatorAggregation } from './aggregations';
|
||||
|
||||
export class GetPreviewData {
|
||||
constructor(private esClient: ElasticsearchClient) {}
|
||||
|
@ -208,6 +209,47 @@ export class GetPreviewData {
|
|||
} catch (err) {
|
||||
throw new InvalidQueryError(`Invalid ES query`);
|
||||
}
|
||||
case 'sli.metric.custom':
|
||||
const timestampField = params.indicator.params.timestampField;
|
||||
const filterQuery = getElastichsearchQueryOrThrow(params.indicator.params.filter);
|
||||
const getCustomMetricIndicatorAggregation = new GetCustomMetricIndicatorAggregation(
|
||||
params.indicator
|
||||
);
|
||||
const result = await this.esClient.search({
|
||||
index: params.indicator.params.index,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [{ range: { [timestampField]: { gte: 'now-60m' } } }, filterQuery],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
perMinute: {
|
||||
date_histogram: {
|
||||
field: timestampField,
|
||||
fixed_interval: '1m',
|
||||
},
|
||||
aggs: {
|
||||
...getCustomMetricIndicatorAggregation.execute({
|
||||
type: 'good',
|
||||
aggregationKey: 'good',
|
||||
}),
|
||||
...getCustomMetricIndicatorAggregation.execute({
|
||||
type: 'total',
|
||||
aggregationKey: 'total',
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore buckets is not improperly typed
|
||||
return result.aggregations?.perMinute.buckets.map((bucket) => ({
|
||||
date: bucket.key_as_string,
|
||||
sliValue:
|
||||
!!bucket.good && !!bucket.total
|
||||
? computeSLI(bucket.good.value, bucket.total.value)
|
||||
: null,
|
||||
}));
|
||||
|
||||
default:
|
||||
return [];
|
||||
|
|
|
@ -17,10 +17,7 @@ import {
|
|||
getSLOTransformId,
|
||||
} from '../../../assets/constants';
|
||||
import { MetricCustomIndicator, SLO } from '../../../domain/models';
|
||||
|
||||
type MetricCustomMetricDef =
|
||||
| MetricCustomIndicator['params']['good']
|
||||
| MetricCustomIndicator['params']['total'];
|
||||
import { GetCustomMetricIndicatorAggregation } from '../aggregations';
|
||||
|
||||
export const INVALID_EQUATION_REGEX = /[^A-Z|+|\-|\s|\d+|\.|\(|\)|\/|\*|>|<|=|\?|\:|&|\!|\|]+/g;
|
||||
|
||||
|
@ -61,48 +58,6 @@ export class MetricCustomTransformGenerator extends TransformGenerator {
|
|||
};
|
||||
}
|
||||
|
||||
private buildMetricAggregations(type: 'good' | 'total', metricDef: MetricCustomMetricDef) {
|
||||
return metricDef.metrics.reduce((acc, metric) => {
|
||||
const filter = metric.filter
|
||||
? getElastichsearchQueryOrThrow(metric.filter)
|
||||
: { match_all: {} };
|
||||
return {
|
||||
...acc,
|
||||
[`_${type}_${metric.name}`]: {
|
||||
filter,
|
||||
aggs: {
|
||||
sum: {
|
||||
[metric.aggregation]: { field: metric.field },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
|
||||
private convertEquationToPainless(bucketsPath: Record<string, string>, equation: string) {
|
||||
const workingEquation = equation || Object.keys(bucketsPath).join(' + ');
|
||||
return Object.keys(bucketsPath).reduce((acc, key) => {
|
||||
return acc.replace(key, `params.${key}`);
|
||||
}, workingEquation);
|
||||
}
|
||||
|
||||
private buildMetricEquation(type: 'good' | 'total', metricDef: MetricCustomMetricDef) {
|
||||
const bucketsPath = metricDef.metrics.reduce(
|
||||
(acc, metric) => ({ ...acc, [metric.name]: `_${type}_${metric.name}>sum` }),
|
||||
{}
|
||||
);
|
||||
return {
|
||||
bucket_script: {
|
||||
buckets_path: bucketsPath,
|
||||
script: {
|
||||
source: this.convertEquationToPainless(bucketsPath, metricDef.equation),
|
||||
lang: 'painless',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private buildAggregations(slo: SLO, indicator: MetricCustomIndicator) {
|
||||
if (indicator.params.good.equation.match(INVALID_EQUATION_REGEX)) {
|
||||
throw new Error(`Invalid equation: ${indicator.params.good.equation}`);
|
||||
|
@ -112,14 +67,16 @@ export class MetricCustomTransformGenerator extends TransformGenerator {
|
|||
throw new Error(`Invalid equation: ${indicator.params.total.equation}`);
|
||||
}
|
||||
|
||||
const goodAggregations = this.buildMetricAggregations('good', indicator.params.good);
|
||||
const totalAggregations = this.buildMetricAggregations('total', indicator.params.total);
|
||||
|
||||
const getCustomMetricIndicatorAggregation = new GetCustomMetricIndicatorAggregation(indicator);
|
||||
return {
|
||||
...goodAggregations,
|
||||
...totalAggregations,
|
||||
'slo.numerator': this.buildMetricEquation('good', indicator.params.good),
|
||||
'slo.denominator': this.buildMetricEquation('total', indicator.params.total),
|
||||
...getCustomMetricIndicatorAggregation.execute({
|
||||
type: 'good',
|
||||
aggregationKey: 'slo.numerator',
|
||||
}),
|
||||
...getCustomMetricIndicatorAggregation.execute({
|
||||
type: 'total',
|
||||
aggregationKey: 'slo.denominator',
|
||||
}),
|
||||
...(timeslicesBudgetingMethodSchema.is(slo.budgetingMethod) && {
|
||||
'slo.isGoodSlice': {
|
||||
bucket_script: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue