[8.12] [SLOs] Use named keys to reference range aggregations for APM Latency and Histogram Metric (#173548) (#174505)

# Backport

This will backport the following commits from `main` to `8.12`:
- [[SLOs] Use named keys to reference range aggregations for APM Latency
and Histogram Metric
(#173548)](https://github.com/elastic/kibana/pull/173548)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT
[{"author":{"name":"Shahzad","email":"shahzad31comp@gmail.com"},"sourceCommit":{"committedDate":"2024-01-05T19:24:58Z","message":"[SLOs]
Use named keys to reference range aggregations for APM Latency and
Histogram Metric (#173548)\n\n## Summary\n\nFixes
https://github.com/elastic/kibana/issues/171329\n\nit now works for
value over 9999\n\nFix APM SLI Preview value calculation !!\n\n\n<img
width=\"1726\"
alt=\"image\"\nsrc=\"d875e2cf-6195-455f-9b28-35c49c6d5eee\">","sha":"57e50a6f47f7c4cb084fddb75b1b2031db10913f","branchLabelMapping":{"^v8.13.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport:prev-minor","Team:obs-ux-management","v8.13.0"],"number":173548,"url":"https://github.com/elastic/kibana/pull/173548","mergeCommit":{"message":"[SLOs]
Use named keys to reference range aggregations for APM Latency and
Histogram Metric (#173548)\n\n## Summary\n\nFixes
https://github.com/elastic/kibana/issues/171329\n\nit now works for
value over 9999\n\nFix APM SLI Preview value calculation !!\n\n\n<img
width=\"1726\"
alt=\"image\"\nsrc=\"d875e2cf-6195-455f-9b28-35c49c6d5eee\">","sha":"57e50a6f47f7c4cb084fddb75b1b2031db10913f"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.13.0","labelRegex":"^v8.13.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/173548","number":173548,"mergeCommit":{"message":"[SLOs]
Use named keys to reference range aggregations for APM Latency and
Histogram Metric (#173548)\n\n## Summary\n\nFixes
https://github.com/elastic/kibana/issues/171329\n\nit now works for
value over 9999\n\nFix APM SLI Preview value calculation !!\n\n\n<img
width=\"1726\"
alt=\"image\"\nsrc=\"d875e2cf-6195-455f-9b28-35c49c6d5eee\">","sha":"57e50a6f47f7c4cb084fddb75b1b2031db10913f"}}]}]
BACKPORT-->

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2024-01-09 22:06:11 +01:00 committed by GitHub
parent 222317c0e1
commit c310ce2afa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 83 additions and 60 deletions

View file

@ -11,6 +11,7 @@ Object {
"ranges": Array [
Object {
"from": 0,
"key": "target",
"to": 100,
},
],
@ -24,7 +25,7 @@ Object {
"goodEvents": Object {
"bucket_script": Object {
"buckets_path": Object {
"value": "_good>total['0.0-100.0']>_count",
"value": "_good>total['target']>_count",
},
"script": "params.value",
},

View file

@ -6,6 +6,7 @@
*/
import { HistogramIndicator } from '@kbn/slo-schema';
import { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { getElastichsearchQueryOrThrow } from '../transform_generators/common';
type HistogramIndicatorDef =
@ -15,7 +16,7 @@ type HistogramIndicatorDef =
export class GetHistogramIndicatorAggregation {
constructor(private indicator: HistogramIndicator) {}
private buildAggregation(type: 'good' | 'total', indicator: HistogramIndicatorDef) {
private buildAggregation(indicator: HistogramIndicatorDef): AggregationsAggregationContainer {
const filter = indicator.filter
? getElastichsearchQueryOrThrow(indicator.filter)
: { match_all: {} };
@ -43,15 +44,6 @@ export class GetHistogramIndicatorAggregation {
throw new Error('Invalid Range: "from" should be less that "to".');
}
const range: { from?: number; to?: number } = {};
if (indicator.from != null) {
range.from = indicator.from;
}
if (indicator.to != null) {
range.to = indicator.to;
}
return {
filter,
aggs: {
@ -59,24 +51,23 @@ export class GetHistogramIndicatorAggregation {
range: {
field: indicator.field,
keyed: true,
ranges: [range],
ranges: [
{
key: 'target',
from: indicator.from,
to: indicator.to,
},
],
},
},
},
};
}
private formatNumberAsFloatString(value: number) {
return value % 1 === 0 ? `${value}.0` : `${value}`;
}
private buildRangeKey(from: number | undefined, to: number | undefined) {
const fromString = from != null ? this.formatNumberAsFloatString(from) : '*';
const toString = to != null ? this.formatNumberAsFloatString(to) : '*';
return `${fromString}-${toString}`;
}
private buildBucketScript(type: 'good' | 'total', indicator: HistogramIndicatorDef) {
private buildBucketScript(
type: 'good' | 'total',
indicator: HistogramIndicatorDef
): AggregationsAggregationContainer {
if (indicator.aggregation === 'value_count') {
return {
bucket_script: {
@ -87,11 +78,10 @@ export class GetHistogramIndicatorAggregation {
},
};
}
const rangeKey = this.buildRangeKey(indicator.from, indicator.to);
return {
bucket_script: {
buckets_path: {
value: `_${type}>total['${rangeKey}']>_count`,
value: `_${type}>total['target']>_count`,
},
script: 'params.value',
},
@ -101,7 +91,7 @@ export class GetHistogramIndicatorAggregation {
public execute({ type, aggregationKey }: { type: 'good' | 'total'; aggregationKey: string }) {
const indicatorDef = this.indicator.params[type];
return {
[`_${type}`]: this.buildAggregation(type, indicatorDef),
[`_${type}`]: this.buildAggregation(indicatorDef),
[aggregationKey]: this.buildBucketScript(type, indicatorDef),
};
}

View file

@ -6,7 +6,6 @@
*/
import { calculateAuto } from '@kbn/calculate-auto';
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
import {
ALL_VALUE,
@ -20,6 +19,9 @@ import {
} from '@kbn/slo-schema';
import { assertNever } from '@kbn/std';
import moment from 'moment';
import { ElasticsearchClient } from '@kbn/core/server';
import { estypes } from '@elastic/elasticsearch';
import { typedSearch } from '../../utils/queries';
import { APMTransactionDurationIndicator } from '../../domain/models';
import { computeSLI } from '../../domain/services';
import { InvalidQueryError } from '../../errors';
@ -43,7 +45,7 @@ export class GetPreviewData {
indicator: APMTransactionDurationIndicator,
options: Options
): Promise<GetPreviewDataResponse> {
const filter = [];
const filter: estypes.QueryDslQueryContainer[] = [];
if (indicator.params.service !== ALL_VALUE)
filter.push({
match: { 'service.name': indicator.params.service },
@ -61,11 +63,11 @@ export class GetPreviewData {
match: { 'transaction.type': indicator.params.transactionType },
});
if (!!indicator.params.filter)
filter.push(getElastichsearchQueryOrThrow(indicator.params.filter));
filter.push(getElasticsearchQueryOrThrow(indicator.params.filter));
const truncatedThreshold = Math.trunc(indicator.params.threshold * 1000);
const result = await this.esClient.search({
const result = await typedSearch(this.esClient, {
index: indicator.params.index,
size: 0,
query: {
@ -89,13 +91,14 @@ export class GetPreviewData {
_good: {
range: {
field: 'transaction.duration.histogram',
ranges: [{ to: truncatedThreshold }],
keyed: true,
ranges: [{ to: truncatedThreshold, key: 'target' }],
},
},
good: {
bucket_script: {
buckets_path: {
_good: `_good['*-${truncatedThreshold}.0']>_count`,
_good: `_good['target']>_count`,
},
script: 'params._good',
},
@ -110,17 +113,21 @@ export class GetPreviewData {
},
});
// @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,
events: {
good: bucket.good?.value ?? 0,
bad: (bucket.total?.value ?? 0) - (bucket.good?.value ?? 0),
total: bucket.total?.value ?? 0,
},
}));
return (
result.aggregations?.perMinute.buckets.map((bucket) => {
const good = (bucket.good?.value as number) ?? 0;
const total = bucket.total?.value ?? 0;
return {
date: bucket.key_as_string,
sliValue: computeSLI(good, total),
events: {
good,
total,
bad: total - good,
},
};
}) ?? []
);
}
private async getAPMTransactionErrorPreviewData(
@ -145,7 +152,7 @@ export class GetPreviewData {
match: { 'transaction.type': indicator.params.transactionType },
});
if (!!indicator.params.filter)
filter.push(getElastichsearchQueryOrThrow(indicator.params.filter));
filter.push(getElasticsearchQueryOrThrow(indicator.params.filter));
const result = await this.esClient.search({
index: indicator.params.index,
@ -208,7 +215,7 @@ export class GetPreviewData {
options: Options
): Promise<GetPreviewDataResponse> {
const getHistogramIndicatorAggregations = new GetHistogramIndicatorAggregation(indicator);
const filterQuery = getElastichsearchQueryOrThrow(indicator.params.filter);
const filterQuery = getElasticsearchQueryOrThrow(indicator.params.filter);
const timestampField = indicator.params.timestampField;
const result = await this.esClient.search({
index: indicator.params.index,
@ -259,7 +266,7 @@ export class GetPreviewData {
options: Options
): Promise<GetPreviewDataResponse> {
const timestampField = indicator.params.timestampField;
const filterQuery = getElastichsearchQueryOrThrow(indicator.params.filter);
const filterQuery = getElasticsearchQueryOrThrow(indicator.params.filter);
const getCustomMetricIndicatorAggregation = new GetCustomMetricIndicatorAggregation(indicator);
const result = await this.esClient.search({
index: indicator.params.index,
@ -310,7 +317,7 @@ export class GetPreviewData {
options: Options
): Promise<GetPreviewDataResponse> {
const timestampField = indicator.params.timestampField;
const filterQuery = getElastichsearchQueryOrThrow(indicator.params.filter);
const filterQuery = getElasticsearchQueryOrThrow(indicator.params.filter);
const getCustomMetricIndicatorAggregation = new GetTimesliceMetricIndicatorAggregation(
indicator
);
@ -349,9 +356,9 @@ export class GetPreviewData {
indicator: KQLCustomIndicator,
options: Options
): Promise<GetPreviewDataResponse> {
const filterQuery = getElastichsearchQueryOrThrow(indicator.params.filter);
const goodQuery = getElastichsearchQueryOrThrow(indicator.params.good);
const totalQuery = getElastichsearchQueryOrThrow(indicator.params.total);
const filterQuery = getElasticsearchQueryOrThrow(indicator.params.filter);
const goodQuery = getElasticsearchQueryOrThrow(indicator.params.good);
const totalQuery = getElasticsearchQueryOrThrow(indicator.params.total);
const timestampField = indicator.params.timestampField;
const result = await this.esClient.search({
index: indicator.params.index,
@ -429,7 +436,7 @@ export class GetPreviewData {
}
}
function getElastichsearchQueryOrThrow(kuery: string | undefined = '') {
function getElasticsearchQueryOrThrow(kuery: string | undefined = '') {
try {
return toElasticsearchQuery(fromKueryExpression(kuery));
} catch (err) {

View file

@ -433,8 +433,10 @@ Object {
"_numerator": Object {
"range": Object {
"field": "transaction.duration.histogram",
"keyed": true,
"ranges": Array [
Object {
"key": "target",
"to": 500000,
},
],
@ -457,7 +459,7 @@ Object {
"slo.numerator": Object {
"bucket_script": Object {
"buckets_path": Object {
"numerator": "_numerator['*-500000.0']>_count",
"numerator": "_numerator['target']>_count",
},
"script": "params.numerator",
},
@ -612,8 +614,10 @@ Object {
"_numerator": Object {
"range": Object {
"field": "transaction.duration.histogram",
"keyed": true,
"ranges": Array [
Object {
"key": "target",
"to": 500000,
},
],
@ -627,7 +631,7 @@ Object {
"slo.numerator": Object {
"bucket_script": Object {
"buckets_path": Object {
"numerator": "_numerator['*-500000.0']>_count",
"numerator": "_numerator['target']>_count",
},
"script": "params.numerator",
},

View file

@ -26,7 +26,7 @@ exports[`Histogram Transform Generator aggregates using the numerator equation 1
Object {
"bucket_script": Object {
"buckets_path": Object {
"value": "_good>total['0.0-100.0']>_count",
"value": "_good>total['target']>_count",
},
"script": "params.value",
},
@ -37,7 +37,7 @@ exports[`Histogram Transform Generator aggregates using the numerator equation w
Object {
"bucket_script": Object {
"buckets_path": Object {
"value": "_good>total['0.0-100.0']>_count",
"value": "_good>total['target']>_count",
},
"script": "params.value",
},
@ -96,6 +96,7 @@ Object {
"ranges": Array [
Object {
"from": 0,
"key": "target",
"to": 100,
},
],
@ -138,7 +139,7 @@ Object {
"slo.numerator": Object {
"bucket_script": Object {
"buckets_path": Object {
"value": "_good>total['0.0-100.0']>_count",
"value": "_good>total['target']>_count",
},
"script": "params.value",
},
@ -257,6 +258,7 @@ Object {
"ranges": Array [
Object {
"from": 0,
"key": "target",
"to": 100,
},
],
@ -290,7 +292,7 @@ Object {
"slo.numerator": Object {
"bucket_script": Object {
"buckets_path": Object {
"value": "_good>total['0.0-100.0']>_count",
"value": "_good>total['target']>_count",
},
"script": "params.value",
},

View file

@ -11,6 +11,7 @@ import {
apmTransactionDurationIndicatorSchema,
timeslicesBudgetingMethodSchema,
} from '@kbn/slo-schema';
import { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { getElastichsearchQueryOrThrow, TransformGenerator } from '.';
import {
getSLOTransformId,
@ -137,7 +138,10 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator
};
}
private buildAggregations(slo: SLO, indicator: APMTransactionDurationIndicator) {
private buildAggregations(
slo: SLO,
indicator: APMTransactionDurationIndicator
): Record<string, AggregationsAggregationContainer> {
// threshold is in ms (milliseconds), but apm data is stored in us (microseconds)
const truncatedThreshold = Math.trunc(indicator.params.threshold * 1000);
@ -145,9 +149,11 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator
_numerator: {
range: {
field: 'transaction.duration.histogram',
keyed: true,
ranges: [
{
to: truncatedThreshold,
key: 'target',
},
],
},
@ -155,7 +161,7 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator
'slo.numerator': {
bucket_script: {
buckets_path: {
numerator: `_numerator['*-${truncatedThreshold}.0']>_count`,
numerator: `_numerator['target']>_count`,
},
script: 'params.numerator',
},

View file

@ -8,6 +8,8 @@ import { reject } from 'lodash';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
import { ESSearchResponse } from '@kbn/es-types';
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
export function isUndefinedOrNull(value: any): value is undefined | null {
return value === undefined || value === null;
@ -68,3 +70,13 @@ export function kqlQuery(kql?: string): estypes.QueryDslQueryContainer[] {
const ast = fromKueryExpression(kql);
return [toElasticsearchQuery(ast)];
}
export async function typedSearch<
DocumentSource extends unknown,
TParams extends estypes.SearchRequest
>(
esClient: ElasticsearchClient,
params: TParams
): Promise<ESSearchResponse<DocumentSource, TParams>> {
return (await esClient.search(params)) as unknown as ESSearchResponse<DocumentSource, TParams>;
}

View file

@ -96,7 +96,8 @@
"@kbn/lens-embeddable-utils",
"@kbn/serverless",
"@kbn/dashboard-plugin",
"@kbn/calculate-auto"
"@kbn/calculate-auto",
"@kbn/es-types"
],
"exclude": [
"target/**/*"