mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
feat(slo): Use summary search index with temporary documents (#162511)
This commit is contained in:
parent
473b9a4a7c
commit
f78c523d57
49 changed files with 914 additions and 180 deletions
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiBadge, EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiBadge, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
|
||||
|
@ -19,11 +19,18 @@ export function SloStatusBadge({ slo }: SloStatusProps) {
|
|||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
{slo.summary.status === 'NO_DATA' && (
|
||||
<EuiBadge color="default">
|
||||
{i18n.translate('xpack.observability.slo.sloStatusBadge.noData', {
|
||||
defaultMessage: 'No data',
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={i18n.translate('xpack.observability.slo.sloStatusBadge.noDataTooltip', {
|
||||
defaultMessage: 'It may take some time before the data is aggregated and available.',
|
||||
})}
|
||||
</EuiBadge>
|
||||
>
|
||||
<EuiBadge color="default">
|
||||
{i18n.translate('xpack.observability.slo.sloStatusBadge.noData', {
|
||||
defaultMessage: 'No data',
|
||||
})}
|
||||
</EuiBadge>
|
||||
</EuiToolTip>
|
||||
)}
|
||||
|
||||
{slo.summary.status === 'HEALTHY' && (
|
||||
|
|
|
@ -10,7 +10,7 @@ import type { Indicator } from '@kbn/slo-schema';
|
|||
interface SloListFilter {
|
||||
kqlQuery: string;
|
||||
page: number;
|
||||
sortBy?: string;
|
||||
sortBy: string;
|
||||
sortDirection: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -77,9 +77,6 @@ export function useCloneSlo() {
|
|||
})
|
||||
);
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: sloKeys.lists(), exact: false });
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ export function useCreateSlo() {
|
|||
|
||||
const [queryKey, previousData] = queriesData?.at(0) ?? [];
|
||||
|
||||
const newItem = { ...slo, id: uuidv1() };
|
||||
const newItem = { ...slo, id: uuidv1(), summary: undefined };
|
||||
|
||||
const optimisticUpdate = {
|
||||
page: previousData?.page ?? 1,
|
||||
|
@ -83,9 +83,6 @@ export function useCreateSlo() {
|
|||
http.basePath.prepend(paths.observability.sloCreateWithEncodedForm(encode(slo)))
|
||||
);
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: sloKeys.lists(), exact: false });
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -76,9 +76,6 @@ export function useDeleteSlo() {
|
|||
})
|
||||
);
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: sloKeys.lists(), exact: false });
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ const LONG_REFETCH_INTERVAL = 1000 * 60; // 1 minute
|
|||
export function useFetchSloList({
|
||||
kqlQuery = '',
|
||||
page = 1,
|
||||
sortBy,
|
||||
sortBy = 'status',
|
||||
sortDirection = 'desc',
|
||||
shouldRefetch,
|
||||
}: SLOListParams | undefined = {}): UseFetchSloListResponse {
|
||||
|
|
|
@ -5,14 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPagination } from '@elastic/eui';
|
||||
import { debounce } from 'lodash';
|
||||
import { useIsMutating } from '@tanstack/react-query';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useFetchSloList } from '../../../hooks/slo/use_fetch_slo_list';
|
||||
import { SloListSearchFilterSortBar, SortField } from './slo_list_search_filter_sort_bar';
|
||||
import { SloListItems } from './slo_list_items';
|
||||
import { SloListSearchFilterSortBar, SortField } from './slo_list_search_filter_sort_bar';
|
||||
|
||||
export interface Props {
|
||||
autoRefresh: boolean;
|
||||
|
@ -20,9 +18,8 @@ export interface Props {
|
|||
|
||||
export function SloList({ autoRefresh }: Props) {
|
||||
const [activePage, setActivePage] = useState(0);
|
||||
|
||||
const [query, setQuery] = useState('');
|
||||
const [sort, setSort] = useState<SortField | undefined>('error_budget_remaining');
|
||||
const [sort, setSort] = useState<SortField | undefined>('status');
|
||||
|
||||
const { isInitialLoading, isLoading, isRefetching, isError, sloList, refetch } = useFetchSloList({
|
||||
page: activePage + 1,
|
||||
|
@ -44,16 +41,16 @@ export function SloList({ autoRefresh }: Props) {
|
|||
refetch();
|
||||
};
|
||||
|
||||
const handleChangeQuery = useMemo(
|
||||
() =>
|
||||
debounce((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setQuery(e.target.value);
|
||||
}, 800),
|
||||
[]
|
||||
);
|
||||
const handleChangeQuery = (newQuery: string) => {
|
||||
setActivePage(0);
|
||||
setQuery(newQuery);
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleChangeSort = (newSort: SortField | undefined) => {
|
||||
setActivePage(0);
|
||||
setSort(newSort);
|
||||
refetch();
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiContextMenuItem,
|
||||
|
@ -19,27 +17,29 @@ import {
|
|||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { HistoricalSummaryResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import type { Rule } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import React, { useState } from 'react';
|
||||
import { rulesLocatorID, sloFeatureId } from '../../../../common';
|
||||
import { SLO_BURN_RATE_RULE_TYPE_ID } from '../../../../common/constants';
|
||||
import { sloKeys } from '../../../hooks/slo/query_key_factory';
|
||||
import { useCapabilities } from '../../../hooks/slo/use_capabilities';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { useCloneSlo } from '../../../hooks/slo/use_clone_slo';
|
||||
import { useGetFilteredRuleTypes } from '../../../hooks/use_get_filtered_rule_types';
|
||||
import { SloSummary } from './slo_summary';
|
||||
import { SloDeleteConfirmationModal } from './slo_delete_confirmation_modal';
|
||||
import { SloBadges } from './badges/slo_badges';
|
||||
import {
|
||||
transformSloResponseToCreateSloForm,
|
||||
transformCreateSLOFormToCreateSLOInput,
|
||||
} from '../../slo_edit/helpers/process_slo_form_values';
|
||||
import { SLO_BURN_RATE_RULE_TYPE_ID } from '../../../../common/constants';
|
||||
import { rulesLocatorID, sloFeatureId } from '../../../../common';
|
||||
import { paths } from '../../../routes/paths';
|
||||
import { useDeleteSlo } from '../../../hooks/slo/use_delete_slo';
|
||||
import type { ActiveAlerts } from '../../../hooks/slo/use_fetch_active_alerts';
|
||||
import type { SloRule } from '../../../hooks/slo/use_fetch_rules_for_slo';
|
||||
import { useGetFilteredRuleTypes } from '../../../hooks/use_get_filtered_rule_types';
|
||||
import type { RulesParams } from '../../../locators/rules';
|
||||
import { paths } from '../../../routes/paths';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import {
|
||||
transformCreateSLOFormToCreateSLOInput,
|
||||
transformSloResponseToCreateSloForm,
|
||||
} from '../../slo_edit/helpers/process_slo_form_values';
|
||||
import { SloBadges } from './badges/slo_badges';
|
||||
import { SloDeleteConfirmationModal } from './slo_delete_confirmation_modal';
|
||||
import { SloSummary } from './slo_summary';
|
||||
|
||||
export interface SloListItemProps {
|
||||
slo: SLOWithSummaryResponse;
|
||||
|
@ -47,7 +47,6 @@ export interface SloListItemProps {
|
|||
historicalSummary?: HistoricalSummaryResponse[];
|
||||
historicalSummaryLoading: boolean;
|
||||
activeAlerts?: ActiveAlerts;
|
||||
onConfirmDelete: (slo: SLOWithSummaryResponse) => void;
|
||||
}
|
||||
|
||||
export function SloListItem({
|
||||
|
@ -56,7 +55,6 @@ export function SloListItem({
|
|||
historicalSummary = [],
|
||||
historicalSummaryLoading,
|
||||
activeAlerts,
|
||||
onConfirmDelete,
|
||||
}: SloListItemProps) {
|
||||
const {
|
||||
application: { navigateToUrl },
|
||||
|
@ -72,6 +70,7 @@ export function SloListItem({
|
|||
const filteredRuleTypes = useGetFilteredRuleTypes();
|
||||
|
||||
const { mutate: cloneSlo } = useCloneSlo();
|
||||
const { mutate: deleteSlo } = useDeleteSlo();
|
||||
|
||||
const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false);
|
||||
const [isAddRuleFlyoutOpen, setIsAddRuleFlyoutOpen] = useState(false);
|
||||
|
@ -100,15 +99,7 @@ export function SloListItem({
|
|||
|
||||
const handleNavigateToRules = async () => {
|
||||
const locator = locators.get<RulesParams>(rulesLocatorID);
|
||||
|
||||
locator?.navigate(
|
||||
{
|
||||
params: { sloId: slo.id },
|
||||
},
|
||||
{
|
||||
replace: false,
|
||||
}
|
||||
);
|
||||
locator?.navigate({ params: { sloId: slo.id } }, { replace: false });
|
||||
};
|
||||
|
||||
const handleClone = () => {
|
||||
|
@ -127,7 +118,7 @@ export function SloListItem({
|
|||
|
||||
const handleDeleteConfirm = () => {
|
||||
setDeleteConfirmationModalOpen(false);
|
||||
onConfirmDelete(slo);
|
||||
deleteSlo({ id: slo.id, name: slo.name });
|
||||
};
|
||||
|
||||
const handleDeleteCancel = () => {
|
||||
|
|
|
@ -4,17 +4,15 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
import type { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import React from 'react';
|
||||
import { useFetchActiveAlerts } from '../../../hooks/slo/use_fetch_active_alerts';
|
||||
import { useFetchRulesForSlo } from '../../../hooks/slo/use_fetch_rules_for_slo';
|
||||
import { useFetchHistoricalSummary } from '../../../hooks/slo/use_fetch_historical_summary';
|
||||
import { useDeleteSlo } from '../../../hooks/slo/use_delete_slo';
|
||||
import { SloListItem } from './slo_list_item';
|
||||
import { useFetchRulesForSlo } from '../../../hooks/slo/use_fetch_rules_for_slo';
|
||||
import { SloListEmpty } from './slo_list_empty';
|
||||
import { SloListError } from './slo_list_error';
|
||||
import { SloListItem } from './slo_list_item';
|
||||
|
||||
export interface Props {
|
||||
sloList: SLOWithSummaryResponse[];
|
||||
|
@ -30,8 +28,6 @@ export function SloListItems({ sloList, loading, error }: Props) {
|
|||
const { isLoading: historicalSummaryLoading, data: historicalSummaryBySlo } =
|
||||
useFetchHistoricalSummary({ sloIds });
|
||||
|
||||
const { mutate: deleteSlo } = useDeleteSlo();
|
||||
|
||||
if (!loading && !error && sloList.length === 0) {
|
||||
return <SloListEmpty />;
|
||||
}
|
||||
|
@ -39,10 +35,6 @@ export function SloListItems({ sloList, loading, error }: Props) {
|
|||
return <SloListError />;
|
||||
}
|
||||
|
||||
const handleDelete = (slo: SLOWithSummaryResponse) => {
|
||||
deleteSlo({ id: slo.id, name: slo.name });
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
{sloList.map((slo) => (
|
||||
|
@ -53,7 +45,6 @@ export function SloListItems({ sloList, loading, error }: Props) {
|
|||
historicalSummary={historicalSummaryBySlo?.[slo.id]}
|
||||
historicalSummaryLoading={historicalSummaryLoading}
|
||||
slo={slo}
|
||||
onConfirmDelete={handleDelete}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
EuiFieldSearch,
|
||||
EuiFilterButton,
|
||||
EuiFilterGroup,
|
||||
EuiFlexGroup,
|
||||
|
@ -18,11 +17,14 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { EuiSelectableOptionCheckedType } from '@elastic/eui/src/components/selectable/selectable_option';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { QueryStringInput } from '@kbn/unified-search-plugin/public';
|
||||
import React, { useState } from 'react';
|
||||
import { useCreateDataView } from '../../../hooks/use_create_data_view';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
|
||||
export interface SloListSearchFilterSortBarProps {
|
||||
loading: boolean;
|
||||
onChangeQuery: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onChangeQuery: (query: string) => void;
|
||||
onChangeSort: (sort: SortField | undefined) => void;
|
||||
}
|
||||
|
||||
|
@ -46,6 +48,7 @@ const SORT_OPTIONS: Array<Item<SortField>> = [
|
|||
defaultMessage: 'SLO status',
|
||||
}),
|
||||
type: 'status',
|
||||
checked: 'on',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.observability.slo.list.sortBy.errorBudgetConsumed', {
|
||||
|
@ -58,7 +61,6 @@ const SORT_OPTIONS: Array<Item<SortField>> = [
|
|||
defaultMessage: 'Error budget remaining',
|
||||
}),
|
||||
type: 'error_budget_remaining',
|
||||
checked: 'on',
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -67,8 +69,14 @@ export function SloListSearchFilterSortBar({
|
|||
onChangeQuery,
|
||||
onChangeSort,
|
||||
}: SloListSearchFilterSortBarProps) {
|
||||
const { data, dataViews, docLinks, http, notifications, storage, uiSettings, unifiedSearch } =
|
||||
useKibana().services;
|
||||
const { dataView } = useCreateDataView({ indexPatternString: '.slo-observability.summary-*' });
|
||||
|
||||
const [isSortPopoverOpen, setSortPopoverOpen] = useState(false);
|
||||
const [sortOptions, setSortOptions] = useState(SORT_OPTIONS);
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
const selectedSort = sortOptions.find((option) => option.checked === 'on');
|
||||
const handleToggleSortButton = () => setSortPopoverOpen(!isSortPopoverOpen);
|
||||
|
||||
|
@ -81,14 +89,30 @@ export function SloListSearchFilterSortBar({
|
|||
return (
|
||||
<EuiFlexGroup direction="row" gutterSize="s">
|
||||
<EuiFlexItem grow>
|
||||
<EuiFieldSearch
|
||||
data-test-subj="o11ySloListSearchFilterSortBarFieldSearch"
|
||||
fullWidth
|
||||
isLoading={loading}
|
||||
onChange={onChangeQuery}
|
||||
<QueryStringInput
|
||||
appName="Observability"
|
||||
bubbleSubmitEvent={false}
|
||||
deps={{
|
||||
data,
|
||||
dataViews,
|
||||
docLinks,
|
||||
http,
|
||||
notifications,
|
||||
storage,
|
||||
uiSettings,
|
||||
unifiedSearch,
|
||||
}}
|
||||
disableAutoFocus
|
||||
onSubmit={() => onChangeQuery(query)}
|
||||
disableLanguageSwitcher
|
||||
isDisabled={loading}
|
||||
indexPatterns={dataView ? [dataView] : []}
|
||||
placeholder={i18n.translate('xpack.observability.slo.list.search', {
|
||||
defaultMessage: 'Search',
|
||||
defaultMessage: 'Search your SLOs...',
|
||||
})}
|
||||
query={{ query: String(query), language: 'kuery' }}
|
||||
size="s"
|
||||
onChange={(value) => setQuery(String(value.query))}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
|
@ -97,6 +121,7 @@ export function SloListSearchFilterSortBar({
|
|||
<EuiPopover
|
||||
button={
|
||||
<EuiFilterButton
|
||||
disabled={loading}
|
||||
iconType="arrowDown"
|
||||
onClick={handleToggleSortButton}
|
||||
isSelected={isSortPopoverOpen}
|
||||
|
@ -122,6 +147,7 @@ export function SloListSearchFilterSortBar({
|
|||
singleSelection
|
||||
options={sortOptions}
|
||||
onChange={handleChangeSort}
|
||||
isLoading={loading}
|
||||
>
|
||||
{(list) => list}
|
||||
</EuiSelectable>
|
||||
|
|
|
@ -69,6 +69,20 @@ const mockKibana = () => {
|
|||
services: {
|
||||
application: { navigateToUrl: mockNavigate },
|
||||
charts: chartPluginMock.createSetupContract(),
|
||||
data: {
|
||||
dataViews: {
|
||||
find: jest.fn().mockReturnValue([]),
|
||||
get: jest.fn().mockReturnValue([]),
|
||||
},
|
||||
},
|
||||
dataViews: {
|
||||
create: jest.fn().mockResolvedValue(42),
|
||||
},
|
||||
docLinks: {
|
||||
links: {
|
||||
query: {},
|
||||
},
|
||||
},
|
||||
http: {
|
||||
basePath: {
|
||||
prepend: (url: string) => url,
|
||||
|
@ -87,6 +101,9 @@ const mockKibana = () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
storage: {
|
||||
get: () => {},
|
||||
},
|
||||
triggersActionsUi: { getAddRuleFlyout: mockGetAddRuleFlyout },
|
||||
uiSettings: {
|
||||
get: (settings: string) => {
|
||||
|
@ -95,6 +112,11 @@ const mockKibana = () => {
|
|||
return '';
|
||||
},
|
||||
},
|
||||
unifiedSearch: {
|
||||
autocomplete: {
|
||||
hasQuerySuggestions: () => {},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -31,8 +31,7 @@ export function SlosPage() {
|
|||
const { hasAtLeast } = useLicense();
|
||||
|
||||
const { isInitialLoading, isLoading, isError, sloList } = useFetchSloList();
|
||||
|
||||
const { total } = sloList || {};
|
||||
const { total } = sloList || { total: 0 };
|
||||
|
||||
const [isAutoRefreshing, setIsAutoRefreshing] = useState<boolean>(true);
|
||||
|
||||
|
|
|
@ -113,6 +113,9 @@ export const getSLOSummaryMappingsTemplate = (name: string) => ({
|
|||
type: 'keyword',
|
||||
ignore_above: 32,
|
||||
},
|
||||
isTempDoc: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -11,25 +11,26 @@ export const SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME = '.slo-observability.sli-mapp
|
|||
export const SLO_COMPONENT_TEMPLATE_SETTINGS_NAME = '.slo-observability.sli-settings';
|
||||
|
||||
export const SLO_INDEX_TEMPLATE_NAME = '.slo-observability.sli';
|
||||
export const SLO_INDEX_TEMPLATE_PATTERN = `${SLO_INDEX_TEMPLATE_NAME}-*`;
|
||||
export const SLO_INDEX_TEMPLATE_PATTERN = `.slo-observability.sli-*`;
|
||||
|
||||
export const SLO_DESTINATION_INDEX_NAME = `${SLO_INDEX_TEMPLATE_NAME}-v${SLO_RESOURCES_VERSION}`;
|
||||
export const SLO_DESTINATION_INDEX_PATTERN = `${SLO_DESTINATION_INDEX_NAME}*`;
|
||||
export const SLO_DESTINATION_INDEX_NAME = `.slo-observability.sli-v${SLO_RESOURCES_VERSION}`;
|
||||
export const SLO_DESTINATION_INDEX_PATTERN = `.slo-observability.sli-v${SLO_RESOURCES_VERSION}*`;
|
||||
|
||||
export const SLO_INGEST_PIPELINE_NAME = `${SLO_INDEX_TEMPLATE_NAME}.monthly`;
|
||||
export const SLO_INGEST_PIPELINE_NAME = `.slo-observability.sli.pipeline`;
|
||||
// slo-observability.sli-v<version>.(YYYY-MM-DD)
|
||||
export const SLO_INGEST_PIPELINE_INDEX_NAME_PREFIX = `${SLO_DESTINATION_INDEX_NAME}.`;
|
||||
export const SLO_INGEST_PIPELINE_INDEX_NAME_PREFIX = `.slo-observability.sli-v${SLO_RESOURCES_VERSION}.`;
|
||||
|
||||
export const SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME = '.slo-observability.summary-mappings';
|
||||
export const SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME = '.slo-observability.summary-settings';
|
||||
export const SLO_SUMMARY_INDEX_TEMPLATE_NAME = '.slo-observability.summary';
|
||||
export const SLO_SUMMARY_INDEX_TEMPLATE_PATTERN = `${SLO_SUMMARY_INDEX_TEMPLATE_NAME}-*`;
|
||||
export const SLO_SUMMARY_INDEX_TEMPLATE_PATTERN = `.slo-observability.summary-*`;
|
||||
|
||||
export const SLO_SUMMARY_TRANSFORM_NAME_PREFIX = 'slo-summary-';
|
||||
export const SLO_SUMMARY_DESTINATION_INDEX_NAME = `${SLO_SUMMARY_INDEX_TEMPLATE_NAME}-v${SLO_RESOURCES_VERSION}`;
|
||||
export const SLO_SUMMARY_DESTINATION_INDEX_PATTERN = `${SLO_SUMMARY_DESTINATION_INDEX_NAME}*`;
|
||||
export const SLO_SUMMARY_DESTINATION_INDEX_NAME = `.slo-observability.summary-v${SLO_RESOURCES_VERSION}`; // store the temporary summary document generated by transform
|
||||
export const SLO_SUMMARY_TEMP_INDEX_NAME = `.slo-observability.summary-v${SLO_RESOURCES_VERSION}.temp`; // store the temporary summary document
|
||||
export const SLO_SUMMARY_DESTINATION_INDEX_PATTERN = `.slo-observability.summary-v${SLO_RESOURCES_VERSION}*`; // include temp and non-temp summary indices
|
||||
|
||||
export const SLO_SUMMARY_INGEST_PIPELINE_NAME = `${SLO_SUMMARY_INDEX_TEMPLATE_NAME}.pipeline`;
|
||||
export const SLO_SUMMARY_INGEST_PIPELINE_NAME = `.slo-observability.summary.pipeline`;
|
||||
|
||||
export const getSLOTransformId = (sloId: string, sloRevision: number) =>
|
||||
`slo-${sloId}-${sloRevision}`;
|
||||
|
|
|
@ -45,6 +45,12 @@ import { getObservabilityServerRouteRepository } from './routes/get_global_obser
|
|||
import { registerRoutes } from './routes/register_routes';
|
||||
import { compositeSlo, slo, SO_COMPOSITE_SLO_TYPE, SO_SLO_TYPE } from './saved_objects';
|
||||
import { threshold } from './saved_objects/threshold';
|
||||
import {
|
||||
DefaultResourceInstaller,
|
||||
DefaultSLOInstaller,
|
||||
DefaultSummaryTransformInstaller,
|
||||
} from './services/slo';
|
||||
|
||||
import { uiSettings } from './ui_settings';
|
||||
|
||||
export type ObservabilityPluginSetup = ReturnType<ObservabilityPlugin['setup']>;
|
||||
|
@ -258,6 +264,20 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
|
|||
logger: this.logger,
|
||||
repository: getObservabilityServerRouteRepository(config),
|
||||
});
|
||||
|
||||
const esInternalClient = coreStart.elasticsearch.client.asInternalUser;
|
||||
|
||||
const sloResourceInstaller = new DefaultResourceInstaller(esInternalClient, this.logger);
|
||||
const sloSummaryInstaller = new DefaultSummaryTransformInstaller(
|
||||
esInternalClient,
|
||||
this.logger
|
||||
);
|
||||
const sloInstaller = new DefaultSLOInstaller(
|
||||
sloResourceInstaller,
|
||||
sloSummaryInstaller,
|
||||
this.logger
|
||||
);
|
||||
sloInstaller.install();
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
@ -21,7 +21,6 @@ import {
|
|||
import type { IndicatorTypes } from '../../domain/models';
|
||||
import {
|
||||
CreateSLO,
|
||||
DefaultResourceInstaller,
|
||||
DefaultSummaryClient,
|
||||
DefaultTransformManager,
|
||||
DeleteSLO,
|
||||
|
@ -37,7 +36,6 @@ import { GetPreviewData } from '../../services/slo/get_preview_data';
|
|||
import { DefaultHistoricalSummaryClient } from '../../services/slo/historical_summary_client';
|
||||
import { ManageSLO } from '../../services/slo/manage_slo';
|
||||
import { DefaultSummarySearchClient } from '../../services/slo/summary_search_client';
|
||||
import { DefaultSummaryTransformInstaller } from '../../services/slo/summary_transform/summary_transform_installer';
|
||||
import {
|
||||
ApmTransactionDurationTransformGenerator,
|
||||
ApmTransactionErrorRateTransformGenerator,
|
||||
|
@ -76,22 +74,10 @@ const createSLORoute = createObservabilityServerRoute({
|
|||
}
|
||||
|
||||
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
const esInternalClient = (await context.core).elasticsearch.client.asInternalUser;
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
|
||||
const sloResourceInstaller = new DefaultResourceInstaller(esInternalClient, logger);
|
||||
const sloSummaryInstaller = new DefaultSummaryTransformInstaller(esInternalClient, logger);
|
||||
try {
|
||||
await sloResourceInstaller.ensureCommonResourcesInstalled();
|
||||
await sloSummaryInstaller.installAndStart();
|
||||
} catch (error) {
|
||||
logger.error('Failed to install SLO common resources and summary transforms', { error });
|
||||
throw error;
|
||||
}
|
||||
|
||||
const repository = new KibanaSavedObjectsSLORepository(soClient);
|
||||
const transformManager = new DefaultTransformManager(transformGenerators, esClient, logger);
|
||||
const createSLO = new CreateSLO(repository, transformManager);
|
||||
const createSLO = new CreateSLO(esClient, repository, transformManager);
|
||||
|
||||
const response = await createSLO.execute(params.body);
|
||||
|
||||
|
|
46
x-pack/plugins/observability/server/services/slo/__snapshots__/create_slo.test.ts.snap
generated
Normal file
46
x-pack/plugins/observability/server/services/slo/__snapshots__/create_slo.test.ts.snap
generated
Normal file
|
@ -0,0 +1,46 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CreateSLO happy path calls the expected services 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"document": Object {
|
||||
"errorBudgetConsumed": 0,
|
||||
"errorBudgetEstimated": false,
|
||||
"errorBudgetInitial": 0.010000000000000009,
|
||||
"errorBudgetRemaining": 1,
|
||||
"goodEvents": 0,
|
||||
"isTempDoc": true,
|
||||
"service": Object {
|
||||
"environment": null,
|
||||
"name": null,
|
||||
},
|
||||
"sliValue": -1,
|
||||
"slo": Object {
|
||||
"budgetingMethod": "occurrences",
|
||||
"description": "irrelevant",
|
||||
"id": "unique-id",
|
||||
"indicator": Object {
|
||||
"type": "sli.apm.transactionErrorRate",
|
||||
},
|
||||
"instanceId": "*",
|
||||
"name": "irrelevant",
|
||||
"revision": 1,
|
||||
"tags": Array [],
|
||||
"timeWindow": Object {
|
||||
"duration": "7d",
|
||||
"type": "rolling",
|
||||
},
|
||||
},
|
||||
"status": "NO_DATA",
|
||||
"statusCode": 0,
|
||||
"totalEvents": 0,
|
||||
"transaction": Object {
|
||||
"name": null,
|
||||
"type": null,
|
||||
},
|
||||
},
|
||||
"id": "slo-unique-id",
|
||||
"index": ".slo-observability.summary-v2.temp",
|
||||
},
|
||||
]
|
||||
`;
|
106
x-pack/plugins/observability/server/services/slo/__snapshots__/summary_search_client.test.ts.snap
generated
Normal file
106
x-pack/plugins/observability/server/services/slo/__snapshots__/summary_search_client.test.ts.snap
generated
Normal file
|
@ -0,0 +1,106 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Summary Search Client returns the summary documents without duplicate temporary summary documents 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"index": ".slo-observability.summary-v2*",
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"terms": Object {
|
||||
"slo.id": Array [
|
||||
"slo-one",
|
||||
"slo_two",
|
||||
"slo-three",
|
||||
"slo-five",
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"isTempDoc": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"wait_for_completion": false,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Summary Search Client returns the summary documents without duplicate temporary summary documents 2`] = `
|
||||
Object {
|
||||
"page": 1,
|
||||
"perPage": 20,
|
||||
"results": Array [
|
||||
Object {
|
||||
"id": "slo-one",
|
||||
"summary": Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0.4,
|
||||
"initial": 0.02,
|
||||
"isEstimated": false,
|
||||
"remaining": 0.6,
|
||||
},
|
||||
"sliValue": 0.9,
|
||||
"status": "HEALTHY",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "slo_two",
|
||||
"summary": Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0.4,
|
||||
"initial": 0.02,
|
||||
"isEstimated": false,
|
||||
"remaining": 0.6,
|
||||
},
|
||||
"sliValue": 0.9,
|
||||
"status": "HEALTHY",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "slo-three",
|
||||
"summary": Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0.4,
|
||||
"initial": 0.02,
|
||||
"isEstimated": false,
|
||||
"remaining": 0.6,
|
||||
},
|
||||
"sliValue": 0.9,
|
||||
"status": "HEALTHY",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "slo-five",
|
||||
"summary": Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0.4,
|
||||
"initial": 0.02,
|
||||
"isEstimated": false,
|
||||
"remaining": 0.6,
|
||||
},
|
||||
"sliValue": 0.9,
|
||||
"status": "HEALTHY",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "slo-four",
|
||||
"summary": Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0.4,
|
||||
"initial": 0.02,
|
||||
"isEstimated": false,
|
||||
"remaining": 0.6,
|
||||
},
|
||||
"sliValue": 0.9,
|
||||
"status": "HEALTHY",
|
||||
},
|
||||
},
|
||||
],
|
||||
"total": 5,
|
||||
}
|
||||
`;
|
49
x-pack/plugins/observability/server/services/slo/__snapshots__/update_slo.test.ts.snap
generated
Normal file
49
x-pack/plugins/observability/server/services/slo/__snapshots__/update_slo.test.ts.snap
generated
Normal file
|
@ -0,0 +1,49 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`UpdateSLO index a temporary summary document 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"document": Object {
|
||||
"errorBudgetConsumed": 0,
|
||||
"errorBudgetEstimated": false,
|
||||
"errorBudgetInitial": 0.0010000000000000009,
|
||||
"errorBudgetRemaining": 1,
|
||||
"goodEvents": 0,
|
||||
"isTempDoc": true,
|
||||
"service": Object {
|
||||
"environment": null,
|
||||
"name": null,
|
||||
},
|
||||
"sliValue": -1,
|
||||
"slo": Object {
|
||||
"budgetingMethod": "occurrences",
|
||||
"description": "irrelevant",
|
||||
"id": "unique-id",
|
||||
"indicator": Object {
|
||||
"type": "sli.apm.transactionErrorRate",
|
||||
},
|
||||
"instanceId": "*",
|
||||
"name": "irrelevant",
|
||||
"revision": 2,
|
||||
"tags": Array [
|
||||
"critical",
|
||||
"k8s",
|
||||
],
|
||||
"timeWindow": Object {
|
||||
"duration": "7d",
|
||||
"type": "rolling",
|
||||
},
|
||||
},
|
||||
"status": "NO_DATA",
|
||||
"statusCode": 0,
|
||||
"totalEvents": 0,
|
||||
"transaction": Object {
|
||||
"name": null,
|
||||
"type": null,
|
||||
},
|
||||
},
|
||||
"id": "slo-unique-id",
|
||||
"index": ".slo-observability.summary-v2.temp",
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ElasticsearchClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { CreateSLO } from './create_slo';
|
||||
import { fiveMinute, oneMinute } from './fixtures/duration';
|
||||
import { createAPMTransactionErrorRateIndicator, createSLOParams } from './fixtures/slo';
|
||||
|
@ -13,19 +14,24 @@ import { SLORepository } from './slo_repository';
|
|||
import { TransformManager } from './transform_manager';
|
||||
|
||||
describe('CreateSLO', () => {
|
||||
let esClientMock: ElasticsearchClientMock;
|
||||
let mockRepository: jest.Mocked<SLORepository>;
|
||||
let mockTransformManager: jest.Mocked<TransformManager>;
|
||||
let createSLO: CreateSLO;
|
||||
|
||||
beforeEach(() => {
|
||||
esClientMock = elasticsearchServiceMock.createElasticsearchClient();
|
||||
mockRepository = createSLORepositoryMock();
|
||||
mockTransformManager = createTransformManagerMock();
|
||||
createSLO = new CreateSLO(mockRepository, mockTransformManager);
|
||||
createSLO = new CreateSLO(esClientMock, mockRepository, mockTransformManager);
|
||||
});
|
||||
|
||||
describe('happy path', () => {
|
||||
it('calls the expected services', async () => {
|
||||
const sloParams = createSLOParams({ indicator: createAPMTransactionErrorRateIndicator() });
|
||||
const sloParams = createSLOParams({
|
||||
id: 'unique-id',
|
||||
indicator: createAPMTransactionErrorRateIndicator(),
|
||||
});
|
||||
mockTransformManager.install.mockResolvedValue('slo-transform-id');
|
||||
|
||||
const response = await createSLO.execute(sloParams);
|
||||
|
@ -33,7 +39,7 @@ describe('CreateSLO', () => {
|
|||
expect(mockRepository.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
...sloParams,
|
||||
id: expect.any(String),
|
||||
id: 'unique-id',
|
||||
settings: {
|
||||
syncDelay: oneMinute(),
|
||||
frequency: oneMinute(),
|
||||
|
@ -47,10 +53,11 @@ describe('CreateSLO', () => {
|
|||
{ throwOnConflict: true }
|
||||
);
|
||||
expect(mockTransformManager.install).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ ...sloParams, id: expect.any(String) })
|
||||
expect.objectContaining({ ...sloParams, id: 'unique-id' })
|
||||
);
|
||||
expect(mockTransformManager.start).toHaveBeenCalledWith('slo-transform-id');
|
||||
expect(response).toEqual(expect.objectContaining({ id: expect.any(String) }));
|
||||
expect(response).toEqual(expect.objectContaining({ id: 'unique-id' }));
|
||||
expect(esClientMock.index.mock.calls[0]).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('overrides the default values when provided', async () => {
|
||||
|
|
|
@ -5,15 +5,22 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { CreateSLOParams, CreateSLOResponse } from '@kbn/slo-schema';
|
||||
import { v1 as uuidv1 } from 'uuid';
|
||||
import { SLO_SUMMARY_TEMP_INDEX_NAME } from '../../assets/constants';
|
||||
import { Duration, DurationUnit, SLO } from '../../domain/models';
|
||||
import { validateSLO } from '../../domain/services';
|
||||
import { SLORepository } from './slo_repository';
|
||||
import { createTempSummaryDocument } from './summary_transform/helpers/create_temp_summary';
|
||||
import { TransformManager } from './transform_manager';
|
||||
|
||||
export class CreateSLO {
|
||||
constructor(private repository: SLORepository, private transformManager: TransformManager) {}
|
||||
constructor(
|
||||
private esClient: ElasticsearchClient,
|
||||
private repository: SLORepository,
|
||||
private transformManager: TransformManager
|
||||
) {}
|
||||
|
||||
public async execute(params: CreateSLOParams): Promise<CreateSLOResponse> {
|
||||
const slo = this.toSLO(params);
|
||||
|
@ -39,6 +46,12 @@ export class CreateSLO {
|
|||
throw err;
|
||||
}
|
||||
|
||||
await this.esClient.index({
|
||||
index: SLO_SUMMARY_TEMP_INDEX_NAME,
|
||||
id: `slo-${slo.id}`,
|
||||
document: createTempSummaryDocument(slo),
|
||||
});
|
||||
|
||||
return this.toResponse(slo);
|
||||
}
|
||||
|
||||
|
|
|
@ -20,12 +20,10 @@ export class FindSLO {
|
|||
) {}
|
||||
|
||||
public async execute(params: FindSLOParams): Promise<FindSLOResponse> {
|
||||
const pagination: Pagination = toPagination(params);
|
||||
|
||||
const sloSummaryList = await this.summarySearchClient.search(
|
||||
params.kqlQuery ?? '',
|
||||
toSort(params),
|
||||
pagination
|
||||
toPagination(params)
|
||||
);
|
||||
|
||||
const sloList = await this.repository.findAllByIds(sloSummaryList.results.map((slo) => slo.id));
|
||||
|
@ -41,10 +39,12 @@ export class FindSLO {
|
|||
}
|
||||
|
||||
function mergeSloWithSummary(sloList: SLO[], sloSummaryList: SLOSummary[]): SLOWithSummary[] {
|
||||
return sloSummaryList.map((sloSummary) => ({
|
||||
...sloList.find((s) => s.id === sloSummary.id)!,
|
||||
summary: sloSummary.summary,
|
||||
}));
|
||||
return sloSummaryList
|
||||
.filter((sloSummary) => sloList.some((s) => s.id === sloSummary.id))
|
||||
.map((sloSummary) => ({
|
||||
...sloList.find((s) => s.id === sloSummary.id)!,
|
||||
summary: sloSummary.summary,
|
||||
}));
|
||||
}
|
||||
|
||||
function toPagination(params: FindSLOParams): Pagination {
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 { v1 as uuidv1 } from 'uuid';
|
||||
|
||||
export const aSummaryDocument = ({
|
||||
id = uuidv1(),
|
||||
sliValue = 0.9,
|
||||
consumed = 0.4,
|
||||
isTempDoc = false,
|
||||
status = 'HEALTHY',
|
||||
} = {}) => {
|
||||
return {
|
||||
goodEvents: 96,
|
||||
totalEvents: 100,
|
||||
errorBudgetEstimated: false,
|
||||
errorBudgetRemaining: 1 - consumed,
|
||||
errorBudgetConsumed: consumed,
|
||||
isTempDoc,
|
||||
service: {
|
||||
environment: null,
|
||||
name: null,
|
||||
},
|
||||
slo: {
|
||||
indicator: {
|
||||
type: 'sli.kql.custom',
|
||||
},
|
||||
timeWindow: {
|
||||
duration: '30d',
|
||||
type: 'rolling',
|
||||
},
|
||||
instanceId: '*',
|
||||
name: 'irrelevant',
|
||||
description: '',
|
||||
id,
|
||||
budgetingMethod: 'occurrences',
|
||||
revision: 1,
|
||||
tags: ['tag-one', 'tag-two', 'irrelevant'],
|
||||
},
|
||||
errorBudgetInitial: 0.02,
|
||||
transaction: {
|
||||
name: null,
|
||||
type: null,
|
||||
},
|
||||
sliValue,
|
||||
statusCode: 4,
|
||||
status,
|
||||
};
|
||||
};
|
||||
|
||||
export const aHitFromSummaryIndex = (_source: any) => {
|
||||
return {
|
||||
_index: '.slo-observability.summary-v2',
|
||||
_id: uuidv1(),
|
||||
_score: 1,
|
||||
_source,
|
||||
};
|
||||
};
|
||||
|
||||
export const aHitFromTempSummaryIndex = (_source: any) => {
|
||||
return {
|
||||
_index: '.slo-observability.summary-v2.temp',
|
||||
_id: uuidv1(),
|
||||
_score: 1,
|
||||
_source,
|
||||
};
|
||||
};
|
|
@ -12,6 +12,8 @@ export * from './find_slo';
|
|||
export * from './get_slo';
|
||||
export * from './historical_summary_client';
|
||||
export * from './resource_installer';
|
||||
export * from './slo_installer';
|
||||
export * from './summary_transform/summary_transform_installer';
|
||||
export * from './sli_client';
|
||||
export * from './slo_repository';
|
||||
export * from './transform_manager';
|
||||
|
|
|
@ -10,6 +10,7 @@ import { SLIClient } from '../sli_client';
|
|||
import { SLORepository } from '../slo_repository';
|
||||
import { SummaryClient } from '../summary_client';
|
||||
import { SummarySearchClient } from '../summary_search_client';
|
||||
import { SummaryTransformInstaller } from '../summary_transform/summary_transform_installer';
|
||||
import { TransformManager } from '../transform_manager';
|
||||
|
||||
const createResourceInstallerMock = (): jest.Mocked<ResourceInstaller> => {
|
||||
|
@ -18,6 +19,12 @@ const createResourceInstallerMock = (): jest.Mocked<ResourceInstaller> => {
|
|||
};
|
||||
};
|
||||
|
||||
const createSummaryTransformInstallerMock = (): jest.Mocked<SummaryTransformInstaller> => {
|
||||
return {
|
||||
installAndStart: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
const createTransformManagerMock = (): jest.Mocked<TransformManager> => {
|
||||
return {
|
||||
install: jest.fn(),
|
||||
|
@ -56,6 +63,7 @@ const createSLIClientMock = (): jest.Mocked<SLIClient> => {
|
|||
|
||||
export {
|
||||
createResourceInstallerMock,
|
||||
createSummaryTransformInstallerMock,
|
||||
createTransformManagerMock,
|
||||
createSLORepositoryMock,
|
||||
createSummaryClientMock,
|
||||
|
|
|
@ -11,32 +11,32 @@ import type {
|
|||
IngestPutPipelineRequest,
|
||||
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { ElasticsearchClient, Logger } from '@kbn/core/server';
|
||||
|
||||
import {
|
||||
SLO_INGEST_PIPELINE_NAME,
|
||||
SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME,
|
||||
SLO_COMPONENT_TEMPLATE_SETTINGS_NAME,
|
||||
SLO_INDEX_TEMPLATE_NAME,
|
||||
SLO_RESOURCES_VERSION,
|
||||
SLO_INDEX_TEMPLATE_PATTERN,
|
||||
SLO_INGEST_PIPELINE_INDEX_NAME_PREFIX,
|
||||
SLO_SUMMARY_INDEX_TEMPLATE_NAME,
|
||||
SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME,
|
||||
SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME,
|
||||
SLO_SUMMARY_INDEX_TEMPLATE_PATTERN,
|
||||
SLO_DESTINATION_INDEX_NAME,
|
||||
SLO_SUMMARY_DESTINATION_INDEX_NAME,
|
||||
SLO_SUMMARY_INGEST_PIPELINE_NAME,
|
||||
} from '../../assets/constants';
|
||||
import { getSLOMappingsTemplate } from '../../assets/component_templates/slo_mappings_template';
|
||||
import { getSLOSettingsTemplate } from '../../assets/component_templates/slo_settings_template';
|
||||
import { getSLOIndexTemplate } from '../../assets/index_templates/slo_index_templates';
|
||||
import { getSLOPipelineTemplate } from '../../assets/ingest_templates/slo_pipeline_template';
|
||||
import { getSLOSummaryMappingsTemplate } from '../../assets/component_templates/slo_summary_mappings_template';
|
||||
import { getSLOSummarySettingsTemplate } from '../../assets/component_templates/slo_summary_settings_template';
|
||||
import {
|
||||
SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME,
|
||||
SLO_COMPONENT_TEMPLATE_SETTINGS_NAME,
|
||||
SLO_DESTINATION_INDEX_NAME,
|
||||
SLO_INDEX_TEMPLATE_NAME,
|
||||
SLO_INDEX_TEMPLATE_PATTERN,
|
||||
SLO_INGEST_PIPELINE_INDEX_NAME_PREFIX,
|
||||
SLO_INGEST_PIPELINE_NAME,
|
||||
SLO_RESOURCES_VERSION,
|
||||
SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME,
|
||||
SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME,
|
||||
SLO_SUMMARY_DESTINATION_INDEX_NAME,
|
||||
SLO_SUMMARY_INDEX_TEMPLATE_NAME,
|
||||
SLO_SUMMARY_INDEX_TEMPLATE_PATTERN,
|
||||
SLO_SUMMARY_INGEST_PIPELINE_NAME,
|
||||
SLO_SUMMARY_TEMP_INDEX_NAME,
|
||||
} from '../../assets/constants';
|
||||
import { getSLOIndexTemplate } from '../../assets/index_templates/slo_index_templates';
|
||||
import { getSLOSummaryIndexTemplate } from '../../assets/index_templates/slo_summary_index_templates';
|
||||
import { retryTransientEsErrors } from '../../utils/retry';
|
||||
import { getSLOPipelineTemplate } from '../../assets/ingest_templates/slo_pipeline_template';
|
||||
import { getSLOSummaryPipelineTemplate } from '../../assets/ingest_templates/slo_summary_pipeline_template';
|
||||
import { retryTransientEsErrors } from '../../utils/retry';
|
||||
|
||||
export interface ResourceInstaller {
|
||||
ensureCommonResourcesInstalled(): Promise<void>;
|
||||
|
@ -88,10 +88,9 @@ export class DefaultResourceInstaller implements ResourceInstaller {
|
|||
)
|
||||
);
|
||||
|
||||
await this.execute(() => this.esClient.indices.create({ index: SLO_DESTINATION_INDEX_NAME }));
|
||||
await this.execute(() =>
|
||||
this.esClient.indices.create({ index: SLO_SUMMARY_DESTINATION_INDEX_NAME })
|
||||
);
|
||||
await this.createIndex(SLO_DESTINATION_INDEX_NAME);
|
||||
await this.createIndex(SLO_SUMMARY_DESTINATION_INDEX_NAME);
|
||||
await this.createIndex(SLO_SUMMARY_TEMP_INDEX_NAME);
|
||||
|
||||
await this.createOrUpdateIngestPipelineTemplate(
|
||||
getSLOPipelineTemplate(SLO_INGEST_PIPELINE_NAME, SLO_INGEST_PIPELINE_INDEX_NAME_PREFIX)
|
||||
|
@ -178,20 +177,30 @@ export class DefaultResourceInstaller implements ResourceInstaller {
|
|||
}
|
||||
|
||||
private async createOrUpdateComponentTemplate(template: ClusterPutComponentTemplateRequest) {
|
||||
this.logger.debug(`Installing SLO component template ${template.name}`);
|
||||
this.logger.info(`Installing SLO component template [${template.name}]`);
|
||||
return this.execute(() => this.esClient.cluster.putComponentTemplate(template));
|
||||
}
|
||||
|
||||
private async createOrUpdateIndexTemplate(template: IndicesPutIndexTemplateRequest) {
|
||||
this.logger.debug(`Installing SLO index template ${template.name}`);
|
||||
this.logger.info(`Installing SLO index template [${template.name}]`);
|
||||
return this.execute(() => this.esClient.indices.putIndexTemplate(template));
|
||||
}
|
||||
|
||||
private async createOrUpdateIngestPipelineTemplate(template: IngestPutPipelineRequest) {
|
||||
this.logger.debug(`Installing SLO ingest pipeline template ${template.id}`);
|
||||
this.logger.info(`Installing SLO ingest pipeline [${template.id}]`);
|
||||
return this.execute(() => this.esClient.ingest.putPipeline(template));
|
||||
}
|
||||
|
||||
private async createIndex(indexName: string) {
|
||||
try {
|
||||
await this.execute(() => this.esClient.indices.create({ index: indexName }));
|
||||
} catch (err) {
|
||||
if (err?.meta?.body?.error?.type !== 'resource_already_exists_exception') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async execute<T>(esCall: () => Promise<T>): Promise<T> {
|
||||
return await retryTransientEsErrors(esCall, { logger: this.logger });
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { MockedLogger } from '@kbn/logging-mocks';
|
||||
import { createResourceInstallerMock, createSummaryTransformInstallerMock } from './mocks';
|
||||
import { DefaultSLOInstaller } from './slo_installer';
|
||||
|
||||
describe('SLO Installer', () => {
|
||||
let loggerMock: jest.Mocked<MockedLogger>;
|
||||
|
||||
beforeEach(() => {
|
||||
loggerMock = loggingSystemMock.createLogger();
|
||||
});
|
||||
|
||||
it('handles concurrent installation', async () => {
|
||||
const resourceInstaller = createResourceInstallerMock();
|
||||
const summaryTransformInstaller = createSummaryTransformInstallerMock();
|
||||
const service = new DefaultSLOInstaller(
|
||||
resourceInstaller,
|
||||
summaryTransformInstaller,
|
||||
loggerMock
|
||||
);
|
||||
|
||||
await Promise.all([service.install(), service.install()]);
|
||||
|
||||
expect(resourceInstaller.ensureCommonResourcesInstalled).toHaveBeenCalledTimes(1);
|
||||
expect(summaryTransformInstaller.installAndStart).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -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 { Logger } from '@kbn/core/server';
|
||||
import { ResourceInstaller, SummaryTransformInstaller } from '.';
|
||||
|
||||
export interface SLOInstaller {
|
||||
install(): Promise<void>;
|
||||
}
|
||||
|
||||
export class DefaultSLOInstaller implements SLOInstaller {
|
||||
private isInstalling: boolean = false;
|
||||
|
||||
constructor(
|
||||
private sloResourceInstaller: ResourceInstaller,
|
||||
private sloSummaryInstaller: SummaryTransformInstaller,
|
||||
private logger: Logger
|
||||
) {}
|
||||
|
||||
public async install() {
|
||||
if (this.isInstalling || process.env.CI) {
|
||||
return;
|
||||
}
|
||||
this.isInstalling = true;
|
||||
|
||||
let installTimeout;
|
||||
try {
|
||||
installTimeout = setTimeout(() => (this.isInstalling = false), 60000);
|
||||
|
||||
await this.sloResourceInstaller.ensureCommonResourcesInstalled();
|
||||
await this.sloSummaryInstaller.installAndStart();
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to install SLO common resources and summary transforms', {
|
||||
error,
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
this.isInstalling = false;
|
||||
clearTimeout(installTimeout);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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 { ElasticsearchClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import {
|
||||
aHitFromSummaryIndex,
|
||||
aHitFromTempSummaryIndex,
|
||||
aSummaryDocument,
|
||||
} from './fixtures/summary_search_document';
|
||||
import {
|
||||
DefaultSummarySearchClient,
|
||||
Pagination,
|
||||
Sort,
|
||||
SummarySearchClient,
|
||||
} from './summary_search_client';
|
||||
|
||||
const defaultSort: Sort = {
|
||||
field: 'sli_value',
|
||||
direction: 'asc',
|
||||
};
|
||||
const defaultPagination: Pagination = {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
};
|
||||
|
||||
describe('Summary Search Client', () => {
|
||||
let esClientMock: ElasticsearchClientMock;
|
||||
let service: SummarySearchClient;
|
||||
|
||||
beforeEach(() => {
|
||||
esClientMock = elasticsearchServiceMock.createElasticsearchClient();
|
||||
service = new DefaultSummarySearchClient(esClientMock, loggerMock.create());
|
||||
});
|
||||
|
||||
it('returns an empty response on error', async () => {
|
||||
esClientMock.count.mockRejectedValue(new Error('Cannot reach es'));
|
||||
|
||||
await expect(service.search('', defaultSort, defaultPagination)).resolves
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"page": 1,
|
||||
"perPage": 20,
|
||||
"results": Array [],
|
||||
"total": 0,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns an empty response when the kql filter returns no document count', async () => {
|
||||
esClientMock.count.mockResolvedValue({
|
||||
count: 0,
|
||||
_shards: { failed: 0, successful: 1, total: 1 },
|
||||
});
|
||||
|
||||
await expect(service.search('', defaultSort, defaultPagination)).resolves
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"page": 1,
|
||||
"perPage": 20,
|
||||
"results": Array [],
|
||||
"total": 0,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns the summary documents without duplicate temporary summary documents', async () => {
|
||||
const SLO_ID1 = 'slo-one';
|
||||
const SLO_ID2 = 'slo_two';
|
||||
const SLO_ID3 = 'slo-three';
|
||||
const SLO_ID4 = 'slo-four';
|
||||
const SLO_ID5 = 'slo-five';
|
||||
esClientMock.count.mockResolvedValue({
|
||||
count: 8,
|
||||
_shards: { failed: 0, successful: 1, total: 1 },
|
||||
});
|
||||
esClientMock.search.mockResolvedValue({
|
||||
took: 0,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 2,
|
||||
successful: 2,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
},
|
||||
hits: {
|
||||
total: {
|
||||
value: 6,
|
||||
relation: 'eq',
|
||||
},
|
||||
max_score: 1,
|
||||
hits: [
|
||||
aHitFromSummaryIndex(aSummaryDocument({ id: SLO_ID1 })),
|
||||
aHitFromSummaryIndex(aSummaryDocument({ id: SLO_ID2 })),
|
||||
aHitFromSummaryIndex(aSummaryDocument({ id: SLO_ID3 })),
|
||||
aHitFromSummaryIndex(aSummaryDocument({ id: SLO_ID5 })), // no related temp doc
|
||||
aHitFromTempSummaryIndex(aSummaryDocument({ id: SLO_ID1, isTempDoc: true })), // removed as dup
|
||||
aHitFromTempSummaryIndex(aSummaryDocument({ id: SLO_ID2, isTempDoc: true })), // removed as dup
|
||||
aHitFromTempSummaryIndex(aSummaryDocument({ id: SLO_ID3, isTempDoc: true })), // removed as dup
|
||||
aHitFromTempSummaryIndex(aSummaryDocument({ id: SLO_ID4, isTempDoc: true })), // kept
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const results = await service.search('', defaultSort, defaultPagination);
|
||||
|
||||
expect(esClientMock.deleteByQuery).toHaveBeenCalled();
|
||||
expect(esClientMock.deleteByQuery.mock.calls[0]).toMatchSnapshot();
|
||||
expect(results).toMatchSnapshot();
|
||||
expect(results.total).toBe(5);
|
||||
});
|
||||
});
|
|
@ -7,8 +7,10 @@
|
|||
|
||||
import { ElasticsearchClient, Logger } from '@kbn/core/server';
|
||||
import { assertNever } from '@kbn/std';
|
||||
import _ from 'lodash';
|
||||
import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../assets/constants';
|
||||
import { SLOId, Status, Summary } from '../../domain/models';
|
||||
import { toHighPrecision } from '../../utils/number';
|
||||
import { getElastichsearchQueryOrThrow } from './transform_generators';
|
||||
|
||||
interface EsSummaryDocument {
|
||||
|
@ -23,6 +25,7 @@ interface EsSummaryDocument {
|
|||
errorBudgetEstimated: boolean;
|
||||
statusCode: number;
|
||||
status: Status;
|
||||
isTempDoc: boolean;
|
||||
}
|
||||
|
||||
export interface Paginated<T> {
|
||||
|
@ -61,39 +64,72 @@ export class DefaultSummarySearchClient implements SummarySearchClient {
|
|||
pagination: Pagination
|
||||
): Promise<Paginated<SLOSummary>> {
|
||||
try {
|
||||
const result = await this.esClient.search<EsSummaryDocument>({
|
||||
const { count: total } = await this.esClient.count({
|
||||
index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
|
||||
query: getElastichsearchQueryOrThrow(kqlQuery),
|
||||
});
|
||||
|
||||
if (total === 0) {
|
||||
return { total: 0, perPage: pagination.perPage, page: pagination.page, results: [] };
|
||||
}
|
||||
|
||||
const summarySearch = await this.esClient.search<EsSummaryDocument>({
|
||||
index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
|
||||
query: getElastichsearchQueryOrThrow(kqlQuery),
|
||||
sort: {
|
||||
// non-temp first, then temp documents
|
||||
isTempDoc: {
|
||||
order: 'asc',
|
||||
},
|
||||
[toDocumentSortField(sort.field)]: {
|
||||
order: sort.direction,
|
||||
},
|
||||
},
|
||||
from: (pagination.page - 1) * pagination.perPage,
|
||||
size: pagination.perPage,
|
||||
size: pagination.perPage * 2, // twice as much as we return, in case they are all duplicate temp/non-temp summary
|
||||
});
|
||||
|
||||
const total =
|
||||
typeof result.hits.total === 'number' ? result.hits.total : result.hits.total?.value;
|
||||
const [tempSummaryDocuments, summaryDocuments] = _.partition(
|
||||
summarySearch.hits.hits,
|
||||
(doc) => !!doc._source?.isTempDoc
|
||||
);
|
||||
|
||||
if (total === undefined || total === 0) {
|
||||
return { total: 0, perPage: pagination.perPage, page: pagination.page, results: [] };
|
||||
}
|
||||
// Always attempt to delete temporary summary documents with an existing non-temp summary document
|
||||
// The temp summary documents are _eventually_ removed as we get through the real summary documents
|
||||
const summarySloIds = summaryDocuments.map((doc) => doc._source?.slo.id);
|
||||
await this.esClient.deleteByQuery({
|
||||
index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
|
||||
wait_for_completion: false,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [{ terms: { 'slo.id': summarySloIds } }, { term: { isTempDoc: true } }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const tempSummaryDocumentsDeduped = tempSummaryDocuments.filter(
|
||||
(doc) => !summarySloIds.includes(doc._source?.slo.id)
|
||||
);
|
||||
|
||||
const finalResults = summaryDocuments
|
||||
.concat(tempSummaryDocumentsDeduped)
|
||||
.slice(0, pagination.perPage);
|
||||
|
||||
const finalTotal = total - (tempSummaryDocuments.length - tempSummaryDocumentsDeduped.length);
|
||||
return {
|
||||
total,
|
||||
total: finalTotal,
|
||||
perPage: pagination.perPage,
|
||||
page: pagination.page,
|
||||
results: result.hits.hits.map((doc) => ({
|
||||
results: finalResults.map((doc) => ({
|
||||
id: doc._source!.slo.id,
|
||||
summary: {
|
||||
errorBudget: {
|
||||
initial: doc._source!.errorBudgetInitial,
|
||||
consumed: doc._source!.errorBudgetConsumed,
|
||||
remaining: doc._source!.errorBudgetRemaining,
|
||||
initial: toHighPrecision(doc._source!.errorBudgetInitial),
|
||||
consumed: toHighPrecision(doc._source!.errorBudgetConsumed),
|
||||
remaining: toHighPrecision(doc._source!.errorBudgetRemaining),
|
||||
isEstimated: doc._source!.errorBudgetEstimated,
|
||||
},
|
||||
sliValue: doc._source!.sliValue,
|
||||
sliValue: toHighPrecision(doc._source!.sliValue),
|
||||
status: doc._source!.status,
|
||||
},
|
||||
})),
|
||||
|
|
|
@ -85,6 +85,11 @@ Array [
|
|||
"field": "errorBudgetEstimated",
|
||||
},
|
||||
},
|
||||
"isTempDoc": Object {
|
||||
"terms": Object {
|
||||
"field": "isTempDoc",
|
||||
},
|
||||
},
|
||||
"service.environment": Object {
|
||||
"terms": Object {
|
||||
"field": "service.environment",
|
||||
|
@ -200,11 +205,15 @@ Array [
|
|||
"script": "emit(false)",
|
||||
"type": "boolean",
|
||||
},
|
||||
"isTempDoc": Object {
|
||||
"script": "emit(false)",
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
},
|
||||
"sync": Object {
|
||||
"time": Object {
|
||||
"delay": "60s",
|
||||
"delay": "125s",
|
||||
"field": "@timestamp",
|
||||
},
|
||||
},
|
||||
|
@ -299,6 +308,11 @@ Array [
|
|||
"field": "errorBudgetEstimated",
|
||||
},
|
||||
},
|
||||
"isTempDoc": Object {
|
||||
"terms": Object {
|
||||
"field": "isTempDoc",
|
||||
},
|
||||
},
|
||||
"service.environment": Object {
|
||||
"terms": Object {
|
||||
"field": "service.environment",
|
||||
|
@ -414,11 +428,15 @@ Array [
|
|||
"script": "emit(false)",
|
||||
"type": "boolean",
|
||||
},
|
||||
"isTempDoc": Object {
|
||||
"script": "emit(false)",
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
},
|
||||
"sync": Object {
|
||||
"time": Object {
|
||||
"delay": "60s",
|
||||
"delay": "125s",
|
||||
"field": "@timestamp",
|
||||
},
|
||||
},
|
||||
|
@ -513,6 +531,11 @@ Array [
|
|||
"field": "errorBudgetEstimated",
|
||||
},
|
||||
},
|
||||
"isTempDoc": Object {
|
||||
"terms": Object {
|
||||
"field": "isTempDoc",
|
||||
},
|
||||
},
|
||||
"service.environment": Object {
|
||||
"terms": Object {
|
||||
"field": "service.environment",
|
||||
|
@ -628,11 +651,15 @@ Array [
|
|||
"script": "emit(false)",
|
||||
"type": "boolean",
|
||||
},
|
||||
"isTempDoc": Object {
|
||||
"script": "emit(false)",
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
},
|
||||
"sync": Object {
|
||||
"time": Object {
|
||||
"delay": "60s",
|
||||
"delay": "125s",
|
||||
"field": "@timestamp",
|
||||
},
|
||||
},
|
||||
|
@ -740,6 +767,11 @@ Array [
|
|||
"field": "errorBudgetEstimated",
|
||||
},
|
||||
},
|
||||
"isTempDoc": Object {
|
||||
"terms": Object {
|
||||
"field": "isTempDoc",
|
||||
},
|
||||
},
|
||||
"service.environment": Object {
|
||||
"terms": Object {
|
||||
"field": "service.environment",
|
||||
|
@ -855,11 +887,15 @@ Array [
|
|||
"script": "emit(false)",
|
||||
"type": "boolean",
|
||||
},
|
||||
"isTempDoc": Object {
|
||||
"script": "emit(false)",
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
},
|
||||
"sync": Object {
|
||||
"time": Object {
|
||||
"delay": "60s",
|
||||
"delay": "125s",
|
||||
"field": "@timestamp",
|
||||
},
|
||||
},
|
||||
|
@ -982,6 +1018,11 @@ Array [
|
|||
"field": "errorBudgetEstimated",
|
||||
},
|
||||
},
|
||||
"isTempDoc": Object {
|
||||
"terms": Object {
|
||||
"field": "isTempDoc",
|
||||
},
|
||||
},
|
||||
"service.environment": Object {
|
||||
"terms": Object {
|
||||
"field": "service.environment",
|
||||
|
@ -1097,11 +1138,15 @@ Array [
|
|||
"script": "emit(false)",
|
||||
"type": "boolean",
|
||||
},
|
||||
"isTempDoc": Object {
|
||||
"script": "emit(false)",
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
},
|
||||
"sync": Object {
|
||||
"time": Object {
|
||||
"delay": "60s",
|
||||
"delay": "125s",
|
||||
"field": "@timestamp",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { SLO } from '../../../../domain/models';
|
||||
|
||||
export function createTempSummaryDocument(slo: SLO) {
|
||||
return {
|
||||
service: {
|
||||
environment: null,
|
||||
name: null,
|
||||
},
|
||||
transaction: {
|
||||
name: null,
|
||||
type: null,
|
||||
},
|
||||
slo: {
|
||||
indicator: {
|
||||
type: slo.indicator.type,
|
||||
},
|
||||
timeWindow: {
|
||||
duration: slo.timeWindow.duration.format(),
|
||||
type: slo.timeWindow.type,
|
||||
},
|
||||
instanceId: '*',
|
||||
name: slo.name,
|
||||
description: slo.description,
|
||||
id: slo.id,
|
||||
budgetingMethod: slo.budgetingMethod,
|
||||
revision: slo.revision,
|
||||
tags: slo.tags,
|
||||
},
|
||||
goodEvents: 0,
|
||||
totalEvents: 0,
|
||||
errorBudgetEstimated: false,
|
||||
errorBudgetRemaining: 1,
|
||||
errorBudgetConsumed: 0,
|
||||
errorBudgetInitial: 1 - slo.objective.target,
|
||||
sliValue: -1,
|
||||
statusCode: 0,
|
||||
status: 'NO_DATA',
|
||||
isTempDoc: true,
|
||||
};
|
||||
}
|
|
@ -51,7 +51,7 @@ export class DefaultSummaryTransformInstaller implements SummaryTransformInstall
|
|||
!!transform && transform._meta?.version !== SLO_RESOURCES_VERSION;
|
||||
|
||||
if (transformAlreadyInstalled) {
|
||||
this.logger.info(`SLO summary transform: ${transformId} already installed - skipping`);
|
||||
this.logger.info(`SLO summary transform [${transformId}] already installed - skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -63,21 +63,21 @@ export class DefaultSummaryTransformInstaller implements SummaryTransformInstall
|
|||
await this.startTransform(transformId);
|
||||
}
|
||||
|
||||
this.logger.info(`All SLO summary transforms installed and started`);
|
||||
this.logger.info(`SLO summary transforms installed and started`);
|
||||
}
|
||||
|
||||
private async installTransform(
|
||||
transformId: string,
|
||||
transformTemplate: TransformPutTransformRequest
|
||||
) {
|
||||
this.logger.info(`Installing SLO summary transform: ${transformId}`);
|
||||
this.logger.info(`Installing SLO summary transform [${transformId}]`);
|
||||
await this.execute(() =>
|
||||
this.esClient.transform.putTransform(transformTemplate, { ignore: [409] })
|
||||
);
|
||||
}
|
||||
|
||||
private async deletePreviousTransformVersion(transformId: string) {
|
||||
this.logger.info(`Deleting previous SLO summary transform: ${transformId}`);
|
||||
this.logger.info(`Deleting previous SLO summary transform [${transformId}]`);
|
||||
await this.execute(() =>
|
||||
this.esClient.transform.stopTransform(
|
||||
{ transform_id: transformId, allow_no_match: true, force: true },
|
||||
|
@ -93,7 +93,7 @@ export class DefaultSummaryTransformInstaller implements SummaryTransformInstall
|
|||
}
|
||||
|
||||
private async startTransform(transformId: string) {
|
||||
this.logger.info(`Starting SLO summary transform: ${transformId} - noop if already running`);
|
||||
this.logger.info(`Starting SLO summary transform [${transformId}]`);
|
||||
await this.execute(() =>
|
||||
this.esClient.transform.startTransform({ transform_id: transformId }, { ignore: [409] })
|
||||
);
|
||||
|
|
|
@ -61,6 +61,12 @@ export const groupBy = {
|
|||
field: 'errorBudgetEstimated',
|
||||
},
|
||||
},
|
||||
// Differentiate the temporary document from the summary one
|
||||
isTempDoc: {
|
||||
terms: {
|
||||
field: 'isTempDoc',
|
||||
},
|
||||
},
|
||||
// optional fields: only specified for APM indicators. Must include missing_bucket:true
|
||||
'service.name': {
|
||||
terms: {
|
||||
|
|
|
@ -28,6 +28,10 @@ export const SUMMARY_OCCURRENCES_30D_ROLLING: TransformPutTransformRequest = {
|
|||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
isTempDoc: {
|
||||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -134,11 +138,12 @@ export const SUMMARY_OCCURRENCES_30D_ROLLING: TransformPutTransformRequest = {
|
|||
sync: {
|
||||
time: {
|
||||
field: '@timestamp',
|
||||
delay: '60s',
|
||||
delay: '125s',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
deduce_mappings: false,
|
||||
max_page_search_size: 8000,
|
||||
},
|
||||
_meta: {
|
||||
version: SLO_RESOURCES_VERSION,
|
||||
|
|
|
@ -28,6 +28,10 @@ export const SUMMARY_OCCURRENCES_7D_ROLLING: TransformPutTransformRequest = {
|
|||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
isTempDoc: {
|
||||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -134,7 +138,7 @@ export const SUMMARY_OCCURRENCES_7D_ROLLING: TransformPutTransformRequest = {
|
|||
sync: {
|
||||
time: {
|
||||
field: '@timestamp',
|
||||
delay: '60s',
|
||||
delay: '125s',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
|
|
|
@ -28,6 +28,10 @@ export const SUMMARY_OCCURRENCES_90D_ROLLING: TransformPutTransformRequest = {
|
|||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
isTempDoc: {
|
||||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -134,7 +138,7 @@ export const SUMMARY_OCCURRENCES_90D_ROLLING: TransformPutTransformRequest = {
|
|||
sync: {
|
||||
time: {
|
||||
field: '@timestamp',
|
||||
delay: '60s',
|
||||
delay: '125s',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
|
|
|
@ -28,6 +28,10 @@ export const SUMMARY_OCCURRENCES_MONTHLY_ALIGNED: TransformPutTransformRequest =
|
|||
type: 'boolean',
|
||||
script: 'emit(true)',
|
||||
},
|
||||
isTempDoc: {
|
||||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -132,7 +136,7 @@ export const SUMMARY_OCCURRENCES_MONTHLY_ALIGNED: TransformPutTransformRequest =
|
|||
sync: {
|
||||
time: {
|
||||
field: '@timestamp',
|
||||
delay: '60s',
|
||||
delay: '125s',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
|
|
|
@ -28,6 +28,10 @@ export const SUMMARY_OCCURRENCES_WEEKLY_ALIGNED: TransformPutTransformRequest =
|
|||
type: 'boolean',
|
||||
script: 'emit(true)',
|
||||
},
|
||||
isTempDoc: {
|
||||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -132,7 +136,7 @@ export const SUMMARY_OCCURRENCES_WEEKLY_ALIGNED: TransformPutTransformRequest =
|
|||
sync: {
|
||||
time: {
|
||||
field: '@timestamp',
|
||||
delay: '60s',
|
||||
delay: '125s',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
|
|
|
@ -28,6 +28,10 @@ export const SUMMARY_TIMESLICES_30D_ROLLING: TransformPutTransformRequest = {
|
|||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
isTempDoc: {
|
||||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -134,7 +138,7 @@ export const SUMMARY_TIMESLICES_30D_ROLLING: TransformPutTransformRequest = {
|
|||
sync: {
|
||||
time: {
|
||||
field: '@timestamp',
|
||||
delay: '60s',
|
||||
delay: '125s',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
|
|
|
@ -28,6 +28,10 @@ export const SUMMARY_TIMESLICES_7D_ROLLING: TransformPutTransformRequest = {
|
|||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
isTempDoc: {
|
||||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -134,7 +138,7 @@ export const SUMMARY_TIMESLICES_7D_ROLLING: TransformPutTransformRequest = {
|
|||
sync: {
|
||||
time: {
|
||||
field: '@timestamp',
|
||||
delay: '60s',
|
||||
delay: '125s',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
|
|
|
@ -28,6 +28,10 @@ export const SUMMARY_TIMESLICES_90D_ROLLING: TransformPutTransformRequest = {
|
|||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
isTempDoc: {
|
||||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -134,7 +138,7 @@ export const SUMMARY_TIMESLICES_90D_ROLLING: TransformPutTransformRequest = {
|
|||
sync: {
|
||||
time: {
|
||||
field: '@timestamp',
|
||||
delay: '60s',
|
||||
delay: '125s',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
|
|
|
@ -28,6 +28,10 @@ export const SUMMARY_TIMESLICES_MONTHLY_ALIGNED: TransformPutTransformRequest =
|
|||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
isTempDoc: {
|
||||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -162,7 +166,7 @@ export const SUMMARY_TIMESLICES_MONTHLY_ALIGNED: TransformPutTransformRequest =
|
|||
sync: {
|
||||
time: {
|
||||
field: '@timestamp',
|
||||
delay: '60s',
|
||||
delay: '125s',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
|
|
|
@ -28,6 +28,10 @@ export const SUMMARY_TIMESLICES_WEEKLY_ALIGNED: TransformPutTransformRequest = {
|
|||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
isTempDoc: {
|
||||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -147,7 +151,7 @@ export const SUMMARY_TIMESLICES_WEEKLY_ALIGNED: TransformPutTransformRequest = {
|
|||
sync: {
|
||||
time: {
|
||||
field: '@timestamp',
|
||||
delay: '60s',
|
||||
delay: '125s',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
|
|
|
@ -106,6 +106,19 @@ describe('UpdateSLO', () => {
|
|||
expectInstallationOfNewSLOTransform();
|
||||
});
|
||||
|
||||
it('index a temporary summary document', async () => {
|
||||
const slo = createSLO({
|
||||
id: 'unique-id',
|
||||
indicator: createAPMTransactionErrorRateIndicator({ environment: 'development' }),
|
||||
});
|
||||
mockRepository.findById.mockResolvedValueOnce(slo);
|
||||
|
||||
const newIndicator = createAPMTransactionErrorRateIndicator({ environment: 'production' });
|
||||
await updateSLO.execute(slo.id, { indicator: newIndicator });
|
||||
|
||||
expect(mockEsClient.index.mock.calls[0]).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('removes the obsolete data from the SLO previous revision', async () => {
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionErrorRateIndicator({ environment: 'development' }),
|
||||
|
|
|
@ -11,10 +11,12 @@ import {
|
|||
getSLOTransformId,
|
||||
SLO_DESTINATION_INDEX_PATTERN,
|
||||
SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
|
||||
SLO_SUMMARY_TEMP_INDEX_NAME,
|
||||
} from '../../assets/constants';
|
||||
import { SLO } from '../../domain/models';
|
||||
import { validateSLO } from '../../domain/services';
|
||||
import { SLORepository } from './slo_repository';
|
||||
import { createTempSummaryDocument } from './summary_transform/helpers/create_temp_summary';
|
||||
import { TransformManager } from './transform_manager';
|
||||
|
||||
export class UpdateSLO {
|
||||
|
@ -38,6 +40,12 @@ export class UpdateSLO {
|
|||
await this.transformManager.install(updatedSlo);
|
||||
await this.transformManager.start(getSLOTransformId(updatedSlo.id, updatedSlo.revision));
|
||||
|
||||
await this.esClient.index({
|
||||
index: SLO_SUMMARY_TEMP_INDEX_NAME,
|
||||
id: `slo-${updatedSlo.id}`,
|
||||
document: createTempSummaryDocument(updatedSlo),
|
||||
});
|
||||
|
||||
return this.toResponse(updatedSlo);
|
||||
}
|
||||
|
||||
|
|
|
@ -27619,7 +27619,6 @@
|
|||
"xpack.observability.slo.duration.minutely": "Par minute",
|
||||
"xpack.observability.slo.duration.monthly": "Mensuel",
|
||||
"xpack.observability.slo.duration.weekly": "Hebdomadaire",
|
||||
"xpack.observability.slo.duration.yearly": "Annuel",
|
||||
"xpack.observability.slo.feedbackButtonLabel": "Dites-nous ce que vous pensez !",
|
||||
"xpack.observability.slo.globalDiagnosis.errorNotification": "Vous ne disposez pas des autorisations nécessaires pour utiliser cette fonctionnalité.",
|
||||
"xpack.observability.slo.indicators.apmAvailability": "Disponibilité APM",
|
||||
|
|
|
@ -27619,7 +27619,6 @@
|
|||
"xpack.observability.slo.duration.minutely": "毎分",
|
||||
"xpack.observability.slo.duration.monthly": "月ごと",
|
||||
"xpack.observability.slo.duration.weekly": "週ごと",
|
||||
"xpack.observability.slo.duration.yearly": "年ごと",
|
||||
"xpack.observability.slo.feedbackButtonLabel": "ご意見をお聞かせください。",
|
||||
"xpack.observability.slo.globalDiagnosis.errorNotification": "この機能を使用する権限がありません。",
|
||||
"xpack.observability.slo.indicators.apmAvailability": "APM可用性",
|
||||
|
|
|
@ -27617,7 +27617,6 @@
|
|||
"xpack.observability.slo.duration.minutely": "每分钟",
|
||||
"xpack.observability.slo.duration.monthly": "每月",
|
||||
"xpack.observability.slo.duration.weekly": "每周",
|
||||
"xpack.observability.slo.duration.yearly": "每年",
|
||||
"xpack.observability.slo.feedbackButtonLabel": "告诉我们您的看法!",
|
||||
"xpack.observability.slo.globalDiagnosis.errorNotification": "您没有适当权限,无法使用此功能。",
|
||||
"xpack.observability.slo.indicators.apmAvailability": "APM 可用性",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue