mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[SLO] Add indicator to support histogram fields (#161582)
## Summary
This PR add a new indicator to support histogram fields. This will allow
you to either use a `range` aggregation or `value_count` aggregation for
the good and total events; including support for filtering with KQL on
both event types. When using a `range` aggregation, both the `from` and
`to` thresholds are required for the range and events will be to total
number of events within that range.[ Keep in mind, with the `range`
aggregation, the range includes the `from` value and excludes the `to`
value.](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-range-aggregation.html)
This PR also includes support for using the histogram field for a
"Custom Metric" indicator, `sum` is calculated on the values and not the
counts. If you need it calculated on the counts then you have to use the
histogram indicator.
<img width="776" alt="image"
src="1d46b722
-df13-417e-bf3b-b3c450933da2">
---------
Co-authored-by: Kevin Delemme <kdelemme@gmail.com>
This commit is contained in:
parent
a16f9482e3
commit
06f7cbf9b6
30 changed files with 1924 additions and 286 deletions
|
@ -15,6 +15,7 @@ import {
|
|||
indicatorTypesSchema,
|
||||
kqlCustomIndicatorSchema,
|
||||
metricCustomIndicatorSchema,
|
||||
histogramIndicatorSchema,
|
||||
objectiveSchema,
|
||||
optionalSettingsSchema,
|
||||
previewDataSchema,
|
||||
|
@ -200,6 +201,7 @@ type Indicator = t.OutputOf<typeof indicatorSchema>;
|
|||
type APMTransactionErrorRateIndicator = t.OutputOf<typeof apmTransactionErrorRateIndicatorSchema>;
|
||||
type APMTransactionDurationIndicator = t.OutputOf<typeof apmTransactionDurationIndicatorSchema>;
|
||||
type MetricCustomIndicator = t.OutputOf<typeof metricCustomIndicatorSchema>;
|
||||
type HistogramIndicator = t.OutputOf<typeof histogramIndicatorSchema>;
|
||||
type KQLCustomIndicator = t.OutputOf<typeof kqlCustomIndicatorSchema>;
|
||||
|
||||
export {
|
||||
|
@ -247,6 +249,7 @@ export type {
|
|||
IndicatorType,
|
||||
Indicator,
|
||||
MetricCustomIndicator,
|
||||
HistogramIndicator,
|
||||
KQLCustomIndicator,
|
||||
TimeWindow,
|
||||
};
|
||||
|
|
|
@ -85,6 +85,47 @@ const metricCustomIndicatorSchema = t.type({
|
|||
}),
|
||||
});
|
||||
|
||||
const rangeHistogramMetricType = t.literal('range');
|
||||
const rangeBasedHistogramMetricDef = t.intersection([
|
||||
t.type({
|
||||
field: t.string,
|
||||
aggregation: rangeHistogramMetricType,
|
||||
from: t.number,
|
||||
to: t.number,
|
||||
}),
|
||||
t.partial({
|
||||
filter: t.string,
|
||||
}),
|
||||
]);
|
||||
|
||||
const valueCountHistogramMetricType = t.literal('value_count');
|
||||
const valueCountBasedHistogramMetricDef = t.intersection([
|
||||
t.type({
|
||||
field: t.string,
|
||||
aggregation: valueCountHistogramMetricType,
|
||||
}),
|
||||
t.partial({
|
||||
filter: t.string,
|
||||
}),
|
||||
]);
|
||||
|
||||
const histogramMetricDef = t.union([
|
||||
valueCountBasedHistogramMetricDef,
|
||||
rangeBasedHistogramMetricDef,
|
||||
]);
|
||||
|
||||
const histogramIndicatorTypeSchema = t.literal('sli.histogram.custom');
|
||||
const histogramIndicatorSchema = t.type({
|
||||
type: histogramIndicatorTypeSchema,
|
||||
params: t.type({
|
||||
index: t.string,
|
||||
timestampField: t.string,
|
||||
filter: t.string,
|
||||
good: histogramMetricDef,
|
||||
total: histogramMetricDef,
|
||||
}),
|
||||
});
|
||||
|
||||
const indicatorDataSchema = t.type({
|
||||
dateRange: dateRangeSchema,
|
||||
good: t.number,
|
||||
|
@ -96,6 +137,7 @@ const indicatorTypesSchema = t.union([
|
|||
apmTransactionErrorRateIndicatorTypeSchema,
|
||||
kqlCustomIndicatorTypeSchema,
|
||||
metricCustomIndicatorTypeSchema,
|
||||
histogramIndicatorTypeSchema,
|
||||
]);
|
||||
|
||||
// Validate that a string is a comma separated list of indicator types,
|
||||
|
@ -122,6 +164,7 @@ const indicatorSchema = t.union([
|
|||
apmTransactionErrorRateIndicatorSchema,
|
||||
kqlCustomIndicatorSchema,
|
||||
metricCustomIndicatorSchema,
|
||||
histogramIndicatorSchema,
|
||||
]);
|
||||
|
||||
export {
|
||||
|
@ -133,6 +176,8 @@ export {
|
|||
kqlCustomIndicatorTypeSchema,
|
||||
metricCustomIndicatorTypeSchema,
|
||||
metricCustomIndicatorSchema,
|
||||
histogramIndicatorTypeSchema,
|
||||
histogramIndicatorSchema,
|
||||
indicatorSchema,
|
||||
indicatorTypesArraySchema,
|
||||
indicatorTypesSchema,
|
||||
|
|
|
@ -8,14 +8,17 @@ info:
|
|||
license:
|
||||
name: Elastic License 2.0
|
||||
url: https://www.elastic.co/licensing/elastic-license
|
||||
servers:
|
||||
- url: http://localhost:5601
|
||||
description: local
|
||||
security:
|
||||
- basicAuth: []
|
||||
- apiKeyAuth: []
|
||||
tags:
|
||||
- name: slo
|
||||
description: SLO APIs enable you to define, manage and track service-level objectives
|
||||
- name: composite slo
|
||||
description: Composite SLO APIs enable you to define, manage and track a group of SLOs.
|
||||
servers:
|
||||
- url: http://localhost:5601
|
||||
description: local
|
||||
paths:
|
||||
/s/{spaceId}/api/observability/slos:
|
||||
post:
|
||||
|
@ -697,6 +700,103 @@ components:
|
|||
description: The type of indicator.
|
||||
type: string
|
||||
example: sli.metric.custom
|
||||
indicator_properties_histogram:
|
||||
title: Histogram indicator
|
||||
required:
|
||||
- type
|
||||
- params
|
||||
description: Defines properties for a histogram indicator type
|
||||
type: object
|
||||
properties:
|
||||
params:
|
||||
description: An object containing the indicator parameters.
|
||||
type: object
|
||||
nullable: false
|
||||
required:
|
||||
- index
|
||||
- timestampField
|
||||
- good
|
||||
- total
|
||||
properties:
|
||||
index:
|
||||
description: The index or index pattern to use
|
||||
type: string
|
||||
example: my-service-*
|
||||
filter:
|
||||
description: the KQL query to filter the documents with.
|
||||
type: string
|
||||
example: 'field.environment : "production" and service.name : "my-service"'
|
||||
timestampField:
|
||||
description: |
|
||||
The timestamp field used in the source indice.
|
||||
type: string
|
||||
example: timestamp
|
||||
good:
|
||||
description: |
|
||||
An object defining the "good" events
|
||||
type: object
|
||||
required:
|
||||
- aggregation
|
||||
- field
|
||||
properties:
|
||||
field:
|
||||
description: The field use to aggregate the good events.
|
||||
type: string
|
||||
example: processor.latency
|
||||
aggregation:
|
||||
description: The type of aggregation to use.
|
||||
type: string
|
||||
example: value_count
|
||||
enum:
|
||||
- value_count
|
||||
- range
|
||||
filter:
|
||||
description: The filter for good events.
|
||||
type: string
|
||||
example: 'processor.outcome: "success"'
|
||||
from:
|
||||
description: The starting value of the range. Only required for "range" aggregations.
|
||||
type: number
|
||||
example: 0
|
||||
to:
|
||||
description: The ending value of the range. Only required for "range" aggregations.
|
||||
type: number
|
||||
example: 100
|
||||
total:
|
||||
description: |
|
||||
An object defining the "total" events
|
||||
type: object
|
||||
required:
|
||||
- aggregation
|
||||
- field
|
||||
properties:
|
||||
field:
|
||||
description: The field use to aggregate the good events.
|
||||
type: string
|
||||
example: processor.latency
|
||||
aggregation:
|
||||
description: The type of aggregation to use.
|
||||
type: string
|
||||
example: value_count
|
||||
enum:
|
||||
- value_count
|
||||
- range
|
||||
filter:
|
||||
description: The filter for total events.
|
||||
type: string
|
||||
example: 'processor.outcome : *'
|
||||
from:
|
||||
description: The starting value of the range. Only required for "range" aggregations.
|
||||
type: number
|
||||
example: 0
|
||||
to:
|
||||
description: The ending value of the range. Only required for "range" aggregations.
|
||||
type: number
|
||||
example: 100
|
||||
type:
|
||||
description: The type of indicator.
|
||||
type: string
|
||||
example: sli.histogram.custom
|
||||
time_window:
|
||||
title: Time window
|
||||
required:
|
||||
|
@ -816,6 +916,7 @@ components:
|
|||
- $ref: '#/components/schemas/indicator_properties_apm_availability'
|
||||
- $ref: '#/components/schemas/indicator_properties_apm_latency'
|
||||
- $ref: '#/components/schemas/indicator_properties_custom_metric'
|
||||
- $ref: '#/components/schemas/indicator_properties_histogram'
|
||||
timeWindow:
|
||||
$ref: '#/components/schemas/time_window'
|
||||
budgetingMethod:
|
||||
|
@ -958,6 +1059,7 @@ components:
|
|||
- $ref: '#/components/schemas/indicator_properties_apm_availability'
|
||||
- $ref: '#/components/schemas/indicator_properties_apm_latency'
|
||||
- $ref: '#/components/schemas/indicator_properties_custom_metric'
|
||||
- $ref: '#/components/schemas/indicator_properties_histogram'
|
||||
timeWindow:
|
||||
$ref: '#/components/schemas/time_window'
|
||||
budgetingMethod:
|
||||
|
@ -1010,6 +1112,7 @@ components:
|
|||
- $ref: '#/components/schemas/indicator_properties_apm_availability'
|
||||
- $ref: '#/components/schemas/indicator_properties_apm_latency'
|
||||
- $ref: '#/components/schemas/indicator_properties_custom_metric'
|
||||
- $ref: '#/components/schemas/indicator_properties_histogram'
|
||||
timeWindow:
|
||||
$ref: '#/components/schemas/time_window'
|
||||
budgetingMethod:
|
||||
|
@ -1054,6 +1157,3 @@ components:
|
|||
example: 0.9836
|
||||
errorBudget:
|
||||
$ref: '#/components/schemas/error_budget'
|
||||
security:
|
||||
- basicAuth: []
|
||||
- apiKeyAuth: []
|
||||
|
|
|
@ -26,6 +26,7 @@ properties:
|
|||
- $ref: "indicator_properties_apm_availability.yaml"
|
||||
- $ref: "indicator_properties_apm_latency.yaml"
|
||||
- $ref: "indicator_properties_custom_metric.yaml"
|
||||
- $ref: 'indicator_properties_histogram.yaml'
|
||||
timeWindow:
|
||||
$ref: "time_window.yaml"
|
||||
budgetingMethod:
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
title: Histogram indicator
|
||||
required:
|
||||
- type
|
||||
- params
|
||||
description: Defines properties for a histogram indicator type
|
||||
type: object
|
||||
properties:
|
||||
params:
|
||||
description: An object containing the indicator parameters.
|
||||
type: object
|
||||
nullable: false
|
||||
required:
|
||||
- index
|
||||
- timestampField
|
||||
- good
|
||||
- total
|
||||
properties:
|
||||
index:
|
||||
description: The index or index pattern to use
|
||||
type: string
|
||||
example: my-service-*
|
||||
filter:
|
||||
description: the KQL query to filter the documents with.
|
||||
type: string
|
||||
example: 'field.environment : "production" and service.name : "my-service"'
|
||||
timestampField:
|
||||
description: >
|
||||
The timestamp field used in the source indice.
|
||||
type: string
|
||||
example: timestamp
|
||||
good:
|
||||
description: >
|
||||
An object defining the "good" events
|
||||
type: object
|
||||
required:
|
||||
- aggregation
|
||||
- field
|
||||
properties:
|
||||
field:
|
||||
description: The field use to aggregate the good events.
|
||||
type: string
|
||||
example: processor.latency
|
||||
aggregation:
|
||||
description: The type of aggregation to use.
|
||||
type: string
|
||||
example: value_count
|
||||
enum: [value_count, range]
|
||||
filter:
|
||||
description: The filter for good events.
|
||||
type: string
|
||||
example: "processor.outcome: \"success\""
|
||||
from:
|
||||
description: The starting value of the range. Only required for "range" aggregations.
|
||||
type: number
|
||||
example: 0
|
||||
to:
|
||||
description: The ending value of the range. Only required for "range" aggregations.
|
||||
type: number
|
||||
example: 100
|
||||
total:
|
||||
description: >
|
||||
An object defining the "total" events
|
||||
type: object
|
||||
required:
|
||||
- aggregation
|
||||
- field
|
||||
properties:
|
||||
field:
|
||||
description: The field use to aggregate the good events.
|
||||
type: string
|
||||
example: processor.latency
|
||||
aggregation:
|
||||
description: The type of aggregation to use.
|
||||
type: string
|
||||
example: value_count
|
||||
enum: [value_count, range]
|
||||
filter:
|
||||
description: The filter for total events.
|
||||
type: string
|
||||
example: "processor.outcome : *"
|
||||
from:
|
||||
description: The starting value of the range. Only required for "range" aggregations.
|
||||
type: number
|
||||
example: 0
|
||||
to:
|
||||
description: The ending value of the range. Only required for "range" aggregations.
|
||||
type: number
|
||||
example: 100
|
||||
type:
|
||||
description: The type of indicator.
|
||||
type: string
|
||||
example: sli.histogram.custom
|
|
@ -19,6 +19,7 @@ properties:
|
|||
- $ref: "indicator_properties_apm_availability.yaml"
|
||||
- $ref: "indicator_properties_apm_latency.yaml"
|
||||
- $ref: "indicator_properties_custom_metric.yaml"
|
||||
- $ref: "indicator_properties_histogram.yaml"
|
||||
timeWindow:
|
||||
$ref: "time_window.yaml"
|
||||
budgetingMethod:
|
||||
|
|
|
@ -16,6 +16,7 @@ properties:
|
|||
- $ref: "indicator_properties_apm_availability.yaml"
|
||||
- $ref: "indicator_properties_apm_latency.yaml"
|
||||
- $ref: "indicator_properties_custom_metric.yaml"
|
||||
- $ref: "indicator_properties_histogram.yaml"
|
||||
timeWindow:
|
||||
$ref: "time_window.yaml"
|
||||
budgetingMethod:
|
||||
|
|
|
@ -16,20 +16,13 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import {
|
||||
Field,
|
||||
useFetchIndexPatternFields,
|
||||
} from '../../../../hooks/slo/use_fetch_index_pattern_fields';
|
||||
import { useFetchIndexPatternFields } from '../../../../hooks/slo/use_fetch_index_pattern_fields';
|
||||
import { createOptionsFromFields } from '../../helpers/create_options';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { DataPreviewChart } from '../common/data_preview_chart';
|
||||
import { QueryBuilder } from '../common/query_builder';
|
||||
import { IndexSelection } from '../custom_common/index_selection';
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function CustomKqlIndicatorTypeForm() {
|
||||
const { control, watch, getFieldState } = useFormContext<CreateSLOForm>();
|
||||
|
||||
|
@ -80,7 +73,7 @@ export function CustomKqlIndicatorTypeForm() {
|
|||
|
||||
field.onChange('');
|
||||
}}
|
||||
options={createOptions(timestampFields)}
|
||||
options={createOptionsFromFields(timestampFields)}
|
||||
selectedOptions={
|
||||
!!index &&
|
||||
!!field.value &&
|
||||
|
@ -186,9 +179,3 @@ export function CustomKqlIndicatorTypeForm() {
|
|||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function createOptions(fields: Field[]): Option[] {
|
||||
return fields
|
||||
.map((field) => ({ label: field.name, value: field.name }))
|
||||
.sort((a, b) => String(a.label).localeCompare(b.label));
|
||||
}
|
||||
|
|
|
@ -17,10 +17,8 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import {
|
||||
Field,
|
||||
useFetchIndexPatternFields,
|
||||
} from '../../../../hooks/slo/use_fetch_index_pattern_fields';
|
||||
import { useFetchIndexPatternFields } from '../../../../hooks/slo/use_fetch_index_pattern_fields';
|
||||
import { createOptionsFromFields } from '../../helpers/create_options';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { DataPreviewChart } from '../common/data_preview_chart';
|
||||
import { QueryBuilder } from '../common/query_builder';
|
||||
|
@ -29,11 +27,6 @@ import { MetricIndicator } from './metric_indicator';
|
|||
|
||||
export { NEW_CUSTOM_METRIC } from './metric_indicator';
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function CustomMetricIndicatorTypeForm() {
|
||||
const { control, watch, getFieldState } = useFormContext<CreateSLOForm>();
|
||||
|
||||
|
@ -85,7 +78,7 @@ export function CustomMetricIndicatorTypeForm() {
|
|||
|
||||
field.onChange('');
|
||||
}}
|
||||
options={createOptions(timestampFields)}
|
||||
options={createOptionsFromFields(timestampFields)}
|
||||
selectedOptions={
|
||||
!!watch('indicator.params.index') &&
|
||||
!!field.value &&
|
||||
|
@ -226,9 +219,3 @@ export function CustomMetricIndicatorTypeForm() {
|
|||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function createOptions(fields: Field[]): Option[] {
|
||||
return fields
|
||||
.map((field) => ({ label: field.name, value: field.name }))
|
||||
.sort((a, b) => String(a.label).localeCompare(b.label));
|
||||
}
|
||||
|
|
|
@ -22,14 +22,10 @@ import { first, range, xor } from 'lodash';
|
|||
import React, { ReactNode } from 'react';
|
||||
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { Field } from '../../../../hooks/slo/use_fetch_index_pattern_fields';
|
||||
import { createOptionsFromFields } from '../../helpers/create_options';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { QueryBuilder } from '../common/query_builder';
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface MetricIndicatorProps {
|
||||
type: 'good' | 'total';
|
||||
indexFields: Field[] | undefined;
|
||||
|
@ -52,16 +48,12 @@ const validateEquation = (value: string) => {
|
|||
return result === null;
|
||||
};
|
||||
|
||||
function createOptions(fields: Field[]): Option[] {
|
||||
return fields
|
||||
.map((field) => ({ label: field.name, value: field.name }))
|
||||
.sort((a, b) => String(a.label).localeCompare(b.label));
|
||||
}
|
||||
|
||||
function createEquationFromMetric(names: string[]) {
|
||||
return names.join(' + ');
|
||||
}
|
||||
|
||||
const SUPPORTED_FIELD_TYPES = ['number', 'histogram'];
|
||||
|
||||
export function MetricIndicator({
|
||||
type,
|
||||
indexFields,
|
||||
|
@ -73,7 +65,9 @@ export function MetricIndicator({
|
|||
equationTooltip,
|
||||
}: MetricIndicatorProps) {
|
||||
const { control, watch, setValue, register } = useFormContext<CreateSLOForm>();
|
||||
const metricFields = (indexFields ?? []).filter((field) => field.type === 'number');
|
||||
const metricFields = (indexFields ?? []).filter((field) =>
|
||||
SUPPORTED_FIELD_TYPES.includes(field.type)
|
||||
);
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
|
@ -166,7 +160,7 @@ export function MetricIndicator({
|
|||
]
|
||||
: []
|
||||
}
|
||||
options={createOptions(metricFields)}
|
||||
options={createOptionsFromFields(metricFields)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,296 @@
|
|||
/*
|
||||
* 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 React, { Fragment } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiFieldNumber,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiIconTip,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { QueryBuilder } from '../common/query_builder';
|
||||
import { Field } from '../../../../hooks/slo/use_fetch_index_pattern_fields';
|
||||
import { createOptionsFromFields } from '../../helpers/create_options';
|
||||
|
||||
interface HistogramIndicatorProps {
|
||||
type: 'good' | 'total';
|
||||
indexFields: Field[] | undefined;
|
||||
isLoadingIndex: boolean;
|
||||
}
|
||||
|
||||
const AGGREGATIONS = {
|
||||
value_count: {
|
||||
value: 'value_count',
|
||||
label: i18n.translate('xpack.observability.slo.sloEdit.sliType.histogram.valueCountLabel', {
|
||||
defaultMessage: 'Value count',
|
||||
}),
|
||||
},
|
||||
range: {
|
||||
value: 'range',
|
||||
label: i18n.translate('xpack.observability.slo.sloEdit.sliType.histogram.rangeLabel', {
|
||||
defaultMessage: 'Range',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const AGGREGATION_OPTIONS = Object.values(AGGREGATIONS);
|
||||
|
||||
export function HistogramIndicator({ type, indexFields, isLoadingIndex }: HistogramIndicatorProps) {
|
||||
const { control, watch } = useFormContext<CreateSLOForm>();
|
||||
|
||||
const histogramFields = (indexFields ?? []).filter((field) => field.type === 'histogram');
|
||||
const indexPattern = watch('indicator.params.index');
|
||||
const aggregation = watch(`indicator.params.${type}.aggregation`);
|
||||
|
||||
const aggregationTooltip = (
|
||||
<EuiIconTip
|
||||
content={i18n.translate(
|
||||
'xpack.observability.slo.sloEdit.sliType.histogram.aggregationTooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'The "value count" aggreation will return the total count for the histogram field. Range will return the count from the histogram field that is within the range defined below.',
|
||||
}
|
||||
)}
|
||||
position="top"
|
||||
/>
|
||||
);
|
||||
|
||||
const fromTooltip = (
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.observability.slo.sloEdit.sliType.histogram.fromTooltip', {
|
||||
defaultMessage: 'The "from" value is inclusive.',
|
||||
})}
|
||||
position="top"
|
||||
/>
|
||||
);
|
||||
|
||||
const toTooltip = (
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.observability.slo.sloEdit.sliType.histogram.toTooltip', {
|
||||
defaultMessage: 'The "to" value is NOT inclusive.',
|
||||
})}
|
||||
position="top"
|
||||
/>
|
||||
);
|
||||
|
||||
const aggregationLabel = i18n.translate(
|
||||
'xpack.observability.slo.sloEdit.sliType.histogram.aggregationLabel',
|
||||
{ defaultMessage: 'Aggregation' }
|
||||
);
|
||||
|
||||
const metricLabel = i18n.translate(
|
||||
'xpack.observability.slo.sloEdit.sliType.histogram.metricLabel',
|
||||
{ defaultMessage: 'Field' }
|
||||
);
|
||||
|
||||
const toLabel = i18n.translate('xpack.observability.slo.sloEdit.sliType.histogram.toLabel', {
|
||||
defaultMessage: 'To',
|
||||
});
|
||||
|
||||
const fromLabel = i18n.translate('xpack.observability.slo.sloEdit.sliType.histogram.fromLabel', {
|
||||
defaultMessage: 'From',
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={
|
||||
<span>
|
||||
{aggregationLabel} {aggregationTooltip}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name={`indicator.params.${type}.aggregation`}
|
||||
rules={{ required: true }}
|
||||
control={control}
|
||||
render={({ field: { ref, ...field }, fieldState }) => (
|
||||
<EuiComboBox
|
||||
{...field}
|
||||
async
|
||||
fullWidth
|
||||
singleSelection={{ asPlainText: true }}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.observability.slo.sloEdit.sliType.histogram.aggregation.placeholder',
|
||||
{ defaultMessage: 'Select an aggregation' }
|
||||
)}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.observability.slo.sloEdit.sliType.histogram.aggregation.placeholder',
|
||||
{ defaultMessage: 'Select an aggregation' }
|
||||
)}
|
||||
isInvalid={fieldState.invalid}
|
||||
isDisabled={!indexPattern}
|
||||
isLoading={!!indexPattern && isLoadingIndex}
|
||||
onChange={(selected: EuiComboBoxOptionOption[]) => {
|
||||
if (selected.length) {
|
||||
return field.onChange(selected[0].value);
|
||||
}
|
||||
field.onChange('');
|
||||
}}
|
||||
selectedOptions={!!field.value ? [AGGREGATIONS[field.value]] : []}
|
||||
options={AGGREGATION_OPTIONS}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow fullWidth label={<span>{metricLabel}</span>}>
|
||||
<Controller
|
||||
name={`indicator.params.${type}.field`}
|
||||
defaultValue=""
|
||||
rules={{ required: true }}
|
||||
control={control}
|
||||
render={({ field: { ref, ...field }, fieldState }) => (
|
||||
<EuiComboBox
|
||||
{...field}
|
||||
async
|
||||
fullWidth
|
||||
singleSelection={{ asPlainText: true }}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.observability.slo.sloEdit.sliType.histogram.metricField.placeholder',
|
||||
{ defaultMessage: 'Select a histogram field' }
|
||||
)}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.observability.slo.sloEdit.sliType.histogram.metricField.placeholder',
|
||||
{ defaultMessage: 'Select a histogram field' }
|
||||
)}
|
||||
isInvalid={fieldState.invalid}
|
||||
isDisabled={!indexPattern}
|
||||
isLoading={!!indexPattern && isLoadingIndex}
|
||||
onChange={(selected: EuiComboBoxOptionOption[]) => {
|
||||
if (selected.length) {
|
||||
return field.onChange(selected[0].value);
|
||||
}
|
||||
field.onChange('');
|
||||
}}
|
||||
selectedOptions={
|
||||
!!indexPattern &&
|
||||
!!field.value &&
|
||||
histogramFields.some((histoField) => histoField.name === field.value)
|
||||
? [
|
||||
{
|
||||
value: field.value,
|
||||
label: field.value,
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
options={createOptionsFromFields(histogramFields)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
{aggregation === 'range' && (
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={
|
||||
<span>
|
||||
{fromLabel} {fromTooltip}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name={`indicator.params.${type}.from`}
|
||||
defaultValue={NaN}
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { ref, ...field }, fieldState }) => (
|
||||
<EuiFieldNumber
|
||||
{...field}
|
||||
required
|
||||
fullWidth
|
||||
isInvalid={fieldState.invalid}
|
||||
value={String(field.value)}
|
||||
data-test-subj="histogramRangeFrom"
|
||||
min={0}
|
||||
onChange={(event) => field.onChange(Number(event.target.value))}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={
|
||||
<span>
|
||||
{toLabel} {toTooltip}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name={`indicator.params.${type}.to`}
|
||||
defaultValue={NaN}
|
||||
rules={{ required: true }}
|
||||
control={control}
|
||||
render={({ field: { ref, ...field }, fieldState }) => (
|
||||
<EuiFieldNumber
|
||||
{...field}
|
||||
required
|
||||
fullWidth
|
||||
isInvalid={fieldState.invalid}
|
||||
value={String(field.value)}
|
||||
data-test-subj="histogramRangeTo"
|
||||
min={0}
|
||||
onChange={(event) => field.onChange(Number(event.target.value))}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
<QueryBuilder
|
||||
dataTestSubj={`histogramIndicatorForm${type}QueryInput`}
|
||||
indexPatternString={indexPattern}
|
||||
label={i18n.translate(
|
||||
'xpack.observability.slo.sloEdit.sliType.histogram.kqlFilterLabel',
|
||||
{
|
||||
defaultMessage: 'KQL filter',
|
||||
}
|
||||
)}
|
||||
name={`indicator.params.${type}.filter`}
|
||||
placeholder=""
|
||||
required={false}
|
||||
tooltip={
|
||||
<EuiIconTip
|
||||
content={i18n.translate(
|
||||
'xpack.observability.slo.sloEdit.sliType.histogram.query.tooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'This KQL query should return a subset of events for this indicator.',
|
||||
}
|
||||
)}
|
||||
position="top"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiIconTip,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useFetchIndexPatternFields } from '../../../../hooks/slo/use_fetch_index_pattern_fields';
|
||||
import { createOptionsFromFields } from '../../helpers/create_options';
|
||||
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 { HistogramIndicator } from './histogram_indicator';
|
||||
|
||||
export function HistogramIndicatorTypeForm() {
|
||||
const { control, watch, getFieldState } = useFormContext<CreateSLOForm>();
|
||||
|
||||
const index = watch('indicator.params.index');
|
||||
const { isLoading, data: indexFields } = useFetchIndexPatternFields(index);
|
||||
const timestampFields = (indexFields ?? []).filter((field) => field.type === 'date');
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexGroup direction="row" gutterSize="l">
|
||||
<EuiFlexItem>
|
||||
<IndexSelection />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.observability.slo.sloEdit.sliType.histogram.timestampField.label',
|
||||
{ defaultMessage: 'Timestamp field' }
|
||||
)}
|
||||
isInvalid={getFieldState('indicator.params.timestampField').invalid}
|
||||
>
|
||||
<Controller
|
||||
name="indicator.params.timestampField"
|
||||
defaultValue=""
|
||||
rules={{ required: true }}
|
||||
control={control}
|
||||
render={({ field: { ref, ...field }, fieldState }) => (
|
||||
<EuiComboBox
|
||||
{...field}
|
||||
async
|
||||
placeholder={i18n.translate(
|
||||
'xpack.observability.slo.sloEdit.sliType.histogram.timestampField.placeholder',
|
||||
{ defaultMessage: 'Select a timestamp field' }
|
||||
)}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.observability.slo.sloEdit.sliType.histogram.timestampField.placeholder',
|
||||
{ defaultMessage: 'Select a timestamp field' }
|
||||
)}
|
||||
data-test-subj="histogramIndicatorFormTimestampFieldSelect"
|
||||
isClearable
|
||||
isDisabled={!index}
|
||||
isInvalid={fieldState.invalid}
|
||||
isLoading={!!index && isLoading}
|
||||
onChange={(selected: EuiComboBoxOptionOption[]) => {
|
||||
if (selected.length) {
|
||||
return field.onChange(selected[0].value);
|
||||
}
|
||||
|
||||
field.onChange('');
|
||||
}}
|
||||
options={createOptionsFromFields(timestampFields)}
|
||||
selectedOptions={
|
||||
!!index &&
|
||||
!!field.value &&
|
||||
timestampFields.some((timestampField) => timestampField.name === field.value)
|
||||
? [{ value: field.value, label: field.value }]
|
||||
: []
|
||||
}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexItem>
|
||||
<QueryBuilder
|
||||
dataTestSubj="histogramIndicatorFormQueryFilterInput"
|
||||
indexPatternString={watch('indicator.params.index')}
|
||||
label={i18n.translate('xpack.observability.slo.sloEdit.sliType.histogram.queryFilter', {
|
||||
defaultMessage: 'Query filter',
|
||||
})}
|
||||
name="indicator.params.filter"
|
||||
placeholder={i18n.translate(
|
||||
'xpack.observability.slo.sloEdit.sliType.histogram.customFilter',
|
||||
{ defaultMessage: 'Custom filter to apply on the index' }
|
||||
)}
|
||||
tooltip={
|
||||
<EuiIconTip
|
||||
content={i18n.translate(
|
||||
'xpack.observability.slo.sloEdit.sliType.histogram.customFilter.tooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'This KQL query can be used to filter the documents with some relevant criteria.',
|
||||
}
|
||||
)}
|
||||
position="top"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.observability.slo.sloEdit.sliType.histogram.goodTitle"
|
||||
defaultMessage="Good events"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<HistogramIndicator type="good" indexFields={indexFields} isLoadingIndex={isLoading} />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.observability.slo.sloEdit.sliType.histogram.totalTitle"
|
||||
defaultMessage="Total events"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<HistogramIndicator type="total" indexFields={indexFields} isLoadingIndex={isLoading} />
|
||||
</EuiFlexItem>
|
||||
|
||||
<DataPreviewChart />
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -16,6 +16,7 @@ import { ApmAvailabilityIndicatorTypeForm } from './apm_availability/apm_availab
|
|||
import { ApmLatencyIndicatorTypeForm } from './apm_latency/apm_latency_indicator_type_form';
|
||||
import { CustomKqlIndicatorTypeForm } from './custom_kql/custom_kql_indicator_type_form';
|
||||
import { CustomMetricIndicatorTypeForm } from './custom_metric/custom_metric_type_form';
|
||||
import { HistogramIndicatorTypeForm } from './histogram/histogram_indicator_type_form';
|
||||
import { maxWidth } from './slo_edit_form';
|
||||
|
||||
interface SloEditFormIndicatorSectionProps {
|
||||
|
@ -36,6 +37,8 @@ export function SloEditFormIndicatorSection({ isEditMode }: SloEditFormIndicator
|
|||
return <ApmAvailabilityIndicatorTypeForm />;
|
||||
case 'sli.metric.custom':
|
||||
return <CustomMetricIndicatorTypeForm />;
|
||||
case 'sli.histogram.custom':
|
||||
return <HistogramIndicatorTypeForm />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
APMTransactionDurationIndicator,
|
||||
APMTransactionErrorRateIndicator,
|
||||
BudgetingMethod,
|
||||
HistogramIndicator,
|
||||
IndicatorType,
|
||||
KQLCustomIndicator,
|
||||
MetricCustomIndicator,
|
||||
|
@ -22,6 +23,7 @@ import {
|
|||
INDICATOR_APM_LATENCY,
|
||||
INDICATOR_CUSTOM_KQL,
|
||||
INDICATOR_CUSTOM_METRIC,
|
||||
INDICATOR_HISTOGRAM,
|
||||
} from '../../utils/slo/labels';
|
||||
import { CreateSLOForm } from './types';
|
||||
|
||||
|
@ -37,6 +39,10 @@ export const SLI_OPTIONS: Array<{
|
|||
value: 'sli.metric.custom',
|
||||
text: INDICATOR_CUSTOM_METRIC,
|
||||
},
|
||||
{
|
||||
value: 'sli.histogram.custom',
|
||||
text: INDICATOR_HISTOGRAM,
|
||||
},
|
||||
{
|
||||
value: 'sli.apm.transactionDuration',
|
||||
text: INDICATOR_APM_LATENCY,
|
||||
|
@ -118,6 +124,23 @@ export const CUSTOM_METRIC_DEFAULT_VALUES: MetricCustomIndicator = {
|
|||
},
|
||||
};
|
||||
|
||||
export const HISTOGRAM_DEFAULT_VALUES: HistogramIndicator = {
|
||||
type: 'sli.histogram.custom' as const,
|
||||
params: {
|
||||
index: '',
|
||||
timestampField: '',
|
||||
filter: '',
|
||||
good: {
|
||||
field: '',
|
||||
aggregation: 'value_count' as const,
|
||||
},
|
||||
total: {
|
||||
field: '',
|
||||
aggregation: 'value_count' as const,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const APM_LATENCY_DEFAULT_VALUES: APMTransactionDurationIndicator = {
|
||||
type: 'sli.apm.transactionDuration' as const,
|
||||
params: {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { Field } from '../../../hooks/slo/use_fetch_index_pattern_fields';
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function createOptionsFromFields(fields: Field[]): Option[] {
|
||||
return fields
|
||||
.map((field) => ({ label: field.name, value: field.name }))
|
||||
.sort((a, b) => String(a.label).localeCompare(b.label));
|
||||
}
|
|
@ -54,6 +54,48 @@ export function useSectionFormValidation({ getFieldState, getValues, formState,
|
|||
isGoodParamsValid() &&
|
||||
isTotalParamsValid();
|
||||
break;
|
||||
case 'sli.histogram.custom':
|
||||
const isRangeValid = (type: 'good' | 'total') => {
|
||||
const aggregation = getValues(`indicator.params.${type}.aggregation`);
|
||||
// If aggreagtion is a value count we can exit early with true
|
||||
if (aggregation === 'value_count') {
|
||||
return true;
|
||||
}
|
||||
const from = getValues(`indicator.params.${type}.from`);
|
||||
const to = getValues(`indicator.params.${type}.to`);
|
||||
// If both from and to are defined and from is less that to, return true
|
||||
if (from != null && to != null && from < to) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
isIndicatorSectionValid =
|
||||
(
|
||||
[
|
||||
'indicator.params.index',
|
||||
'indicator.params.filter',
|
||||
'indicator.params.timestampField',
|
||||
'indicator.params.good.aggregation',
|
||||
'indicator.params.total.aggregation',
|
||||
'indicator.params.good.field',
|
||||
'indicator.params.total.field',
|
||||
'indicator.params.good.filter',
|
||||
'indicator.params.total.filter',
|
||||
] as const
|
||||
).every((field) => !getFieldState(field).invalid) &&
|
||||
(
|
||||
[
|
||||
'indicator.params.good.aggregation',
|
||||
'indicator.params.total.aggregation',
|
||||
'indicator.params.good.field',
|
||||
'indicator.params.total.field',
|
||||
'indicator.params.index',
|
||||
'indicator.params.timestampField',
|
||||
] as const
|
||||
).every((field) => !!getValues(field)) &&
|
||||
isRangeValid('good') &&
|
||||
isRangeValid('total');
|
||||
break;
|
||||
case 'sli.kql.custom':
|
||||
isIndicatorSectionValid =
|
||||
(
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
APM_LATENCY_DEFAULT_VALUES,
|
||||
CUSTOM_KQL_DEFAULT_VALUES,
|
||||
CUSTOM_METRIC_DEFAULT_VALUES,
|
||||
HISTOGRAM_DEFAULT_VALUES,
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES,
|
||||
} from '../constants';
|
||||
import { CreateSLOForm } from '../types';
|
||||
|
@ -58,6 +59,16 @@ export function useUnregisterFields({ isEditMode }: { isEditMode: boolean }) {
|
|||
}
|
||||
);
|
||||
break;
|
||||
case 'sli.histogram.custom':
|
||||
reset(
|
||||
Object.assign({}, SLO_EDIT_FORM_DEFAULT_VALUES, {
|
||||
indicator: HISTOGRAM_DEFAULT_VALUES,
|
||||
}),
|
||||
{
|
||||
keepDefaultValues: true,
|
||||
}
|
||||
);
|
||||
break;
|
||||
case 'sli.apm.transactionDuration':
|
||||
reset(
|
||||
Object.assign({}, SLO_EDIT_FORM_DEFAULT_VALUES, {
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
INDICATOR_APM_LATENCY,
|
||||
INDICATOR_CUSTOM_KQL,
|
||||
INDICATOR_CUSTOM_METRIC,
|
||||
INDICATOR_HISTOGRAM,
|
||||
} from '../../../utils/slo/labels';
|
||||
|
||||
export interface SloListSearchFilterSortBarProps {
|
||||
|
@ -38,7 +39,8 @@ export type FilterType =
|
|||
| 'sli.apm.transactionDuration'
|
||||
| 'sli.apm.transactionErrorRate'
|
||||
| 'sli.kql.custom'
|
||||
| 'sli.metric.custom';
|
||||
| 'sli.metric.custom'
|
||||
| 'sli.histogram.custom';
|
||||
|
||||
export type Item<T> = EuiSelectableOption & {
|
||||
label: string;
|
||||
|
@ -79,6 +81,10 @@ const INDICATOR_TYPE_OPTIONS: Array<Item<FilterType>> = [
|
|||
label: INDICATOR_CUSTOM_METRIC,
|
||||
type: 'sli.metric.custom',
|
||||
},
|
||||
{
|
||||
label: INDICATOR_HISTOGRAM,
|
||||
type: 'sli.histogram.custom',
|
||||
},
|
||||
];
|
||||
|
||||
export function SloListSearchFilterSortBar({
|
||||
|
|
|
@ -21,6 +21,10 @@ export const INDICATOR_CUSTOM_METRIC = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const INDICATOR_HISTOGRAM = i18n.translate('xpack.observability.slo.indicators.histogram', {
|
||||
defaultMessage: 'Histogram Metric',
|
||||
});
|
||||
|
||||
export const INDICATOR_APM_LATENCY = i18n.translate(
|
||||
'xpack.observability.slo.indicators.apmLatency',
|
||||
{ defaultMessage: 'APM latency' }
|
||||
|
@ -47,8 +51,11 @@ export function toIndicatorTypeLabel(
|
|||
case 'sli.metric.custom':
|
||||
return INDICATOR_CUSTOM_METRIC;
|
||||
|
||||
case 'sli.histogram.custom':
|
||||
return INDICATOR_HISTOGRAM;
|
||||
|
||||
default:
|
||||
assertNever(indicatorType);
|
||||
assertNever(indicatorType as never);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
import {
|
||||
ApmTransactionDurationTransformGenerator,
|
||||
ApmTransactionErrorRateTransformGenerator,
|
||||
HistogramTransformGenerator,
|
||||
KQLCustomTransformGenerator,
|
||||
MetricCustomTransformGenerator,
|
||||
TransformGenerator,
|
||||
|
@ -51,6 +52,7 @@ const transformGenerators: Record<IndicatorTypes, TransformGenerator> = {
|
|||
'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator(),
|
||||
'sli.kql.custom': new KQLCustomTransformGenerator(),
|
||||
'sli.metric.custom': new MetricCustomTransformGenerator(),
|
||||
'sli.histogram.custom': new HistogramTransformGenerator(),
|
||||
};
|
||||
|
||||
const isLicenseAtLeastPlatinum = async (context: ObservabilityRequestHandlerContext) => {
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GetHistogramIndicatorAggregation should generate a aggregation for good events 1`] = `
|
||||
Object {
|
||||
"_good": Object {
|
||||
"aggs": Object {
|
||||
"total": Object {
|
||||
"range": Object {
|
||||
"field": "latency",
|
||||
"keyed": true,
|
||||
"ranges": Array [
|
||||
Object {
|
||||
"from": 0,
|
||||
"to": 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"filter": Object {
|
||||
"match_all": Object {},
|
||||
},
|
||||
},
|
||||
"goodEvents": Object {
|
||||
"bucket_script": Object {
|
||||
"buckets_path": Object {
|
||||
"value": "_good>total['0.0-100.0']>_count",
|
||||
},
|
||||
"script": "params.value",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`GetHistogramIndicatorAggregation should generate a aggregation for total events 1`] = `
|
||||
Object {
|
||||
"_total": Object {
|
||||
"aggs": Object {
|
||||
"total": Object {
|
||||
"value_count": Object {
|
||||
"field": "latency",
|
||||
},
|
||||
},
|
||||
},
|
||||
"filter": Object {
|
||||
"match_all": Object {},
|
||||
},
|
||||
},
|
||||
"totalEvents": Object {
|
||||
"bucket_script": Object {
|
||||
"buckets_path": Object {
|
||||
"value": "_total>total",
|
||||
},
|
||||
"script": "params.value",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { createHistogramIndicator } from '../fixtures/slo';
|
||||
import { GetHistogramIndicatorAggregation } from './get_histogram_indicator_aggregation';
|
||||
|
||||
describe('GetHistogramIndicatorAggregation', () => {
|
||||
it('should generate a aggregation for good events', () => {
|
||||
const getHistogramIndicatorAggregations = new GetHistogramIndicatorAggregation(
|
||||
createHistogramIndicator()
|
||||
);
|
||||
expect(
|
||||
getHistogramIndicatorAggregations.execute({ type: 'good', aggregationKey: 'goodEvents' })
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate a aggregation for total events', () => {
|
||||
const getHistogramIndicatorAggregations = new GetHistogramIndicatorAggregation(
|
||||
createHistogramIndicator()
|
||||
);
|
||||
expect(
|
||||
getHistogramIndicatorAggregations.execute({ type: 'total', aggregationKey: 'totalEvents' })
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should throw and error when the "from" is greater than "to"', () => {
|
||||
const getHistogramIndicatorAggregations = new GetHistogramIndicatorAggregation(
|
||||
createHistogramIndicator({
|
||||
good: {
|
||||
field: 'latency',
|
||||
aggregation: 'range',
|
||||
from: 100,
|
||||
to: 0,
|
||||
filter: '',
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(() =>
|
||||
getHistogramIndicatorAggregations.execute({ type: 'good', aggregationKey: 'goodEvents' })
|
||||
).toThrow('Invalid Range: "from" should be less that "to".');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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 { HistogramIndicator } from '@kbn/slo-schema';
|
||||
import { getElastichsearchQueryOrThrow } from '../transform_generators/common';
|
||||
|
||||
type HistogramIndicatorDef =
|
||||
| HistogramIndicator['params']['good']
|
||||
| HistogramIndicator['params']['total'];
|
||||
|
||||
export class GetHistogramIndicatorAggregation {
|
||||
constructor(private indicator: HistogramIndicator) {}
|
||||
|
||||
private buildAggregation(type: 'good' | 'total', indicator: HistogramIndicatorDef) {
|
||||
const filter = indicator.filter
|
||||
? getElastichsearchQueryOrThrow(indicator.filter)
|
||||
: { match_all: {} };
|
||||
if (indicator.aggregation === 'value_count') {
|
||||
return {
|
||||
filter,
|
||||
aggs: {
|
||||
total: {
|
||||
value_count: { field: indicator.field },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (indicator.aggregation === 'range' && (indicator.from == null || indicator.to == null)) {
|
||||
throw new Error('Invalid Range: both "from" or "to" are required for a range aggregation.');
|
||||
}
|
||||
|
||||
if (
|
||||
indicator.aggregation === 'range' &&
|
||||
indicator.from != null &&
|
||||
indicator.to != null &&
|
||||
indicator.from >= indicator.to
|
||||
) {
|
||||
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: {
|
||||
total: {
|
||||
range: {
|
||||
field: indicator.field,
|
||||
keyed: true,
|
||||
ranges: [range],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
if (indicator.aggregation === 'value_count') {
|
||||
return {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
value: `_${type}>total`,
|
||||
},
|
||||
script: 'params.value',
|
||||
},
|
||||
};
|
||||
}
|
||||
const rangeKey = this.buildRangeKey(indicator.from, indicator.to);
|
||||
return {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
value: `_${type}>total['${rangeKey}']>_count`,
|
||||
},
|
||||
script: 'params.value',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public execute({ type, aggregationKey }: { type: 'good' | 'total'; aggregationKey: string }) {
|
||||
const indicatorDef = this.indicator.params[type];
|
||||
return {
|
||||
[`_${type}`]: this.buildAggregation(type, indicatorDef),
|
||||
[aggregationKey]: this.buildBucketScript(type, indicatorDef),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -5,4 +5,5 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './get_histogram_indicator_aggregation';
|
||||
export * from './get_custom_metric_indicator_aggregation';
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { cloneDeep } from 'lodash';
|
||||
import { v1 as uuidv1 } from 'uuid';
|
||||
import { SavedObject } from '@kbn/core-saved-objects-server';
|
||||
import { sloSchema, CreateSLOParams } from '@kbn/slo-schema';
|
||||
import { sloSchema, CreateSLOParams, HistogramIndicator } from '@kbn/slo-schema';
|
||||
|
||||
import { SO_SLO_TYPE } from '../../../saved_objects';
|
||||
import {
|
||||
|
@ -92,6 +92,30 @@ export const createMetricCustomIndicator = (
|
|||
},
|
||||
});
|
||||
|
||||
export const createHistogramIndicator = (
|
||||
params: Partial<HistogramIndicator['params']> = {}
|
||||
): HistogramIndicator => ({
|
||||
type: 'sli.histogram.custom',
|
||||
params: {
|
||||
index: 'my-index*',
|
||||
filter: 'labels.groupId: group-3',
|
||||
good: {
|
||||
field: 'latency',
|
||||
aggregation: 'range',
|
||||
from: 0,
|
||||
to: 100,
|
||||
filter: '',
|
||||
},
|
||||
total: {
|
||||
field: 'latency',
|
||||
aggregation: 'value_count',
|
||||
filter: '',
|
||||
},
|
||||
timestampField: 'log_timestamp',
|
||||
...params,
|
||||
},
|
||||
});
|
||||
|
||||
const defaultSLO: Omit<SLO, 'id' | 'revision' | 'createdAt' | 'updatedAt'> = {
|
||||
name: 'irrelevant',
|
||||
description: 'irrelevant',
|
||||
|
|
|
@ -7,250 +7,311 @@
|
|||
|
||||
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
|
||||
import { ALL_VALUE, GetPreviewDataParams, GetPreviewDataResponse } from '@kbn/slo-schema';
|
||||
import {
|
||||
ALL_VALUE,
|
||||
APMTransactionErrorRateIndicator,
|
||||
GetPreviewDataParams,
|
||||
GetPreviewDataResponse,
|
||||
HistogramIndicator,
|
||||
KQLCustomIndicator,
|
||||
MetricCustomIndicator,
|
||||
} from '@kbn/slo-schema';
|
||||
import { APMTransactionDurationIndicator } from '../../domain/models';
|
||||
import { computeSLI } from '../../domain/services';
|
||||
import { InvalidQueryError } from '../../errors';
|
||||
import { GetCustomMetricIndicatorAggregation } from './aggregations';
|
||||
import {
|
||||
GetHistogramIndicatorAggregation,
|
||||
GetCustomMetricIndicatorAggregation,
|
||||
} from './aggregations';
|
||||
|
||||
export class GetPreviewData {
|
||||
constructor(private esClient: ElasticsearchClient) {}
|
||||
|
||||
private async getAPMTransactionDurationPreviewData(
|
||||
indicator: APMTransactionDurationIndicator
|
||||
): Promise<GetPreviewDataResponse> {
|
||||
const filter = [];
|
||||
if (indicator.params.service !== ALL_VALUE)
|
||||
filter.push({
|
||||
match: { 'service.name': indicator.params.service },
|
||||
});
|
||||
if (indicator.params.environment !== ALL_VALUE)
|
||||
filter.push({
|
||||
match: { 'service.environment': indicator.params.environment },
|
||||
});
|
||||
if (indicator.params.transactionName !== ALL_VALUE)
|
||||
filter.push({
|
||||
match: { 'transaction.name': indicator.params.transactionName },
|
||||
});
|
||||
if (indicator.params.transactionType !== ALL_VALUE)
|
||||
filter.push({
|
||||
match: { 'transaction.type': indicator.params.transactionType },
|
||||
});
|
||||
if (!!indicator.params.filter)
|
||||
filter.push(getElastichsearchQueryOrThrow(indicator.params.filter));
|
||||
|
||||
const truncatedThreshold = Math.trunc(indicator.params.threshold * 1000);
|
||||
|
||||
const result = await this.esClient.search({
|
||||
index: indicator.params.index,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ range: { '@timestamp': { gte: 'now-60m' } } },
|
||||
{ terms: { 'processor.event': ['metric'] } },
|
||||
{ term: { 'metricset.name': 'transaction' } },
|
||||
{ exists: { field: 'transaction.duration.histogram' } },
|
||||
...filter,
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
perMinute: {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: '1m',
|
||||
},
|
||||
aggs: {
|
||||
_good: {
|
||||
range: {
|
||||
field: 'transaction.duration.histogram',
|
||||
ranges: [{ to: truncatedThreshold }],
|
||||
},
|
||||
},
|
||||
good: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
_good: `_good['*-${truncatedThreshold}.0']>_count`,
|
||||
},
|
||||
script: 'params._good',
|
||||
},
|
||||
},
|
||||
total: {
|
||||
value_count: {
|
||||
field: 'transaction.duration.histogram',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// @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,
|
||||
}));
|
||||
}
|
||||
|
||||
private async getAPMTranscationErrorPreviewData(
|
||||
indicator: APMTransactionErrorRateIndicator
|
||||
): Promise<GetPreviewDataResponse> {
|
||||
const filter = [];
|
||||
if (indicator.params.service !== ALL_VALUE)
|
||||
filter.push({
|
||||
match: { 'service.name': indicator.params.service },
|
||||
});
|
||||
if (indicator.params.environment !== ALL_VALUE)
|
||||
filter.push({
|
||||
match: { 'service.environment': indicator.params.environment },
|
||||
});
|
||||
if (indicator.params.transactionName !== ALL_VALUE)
|
||||
filter.push({
|
||||
match: { 'transaction.name': indicator.params.transactionName },
|
||||
});
|
||||
if (indicator.params.transactionType !== ALL_VALUE)
|
||||
filter.push({
|
||||
match: { 'transaction.type': indicator.params.transactionType },
|
||||
});
|
||||
if (!!indicator.params.filter)
|
||||
filter.push(getElastichsearchQueryOrThrow(indicator.params.filter));
|
||||
|
||||
const result = await this.esClient.search({
|
||||
index: indicator.params.index,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ range: { '@timestamp': { gte: 'now-60m' } } },
|
||||
{ terms: { 'processor.event': ['metric'] } },
|
||||
{ term: { 'metricset.name': 'transaction' } },
|
||||
{ exists: { field: 'transaction.duration.histogram' } },
|
||||
{ exists: { field: 'transaction.result' } },
|
||||
...filter,
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
perMinute: {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: '1m',
|
||||
},
|
||||
aggs: {
|
||||
good: {
|
||||
filter: {
|
||||
bool: {
|
||||
should: {
|
||||
match: {
|
||||
'event.outcome': 'success',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
total: {
|
||||
value_count: {
|
||||
field: 'transaction.duration.histogram',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// @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.doc_count, bucket.total.value)
|
||||
: null,
|
||||
}));
|
||||
}
|
||||
|
||||
private async getHistogramPreviewData(
|
||||
indicator: HistogramIndicator
|
||||
): Promise<GetPreviewDataResponse> {
|
||||
const getHistogramIndicatorAggregations = new GetHistogramIndicatorAggregation(indicator);
|
||||
const filterQuery = getElastichsearchQueryOrThrow(indicator.params.filter);
|
||||
const timestampField = indicator.params.timestampField;
|
||||
const options = {
|
||||
index: indicator.params.index,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [{ range: { [timestampField]: { gte: 'now-60m' } } }, filterQuery],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
perMinute: {
|
||||
date_histogram: {
|
||||
field: timestampField,
|
||||
fixed_interval: '1m',
|
||||
},
|
||||
aggs: {
|
||||
...getHistogramIndicatorAggregations.execute({
|
||||
type: 'good',
|
||||
aggregationKey: 'good',
|
||||
}),
|
||||
...getHistogramIndicatorAggregations.execute({
|
||||
type: 'total',
|
||||
aggregationKey: 'total',
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = await this.esClient.search(options);
|
||||
|
||||
// @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,
|
||||
}));
|
||||
}
|
||||
|
||||
private async getCustomMetricPreviewData(
|
||||
indicator: MetricCustomIndicator
|
||||
): Promise<GetPreviewDataResponse> {
|
||||
const timestampField = indicator.params.timestampField;
|
||||
const filterQuery = getElastichsearchQueryOrThrow(indicator.params.filter);
|
||||
const getCustomMetricIndicatorAggregation = new GetCustomMetricIndicatorAggregation(indicator);
|
||||
const result = await this.esClient.search({
|
||||
index: 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,
|
||||
}));
|
||||
}
|
||||
|
||||
private async getCustomKQLPreviewData(
|
||||
indicator: KQLCustomIndicator
|
||||
): Promise<GetPreviewDataResponse> {
|
||||
const filterQuery = getElastichsearchQueryOrThrow(indicator.params.filter);
|
||||
const goodQuery = getElastichsearchQueryOrThrow(indicator.params.good);
|
||||
const totalQuery = getElastichsearchQueryOrThrow(indicator.params.total);
|
||||
const timestampField = indicator.params.timestampField;
|
||||
const result = await this.esClient.search({
|
||||
index: indicator.params.index,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [{ range: { [timestampField]: { gte: 'now-60m' } } }, filterQuery],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
perMinute: {
|
||||
date_histogram: {
|
||||
field: timestampField,
|
||||
fixed_interval: '1m',
|
||||
},
|
||||
aggs: {
|
||||
good: { filter: goodQuery },
|
||||
total: { filter: totalQuery },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// @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.doc_count, bucket.total.doc_count)
|
||||
: null,
|
||||
}));
|
||||
}
|
||||
|
||||
public async execute(params: GetPreviewDataParams): Promise<GetPreviewDataResponse> {
|
||||
switch (params.indicator.type) {
|
||||
case 'sli.apm.transactionDuration':
|
||||
try {
|
||||
const filter = [];
|
||||
if (params.indicator.params.service !== ALL_VALUE)
|
||||
filter.push({
|
||||
match: { 'service.name': params.indicator.params.service },
|
||||
});
|
||||
if (params.indicator.params.environment !== ALL_VALUE)
|
||||
filter.push({
|
||||
match: { 'service.environment': params.indicator.params.environment },
|
||||
});
|
||||
if (params.indicator.params.transactionName !== ALL_VALUE)
|
||||
filter.push({
|
||||
match: { 'transaction.name': params.indicator.params.transactionName },
|
||||
});
|
||||
if (params.indicator.params.transactionType !== ALL_VALUE)
|
||||
filter.push({
|
||||
match: { 'transaction.type': params.indicator.params.transactionType },
|
||||
});
|
||||
if (!!params.indicator.params.filter)
|
||||
filter.push(getElastichsearchQueryOrThrow(params.indicator.params.filter));
|
||||
|
||||
const truncatedThreshold = Math.trunc(params.indicator.params.threshold * 1000);
|
||||
|
||||
const result = await this.esClient.search({
|
||||
index: params.indicator.params.index,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ range: { '@timestamp': { gte: 'now-60m' } } },
|
||||
{ terms: { 'processor.event': ['metric'] } },
|
||||
{ term: { 'metricset.name': 'transaction' } },
|
||||
{ exists: { field: 'transaction.duration.histogram' } },
|
||||
...filter,
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
perMinute: {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: '1m',
|
||||
},
|
||||
aggs: {
|
||||
_good: {
|
||||
range: {
|
||||
field: 'transaction.duration.histogram',
|
||||
ranges: [{ to: truncatedThreshold }],
|
||||
},
|
||||
},
|
||||
good: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
_good: `_good['*-${truncatedThreshold}.0']>_count`,
|
||||
},
|
||||
script: 'params._good',
|
||||
},
|
||||
},
|
||||
total: {
|
||||
value_count: {
|
||||
field: 'transaction.duration.histogram',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// @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,
|
||||
}));
|
||||
} catch (err) {
|
||||
throw new InvalidQueryError(`Invalid ES query`);
|
||||
}
|
||||
return this.getAPMTransactionDurationPreviewData(params.indicator);
|
||||
case 'sli.apm.transactionErrorRate':
|
||||
try {
|
||||
const filter = [];
|
||||
if (params.indicator.params.service !== ALL_VALUE)
|
||||
filter.push({
|
||||
match: { 'service.name': params.indicator.params.service },
|
||||
});
|
||||
if (params.indicator.params.environment !== ALL_VALUE)
|
||||
filter.push({
|
||||
match: { 'service.environment': params.indicator.params.environment },
|
||||
});
|
||||
if (params.indicator.params.transactionName !== ALL_VALUE)
|
||||
filter.push({
|
||||
match: { 'transaction.name': params.indicator.params.transactionName },
|
||||
});
|
||||
if (params.indicator.params.transactionType !== ALL_VALUE)
|
||||
filter.push({
|
||||
match: { 'transaction.type': params.indicator.params.transactionType },
|
||||
});
|
||||
if (!!params.indicator.params.filter)
|
||||
filter.push(getElastichsearchQueryOrThrow(params.indicator.params.filter));
|
||||
|
||||
const result = await this.esClient.search({
|
||||
index: params.indicator.params.index,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ range: { '@timestamp': { gte: 'now-60m' } } },
|
||||
{ terms: { 'processor.event': ['metric'] } },
|
||||
{ term: { 'metricset.name': 'transaction' } },
|
||||
{ exists: { field: 'transaction.duration.histogram' } },
|
||||
{ exists: { field: 'transaction.result' } },
|
||||
...filter,
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
perMinute: {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: '1m',
|
||||
},
|
||||
aggs: {
|
||||
good: {
|
||||
filter: {
|
||||
bool: {
|
||||
should: {
|
||||
match: {
|
||||
'event.outcome': 'success',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
total: {
|
||||
value_count: {
|
||||
field: 'transaction.duration.histogram',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// @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.doc_count, bucket.total.value)
|
||||
: null,
|
||||
}));
|
||||
} catch (err) {
|
||||
throw new InvalidQueryError(`Invalid ES query`);
|
||||
}
|
||||
return this.getAPMTranscationErrorPreviewData(params.indicator);
|
||||
case 'sli.kql.custom':
|
||||
try {
|
||||
const filterQuery = getElastichsearchQueryOrThrow(params.indicator.params.filter);
|
||||
const goodQuery = getElastichsearchQueryOrThrow(params.indicator.params.good);
|
||||
const totalQuery = getElastichsearchQueryOrThrow(params.indicator.params.total);
|
||||
const timestampField = params.indicator.params.timestampField;
|
||||
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: {
|
||||
good: { filter: goodQuery },
|
||||
total: { filter: totalQuery },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// @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.doc_count, bucket.total.doc_count)
|
||||
: null,
|
||||
}));
|
||||
} catch (err) {
|
||||
throw new InvalidQueryError(`Invalid ES query`);
|
||||
}
|
||||
return this.getCustomKQLPreviewData(params.indicator);
|
||||
case 'sli.histogram.custom':
|
||||
return this.getHistogramPreviewData(params.indicator);
|
||||
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,
|
||||
}));
|
||||
|
||||
return this.getCustomMetricPreviewData(params.indicator);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -0,0 +1,317 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Histogram Transform Generator aggregates using the denominator equation 1`] = `
|
||||
Object {
|
||||
"bucket_script": Object {
|
||||
"buckets_path": Object {
|
||||
"value": "_total>total",
|
||||
},
|
||||
"script": "params.value",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Histogram Transform Generator aggregates using the denominator equation with filter 1`] = `
|
||||
Object {
|
||||
"bucket_script": Object {
|
||||
"buckets_path": Object {
|
||||
"value": "_total>total",
|
||||
},
|
||||
"script": "params.value",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
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",
|
||||
},
|
||||
"script": "params.value",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Histogram Transform Generator aggregates using the numerator equation with filter 1`] = `
|
||||
Object {
|
||||
"bucket_script": Object {
|
||||
"buckets_path": Object {
|
||||
"value": "_good>total['0.0-100.0']>_count",
|
||||
},
|
||||
"script": "params.value",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Histogram Transform Generator filters the source using the kql query 1`] = `
|
||||
Object {
|
||||
"bool": Object {
|
||||
"minimum_should_match": 1,
|
||||
"should": Array [
|
||||
Object {
|
||||
"match": Object {
|
||||
"labels.groupId": "group-4",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Histogram Transform Generator returns the expected transform params for timeslices slo 1`] = `
|
||||
Object {
|
||||
"_meta": Object {
|
||||
"managed": true,
|
||||
"managed_by": "observability",
|
||||
"version": 1,
|
||||
},
|
||||
"description": "Rolled-up SLI data for SLO: irrelevant",
|
||||
"dest": Object {
|
||||
"index": ".slo-observability.sli-v1",
|
||||
"pipeline": ".slo-observability.sli.monthly",
|
||||
},
|
||||
"frequency": "1m",
|
||||
"pivot": Object {
|
||||
"aggregations": Object {
|
||||
"_good": Object {
|
||||
"aggs": Object {
|
||||
"total": Object {
|
||||
"range": Object {
|
||||
"field": "latency",
|
||||
"keyed": true,
|
||||
"ranges": Array [
|
||||
Object {
|
||||
"from": 0,
|
||||
"to": 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"filter": Object {
|
||||
"match_all": Object {},
|
||||
},
|
||||
},
|
||||
"_total": Object {
|
||||
"aggs": Object {
|
||||
"total": Object {
|
||||
"value_count": Object {
|
||||
"field": "latency",
|
||||
},
|
||||
},
|
||||
},
|
||||
"filter": Object {
|
||||
"match_all": Object {},
|
||||
},
|
||||
},
|
||||
"slo.denominator": Object {
|
||||
"bucket_script": Object {
|
||||
"buckets_path": Object {
|
||||
"value": "_total>total",
|
||||
},
|
||||
"script": "params.value",
|
||||
},
|
||||
},
|
||||
"slo.isGoodSlice": Object {
|
||||
"bucket_script": Object {
|
||||
"buckets_path": Object {
|
||||
"goodEvents": "slo.numerator>value",
|
||||
"totalEvents": "slo.denominator>value",
|
||||
},
|
||||
"script": "params.goodEvents / params.totalEvents >= 0.95 ? 1 : 0",
|
||||
},
|
||||
},
|
||||
"slo.numerator": Object {
|
||||
"bucket_script": Object {
|
||||
"buckets_path": Object {
|
||||
"value": "_good>total['0.0-100.0']>_count",
|
||||
},
|
||||
"script": "params.value",
|
||||
},
|
||||
},
|
||||
},
|
||||
"group_by": Object {
|
||||
"@timestamp": Object {
|
||||
"date_histogram": Object {
|
||||
"field": "log_timestamp",
|
||||
"fixed_interval": "2m",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
},
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.revision",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"settings": Object {
|
||||
"deduce_mappings": false,
|
||||
},
|
||||
"source": Object {
|
||||
"index": "my-index*",
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"minimum_should_match": 1,
|
||||
"should": Array [
|
||||
Object {
|
||||
"match": Object {
|
||||
"labels.groupId": "group-3",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"runtime_mappings": Object {
|
||||
"slo.id": Object {
|
||||
"script": Object {
|
||||
"source": Any<String>,
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"script": Object {
|
||||
"source": "emit(1)",
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
},
|
||||
},
|
||||
"sync": Object {
|
||||
"time": Object {
|
||||
"delay": "1m",
|
||||
"field": "log_timestamp",
|
||||
},
|
||||
},
|
||||
"transform_id": Any<String>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Histogram Transform Generator returns the expected transform params with every specified indicator params 1`] = `
|
||||
Object {
|
||||
"_meta": Object {
|
||||
"managed": true,
|
||||
"managed_by": "observability",
|
||||
"version": 1,
|
||||
},
|
||||
"description": "Rolled-up SLI data for SLO: irrelevant",
|
||||
"dest": Object {
|
||||
"index": ".slo-observability.sli-v1",
|
||||
"pipeline": ".slo-observability.sli.monthly",
|
||||
},
|
||||
"frequency": "1m",
|
||||
"pivot": Object {
|
||||
"aggregations": Object {
|
||||
"_good": Object {
|
||||
"aggs": Object {
|
||||
"total": Object {
|
||||
"range": Object {
|
||||
"field": "latency",
|
||||
"keyed": true,
|
||||
"ranges": Array [
|
||||
Object {
|
||||
"from": 0,
|
||||
"to": 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"filter": Object {
|
||||
"match_all": Object {},
|
||||
},
|
||||
},
|
||||
"_total": Object {
|
||||
"aggs": Object {
|
||||
"total": Object {
|
||||
"value_count": Object {
|
||||
"field": "latency",
|
||||
},
|
||||
},
|
||||
},
|
||||
"filter": Object {
|
||||
"match_all": Object {},
|
||||
},
|
||||
},
|
||||
"slo.denominator": Object {
|
||||
"bucket_script": Object {
|
||||
"buckets_path": Object {
|
||||
"value": "_total>total",
|
||||
},
|
||||
"script": "params.value",
|
||||
},
|
||||
},
|
||||
"slo.numerator": Object {
|
||||
"bucket_script": Object {
|
||||
"buckets_path": Object {
|
||||
"value": "_good>total['0.0-100.0']>_count",
|
||||
},
|
||||
"script": "params.value",
|
||||
},
|
||||
},
|
||||
},
|
||||
"group_by": Object {
|
||||
"@timestamp": Object {
|
||||
"date_histogram": Object {
|
||||
"field": "log_timestamp",
|
||||
"fixed_interval": "1m",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
},
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.revision",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"settings": Object {
|
||||
"deduce_mappings": false,
|
||||
},
|
||||
"source": Object {
|
||||
"index": "my-index*",
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"minimum_should_match": 1,
|
||||
"should": Array [
|
||||
Object {
|
||||
"match": Object {
|
||||
"labels.groupId": "group-3",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"runtime_mappings": Object {
|
||||
"slo.id": Object {
|
||||
"script": Object {
|
||||
"source": Any<String>,
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"script": Object {
|
||||
"source": "emit(1)",
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
},
|
||||
},
|
||||
"sync": Object {
|
||||
"time": Object {
|
||||
"delay": "1m",
|
||||
"field": "log_timestamp",
|
||||
},
|
||||
},
|
||||
"transform_id": Any<String>,
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* 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 {
|
||||
createHistogramIndicator,
|
||||
createSLO,
|
||||
createSLOWithTimeslicesBudgetingMethod,
|
||||
} from '../fixtures/slo';
|
||||
import { HistogramTransformGenerator } from './histogram';
|
||||
|
||||
const generator = new HistogramTransformGenerator();
|
||||
|
||||
describe('Histogram Transform Generator', () => {
|
||||
describe('validation', () => {
|
||||
it('throws when the good filter is invalid', () => {
|
||||
const anSLO = createSLO({
|
||||
indicator: createHistogramIndicator({
|
||||
good: {
|
||||
field: 'latency',
|
||||
aggregation: 'range',
|
||||
from: 0,
|
||||
to: 100,
|
||||
filter: 'foo:',
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid KQL: foo:/);
|
||||
});
|
||||
it('throws when the total filter is invalid', () => {
|
||||
const anSLO = createSLO({
|
||||
indicator: createHistogramIndicator({
|
||||
good: {
|
||||
field: 'latency',
|
||||
aggregation: 'value_count',
|
||||
filter: 'foo:',
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid KQL: foo:/);
|
||||
});
|
||||
it('throws when the query_filter is invalid', () => {
|
||||
const anSLO = createSLO({
|
||||
indicator: createHistogramIndicator({ filter: '{ kql.query: invalid' }),
|
||||
});
|
||||
expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid KQL/);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the expected transform params with every specified indicator params', async () => {
|
||||
const anSLO = createSLO({ indicator: createHistogramIndicator() });
|
||||
const transform = generator.getTransformParams(anSLO);
|
||||
|
||||
expect(transform).toMatchSnapshot({
|
||||
transform_id: expect.any(String),
|
||||
source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } },
|
||||
});
|
||||
expect(transform.transform_id).toEqual(`slo-${anSLO.id}-${anSLO.revision}`);
|
||||
expect(transform.source.runtime_mappings!['slo.id']).toMatchObject({
|
||||
script: { source: `emit('${anSLO.id}')` },
|
||||
});
|
||||
expect(transform.source.runtime_mappings!['slo.revision']).toMatchObject({
|
||||
script: { source: `emit(${anSLO.revision})` },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the expected transform params for timeslices slo', async () => {
|
||||
const anSLO = createSLOWithTimeslicesBudgetingMethod({
|
||||
indicator: createHistogramIndicator(),
|
||||
});
|
||||
const transform = generator.getTransformParams(anSLO);
|
||||
|
||||
expect(transform).toMatchSnapshot({
|
||||
transform_id: expect.any(String),
|
||||
source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } },
|
||||
});
|
||||
});
|
||||
|
||||
it('filters the source using the kql query', async () => {
|
||||
const anSLO = createSLO({
|
||||
indicator: createHistogramIndicator({ filter: 'labels.groupId: group-4' }),
|
||||
});
|
||||
const transform = generator.getTransformParams(anSLO);
|
||||
|
||||
expect(transform.source.query).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('uses the provided index', async () => {
|
||||
const anSLO = createSLO({
|
||||
indicator: createHistogramIndicator({ index: 'my-own-index*' }),
|
||||
});
|
||||
const transform = generator.getTransformParams(anSLO);
|
||||
|
||||
expect(transform.source.index).toBe('my-own-index*');
|
||||
});
|
||||
|
||||
it('uses the provided timestampField', async () => {
|
||||
const anSLO = createSLO({
|
||||
indicator: createHistogramIndicator({
|
||||
timestampField: 'my-date-field',
|
||||
}),
|
||||
});
|
||||
const transform = generator.getTransformParams(anSLO);
|
||||
|
||||
expect(transform.sync?.time?.field).toBe('my-date-field');
|
||||
// @ts-ignore
|
||||
expect(transform.pivot?.group_by['@timestamp'].date_histogram.field).toBe('my-date-field');
|
||||
});
|
||||
|
||||
it('aggregates using the numerator equation', async () => {
|
||||
const anSLO = createSLO({
|
||||
indicator: createHistogramIndicator(),
|
||||
});
|
||||
const transform = generator.getTransformParams(anSLO);
|
||||
|
||||
expect(transform.pivot!.aggregations!['slo.numerator']).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('aggregates using the numerator equation with filter', async () => {
|
||||
const anSLO = createSLO({
|
||||
indicator: createHistogramIndicator({
|
||||
good: {
|
||||
field: 'latency',
|
||||
aggregation: 'range',
|
||||
from: 0,
|
||||
to: 100,
|
||||
filter: 'foo: "bar"',
|
||||
},
|
||||
}),
|
||||
});
|
||||
const transform = generator.getTransformParams(anSLO);
|
||||
|
||||
expect(transform.pivot!.aggregations!['slo.numerator']).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('aggregates using the denominator equation', async () => {
|
||||
const anSLO = createSLO({
|
||||
indicator: createHistogramIndicator(),
|
||||
});
|
||||
const transform = generator.getTransformParams(anSLO);
|
||||
|
||||
expect(transform.pivot!.aggregations!['slo.denominator']).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('aggregates using the denominator equation with filter', async () => {
|
||||
const anSLO = createSLO({
|
||||
indicator: createHistogramIndicator({
|
||||
total: {
|
||||
field: 'latency',
|
||||
aggregation: 'value_count',
|
||||
filter: 'foo: "bar"',
|
||||
},
|
||||
}),
|
||||
});
|
||||
const transform = generator.getTransformParams(anSLO);
|
||||
|
||||
expect(transform.pivot!.aggregations!['slo.denominator']).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
import {
|
||||
HistogramIndicator,
|
||||
histogramIndicatorSchema,
|
||||
timeslicesBudgetingMethodSchema,
|
||||
} from '@kbn/slo-schema';
|
||||
|
||||
import { InvalidTransformError } from '../../../errors';
|
||||
import { getSLOTransformTemplate } from '../../../assets/transform_templates/slo_transform_template';
|
||||
import { getElastichsearchQueryOrThrow, TransformGenerator } from '.';
|
||||
import {
|
||||
SLO_DESTINATION_INDEX_NAME,
|
||||
SLO_INGEST_PIPELINE_NAME,
|
||||
getSLOTransformId,
|
||||
} from '../../../assets/constants';
|
||||
import { SLO } from '../../../domain/models';
|
||||
import { GetHistogramIndicatorAggregation } from '../aggregations';
|
||||
|
||||
export class HistogramTransformGenerator extends TransformGenerator {
|
||||
public getTransformParams(slo: SLO): TransformPutTransformRequest {
|
||||
if (!histogramIndicatorSchema.is(slo.indicator)) {
|
||||
throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`);
|
||||
}
|
||||
|
||||
return getSLOTransformTemplate(
|
||||
this.buildTransformId(slo),
|
||||
this.buildDescription(slo),
|
||||
this.buildSource(slo, slo.indicator),
|
||||
this.buildDestination(),
|
||||
this.buildGroupBy(slo, slo.indicator.params.timestampField),
|
||||
this.buildAggregations(slo, slo.indicator),
|
||||
this.buildSettings(slo, slo.indicator.params.timestampField)
|
||||
);
|
||||
}
|
||||
|
||||
private buildTransformId(slo: SLO): string {
|
||||
return getSLOTransformId(slo.id, slo.revision);
|
||||
}
|
||||
|
||||
private buildSource(slo: SLO, indicator: HistogramIndicator) {
|
||||
const filter = getElastichsearchQueryOrThrow(indicator.params.filter);
|
||||
return {
|
||||
index: indicator.params.index,
|
||||
runtime_mappings: this.buildCommonRuntimeMappings(slo),
|
||||
query: filter,
|
||||
};
|
||||
}
|
||||
|
||||
private buildDestination() {
|
||||
return {
|
||||
pipeline: SLO_INGEST_PIPELINE_NAME,
|
||||
index: SLO_DESTINATION_INDEX_NAME,
|
||||
};
|
||||
}
|
||||
|
||||
private buildAggregations(slo: SLO, indicator: HistogramIndicator) {
|
||||
const getHistogramIndicatorAggregations = new GetHistogramIndicatorAggregation(indicator);
|
||||
|
||||
return {
|
||||
...getHistogramIndicatorAggregations.execute({
|
||||
type: 'good',
|
||||
aggregationKey: 'slo.numerator',
|
||||
}),
|
||||
...getHistogramIndicatorAggregations.execute({
|
||||
type: 'total',
|
||||
aggregationKey: 'slo.denominator',
|
||||
}),
|
||||
...(timeslicesBudgetingMethodSchema.is(slo.budgetingMethod) && {
|
||||
'slo.isGoodSlice': {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
goodEvents: 'slo.numerator>value',
|
||||
totalEvents: 'slo.denominator>value',
|
||||
},
|
||||
script: `params.goodEvents / params.totalEvents >= ${slo.objective.timesliceTarget} ? 1 : 0`,
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -10,4 +10,5 @@ export * from './apm_transaction_error_rate';
|
|||
export * from './apm_transaction_duration';
|
||||
export * from './kql_custom';
|
||||
export * from './metric_custom';
|
||||
export * from './histogram';
|
||||
export * from './common';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue