mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
feat(slo): SLO grouping values selector (#202364)
This commit is contained in:
parent
c5cc1532d7
commit
7806861c5f
24 changed files with 645 additions and 254 deletions
|
@ -1,22 +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 * as t from 'io-ts';
|
||||
import { allOrAnyStringOrArray } from '../../schema';
|
||||
|
||||
const getSLOInstancesParamsSchema = t.type({
|
||||
path: t.type({ id: t.string }),
|
||||
});
|
||||
|
||||
const getSLOInstancesResponseSchema = t.type({
|
||||
groupBy: allOrAnyStringOrArray,
|
||||
instances: t.array(t.string),
|
||||
});
|
||||
|
||||
type GetSLOInstancesResponse = t.OutputOf<typeof getSLOInstancesResponseSchema>;
|
||||
|
||||
export { getSLOInstancesParamsSchema, getSLOInstancesResponseSchema };
|
||||
export type { GetSLOInstancesResponse };
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 * as t from 'io-ts';
|
||||
import { toBooleanRt } from '@kbn/io-ts-utils';
|
||||
|
||||
const getSLOGroupingsParamsSchema = t.type({
|
||||
path: t.type({ id: t.string }),
|
||||
query: t.intersection([
|
||||
t.type({
|
||||
instanceId: t.string,
|
||||
groupingKey: t.string,
|
||||
}),
|
||||
t.partial({
|
||||
search: t.string,
|
||||
afterKey: t.string,
|
||||
size: t.string,
|
||||
excludeStale: toBooleanRt,
|
||||
remoteName: t.string,
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
const getSLOGroupingsResponseSchema = t.type({
|
||||
groupingKey: t.string,
|
||||
values: t.array(t.string),
|
||||
afterKey: t.union([t.string, t.undefined]),
|
||||
});
|
||||
|
||||
type GetSLOGroupingsParams = t.TypeOf<typeof getSLOGroupingsParamsSchema.props.query>;
|
||||
type GetSLOGroupingsResponse = t.OutputOf<typeof getSLOGroupingsResponseSchema>;
|
||||
|
||||
export { getSLOGroupingsParamsSchema, getSLOGroupingsResponseSchema };
|
||||
export type { GetSLOGroupingsResponse, GetSLOGroupingsParams };
|
|
@ -13,7 +13,7 @@ export * from './find_group';
|
|||
export * from './find_definition';
|
||||
export * from './get';
|
||||
export * from './get_burn_rates';
|
||||
export * from './get_instances';
|
||||
export * from './get_slo_groupings';
|
||||
export * from './get_preview_data';
|
||||
export * from './reset';
|
||||
export * from './manage';
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
export function LoadingState({ dataTestSubj }: { dataTestSubj?: string }) {
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" justifyContent="center" style={{ height: '100%' }}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="xxl" data-test-subj={dataTestSubj} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -32,7 +32,7 @@ export function AutoRefreshButton({ disabled, isAutoRefreshing, onClick }: Props
|
|||
data-test-subj="autoRefreshButton"
|
||||
disabled={disabled}
|
||||
iconSide="left"
|
||||
iconType="play"
|
||||
iconType="refresh"
|
||||
onClick={onClick}
|
||||
>
|
||||
{i18n.translate('xpack.slo.slosPage.autoRefreshButtonLabel', {
|
||||
|
|
|
@ -67,6 +67,15 @@ export const sloKeys = {
|
|||
groupings?: Record<string, unknown>
|
||||
) => [...sloKeys.all, 'preview', indicator, range, groupings] as const,
|
||||
burnRateRules: (search: string) => [...sloKeys.all, 'burnRateRules', search],
|
||||
groupings: (params: {
|
||||
sloId: string;
|
||||
instanceId: string;
|
||||
groupingKey: string;
|
||||
search?: string;
|
||||
afterKey?: string;
|
||||
excludeStale?: boolean;
|
||||
remoteName?: string;
|
||||
}) => [...sloKeys.all, 'fetch_slo_groupings', params] as const,
|
||||
};
|
||||
|
||||
export type SloKeys = typeof sloKeys;
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButtonIcon,
|
||||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiCopy,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import { SLOS_BASE_PATH } from '../../../../../common/locators/paths';
|
||||
import { useFetchSloGroupings } from '../../hooks/use_fetch_slo_instances';
|
||||
import { useGetQueryParams } from '../../hooks/use_get_query_params';
|
||||
|
||||
interface Props {
|
||||
slo: SLOWithSummaryResponse;
|
||||
groupingKey: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
interface Field {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function SLOGroupingValueSelector({ slo, groupingKey, value }: Props) {
|
||||
const isAvailable = window.location.pathname.includes(SLOS_BASE_PATH);
|
||||
const { search: searchParams } = useLocation();
|
||||
const history = useHistory();
|
||||
const { remoteName } = useGetQueryParams();
|
||||
|
||||
const [currentValue, setCurrentValue] = useState<string | undefined>(value);
|
||||
const [options, setOptions] = useState<Field[]>([]);
|
||||
const [search, setSearch] = useState<string | undefined>(undefined);
|
||||
const [debouncedSearch, setDebouncedSearch] = useState<string | undefined>(undefined);
|
||||
useDebounce(() => setDebouncedSearch(search), 500, [search]);
|
||||
|
||||
const { isLoading, isError, data } = useFetchSloGroupings({
|
||||
sloId: slo.id,
|
||||
groupingKey,
|
||||
instanceId: slo.instanceId ?? ALL_VALUE,
|
||||
search: debouncedSearch,
|
||||
remoteName,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setSearch(undefined);
|
||||
setDebouncedSearch(undefined);
|
||||
setOptions(data.values.map(toField));
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const onChange = (selected: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
const newValue = selected[0].value;
|
||||
if (!newValue) return;
|
||||
setCurrentValue(newValue);
|
||||
|
||||
const urlSearchParams = new URLSearchParams(searchParams);
|
||||
const newGroupings = { ...slo.groupings, [groupingKey]: newValue };
|
||||
urlSearchParams.set('instanceId', toInstanceId(newGroupings, slo.groupBy));
|
||||
history.replace({
|
||||
search: urlSearchParams.toString(),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiComboBox<string>
|
||||
css={css`
|
||||
max-width: 500px;
|
||||
`}
|
||||
isClearable={false}
|
||||
compressed
|
||||
prepend={groupingKey}
|
||||
append={
|
||||
currentValue ? (
|
||||
<EuiCopy textToCopy={currentValue}>
|
||||
{(copy) => (
|
||||
<EuiButtonIcon
|
||||
data-test-subj="sloSLOGroupingValueSelectorButton"
|
||||
color="text"
|
||||
iconType="copyClipboard"
|
||||
onClick={copy}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.slo.sLOGroupingValueSelector.copyButton.label',
|
||||
{
|
||||
defaultMessage: 'Copy value to clipboard',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</EuiCopy>
|
||||
) : (
|
||||
<EuiButtonIcon
|
||||
data-test-subj="sloSLOGroupingValueSelectorButton"
|
||||
color="text"
|
||||
disabled={true}
|
||||
iconType="copyClipboard"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.slo.sLOGroupingValueSelector.copyButton.noValueLabel',
|
||||
{ defaultMessage: 'Select a value before' }
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
options={options}
|
||||
isLoading={isLoading}
|
||||
isDisabled={isError || !isAvailable}
|
||||
placeholder={i18n.translate('xpack.slo.sLOGroupingValueSelector.placeholder', {
|
||||
defaultMessage: 'Select a group value',
|
||||
})}
|
||||
selectedOptions={currentValue ? [toField(currentValue)] : []}
|
||||
onChange={onChange}
|
||||
truncationProps={{
|
||||
truncation: 'end',
|
||||
}}
|
||||
onSearchChange={(searchValue: string) => {
|
||||
if (searchValue !== '') {
|
||||
setSearch(searchValue);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
function toField(value: string): Field {
|
||||
return { label: value, value };
|
||||
}
|
||||
|
||||
function toInstanceId(
|
||||
groupings: Record<string, string | number>,
|
||||
groupBy: string | string[]
|
||||
): string {
|
||||
const groups = [groupBy].flat();
|
||||
return groups.map((group) => groupings[group]).join(',');
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import React from 'react';
|
||||
import { SLOGroupingValueSelector } from './slo_grouping_value_selector';
|
||||
|
||||
export function SLOGroupings({ slo }: { slo: SLOWithSummaryResponse }) {
|
||||
const groupings = Object.entries(slo.groupings ?? {});
|
||||
|
||||
if (!groupings.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="row" gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs">
|
||||
<h4>
|
||||
{i18n.translate('xpack.slo.sloDetails.groupings.title', {
|
||||
defaultMessage: 'Instance',
|
||||
})}
|
||||
</h4>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{groupings.map(([groupingKey, groupingValue]) => {
|
||||
return (
|
||||
<SLOGroupingValueSelector
|
||||
key={groupingKey}
|
||||
slo={slo}
|
||||
groupingKey={groupingKey}
|
||||
value={String(groupingValue)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -5,11 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ComponentStory } from '@storybook/react';
|
||||
|
||||
import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator';
|
||||
import React from 'react';
|
||||
import { buildSlo } from '../../../data/slo/slo';
|
||||
import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator';
|
||||
import { HeaderControl as Component, Props } from './header_control';
|
||||
|
||||
export default {
|
||||
|
@ -22,11 +21,10 @@ const Template: ComponentStory<typeof Component> = (props: Props) => <Component
|
|||
|
||||
const defaultProps: Props = {
|
||||
slo: buildSlo(),
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = defaultProps;
|
||||
|
||||
export const WithLoading = Template.bind({});
|
||||
WithLoading.args = { slo: undefined, isLoading: true };
|
||||
WithLoading.args = { slo: undefined };
|
||||
|
|
|
@ -22,9 +22,9 @@ import { SloDeleteModal } from '../../../components/slo/delete_confirmation_moda
|
|||
import { SloResetConfirmationModal } from '../../../components/slo/reset_confirmation_modal/slo_reset_confirmation_modal';
|
||||
import { useCloneSlo } from '../../../hooks/use_clone_slo';
|
||||
import { useFetchRulesForSlo } from '../../../hooks/use_fetch_rules_for_slo';
|
||||
import { useKibana } from '../../../hooks/use_kibana';
|
||||
import { usePermissions } from '../../../hooks/use_permissions';
|
||||
import { useResetSlo } from '../../../hooks/use_reset_slo';
|
||||
import { useKibana } from '../../../hooks/use_kibana';
|
||||
import { convertSliApmParamsToApmAppDeeplinkUrl } from '../../../utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url';
|
||||
import { isApmIndicatorType } from '../../../utils/slo/indicator';
|
||||
import { EditBurnRateRuleFlyout } from '../../slos/components/common/edit_burn_rate_rule_flyout';
|
||||
|
@ -32,11 +32,10 @@ import { useGetQueryParams } from '../hooks/use_get_query_params';
|
|||
import { useSloActions } from '../hooks/use_slo_actions';
|
||||
|
||||
export interface Props {
|
||||
slo?: SLOWithSummaryResponse;
|
||||
isLoading: boolean;
|
||||
slo: SLOWithSummaryResponse;
|
||||
}
|
||||
|
||||
export function HeaderControl({ isLoading, slo }: Props) {
|
||||
export function HeaderControl({ slo }: Props) {
|
||||
const {
|
||||
application: { navigateToUrl, capabilities },
|
||||
http: { basePath },
|
||||
|
@ -58,10 +57,10 @@ export function HeaderControl({ isLoading, slo }: Props) {
|
|||
const { mutateAsync: resetSlo, isLoading: isResetLoading } = useResetSlo();
|
||||
|
||||
const { data: rulesBySlo, refetchRules } = useFetchRulesForSlo({
|
||||
sloIds: slo ? [slo.id] : undefined,
|
||||
sloIds: [slo.id],
|
||||
});
|
||||
|
||||
const rules = slo ? rulesBySlo?.[slo?.id] ?? [] : [];
|
||||
const rules = rulesBySlo?.[slo.id] ?? [];
|
||||
|
||||
const handleActionsClick = () => setIsPopoverOpen((value) => !value);
|
||||
const closePopover = () => setIsPopoverOpen(false);
|
||||
|
@ -92,10 +91,6 @@ export function HeaderControl({ isLoading, slo }: Props) {
|
|||
});
|
||||
|
||||
const handleNavigateToApm = () => {
|
||||
if (!slo) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const url = convertSliApmParamsToApmAppDeeplinkUrl(slo);
|
||||
if (url) {
|
||||
navigateToUrl(basePath.prepend(url));
|
||||
|
@ -105,10 +100,8 @@ export function HeaderControl({ isLoading, slo }: Props) {
|
|||
const navigateToClone = useCloneSlo();
|
||||
|
||||
const handleClone = async () => {
|
||||
if (slo) {
|
||||
setIsPopoverOpen(false);
|
||||
navigateToClone(slo);
|
||||
}
|
||||
setIsPopoverOpen(false);
|
||||
navigateToClone(slo);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
|
@ -140,11 +133,9 @@ export function HeaderControl({ isLoading, slo }: Props) {
|
|||
};
|
||||
|
||||
const handleResetConfirm = async () => {
|
||||
if (slo) {
|
||||
await resetSlo({ id: slo.id, name: slo.name });
|
||||
removeResetQueryParam();
|
||||
setResetConfirmationModalOpen(false);
|
||||
}
|
||||
await resetSlo({ id: slo.id, name: slo.name });
|
||||
removeResetQueryParam();
|
||||
setResetConfirmationModalOpen(false);
|
||||
};
|
||||
|
||||
const handleResetCancel = () => {
|
||||
|
@ -182,8 +173,6 @@ export function HeaderControl({ isLoading, slo }: Props) {
|
|||
iconType="arrowDown"
|
||||
iconSize="s"
|
||||
onClick={handleActionsClick}
|
||||
isLoading={isLoading}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{i18n.translate('xpack.slo.sloDetails.headerControl.actions', {
|
||||
defaultMessage: 'Actions',
|
||||
|
@ -315,7 +304,7 @@ export function HeaderControl({ isLoading, slo }: Props) {
|
|||
refetchRules={refetchRules}
|
||||
/>
|
||||
|
||||
{slo && isRuleFlyoutVisible ? (
|
||||
{isRuleFlyoutVisible ? (
|
||||
<AddRuleFlyout
|
||||
consumer={sloFeatureId}
|
||||
ruleTypeId={SLO_BURN_RATE_RULE_TYPE_ID}
|
||||
|
@ -326,11 +315,11 @@ export function HeaderControl({ isLoading, slo }: Props) {
|
|||
/>
|
||||
) : null}
|
||||
|
||||
{slo && isDeleteConfirmationModalOpen ? (
|
||||
{isDeleteConfirmationModalOpen ? (
|
||||
<SloDeleteModal slo={slo} onCancel={handleDeleteCancel} onSuccess={handleDeleteConfirm} />
|
||||
) : null}
|
||||
|
||||
{slo && isResetConfirmationModalOpen ? (
|
||||
{isResetConfirmationModalOpen ? (
|
||||
<SloResetConfirmationModal
|
||||
slo={slo}
|
||||
onCancel={handleResetCancel}
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSkeletonText, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSkeletonText, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { SloRemoteBadge } from '../../slos/components/badges/slo_remote_badge';
|
||||
import { SLOGroupings } from '../../slos/components/common/slo_groupings';
|
||||
import { SloStatusBadge } from '../../../components/slo/slo_status_badge';
|
||||
import { SloRemoteBadge } from '../../slos/components/badges/slo_remote_badge';
|
||||
import { SLOGroupings } from './groupings/slo_groupings';
|
||||
|
||||
export interface Props {
|
||||
slo?: SLOWithSummaryResponse;
|
||||
|
@ -21,13 +21,11 @@ export interface Props {
|
|||
|
||||
export function HeaderTitle({ isLoading, slo }: Props) {
|
||||
if (isLoading || !slo) {
|
||||
return <EuiSkeletonText lines={1} data-test-subj="loadingTitle" />;
|
||||
return <EuiSkeletonText lines={2} data-test-subj="loadingTitle" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="xs">
|
||||
<SLOGroupings slo={slo} />
|
||||
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
|
@ -61,7 +59,7 @@ export function HeaderTitle({ isLoading, slo }: Props) {
|
|||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
<SLOGroupings slo={slo} />
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ALL_VALUE, GetSLOGroupingsResponse } from '@kbn/slo-schema';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { sloKeys } from '../../../hooks/query_key_factory';
|
||||
import { usePluginContext } from '../../../hooks/use_plugin_context';
|
||||
|
||||
interface Params {
|
||||
sloId: string;
|
||||
groupingKey: string;
|
||||
instanceId: string;
|
||||
afterKey?: string;
|
||||
search?: string;
|
||||
remoteName?: string;
|
||||
}
|
||||
|
||||
interface UseFetchSloGroupingsResponse {
|
||||
data: GetSLOGroupingsResponse | undefined;
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
}
|
||||
|
||||
export function useFetchSloGroupings({
|
||||
sloId,
|
||||
groupingKey,
|
||||
instanceId,
|
||||
afterKey,
|
||||
search,
|
||||
remoteName,
|
||||
}: Params): UseFetchSloGroupingsResponse {
|
||||
const { sloClient } = usePluginContext();
|
||||
|
||||
const { isLoading, isError, data } = useQuery({
|
||||
queryKey: sloKeys.groupings({ sloId, groupingKey, instanceId, afterKey, search, remoteName }),
|
||||
queryFn: async ({ signal }) => {
|
||||
try {
|
||||
return await sloClient.fetch(`GET /internal/observability/slos/{id}/_groupings`, {
|
||||
params: {
|
||||
path: { id: sloId },
|
||||
query: {
|
||||
search,
|
||||
instanceId,
|
||||
groupingKey,
|
||||
afterKey,
|
||||
excludeStale: true,
|
||||
remoteName,
|
||||
},
|
||||
},
|
||||
signal,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Something went wrong. Error: ${error}`);
|
||||
}
|
||||
},
|
||||
enabled: Boolean(!!sloId && !!groupingKey && instanceId !== ALL_VALUE),
|
||||
staleTime: 60 * 1000,
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
return { isLoading, isError, data };
|
||||
}
|
|
@ -5,13 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiNotificationBadge, EuiToolTip } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import React from 'react';
|
||||
import { paths } from '../../../../common/locators/paths';
|
||||
import { useKibana } from '../../../hooks/use_kibana';
|
||||
import { useFetchActiveAlerts } from '../../../hooks/use_fetch_active_alerts';
|
||||
import { useKibana } from '../../../hooks/use_kibana';
|
||||
import {
|
||||
ALERTS_TAB_ID,
|
||||
HISTORY_TAB_ID,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiLoadingSpinner, EuiSkeletonText } from '@elastic/eui';
|
||||
import { EuiSkeletonText } from '@elastic/eui';
|
||||
import type { ChromeBreadcrumb } from '@kbn/core-chrome-browser';
|
||||
import type { IBasePath } from '@kbn/core-http-browser';
|
||||
import { usePerformanceContext } from '@kbn/ebt-tools';
|
||||
|
@ -16,15 +16,16 @@ import { useIsMutating } from '@tanstack/react-query';
|
|||
import dedent from 'dedent';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { LoadingState } from '../../components/loading_state';
|
||||
import { paths } from '../../../common/locators/paths';
|
||||
import { HeaderMenu } from '../../components/header_menu/header_menu';
|
||||
import { AutoRefreshButton } from '../../components/slo/auto_refresh_button';
|
||||
import { useAutoRefreshStorage } from '../../components/slo/auto_refresh_button/hooks/use_auto_refresh_storage';
|
||||
import { useFetchSloDetails } from '../../hooks/use_fetch_slo_details';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
import { useLicense } from '../../hooks/use_license';
|
||||
import { usePermissions } from '../../hooks/use_permissions';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
import PageNotFound from '../404';
|
||||
import { HeaderControl } from './components/header_control';
|
||||
import { HeaderTitle } from './components/header_title';
|
||||
|
@ -125,21 +126,23 @@ export function SloDetailsPage() {
|
|||
pageHeader={{
|
||||
pageTitle: slo?.name ?? <EuiSkeletonText lines={1} />,
|
||||
children: <HeaderTitle isLoading={isPerformingAction} slo={slo} />,
|
||||
rightSideItems: [
|
||||
<HeaderControl isLoading={isPerformingAction} slo={slo} />,
|
||||
<AutoRefreshButton
|
||||
disabled={isPerformingAction}
|
||||
isAutoRefreshing={isAutoRefreshing}
|
||||
onClick={handleToggleAutoRefresh}
|
||||
/>,
|
||||
],
|
||||
rightSideItems: !isLoading
|
||||
? [
|
||||
<HeaderControl slo={slo!} />,
|
||||
<AutoRefreshButton
|
||||
isAutoRefreshing={isAutoRefreshing}
|
||||
onClick={handleToggleAutoRefresh}
|
||||
/>,
|
||||
]
|
||||
: undefined,
|
||||
tabs,
|
||||
}}
|
||||
data-test-subj="sloDetailsPage"
|
||||
>
|
||||
<HeaderMenu />
|
||||
{isLoading && <EuiLoadingSpinner data-test-subj="sloDetailsLoading" />}
|
||||
{!isLoading && (
|
||||
{isLoading ? (
|
||||
<LoadingState dataTestSubj="sloDetailsLoading" />
|
||||
) : (
|
||||
<SloDetails slo={slo!} isAutoRefreshing={isAutoRefreshing} selectedTabId={selectedTabId} />
|
||||
)}
|
||||
</ObservabilityPageTemplate>
|
||||
|
|
|
@ -29,6 +29,7 @@ type Meta = t.TypeOf<typeof metaSchema>;
|
|||
type GroupSummary = t.TypeOf<typeof groupSummarySchema>;
|
||||
type GroupBy = t.TypeOf<typeof groupBySchema>;
|
||||
type StoredSLOSettings = t.OutputOf<typeof sloSettingsSchema>;
|
||||
type SLOSettings = t.TypeOf<typeof sloSettingsSchema>;
|
||||
|
||||
export type {
|
||||
Objective,
|
||||
|
@ -41,4 +42,5 @@ export type {
|
|||
GroupBy,
|
||||
GroupSummary,
|
||||
StoredSLOSettings,
|
||||
SLOSettings,
|
||||
};
|
||||
|
|
|
@ -10,7 +10,6 @@ import * as t from 'io-ts';
|
|||
|
||||
type SLODefinition = t.TypeOf<typeof sloDefinitionSchema>;
|
||||
type StoredSLODefinition = t.OutputOf<typeof sloDefinitionSchema>;
|
||||
|
||||
type SLOId = t.TypeOf<typeof sloIdSchema>;
|
||||
|
||||
export type { SLODefinition, StoredSLODefinition, SLOId };
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
findSloDefinitionsParamsSchema,
|
||||
getPreviewDataParamsSchema,
|
||||
getSLOBurnRatesParamsSchema,
|
||||
getSLOInstancesParamsSchema,
|
||||
getSLOGroupingsParamsSchema,
|
||||
getSLOParamsSchema,
|
||||
manageSLOParamsSchema,
|
||||
putSLOServerlessSettingsParamsSchema,
|
||||
|
@ -49,7 +49,7 @@ import { FindSLODefinitions } from '../../services/find_slo_definitions';
|
|||
import { getBurnRates } from '../../services/get_burn_rates';
|
||||
import { getGlobalDiagnosis } from '../../services/get_diagnosis';
|
||||
import { GetPreviewData } from '../../services/get_preview_data';
|
||||
import { GetSLOInstances } from '../../services/get_slo_instances';
|
||||
import { GetSLOGroupings } from '../../services/get_slo_groupings';
|
||||
import { GetSLOSuggestions } from '../../services/get_slo_suggestions';
|
||||
import { GetSLOsOverview } from '../../services/get_slos_overview';
|
||||
import { DefaultHistoricalSummaryClient } from '../../services/historical_summary_client';
|
||||
|
@ -598,24 +598,32 @@ const fetchHistoricalSummary = createSloServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const getSLOInstancesRoute = createSloServerRoute({
|
||||
endpoint: 'GET /internal/observability/slos/{id}/_instances',
|
||||
const getSLOGroupingsRoute = createSloServerRoute({
|
||||
endpoint: 'GET /internal/observability/slos/{id}/_groupings',
|
||||
options: { access: 'internal' },
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: ['slo_read'],
|
||||
},
|
||||
},
|
||||
params: getSLOInstancesParamsSchema,
|
||||
handler: async ({ context, params, logger, plugins }) => {
|
||||
params: getSLOGroupingsParamsSchema,
|
||||
handler: async ({ context, params, request, logger, plugins }) => {
|
||||
await assertPlatinumLicense(plugins);
|
||||
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
const repository = new KibanaSavedObjectsSLORepository(soClient, logger);
|
||||
const getSLOInstances = new GetSLOInstances(repository, esClient);
|
||||
const [spaceId, settings] = await Promise.all([
|
||||
getSpaceId(plugins, request),
|
||||
getSloSettings(soClient),
|
||||
]);
|
||||
|
||||
return await executeWithErrorHandler(() => getSLOInstances.execute(params.path.id));
|
||||
const repository = new KibanaSavedObjectsSLORepository(soClient, logger);
|
||||
const definitionClient = new SloDefinitionClient(repository, esClient, logger);
|
||||
|
||||
const getSLOGroupings = new GetSLOGroupings(definitionClient, esClient, settings, spaceId);
|
||||
|
||||
return await executeWithErrorHandler(() =>
|
||||
getSLOGroupings.execute(params.path.id, params.query)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -819,7 +827,7 @@ export const getSloRouteRepository = (isServerless?: boolean) => {
|
|||
...getDiagnosisRoute,
|
||||
...getSloBurnRates,
|
||||
...getPreviewData,
|
||||
...getSLOInstancesRoute,
|
||||
...getSLOGroupingsRoute,
|
||||
...resetSLORoute,
|
||||
...findSLOGroupsRoute,
|
||||
...getSLOSuggestionsRoute,
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Get SLO Instances returns all instances of a SLO defined with a 'groupBy' 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"aggs": Object {
|
||||
"instances": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.instanceId",
|
||||
"size": 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
"index": ".slo-observability.sli-v3*",
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
"gte": "now-7d",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"slo.id": "slo-id",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"slo.revision": 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"size": 0,
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 { ALL_VALUE } from '@kbn/slo-schema';
|
||||
import { GetSLOGroupings, SLORepository } from '.';
|
||||
import { createSLO } from './fixtures/slo';
|
||||
import { createSLORepositoryMock } from './mocks';
|
||||
import { SloDefinitionClient } from './slo_definition_client';
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
selectedRemoteClusters: [],
|
||||
staleThresholdInHours: 1,
|
||||
useAllRemoteClusters: false,
|
||||
};
|
||||
|
||||
describe('Get SLO Instances', () => {
|
||||
let repositoryMock: jest.Mocked<SLORepository>;
|
||||
let esClientMock: ElasticsearchClientMock;
|
||||
let definitionClient: SloDefinitionClient;
|
||||
|
||||
beforeEach(() => {
|
||||
repositoryMock = createSLORepositoryMock();
|
||||
esClientMock = elasticsearchServiceMock.createElasticsearchClient();
|
||||
definitionClient = new SloDefinitionClient(
|
||||
repositoryMock,
|
||||
elasticsearchServiceMock.createElasticsearchClient(),
|
||||
loggerMock.create()
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when the SLO is ungrouped', async () => {
|
||||
const slo = createSLO({ groupBy: ALL_VALUE });
|
||||
repositoryMock.findById.mockResolvedValue(slo);
|
||||
|
||||
const service = new GetSLOGroupings(
|
||||
definitionClient,
|
||||
esClientMock,
|
||||
DEFAULT_SETTINGS,
|
||||
'default'
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.execute(slo.id, {
|
||||
instanceId: 'irrelevant',
|
||||
groupingKey: 'irrelevant',
|
||||
})
|
||||
).rejects.toThrowError('Ungrouped SLO cannot be queried for available groupings');
|
||||
});
|
||||
|
||||
it('throws when the provided groupingKey is not part of the SLO groupBy field', async () => {
|
||||
const slo = createSLO({ groupBy: ['abc.efg', 'host.name'] });
|
||||
repositoryMock.findById.mockResolvedValue(slo);
|
||||
|
||||
const service = new GetSLOGroupings(
|
||||
definitionClient,
|
||||
esClientMock,
|
||||
DEFAULT_SETTINGS,
|
||||
'default'
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.execute(slo.id, {
|
||||
instanceId: 'irrelevant',
|
||||
groupingKey: 'not.found',
|
||||
})
|
||||
).rejects.toThrowError("Provided groupingKey doesn't match the SLO's groupBy field");
|
||||
});
|
||||
|
||||
it('throws when the provided instanceId cannot be matched against the SLO grouping keys', async () => {
|
||||
const slo = createSLO({ groupBy: ['abc.efg', 'host.name'] });
|
||||
repositoryMock.findById.mockResolvedValue(slo);
|
||||
|
||||
const service = new GetSLOGroupings(
|
||||
definitionClient,
|
||||
esClientMock,
|
||||
DEFAULT_SETTINGS,
|
||||
'default'
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.execute(slo.id, {
|
||||
instanceId: 'too,many,values',
|
||||
groupingKey: 'host.name',
|
||||
})
|
||||
).rejects.toThrowError('Provided instanceId does not match the number of grouping keys');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* 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 { AggregationsCompositeAggregation } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import { ALL_VALUE, GetSLOGroupingsParams, GetSLOGroupingsResponse } from '@kbn/slo-schema';
|
||||
import { SLO_SUMMARY_DESTINATION_INDEX_NAME } from '../../common/constants';
|
||||
import { SLODefinition, SLOSettings } from '../domain/models';
|
||||
import { SloDefinitionClient } from './slo_definition_client';
|
||||
|
||||
const DEFAULT_SIZE = 100;
|
||||
|
||||
export class GetSLOGroupings {
|
||||
constructor(
|
||||
private definitionClient: SloDefinitionClient,
|
||||
private esClient: ElasticsearchClient,
|
||||
private sloSettings: SLOSettings,
|
||||
private spaceId: string
|
||||
) {}
|
||||
|
||||
public async execute(
|
||||
sloId: string,
|
||||
params: GetSLOGroupingsParams
|
||||
): Promise<GetSLOGroupingsResponse> {
|
||||
const { slo } = await this.definitionClient.execute(sloId, this.spaceId, params.remoteName);
|
||||
|
||||
const groupingKeys = [slo.groupBy].flat();
|
||||
if (groupingKeys.includes(ALL_VALUE) || params.instanceId === ALL_VALUE) {
|
||||
throw new Error('Ungrouped SLO cannot be queried for available groupings');
|
||||
}
|
||||
|
||||
if (!groupingKeys.includes(params.groupingKey)) {
|
||||
throw new Error("Provided groupingKey doesn't match the SLO's groupBy field");
|
||||
}
|
||||
|
||||
const groupingValues = params.instanceId.split(',') ?? [];
|
||||
if (groupingKeys.length !== groupingValues.length) {
|
||||
throw new Error('Provided instanceId does not match the number of grouping keys');
|
||||
}
|
||||
|
||||
const response = await this.esClient.search<
|
||||
unknown,
|
||||
{
|
||||
groupingValues: {
|
||||
buckets: Array<{ key: { value: string } }>;
|
||||
after_key: { value: string };
|
||||
};
|
||||
}
|
||||
>({
|
||||
index: params.remoteName
|
||||
? `${params.remoteName}:${SLO_SUMMARY_DESTINATION_INDEX_NAME}`
|
||||
: SLO_SUMMARY_DESTINATION_INDEX_NAME,
|
||||
...generateQuery(slo, params, this.sloSettings),
|
||||
});
|
||||
|
||||
return {
|
||||
groupingKey: params.groupingKey,
|
||||
values: response.aggregations?.groupingValues.buckets.map((bucket) => bucket.key.value) ?? [],
|
||||
afterKey:
|
||||
response.aggregations?.groupingValues.buckets.length === Number(params.size ?? DEFAULT_SIZE)
|
||||
? response.aggregations?.groupingValues.after_key.value
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function generateQuery(slo: SLODefinition, params: GetSLOGroupingsParams, settings: SLOSettings) {
|
||||
const groupingKeys = [slo.groupBy].flat();
|
||||
const groupingValues = params.instanceId.split(',') ?? [];
|
||||
|
||||
const groupingKeyValuePairs = groupingKeys.map((groupingKey, index) => [
|
||||
groupingKey,
|
||||
groupingValues[index],
|
||||
]);
|
||||
|
||||
const aggs = generateAggs(params);
|
||||
|
||||
const query = {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
term: {
|
||||
'slo.id': slo.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.revision': slo.revision,
|
||||
},
|
||||
},
|
||||
// exclude stale summary documents if specified
|
||||
...(!!params.excludeStale
|
||||
? [
|
||||
{
|
||||
range: {
|
||||
summaryUpdatedAt: {
|
||||
gte: `now-${settings.staleThresholdInHours}h`,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// Set other groupings as term filters
|
||||
...groupingKeyValuePairs
|
||||
.filter(([groupingKey]) => groupingKey !== params.groupingKey)
|
||||
.map(([groupingKey, groupingValue]) => ({
|
||||
term: {
|
||||
[`slo.groupings.${groupingKey}`]: groupingValue,
|
||||
},
|
||||
})),
|
||||
// search on the specified groupingKey
|
||||
...(params.search
|
||||
? [
|
||||
{
|
||||
query_string: {
|
||||
default_field: `slo.groupings.${params.groupingKey}`,
|
||||
query: `*${params.search.replace(/^\*/, '').replace(/\*$/, '')}*`,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs,
|
||||
};
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
function generateAggs(params: GetSLOGroupingsParams): {
|
||||
groupingValues: { composite: AggregationsCompositeAggregation };
|
||||
} {
|
||||
return {
|
||||
groupingValues: {
|
||||
composite: {
|
||||
size: Number(params.size ?? DEFAULT_SIZE),
|
||||
sources: [
|
||||
{
|
||||
value: {
|
||||
terms: {
|
||||
field: `slo.groupings.${params.groupingKey}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
...(params.afterKey ? { after: { value: params.afterKey } } : {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,70 +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 { ElasticsearchClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { createSLO } from './fixtures/slo';
|
||||
import { GetSLOInstances, SLORepository } from '.';
|
||||
import { createSLORepositoryMock } from './mocks';
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
|
||||
describe('Get SLO Instances', () => {
|
||||
let repositoryMock: jest.Mocked<SLORepository>;
|
||||
let esClientMock: ElasticsearchClientMock;
|
||||
|
||||
beforeEach(() => {
|
||||
repositoryMock = createSLORepositoryMock();
|
||||
esClientMock = elasticsearchServiceMock.createElasticsearchClient();
|
||||
});
|
||||
|
||||
it("returns an empty response when the SLO has no 'groupBy' defined", async () => {
|
||||
const slo = createSLO({ groupBy: ALL_VALUE });
|
||||
repositoryMock.findById.mockResolvedValue(slo);
|
||||
|
||||
const service = new GetSLOInstances(repositoryMock, esClientMock);
|
||||
|
||||
const result = await service.execute(slo.id);
|
||||
|
||||
expect(result).toEqual({ groupBy: ALL_VALUE, instances: [] });
|
||||
});
|
||||
|
||||
it("returns all instances of a SLO defined with a 'groupBy'", async () => {
|
||||
const slo = createSLO({ id: 'slo-id', revision: 2, groupBy: 'field.to.host' });
|
||||
repositoryMock.findById.mockResolvedValue(slo);
|
||||
esClientMock.search.mockResolvedValue({
|
||||
took: 100,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 0,
|
||||
successful: 0,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
},
|
||||
hits: {
|
||||
hits: [],
|
||||
},
|
||||
aggregations: {
|
||||
instances: {
|
||||
buckets: [
|
||||
{ key: 'host-aaa', doc_value: 100 },
|
||||
{ key: 'host-bbb', doc_value: 200 },
|
||||
{ key: 'host-ccc', doc_value: 500 },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const service = new GetSLOInstances(repositoryMock, esClientMock);
|
||||
|
||||
const result = await service.execute(slo.id);
|
||||
|
||||
expect(result).toEqual({
|
||||
groupBy: 'field.to.host',
|
||||
instances: ['host-aaa', 'host-bbb', 'host-ccc'],
|
||||
});
|
||||
expect(esClientMock.search.mock.calls[0]).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,50 +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 { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import { ALL_VALUE, GetSLOInstancesResponse } from '@kbn/slo-schema';
|
||||
import { SLO_DESTINATION_INDEX_PATTERN } from '../../common/constants';
|
||||
import { SLORepository } from './slo_repository';
|
||||
|
||||
export class GetSLOInstances {
|
||||
constructor(private repository: SLORepository, private esClient: ElasticsearchClient) {}
|
||||
|
||||
public async execute(sloId: string): Promise<GetSLOInstancesResponse> {
|
||||
const slo = await this.repository.findById(sloId);
|
||||
|
||||
if ([slo.groupBy].flat().includes(ALL_VALUE)) {
|
||||
return { groupBy: ALL_VALUE, instances: [] };
|
||||
}
|
||||
|
||||
const result = await this.esClient.search({
|
||||
index: SLO_DESTINATION_INDEX_PATTERN,
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ range: { '@timestamp': { gte: 'now-7d' } } },
|
||||
{ term: { 'slo.id': slo.id } },
|
||||
{ term: { 'slo.revision': slo.revision } },
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
instances: {
|
||||
terms: {
|
||||
size: 1000,
|
||||
field: 'slo.instanceId',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
const buckets = result?.aggregations?.instances.buckets ?? [];
|
||||
const instances = buckets.map((bucket: { key: string }) => bucket.key);
|
||||
return { groupBy: slo.groupBy, instances };
|
||||
}
|
||||
}
|
|
@ -19,6 +19,6 @@ export * from './transform_manager';
|
|||
export * from './summay_transform_manager';
|
||||
export * from './update_slo';
|
||||
export * from './summary_client';
|
||||
export * from './get_slo_instances';
|
||||
export * from './get_slo_groupings';
|
||||
export * from './find_slo_groups';
|
||||
export * from './get_slo_health';
|
||||
|
|
|
@ -14,10 +14,12 @@ import {
|
|||
SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
|
||||
} from '../../common/constants';
|
||||
import { getListOfSloSummaryIndices } from '../../common/summary_indices';
|
||||
import { StoredSLOSettings } from '../domain/models';
|
||||
import { sloSettingsObjectId, SO_SLO_SETTINGS_TYPE } from '../saved_objects/slo_settings';
|
||||
import { SLOSettings, StoredSLOSettings } from '../domain/models';
|
||||
import { SO_SLO_SETTINGS_TYPE, sloSettingsObjectId } from '../saved_objects/slo_settings';
|
||||
|
||||
export const getSloSettings = async (soClient: SavedObjectsClientContract) => {
|
||||
export const getSloSettings = async (
|
||||
soClient: SavedObjectsClientContract
|
||||
): Promise<SLOSettings> => {
|
||||
try {
|
||||
const soObject = await soClient.get<StoredSLOSettings>(
|
||||
SO_SLO_SETTINGS_TYPE,
|
||||
|
@ -41,7 +43,7 @@ export const getSloSettings = async (soClient: SavedObjectsClientContract) => {
|
|||
export const storeSloSettings = async (
|
||||
soClient: SavedObjectsClientContract,
|
||||
params: PutSLOSettingsParams
|
||||
) => {
|
||||
): Promise<SLOSettings> => {
|
||||
const object = await soClient.create<StoredSLOSettings>(
|
||||
SO_SLO_SETTINGS_TYPE,
|
||||
sloSettingsSchema.encode(params),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue