mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
feat(slo): Add timestampField additional settings (#153395)
This commit is contained in:
parent
b0b50f2978
commit
c0453af53b
32 changed files with 453 additions and 363 deletions
|
@ -50,12 +50,17 @@ const apmTransactionErrorRateIndicatorSchema = t.type({
|
|||
const kqlCustomIndicatorTypeSchema = t.literal('sli.kql.custom');
|
||||
const kqlCustomIndicatorSchema = t.type({
|
||||
type: kqlCustomIndicatorTypeSchema,
|
||||
params: t.type({
|
||||
index: t.string,
|
||||
filter: t.string,
|
||||
good: t.string,
|
||||
total: t.string,
|
||||
}),
|
||||
params: t.intersection([
|
||||
t.type({
|
||||
index: t.string,
|
||||
filter: t.string,
|
||||
good: t.string,
|
||||
total: t.string,
|
||||
}),
|
||||
t.partial({
|
||||
timestampField: t.string,
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
const indicatorDataSchema = t.type({
|
||||
|
|
|
@ -26,7 +26,6 @@ const objectiveSchema = t.intersection([
|
|||
]);
|
||||
|
||||
const settingsSchema = t.type({
|
||||
timestampField: t.string,
|
||||
syncDelay: durationType,
|
||||
frequency: durationType,
|
||||
});
|
||||
|
|
|
@ -134,7 +134,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
|
|||
"siem-ui-timeline": "e9d6b3a9fd7af6dc502293c21cbdb309409f3996",
|
||||
"siem-ui-timeline-note": "13c9d4c142f96624a93a623c6d7cba7e1ae9b5a6",
|
||||
"siem-ui-timeline-pinned-event": "96a43d59b9e2fc11f12255a0cb47ef0a3d83af4c",
|
||||
"slo": "06733daaa5fbe331fdf3b515171978aff483ccf2",
|
||||
"slo": "af2a119f3bceee5f12dc02d7eb4f54040bae4557",
|
||||
"space": "7fc578a1f9f7708cb07479f03953d664ad9f1dae",
|
||||
"spaces-usage-stats": "084bd0f080f94fb5735d7f3cf12f13ec92f36bad",
|
||||
"synthetics-monitor": "96cc312bfa597022f83dfb3b5d1501e27a73e8d5",
|
||||
|
|
|
@ -12,7 +12,7 @@ We currently support the following SLI:
|
|||
|
||||
For the APM SLIs, customer can provide the service, environment, transaction name and type to configure them. For the **APM Latency** SLI, a threshold in milliseconds needs to be provided to discriminate the good and bad responses (events). For the **APM Availability** SLI, a list of good status codes needs to be provided to discriminate the good and bad responses (events). The API supports an optional kql filter to further filter the apm data.
|
||||
|
||||
The **custom KQL** SLI requires an index pattern, an optional filter query, a numerator query, and denominator query.
|
||||
The **custom KQL** SLI requires an index pattern, an optional filter query, a numerator query, and denominator query. A custom 'timestampField' can be provided to override the default @timestamp field.
|
||||
|
||||
## SLO configuration
|
||||
|
||||
|
@ -43,7 +43,6 @@ If a **timeslices** budgeting method is used, we also need to define the **times
|
|||
|
||||
The default settings should be sufficient for most users, but if needed, the following properties can be overwritten:
|
||||
|
||||
- **timestampField**: The date time field to use from the source index
|
||||
- **syncDelay**: The ingest delay in the source data
|
||||
- **frequency**: How often do we query the source data
|
||||
|
||||
|
@ -299,7 +298,8 @@ curl --request POST \
|
|||
"index": "high-cardinality-data-fake_logs*",
|
||||
"good": "latency < 300",
|
||||
"total": "",
|
||||
"filter": "labels.groupId: group-0"
|
||||
"filter": "labels.groupId: group-0",
|
||||
"timestampField": "custom_timestamp"
|
||||
}
|
||||
},
|
||||
"timeWindow": {
|
||||
|
|
|
@ -379,6 +379,11 @@ components:
|
|||
description: the KQL query used to define all events.
|
||||
type: string
|
||||
example: ''
|
||||
timestampField:
|
||||
description: |
|
||||
The timestamp field used in the source indice. If not specified, @timestamp will be used.
|
||||
type: string
|
||||
example: timestamp
|
||||
type:
|
||||
description: The type of indicator.
|
||||
type: string
|
||||
|
@ -554,11 +559,6 @@ components:
|
|||
description: Defines properties for settings.
|
||||
type: object
|
||||
properties:
|
||||
timestampField:
|
||||
description: |
|
||||
The timestamp field used in the source indice. Particularly useful for custom kql indicator type, when the index does not use the default '@timestamp' field
|
||||
type: string
|
||||
example: timestamp
|
||||
syncDelay:
|
||||
description: The synch delay to apply to the transform. Default 1m
|
||||
type: string
|
||||
|
|
|
@ -28,6 +28,11 @@ properties:
|
|||
description: the KQL query used to define all events.
|
||||
type: string
|
||||
example: ''
|
||||
timestampField:
|
||||
description: >
|
||||
The timestamp field used in the source indice. If not specified, @timestamp will be used.
|
||||
type: string
|
||||
example: timestamp
|
||||
type:
|
||||
description: The type of indicator.
|
||||
type: string
|
||||
|
|
|
@ -2,12 +2,6 @@ title: Settings definition
|
|||
description: Defines properties for settings.
|
||||
type: object
|
||||
properties:
|
||||
timestampField:
|
||||
description: >
|
||||
The timestamp field used in the source indice. Particularly useful for custom kql indicator type, when the index
|
||||
does not use the default '@timestamp' field
|
||||
type: string
|
||||
example: timestamp
|
||||
syncDelay:
|
||||
description: The synch delay to apply to the transform. Default 1m
|
||||
type: string
|
||||
|
|
|
@ -38,6 +38,7 @@ const baseSlo: Omit<SLOWithSummaryResponse, 'id'> = {
|
|||
filter: 'baz: foo and bar > 2',
|
||||
good: 'http_status: 2xx',
|
||||
total: 'a query',
|
||||
timestampField: 'custom_timestamp',
|
||||
},
|
||||
},
|
||||
timeWindow: {
|
||||
|
@ -48,7 +49,6 @@ const baseSlo: Omit<SLOWithSummaryResponse, 'id'> = {
|
|||
budgetingMethod: 'occurrences',
|
||||
revision: 1,
|
||||
settings: {
|
||||
timestampField: '@timestamp',
|
||||
syncDelay: '1m',
|
||||
frequency: '1m',
|
||||
},
|
||||
|
|
|
@ -5,10 +5,17 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormLabel,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { CreateSLOInput } from '@kbn/slo-schema';
|
||||
|
||||
import { IndexSelection } from './index_selection';
|
||||
|
@ -16,6 +23,12 @@ import { QueryBuilder } from '../common/query_builder';
|
|||
|
||||
export function CustomKqlIndicatorTypeForm() {
|
||||
const { control, watch } = useFormContext<CreateSLOInput>();
|
||||
const [isAdditionalSettingsOpen, setAdditionalSettingsOpen] = useState<boolean>(false);
|
||||
|
||||
const handleAdditionalSettingsClick = () => {
|
||||
setAdditionalSettingsOpen(!isAdditionalSettingsOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexItem>
|
||||
|
@ -75,6 +88,48 @@ export function CustomKqlIndicatorTypeForm() {
|
|||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexItem>
|
||||
<EuiLink
|
||||
data-test-subj="customKqlIndicatorFormAdditionalSettingsToggle"
|
||||
onClick={handleAdditionalSettingsClick}
|
||||
>
|
||||
<EuiIcon type={isAdditionalSettingsOpen ? 'arrowDown' : 'arrowRight'} />{' '}
|
||||
{i18n.translate('xpack.observability.slo.sloEdit.sliType.additionalSettings.label', {
|
||||
defaultMessage: 'Additional settings',
|
||||
})}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
|
||||
{isAdditionalSettingsOpen && (
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel>
|
||||
{i18n.translate(
|
||||
'xpack.observability.slo.sloEdit.additionalSettings.timestampField.label',
|
||||
{ defaultMessage: 'Timestamp field' }
|
||||
)}
|
||||
</EuiFormLabel>
|
||||
|
||||
<Controller
|
||||
name="indicator.params.timestampField"
|
||||
shouldUnregister
|
||||
control={control}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<EuiFieldText
|
||||
{...field}
|
||||
disabled={!watch('indicator.params.index')}
|
||||
data-test-subj="sloFormAdditionalSettingsTimestampField"
|
||||
placeholder={i18n.translate(
|
||||
'xpack.observability.slo.sloEdit.additionalSettings.timestampField.placeholder',
|
||||
{ defaultMessage: 'Timestamp field used in the index, default to @timestamp' }
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,45 +6,32 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiAvatar,
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFormLabel,
|
||||
EuiPanel,
|
||||
EuiSelect,
|
||||
EuiSpacer,
|
||||
EuiTimeline,
|
||||
EuiTimelineItem,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { EuiAvatar, EuiButton, EuiFlexGroup, EuiTimeline, EuiTimelineItem } from '@elastic/eui';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import type { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { useCreateSlo } from '../../../hooks/slo/use_create_slo';
|
||||
import { useUpdateSlo } from '../../../hooks/slo/use_update_slo';
|
||||
import { useSectionFormValidation } from '../helpers/use_section_form_validation';
|
||||
import { CustomKqlIndicatorTypeForm } from './custom_kql/custom_kql_indicator_type_form';
|
||||
import { SloEditFormDescription } from './slo_edit_form_description';
|
||||
import { SloEditFormObjectives } from './slo_edit_form_objectives';
|
||||
import { SloEditFormDescriptionSection } from './slo_edit_form_description_section';
|
||||
import { SloEditFormObjectiveSection } from './slo_edit_form_objective_section';
|
||||
import {
|
||||
transformValuesToCreateSLOInput,
|
||||
transformSloResponseToCreateSloInput,
|
||||
transformValuesToUpdateSLOInput,
|
||||
} from '../helpers/process_slo_form_values';
|
||||
import { paths } from '../../../config/paths';
|
||||
import { SLI_OPTIONS, SLO_EDIT_FORM_DEFAULT_VALUES } from '../constants';
|
||||
import { ApmLatencyIndicatorTypeForm } from './apm_latency/apm_latency_indicator_type_form';
|
||||
import { ApmAvailabilityIndicatorTypeForm } from './apm_availability/apm_availability_indicator_type_form';
|
||||
import { SLO_EDIT_FORM_DEFAULT_VALUES } from '../constants';
|
||||
import { SloEditFormIndicatorSection } from './slo_edit_form_indicator_section';
|
||||
|
||||
export interface Props {
|
||||
slo: SLOWithSummaryResponse | undefined;
|
||||
}
|
||||
|
||||
const maxWidth = 775;
|
||||
export const maxWidth = 775;
|
||||
|
||||
export function SloEditForm({ slo }: Props) {
|
||||
const {
|
||||
|
@ -58,7 +45,7 @@ export function SloEditForm({ slo }: Props) {
|
|||
values: transformSloResponseToCreateSloInput(slo),
|
||||
mode: 'all',
|
||||
});
|
||||
const { control, watch, getFieldState, getValues, formState } = methods;
|
||||
const { watch, getFieldState, getValues, formState } = methods;
|
||||
|
||||
const { isIndicatorSectionValid, isDescriptionSectionValid, isObjectiveSectionValid } =
|
||||
useSectionFormValidation({
|
||||
|
@ -120,18 +107,8 @@ export function SloEditForm({ slo }: Props) {
|
|||
}
|
||||
};
|
||||
|
||||
const getIndicatorTypeForm = () => {
|
||||
switch (watch('indicator.type')) {
|
||||
case 'sli.kql.custom':
|
||||
return <CustomKqlIndicatorTypeForm />;
|
||||
case 'sli.apm.transactionDuration':
|
||||
return <ApmLatencyIndicatorTypeForm />;
|
||||
case 'sli.apm.transactionErrorRate':
|
||||
return <ApmAvailabilityIndicatorTypeForm />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const getIconColor = (isSectionValid: boolean) =>
|
||||
isSectionValid ? euiThemeVars.euiColorSuccess : euiThemeVars.euiColorPrimary;
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
|
@ -140,83 +117,26 @@ export function SloEditForm({ slo }: Props) {
|
|||
verticalAlign="top"
|
||||
icon={
|
||||
<EuiAvatar
|
||||
color={
|
||||
isIndicatorSectionValid
|
||||
? euiThemeVars.euiColorSuccess
|
||||
: euiThemeVars.euiColorPrimary
|
||||
}
|
||||
color={getIconColor(isIndicatorSectionValid)}
|
||||
iconType={isIndicatorSectionValid ? 'check' : ''}
|
||||
name={isIndicatorSectionValid ? 'Check' : '1'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="none" style={{ maxWidth }}>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
{i18n.translate('xpack.observability.slo.sloEdit.definition.title', {
|
||||
defaultMessage: 'Define SLI',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="xl" />
|
||||
|
||||
<EuiFormLabel>
|
||||
{i18n.translate('xpack.observability.slo.sloEdit.definition.sliType', {
|
||||
defaultMessage: 'SLI type',
|
||||
})}
|
||||
</EuiFormLabel>
|
||||
|
||||
<Controller
|
||||
name="indicator.type"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<EuiSelect
|
||||
data-test-subj="sloFormIndicatorTypeSelect"
|
||||
{...field}
|
||||
options={SLI_OPTIONS}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="xxl" />
|
||||
|
||||
{getIndicatorTypeForm()}
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
</EuiPanel>
|
||||
<SloEditFormIndicatorSection />
|
||||
</EuiTimelineItem>
|
||||
|
||||
<EuiTimelineItem
|
||||
icon={
|
||||
<EuiAvatar
|
||||
color={
|
||||
isObjectiveSectionValid
|
||||
? euiThemeVars.euiColorSuccess
|
||||
: euiThemeVars.euiColorPrimary
|
||||
}
|
||||
color={getIconColor(isObjectiveSectionValid)}
|
||||
iconType={isObjectiveSectionValid ? 'check' : ''}
|
||||
name={isObjectiveSectionValid ? 'Check' : '2'}
|
||||
/>
|
||||
}
|
||||
verticalAlign="top"
|
||||
>
|
||||
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="none" style={{ maxWidth }}>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
{i18n.translate('xpack.observability.slo.sloEdit.objectives.title', {
|
||||
defaultMessage: 'Set objectives',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="xl" />
|
||||
|
||||
<SloEditFormObjectives />
|
||||
|
||||
<EuiSpacer size="xl" />
|
||||
</EuiPanel>
|
||||
<SloEditFormObjectiveSection />
|
||||
</EuiTimelineItem>
|
||||
|
||||
<EuiTimelineItem
|
||||
|
@ -225,61 +145,42 @@ export function SloEditForm({ slo }: Props) {
|
|||
<EuiAvatar
|
||||
name={isDescriptionSectionValid ? 'Check' : '3'}
|
||||
iconType={isDescriptionSectionValid ? 'check' : ''}
|
||||
color={
|
||||
isDescriptionSectionValid
|
||||
? euiThemeVars.euiColorSuccess
|
||||
: euiThemeVars.euiColorPrimary
|
||||
}
|
||||
color={getIconColor(isDescriptionSectionValid)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="none" style={{ maxWidth }}>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
{i18n.translate('xpack.observability.slo.sloEdit.description.title', {
|
||||
defaultMessage: 'Describe SLO',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<SloEditFormDescriptionSection />
|
||||
|
||||
<EuiSpacer size="xl" />
|
||||
<EuiFlexGroup direction="row" gutterSize="s">
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="sloFormSubmitButton"
|
||||
fill
|
||||
disabled={!formState.isValid}
|
||||
isLoading={isCreateSloLoading || isUpdateSloLoading}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{isEditMode
|
||||
? i18n.translate('xpack.observability.slo.sloEdit.editSloButton', {
|
||||
defaultMessage: 'Update SLO',
|
||||
})
|
||||
: i18n.translate('xpack.observability.slo.sloEdit.createSloButton', {
|
||||
defaultMessage: 'Create SLO',
|
||||
})}
|
||||
</EuiButton>
|
||||
|
||||
<SloEditFormDescription />
|
||||
|
||||
<EuiSpacer size="xl" />
|
||||
|
||||
<EuiFlexGroup direction="row" gutterSize="s">
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="sloFormSubmitButton"
|
||||
fill
|
||||
disabled={!formState.isValid}
|
||||
isLoading={isCreateSloLoading || isUpdateSloLoading}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{isEditMode
|
||||
? i18n.translate('xpack.observability.slo.sloEdit.editSloButton', {
|
||||
defaultMessage: 'Update SLO',
|
||||
})
|
||||
: i18n.translate('xpack.observability.slo.sloEdit.createSloButton', {
|
||||
defaultMessage: 'Create SLO',
|
||||
})}
|
||||
</EuiButton>
|
||||
|
||||
<EuiButton
|
||||
color="ghost"
|
||||
data-test-subj="sloFormCancelButton"
|
||||
fill
|
||||
onClick={() => navigateToUrl(basePath.prepend(paths.observability.slos))}
|
||||
>
|
||||
{i18n.translate('xpack.observability.slo.sloEdit.cancelButton', {
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="xl" />
|
||||
</EuiPanel>
|
||||
<EuiButton
|
||||
color="ghost"
|
||||
data-test-subj="sloFormCancelButton"
|
||||
fill
|
||||
disabled={isCreateSloLoading || isUpdateSloLoading}
|
||||
onClick={() => navigateToUrl(basePath.prepend(paths.observability.slos))}
|
||||
>
|
||||
{i18n.translate('xpack.observability.slo.sloEdit.cancelButton', {
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
</EuiTimelineItem>
|
||||
</EuiTimeline>
|
||||
</FormProvider>
|
||||
|
|
|
@ -1,152 +0,0 @@
|
|||
/*
|
||||
* 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,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormLabel,
|
||||
EuiTextArea,
|
||||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import type { CreateSLOInput } from '@kbn/slo-schema';
|
||||
|
||||
export function SloEditFormDescription() {
|
||||
const { control } = useFormContext<CreateSLOInput>();
|
||||
const sloNameId = useGeneratedHtmlId({ prefix: 'sloName' });
|
||||
const descriptionId = useGeneratedHtmlId({ prefix: 'sloDescription' });
|
||||
const tagsId = useGeneratedHtmlId({ prefix: 'tags' });
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel>
|
||||
{i18n.translate('xpack.observability.slo.sloEdit.description.sloName', {
|
||||
defaultMessage: 'SLO Name',
|
||||
})}
|
||||
</EuiFormLabel>
|
||||
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
id={sloNameId}
|
||||
data-test-subj="sloFormNameInput"
|
||||
placeholder={i18n.translate(
|
||||
'xpack.observability.slo.sloEdit.description.sloNamePlaceholder',
|
||||
{
|
||||
defaultMessage: 'Name for the SLO',
|
||||
}
|
||||
)}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow>
|
||||
<EuiFormLabel>
|
||||
{i18n.translate('xpack.observability.slo.sloEdit.description.sloDescription', {
|
||||
defaultMessage: 'Description',
|
||||
})}
|
||||
</EuiFormLabel>
|
||||
|
||||
<Controller
|
||||
name="description"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<EuiTextArea
|
||||
fullWidth
|
||||
id={descriptionId}
|
||||
data-test-subj="sloFormDescriptionTextArea"
|
||||
placeholder={i18n.translate(
|
||||
'xpack.observability.slo.sloEdit.description.sloDescriptionPlaceholder',
|
||||
{
|
||||
defaultMessage: 'A short description of the SLO',
|
||||
}
|
||||
)}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow>
|
||||
<EuiFormLabel>
|
||||
{i18n.translate('xpack.observability.slo.sloEdit.tags.label', {
|
||||
defaultMessage: 'Tags',
|
||||
})}
|
||||
</EuiFormLabel>
|
||||
<Controller
|
||||
shouldUnregister={true}
|
||||
name="tags"
|
||||
control={control}
|
||||
defaultValue={[]}
|
||||
rules={{ required: false }}
|
||||
render={({ field: { ref, ...field }, fieldState }) => (
|
||||
<EuiComboBox
|
||||
{...field}
|
||||
id={tagsId}
|
||||
fullWidth
|
||||
aria-label={i18n.translate('xpack.observability.slo.sloEdit.tags.placeholder', {
|
||||
defaultMessage: 'Add tags',
|
||||
})}
|
||||
placeholder={i18n.translate('xpack.observability.slo.sloEdit.tags.placeholder', {
|
||||
defaultMessage: 'Add tags',
|
||||
})}
|
||||
isInvalid={!!fieldState.error}
|
||||
options={[]}
|
||||
noSuggestions
|
||||
selectedOptions={generateTagOptions(field.value)}
|
||||
onChange={(selected: EuiComboBoxOptionOption[]) => {
|
||||
if (selected.length) {
|
||||
return field.onChange(selected.map((opts) => opts.value));
|
||||
}
|
||||
|
||||
field.onChange([]);
|
||||
}}
|
||||
onCreateOption={(searchValue: string, options: EuiComboBoxOptionOption[] = []) => {
|
||||
const normalizedSearchValue = searchValue.trim().toLowerCase();
|
||||
|
||||
if (!normalizedSearchValue) {
|
||||
return;
|
||||
}
|
||||
const values = field.value ?? [];
|
||||
|
||||
if (
|
||||
values.findIndex((tag) => tag.trim().toLowerCase() === normalizedSearchValue) ===
|
||||
-1
|
||||
) {
|
||||
field.onChange([...values, searchValue]);
|
||||
}
|
||||
}}
|
||||
isClearable={true}
|
||||
data-test-subj="sloEditApmAvailabilityGoodStatusCodesSelector"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function generateTagOptions(tags: string[] = []) {
|
||||
return tags.map((tag) => ({
|
||||
label: tag,
|
||||
value: tag,
|
||||
'data-test-subj': `${tag}Option`,
|
||||
}));
|
||||
}
|
|
@ -10,12 +10,12 @@ import { ComponentStory } from '@storybook/react';
|
|||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator';
|
||||
import { SloEditFormDescription as Component } from './slo_edit_form_description';
|
||||
import { SloEditFormDescriptionSection as Component } from './slo_edit_form_description_section';
|
||||
import { SLO_EDIT_FORM_DEFAULT_VALUES } from '../constants';
|
||||
|
||||
export default {
|
||||
component: Component,
|
||||
title: 'app/SLO/EditPage/SloEditFormDescription',
|
||||
title: 'app/SLO/EditPage/SloEditFormDescriptionSection',
|
||||
decorators: [KibanaReactStorybookDecorator],
|
||||
};
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* 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,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormLabel,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiTextArea,
|
||||
EuiTitle,
|
||||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import type { CreateSLOInput } from '@kbn/slo-schema';
|
||||
|
||||
import { maxWidth } from './slo_edit_form';
|
||||
|
||||
export function SloEditFormDescriptionSection() {
|
||||
const { control } = useFormContext<CreateSLOInput>();
|
||||
const sloNameId = useGeneratedHtmlId({ prefix: 'sloName' });
|
||||
const descriptionId = useGeneratedHtmlId({ prefix: 'sloDescription' });
|
||||
const tagsId = useGeneratedHtmlId({ prefix: 'tags' });
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="none" style={{ maxWidth }}>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
{i18n.translate('xpack.observability.slo.sloEdit.description.title', {
|
||||
defaultMessage: 'Describe SLO',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="xl" />
|
||||
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel>
|
||||
{i18n.translate('xpack.observability.slo.sloEdit.description.sloName', {
|
||||
defaultMessage: 'SLO Name',
|
||||
})}
|
||||
</EuiFormLabel>
|
||||
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { ref, ...field }, fieldState }) => (
|
||||
<EuiFieldText
|
||||
{...field}
|
||||
fullWidth
|
||||
isInvalid={fieldState.invalid}
|
||||
id={sloNameId}
|
||||
data-test-subj="sloFormNameInput"
|
||||
placeholder={i18n.translate(
|
||||
'xpack.observability.slo.sloEdit.description.sloNamePlaceholder',
|
||||
{
|
||||
defaultMessage: 'Name for the SLO',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow>
|
||||
<EuiFormLabel>
|
||||
{i18n.translate('xpack.observability.slo.sloEdit.description.sloDescription', {
|
||||
defaultMessage: 'Description',
|
||||
})}
|
||||
</EuiFormLabel>
|
||||
|
||||
<Controller
|
||||
name="description"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
rules={{ required: false }}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<EuiTextArea
|
||||
{...field}
|
||||
fullWidth
|
||||
id={descriptionId}
|
||||
data-test-subj="sloFormDescriptionTextArea"
|
||||
placeholder={i18n.translate(
|
||||
'xpack.observability.slo.sloEdit.description.sloDescriptionPlaceholder',
|
||||
{
|
||||
defaultMessage: 'A short description of the SLO',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow>
|
||||
<EuiFormLabel>
|
||||
{i18n.translate('xpack.observability.slo.sloEdit.tags.label', {
|
||||
defaultMessage: 'Tags',
|
||||
})}
|
||||
</EuiFormLabel>
|
||||
<Controller
|
||||
shouldUnregister={true}
|
||||
name="tags"
|
||||
control={control}
|
||||
defaultValue={[]}
|
||||
rules={{ required: false }}
|
||||
render={({ field: { ref, ...field }, fieldState }) => (
|
||||
<EuiComboBox
|
||||
{...field}
|
||||
id={tagsId}
|
||||
fullWidth
|
||||
aria-label={i18n.translate('xpack.observability.slo.sloEdit.tags.placeholder', {
|
||||
defaultMessage: 'Add tags',
|
||||
})}
|
||||
placeholder={i18n.translate('xpack.observability.slo.sloEdit.tags.placeholder', {
|
||||
defaultMessage: 'Add tags',
|
||||
})}
|
||||
isInvalid={!!fieldState.error}
|
||||
options={[]}
|
||||
noSuggestions
|
||||
selectedOptions={generateTagOptions(field.value)}
|
||||
onChange={(selected: EuiComboBoxOptionOption[]) => {
|
||||
if (selected.length) {
|
||||
return field.onChange(selected.map((opts) => opts.value));
|
||||
}
|
||||
|
||||
field.onChange([]);
|
||||
}}
|
||||
onCreateOption={(searchValue: string, options: EuiComboBoxOptionOption[] = []) => {
|
||||
const normalizedSearchValue = searchValue.trim().toLowerCase();
|
||||
|
||||
if (!normalizedSearchValue) {
|
||||
return;
|
||||
}
|
||||
const values = field.value ?? [];
|
||||
|
||||
if (
|
||||
values.findIndex(
|
||||
(tag) => tag.trim().toLowerCase() === normalizedSearchValue
|
||||
) === -1
|
||||
) {
|
||||
field.onChange([...values, searchValue]);
|
||||
}
|
||||
}}
|
||||
isClearable={true}
|
||||
data-test-subj="sloEditApmAvailabilityGoodStatusCodesSelector"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="xl" />
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
function generateTagOptions(tags: string[] = []) {
|
||||
return tags.map((tag) => ({
|
||||
label: tag,
|
||||
value: tag,
|
||||
'data-test-subj': `${tag}Option`,
|
||||
}));
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 { EuiFormLabel, EuiPanel, EuiSelect, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CreateSLOInput } from '@kbn/slo-schema';
|
||||
import React from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { SLI_OPTIONS } from '../constants';
|
||||
import { ApmAvailabilityIndicatorTypeForm } from './apm_availability/apm_availability_indicator_type_form';
|
||||
import { ApmLatencyIndicatorTypeForm } from './apm_latency/apm_latency_indicator_type_form';
|
||||
import { CustomKqlIndicatorTypeForm } from './custom_kql/custom_kql_indicator_type_form';
|
||||
import { maxWidth } from './slo_edit_form';
|
||||
|
||||
export function SloEditFormIndicatorSection() {
|
||||
const { control, watch } = useFormContext<CreateSLOInput>();
|
||||
|
||||
const getIndicatorTypeForm = () => {
|
||||
switch (watch('indicator.type')) {
|
||||
case 'sli.kql.custom':
|
||||
return <CustomKqlIndicatorTypeForm />;
|
||||
case 'sli.apm.transactionDuration':
|
||||
return <ApmLatencyIndicatorTypeForm />;
|
||||
case 'sli.apm.transactionErrorRate':
|
||||
return <ApmAvailabilityIndicatorTypeForm />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="none" style={{ maxWidth }}>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
{i18n.translate('xpack.observability.slo.sloEdit.definition.title', {
|
||||
defaultMessage: 'Define SLI',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="xl" />
|
||||
|
||||
<EuiFormLabel>
|
||||
{i18n.translate('xpack.observability.slo.sloEdit.definition.sliType', {
|
||||
defaultMessage: 'Choose the SLI type',
|
||||
})}
|
||||
</EuiFormLabel>
|
||||
|
||||
<Controller
|
||||
name="indicator.type"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<EuiSelect
|
||||
{...field}
|
||||
required
|
||||
data-test-subj="sloFormIndicatorTypeSelect"
|
||||
options={SLI_OPTIONS}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="xxl" />
|
||||
|
||||
{getIndicatorTypeForm()}
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -10,12 +10,12 @@ import { ComponentStory } from '@storybook/react';
|
|||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator';
|
||||
import { SloEditFormObjectives as Component } from './slo_edit_form_objectives';
|
||||
import { SloEditFormObjectiveSection as Component } from './slo_edit_form_objective_section';
|
||||
import { SLO_EDIT_FORM_DEFAULT_VALUES } from '../constants';
|
||||
|
||||
export default {
|
||||
component: Component,
|
||||
title: 'app/SLO/EditPage/SloEditFormObjectives',
|
||||
title: 'app/SLO/EditPage/SloEditFormObjectiveSection',
|
||||
decorators: [KibanaReactStorybookDecorator],
|
||||
};
|
||||
|
|
@ -11,24 +11,36 @@ import {
|
|||
EuiFlexGrid,
|
||||
EuiFlexItem,
|
||||
EuiFormLabel,
|
||||
EuiPanel,
|
||||
EuiSelect,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import type { CreateSLOInput } from '@kbn/slo-schema';
|
||||
|
||||
import { SloEditFormObjectivesTimeslices } from './slo_edit_form_objectives_timeslices';
|
||||
import { SloEditFormObjectiveSectionTimeslices } from './slo_edit_form_objective_section_timeslices';
|
||||
import { BUDGETING_METHOD_OPTIONS, TIMEWINDOW_OPTIONS } from '../constants';
|
||||
import { maxWidth } from './slo_edit_form';
|
||||
|
||||
export function SloEditFormObjectives() {
|
||||
export function SloEditFormObjectiveSection() {
|
||||
const { control, watch } = useFormContext<CreateSLOInput>();
|
||||
const budgetingSelect = useGeneratedHtmlId({ prefix: 'budgetingSelect' });
|
||||
const timeWindowSelect = useGeneratedHtmlId({ prefix: 'timeWindowSelect' });
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="none" style={{ maxWidth }}>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
{i18n.translate('xpack.observability.slo.sloEdit.objectives.title', {
|
||||
defaultMessage: 'Set objectives',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="xl" />
|
||||
<EuiFlexGrid columns={3}>
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel>
|
||||
|
@ -43,10 +55,11 @@ export function SloEditFormObjectives() {
|
|||
rules={{ required: true }}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<EuiSelect
|
||||
{...field}
|
||||
required
|
||||
id={budgetingSelect}
|
||||
data-test-subj="sloFormBudgetingMethodSelect"
|
||||
options={BUDGETING_METHOD_OPTIONS}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -65,10 +78,11 @@ export function SloEditFormObjectives() {
|
|||
rules={{ required: true }}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<EuiSelect
|
||||
{...field}
|
||||
required
|
||||
id={timeWindowSelect}
|
||||
data-test-subj="sloFormTimeWindowDurationSelect"
|
||||
options={TIMEWINDOW_OPTIONS}
|
||||
{...field}
|
||||
value={String(field.value)}
|
||||
/>
|
||||
)}
|
||||
|
@ -90,10 +104,12 @@ export function SloEditFormObjectives() {
|
|||
min: 0.001,
|
||||
max: 99.999,
|
||||
}}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
render={({ field: { ref, ...field }, fieldState }) => (
|
||||
<EuiFieldNumber
|
||||
data-test-subj="sloFormObjectiveTargetInput"
|
||||
{...field}
|
||||
required
|
||||
isInvalid={fieldState.invalid}
|
||||
data-test-subj="sloFormObjectiveTargetInput"
|
||||
value={String(field.value)}
|
||||
min={0.001}
|
||||
max={99.999}
|
||||
|
@ -108,9 +124,10 @@ export function SloEditFormObjectives() {
|
|||
{watch('budgetingMethod') === 'timeslices' ? (
|
||||
<>
|
||||
<EuiSpacer size="xl" />
|
||||
<SloEditFormObjectivesTimeslices />
|
||||
<SloEditFormObjectiveSectionTimeslices />
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
<EuiSpacer size="xl" />
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -10,12 +10,12 @@ import { ComponentStory } from '@storybook/react';
|
|||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator';
|
||||
import { SloEditFormObjectivesTimeslices as Component } from './slo_edit_form_objectives_timeslices';
|
||||
import { SloEditFormObjectiveSectionTimeslices as Component } from './slo_edit_form_objective_section_timeslices';
|
||||
import { SLO_EDIT_FORM_DEFAULT_VALUES } from '../constants';
|
||||
|
||||
export default {
|
||||
component: Component,
|
||||
title: 'app/SLO/EditPage/SloEditFormObjectivesTimeslices',
|
||||
title: 'app/SLO/EditPage/SloEditFormObjectiveSectionTimeslices',
|
||||
decorators: [KibanaReactStorybookDecorator],
|
||||
};
|
||||
|
|
@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import type { CreateSLOInput } from '@kbn/slo-schema';
|
||||
|
||||
export function SloEditFormObjectivesTimeslices() {
|
||||
export function SloEditFormObjectiveSectionTimeslices() {
|
||||
const { control } = useFormContext<CreateSLOInput>();
|
||||
return (
|
||||
<EuiFlexGrid columns={3}>
|
||||
|
@ -31,9 +31,11 @@ export function SloEditFormObjectivesTimeslices() {
|
|||
min: 0.001,
|
||||
max: 99.999,
|
||||
}}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
render={({ field: { ref, ...field }, fieldState }) => (
|
||||
<EuiFieldNumber
|
||||
{...field}
|
||||
required
|
||||
isInvalid={fieldState.invalid}
|
||||
value={String(field.value)}
|
||||
data-test-subj="sloFormObjectiveTimesliceTargetInput"
|
||||
min={0.001}
|
||||
|
@ -58,9 +60,11 @@ export function SloEditFormObjectivesTimeslices() {
|
|||
defaultValue="1"
|
||||
control={control}
|
||||
rules={{ required: true, min: 1, max: 120 }}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
render={({ field: { ref, ...field }, fieldState }) => (
|
||||
<EuiFieldNumber
|
||||
{...field}
|
||||
isInvalid={fieldState.invalid}
|
||||
required
|
||||
data-test-subj="sloFormObjectiveTimesliceWindowInput"
|
||||
value={String(field.value)}
|
||||
min={1}
|
|
@ -415,6 +415,7 @@ describe('SLO Edit Page', () => {
|
|||
"filter": "baz: foo and bar > 2",
|
||||
"good": "http_status: 2xx",
|
||||
"index": "some-index",
|
||||
"timestampField": "custom_timestamp",
|
||||
"total": "a query",
|
||||
},
|
||||
"type": "sli.kql.custom",
|
||||
|
@ -426,7 +427,6 @@ describe('SLO Edit Page', () => {
|
|||
"settings": Object {
|
||||
"frequency": "1m",
|
||||
"syncDelay": "1m",
|
||||
"timestampField": "@timestamp",
|
||||
},
|
||||
"tags": Array [
|
||||
"k8s",
|
||||
|
|
|
@ -41,7 +41,6 @@ describe('validateSLO', () => {
|
|||
const slo = createSLO({
|
||||
settings: {
|
||||
frequency: sixHours(),
|
||||
timestampField: '@timestamp',
|
||||
syncDelay: oneMinute(),
|
||||
},
|
||||
});
|
||||
|
@ -52,7 +51,6 @@ describe('validateSLO', () => {
|
|||
const slo = createSLO({
|
||||
settings: {
|
||||
frequency: oneMinute(),
|
||||
timestampField: '@timestamp',
|
||||
syncDelay: sixHours(),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -49,7 +49,6 @@ export const slo: SavedObjectsType = {
|
|||
},
|
||||
settings: {
|
||||
properties: {
|
||||
timestampField: { type: 'keyword' },
|
||||
syncDelay: { type: 'keyword' },
|
||||
frequency: { type: 'keyword' },
|
||||
},
|
||||
|
|
|
@ -43,7 +43,6 @@ describe('CreateSLO', () => {
|
|||
...sloParams,
|
||||
id: expect.any(String),
|
||||
settings: {
|
||||
timestampField: '@timestamp',
|
||||
syncDelay: oneMinute(),
|
||||
frequency: oneMinute(),
|
||||
},
|
||||
|
@ -66,7 +65,6 @@ describe('CreateSLO', () => {
|
|||
indicator: createAPMTransactionErrorRateIndicator(),
|
||||
tags: ['one', 'two'],
|
||||
settings: {
|
||||
timestampField: '@timestamp2',
|
||||
syncDelay: fiveMinute(),
|
||||
},
|
||||
});
|
||||
|
@ -80,7 +78,6 @@ describe('CreateSLO', () => {
|
|||
...sloParams,
|
||||
id: expect.any(String),
|
||||
settings: {
|
||||
timestampField: '@timestamp2',
|
||||
syncDelay: fiveMinute(),
|
||||
frequency: oneMinute(),
|
||||
},
|
||||
|
|
|
@ -57,7 +57,6 @@ export class CreateSLO {
|
|||
...params,
|
||||
id: uuidv1(),
|
||||
settings: {
|
||||
timestampField: params.settings?.timestampField ?? '@timestamp',
|
||||
syncDelay: params.settings?.syncDelay ?? new Duration(1, DurationUnit.Minute),
|
||||
frequency: params.settings?.frequency ?? new Duration(1, DurationUnit.Minute),
|
||||
},
|
||||
|
|
|
@ -66,7 +66,6 @@ describe('FindSLO', () => {
|
|||
isRolling: true,
|
||||
},
|
||||
settings: {
|
||||
timestampField: '@timestamp',
|
||||
syncDelay: '1m',
|
||||
frequency: '1m',
|
||||
},
|
||||
|
|
|
@ -78,7 +78,6 @@ const defaultSLO: Omit<SLO, 'id' | 'revision' | 'createdAt' | 'updatedAt'> = {
|
|||
},
|
||||
indicator: createAPMTransactionDurationIndicator(),
|
||||
settings: {
|
||||
timestampField: '@timestamp',
|
||||
syncDelay: new Duration(1, DurationUnit.Minute),
|
||||
frequency: new Duration(1, DurationUnit.Minute),
|
||||
},
|
||||
|
|
|
@ -59,9 +59,13 @@ export async function getSloDiagnosis(
|
|||
let dataSample;
|
||||
if (sloSavedObject?.attributes.indicator.params.index) {
|
||||
const slo = sloSavedObject.attributes;
|
||||
const sortField =
|
||||
'timestampField' in slo.indicator.params
|
||||
? slo.indicator.params.timestampField ?? '@timestamp'
|
||||
: '@timestamp';
|
||||
dataSample = await esClient.search({
|
||||
index: slo.indicator.params.index,
|
||||
sort: { [slo.settings.timestampField]: 'desc' },
|
||||
sort: { [sortField]: 'desc' },
|
||||
size: 5,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -66,7 +66,6 @@ describe('GetSLO', () => {
|
|||
isRolling: true,
|
||||
},
|
||||
settings: {
|
||||
timestampField: '@timestamp',
|
||||
syncDelay: '1m',
|
||||
frequency: '1m',
|
||||
},
|
||||
|
|
|
@ -34,7 +34,7 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator
|
|||
this.buildDescription(slo),
|
||||
this.buildSource(slo, slo.indicator),
|
||||
this.buildDestination(),
|
||||
this.buildCommonGroupBy(slo),
|
||||
this.buildGroupBy(slo),
|
||||
this.buildAggregations(slo, slo.indicator),
|
||||
this.buildSettings(slo)
|
||||
);
|
||||
|
@ -48,7 +48,7 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator
|
|||
const queryFilter: Query[] = [
|
||||
{
|
||||
range: {
|
||||
[slo.settings.timestampField]: {
|
||||
'@timestamp': {
|
||||
gte: `now-${slo.timeWindow.duration.format()}`,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -38,7 +38,7 @@ export class ApmTransactionErrorRateTransformGenerator extends TransformGenerato
|
|||
this.buildDescription(slo),
|
||||
this.buildSource(slo, slo.indicator),
|
||||
this.buildDestination(),
|
||||
this.buildCommonGroupBy(slo),
|
||||
this.buildGroupBy(slo),
|
||||
this.buildAggregations(slo, slo.indicator),
|
||||
this.buildSettings(slo)
|
||||
);
|
||||
|
@ -52,7 +52,7 @@ export class ApmTransactionErrorRateTransformGenerator extends TransformGenerato
|
|||
const queryFilter: Query[] = [
|
||||
{
|
||||
range: {
|
||||
[slo.settings.timestampField]: {
|
||||
'@timestamp': {
|
||||
gte: `now-${slo.timeWindow.duration.format()}`,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -83,6 +83,19 @@ describe('KQL Custom Transform Generator', () => {
|
|||
expect(transform.source.index).toBe('my-own-index*');
|
||||
});
|
||||
|
||||
it('uses the provided timestampField', async () => {
|
||||
const anSLO = createSLO({
|
||||
indicator: createKQLCustomIndicator({
|
||||
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 kql', async () => {
|
||||
const anSLO = createSLO({
|
||||
indicator: createKQLCustomIndicator({
|
||||
|
|
|
@ -29,9 +29,9 @@ export class KQLCustomTransformGenerator extends TransformGenerator {
|
|||
this.buildDescription(slo),
|
||||
this.buildSource(slo, slo.indicator),
|
||||
this.buildDestination(),
|
||||
this.buildCommonGroupBy(slo),
|
||||
this.buildGroupBy(slo, this.getTimestampFieldOrDefault(slo.indicator.params.timestampField)),
|
||||
this.buildAggregations(slo, slo.indicator),
|
||||
this.buildSettings(slo)
|
||||
this.buildSettings(slo, this.getTimestampFieldOrDefault(slo.indicator.params.timestampField))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -79,4 +79,8 @@ export class KQLCustomTransformGenerator extends TransformGenerator {
|
|||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private getTimestampFieldOrDefault(timestampField: string | undefined): string {
|
||||
return !!timestampField && timestampField.length > 0 ? timestampField : '@timestamp';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ export abstract class TransformGenerator {
|
|||
return `Rolled-up SLI data for SLO: ${slo.name}`;
|
||||
}
|
||||
|
||||
public buildCommonGroupBy(slo: SLO) {
|
||||
public buildGroupBy(slo: SLO, sourceIndexTimestampField: string | undefined = '@timestamp') {
|
||||
let fixedInterval = '1m';
|
||||
if (timeslicesBudgetingMethodSchema.is(slo.budgetingMethod)) {
|
||||
fixedInterval = slo.objective.timesliceWindow!.format();
|
||||
|
@ -53,20 +53,23 @@ export abstract class TransformGenerator {
|
|||
field: 'slo.revision',
|
||||
},
|
||||
},
|
||||
// Field used in the destination index, using @timestamp as per mapping definition
|
||||
// timestamp field defined in the destination index
|
||||
'@timestamp': {
|
||||
date_histogram: {
|
||||
field: slo.settings.timestampField,
|
||||
field: sourceIndexTimestampField, // timestamp field defined in the source index
|
||||
fixed_interval: fixedInterval,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public buildSettings(slo: SLO): TransformSettings {
|
||||
public buildSettings(
|
||||
slo: SLO,
|
||||
sourceIndexTimestampField: string | undefined = '@timestamp'
|
||||
): TransformSettings {
|
||||
return {
|
||||
frequency: slo.settings.frequency.format(),
|
||||
sync_field: slo.settings.timestampField,
|
||||
sync_field: sourceIndexTimestampField, // timestamp field defined in the source index
|
||||
sync_delay: slo.settings.syncDelay.format(),
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue