feat(slo): Update search with the new summary data (#162336)

This commit is contained in:
Kevin Delemme 2023-07-24 09:12:10 -04:00
parent b760b2cbed
commit 473b9a4a7c
36 changed files with 570 additions and 739 deletions

View file

@ -7,15 +7,17 @@
import * as t from 'io-ts';
import {
apmTransactionDurationIndicatorSchema,
apmTransactionErrorRateIndicatorSchema,
budgetingMethodSchema,
dateType,
durationType,
histogramIndicatorSchema,
historicalSummarySchema,
indicatorSchema,
indicatorTypesArraySchema,
indicatorTypesSchema,
kqlCustomIndicatorSchema,
metricCustomIndicatorSchema,
histogramIndicatorSchema,
objectiveSchema,
optionalSettingsSchema,
previewDataSchema,
@ -24,9 +26,6 @@ import {
summarySchema,
tagsSchema,
timeWindowSchema,
apmTransactionErrorRateIndicatorSchema,
apmTransactionDurationIndicatorSchema,
durationType,
timeWindowTypeSchema,
} from '../schema';
@ -69,12 +68,16 @@ const getSLOParamsSchema = t.type({
});
const sortDirectionSchema = t.union([t.literal('asc'), t.literal('desc')]);
const sortBySchema = t.union([t.literal('creationTime'), t.literal('indicatorType')]);
const sortBySchema = t.union([
t.literal('error_budget_consumed'),
t.literal('error_budget_remaining'),
t.literal('sli_value'),
t.literal('status'),
]);
const findSLOParamsSchema = t.partial({
query: t.partial({
name: t.string,
indicatorTypes: indicatorTypesArraySchema,
kqlQuery: t.string,
page: t.string,
perPage: t.string,
sortBy: sortBySchema,

View file

@ -30,7 +30,7 @@ describe('SLO Selector', () => {
render(<SloSelector onSelected={onSelectedSpy} />);
expect(screen.getByTestId('sloSelector')).toBeTruthy();
expect(useFetchSloListMock).toHaveBeenCalledWith({ name: '' });
expect(useFetchSloListMock).toHaveBeenCalledWith({ kqlQuery: 'slo.name:*' });
});
it('searches SLOs when typing', async () => {
@ -42,6 +42,6 @@ describe('SLO Selector', () => {
await wait(310); // debounce delay
});
expect(useFetchSloListMock).toHaveBeenCalledWith({ name: 'latency' });
expect(useFetchSloListMock).toHaveBeenCalledWith({ kqlQuery: 'slo.name:latency*' });
});
});

View file

@ -23,7 +23,7 @@ function SloSelector({ initialSlo, onSelected, errors }: Props) {
const [options, setOptions] = useState<Array<EuiComboBoxOptionOption<string>>>([]);
const [selectedOptions, setSelectedOptions] = useState<Array<EuiComboBoxOptionOption<string>>>();
const [searchValue, setSearchValue] = useState<string>('');
const { isLoading, sloList } = useFetchSloList({ name: searchValue });
const { isLoading, sloList } = useFetchSloList({ kqlQuery: `slo.name:${searchValue}*` });
const hasError = errors !== undefined && errors.length > 0;
useEffect(() => {

View file

@ -8,10 +8,10 @@
import type { Indicator } from '@kbn/slo-schema';
interface SloListFilter {
name: string;
kqlQuery: string;
page: number;
sortBy: string;
indicatorTypes: string[];
sortBy?: string;
sortDirection: string;
}
interface CompositeSloKeyFilter {

View file

@ -20,10 +20,10 @@ import { useKibana } from '../../utils/kibana_react';
import { sloKeys } from './query_key_factory';
interface SLOListParams {
name?: string;
kqlQuery?: string;
page?: number;
sortBy?: string;
indicatorTypes?: string[];
sortDirection?: 'asc' | 'desc';
shouldRefetch?: boolean;
}
@ -43,10 +43,10 @@ const SHORT_REFETCH_INTERVAL = 1000 * 5; // 5 seconds
const LONG_REFETCH_INTERVAL = 1000 * 60; // 1 minute
export function useFetchSloList({
name = '',
kqlQuery = '',
page = 1,
sortBy = 'creationTime',
indicatorTypes = [],
sortBy,
sortDirection = 'desc',
shouldRefetch,
}: SLOListParams | undefined = {}): UseFetchSloListResponse {
const {
@ -61,18 +61,15 @@ export function useFetchSloList({
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data, refetch } = useQuery(
{
queryKey: sloKeys.list({ name, page, sortBy, indicatorTypes }),
queryKey: sloKeys.list({ kqlQuery, page, sortBy, sortDirection }),
queryFn: async ({ signal }) => {
try {
const response = await http.get<FindSLOResponse>(`/api/observability/slos`, {
query: {
...(page && { page }),
...(name && { name }),
...(kqlQuery && { kqlQuery }),
...(sortBy && { sortBy }),
...(indicatorTypes &&
indicatorTypes.length > 0 && {
indicatorTypes: indicatorTypes.join(','),
}),
...(sortDirection && { sortDirection }),
...(page && { page }),
},
signal,
});

View file

@ -11,11 +11,7 @@ import { debounce } from 'lodash';
import { useIsMutating } from '@tanstack/react-query';
import { useFetchSloList } from '../../../hooks/slo/use_fetch_slo_list';
import {
FilterType,
SloListSearchFilterSortBar,
SortType,
} from './slo_list_search_filter_sort_bar';
import { SloListSearchFilterSortBar, SortField } from './slo_list_search_filter_sort_bar';
import { SloListItems } from './slo_list_items';
export interface Props {
@ -26,14 +22,13 @@ export function SloList({ autoRefresh }: Props) {
const [activePage, setActivePage] = useState(0);
const [query, setQuery] = useState('');
const [sort, setSort] = useState<SortType>('creationTime');
const [indicatorTypeFilter, setIndicatorTypeFilter] = useState<FilterType[]>([]);
const [sort, setSort] = useState<SortField | undefined>('error_budget_remaining');
const { isInitialLoading, isLoading, isRefetching, isError, sloList, refetch } = useFetchSloList({
page: activePage + 1,
name: query,
kqlQuery: query,
sortBy: sort,
indicatorTypes: indicatorTypeFilter,
sortDirection: 'desc',
shouldRefetch: autoRefresh,
});
@ -53,18 +48,14 @@ export function SloList({ autoRefresh }: Props) {
() =>
debounce((e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
}, 300),
}, 800),
[]
);
const handleChangeSort = (newSort: SortType) => {
const handleChangeSort = (newSort: SortField | undefined) => {
setSort(newSort);
};
const handleChangeIndicatorTypeFilter = (newFilter: FilterType[]) => {
setIndicatorTypeFilter(newFilter);
};
return (
<EuiFlexGroup direction="column" gutterSize="m" data-test-subj="sloList">
<EuiFlexItem grow>
@ -80,7 +71,6 @@ export function SloList({ autoRefresh }: Props) {
}
onChangeQuery={handleChangeQuery}
onChangeSort={handleChangeSort}
onChangeIndicatorTypeFilter={handleChangeIndicatorTypeFilter}
/>
</EuiFlexItem>

View file

@ -28,7 +28,6 @@ const defaultProps: SloListSearchFilterSortBarProps = {
loading: false,
onChangeQuery: () => {},
onChangeSort: () => {},
onChangeIndicatorTypeFilter: () => {},
};
export const SloListSearchFilterSortBar = Template.bind({});

View file

@ -18,29 +18,15 @@ import {
} from '@elastic/eui';
import { EuiSelectableOptionCheckedType } from '@elastic/eui/src/components/selectable/selectable_option';
import { i18n } from '@kbn/i18n';
import React, { useEffect, useState } from 'react';
import {
INDICATOR_APM_AVAILABILITY,
INDICATOR_APM_LATENCY,
INDICATOR_CUSTOM_KQL,
INDICATOR_CUSTOM_METRIC,
INDICATOR_HISTOGRAM,
} from '../../../utils/slo/labels';
import React, { useState } from 'react';
export interface SloListSearchFilterSortBarProps {
loading: boolean;
onChangeQuery: (e: React.ChangeEvent<HTMLInputElement>) => void;
onChangeSort: (sort: SortType) => void;
onChangeIndicatorTypeFilter: (filter: FilterType[]) => void;
onChangeSort: (sort: SortField | undefined) => void;
}
export type SortType = 'creationTime' | 'indicatorType';
export type FilterType =
| 'sli.apm.transactionDuration'
| 'sli.apm.transactionErrorRate'
| 'sli.kql.custom'
| 'sli.metric.custom'
| 'sli.histogram.custom';
export type SortField = 'sli_value' | 'error_budget_consumed' | 'error_budget_remaining' | 'status';
export type Item<T> = EuiSelectableOption & {
label: string;
@ -48,83 +34,50 @@ export type Item<T> = EuiSelectableOption & {
checked?: EuiSelectableOptionCheckedType;
};
const SORT_OPTIONS: Array<Item<SortType>> = [
const SORT_OPTIONS: Array<Item<SortField>> = [
{
label: i18n.translate('xpack.observability.slo.list.sortBy.creationTime', {
defaultMessage: 'Creation time',
label: i18n.translate('xpack.observability.slo.list.sortBy.sliValue', {
defaultMessage: 'SLI value',
}),
type: 'creationTime',
type: 'sli_value',
},
{
label: i18n.translate('xpack.observability.slo.list.sortBy.sloStatus', {
defaultMessage: 'SLO status',
}),
type: 'status',
},
{
label: i18n.translate('xpack.observability.slo.list.sortBy.errorBudgetConsumed', {
defaultMessage: 'Error budget consumed',
}),
type: 'error_budget_consumed',
},
{
label: i18n.translate('xpack.observability.slo.list.sortBy.errorBudgetRemaining', {
defaultMessage: 'Error budget remaining',
}),
type: 'error_budget_remaining',
checked: 'on',
},
{
label: i18n.translate('xpack.observability.slo.list.sortBy.indicatorType', {
defaultMessage: 'Indicator type',
}),
type: 'indicatorType',
},
];
const INDICATOR_TYPE_OPTIONS: Array<Item<FilterType>> = [
{
label: INDICATOR_APM_LATENCY,
type: 'sli.apm.transactionDuration',
},
{
label: INDICATOR_APM_AVAILABILITY,
type: 'sli.apm.transactionErrorRate',
},
{
label: INDICATOR_CUSTOM_KQL,
type: 'sli.kql.custom',
},
{
label: INDICATOR_CUSTOM_METRIC,
type: 'sli.metric.custom',
},
{
label: INDICATOR_HISTOGRAM,
type: 'sli.histogram.custom',
},
];
export function SloListSearchFilterSortBar({
loading,
onChangeQuery,
onChangeSort,
onChangeIndicatorTypeFilter,
}: SloListSearchFilterSortBarProps) {
const [isFilterPopoverOpen, setFilterPopoverOpen] = useState(false);
const [isSortPopoverOpen, setSortPopoverOpen] = useState(false);
const [sortOptions, setSortOptions] = useState(SORT_OPTIONS);
const [indicatorTypeOptions, setIndicatorTypeOptions] = useState(INDICATOR_TYPE_OPTIONS);
const selectedSort = sortOptions.find((option) => option.checked === 'on');
const selectedIndicatorTypeFilter = indicatorTypeOptions.filter(
(option) => option.checked === 'on'
);
const handleToggleFilterButton = () => setFilterPopoverOpen(!isFilterPopoverOpen);
const handleToggleSortButton = () => setSortPopoverOpen(!isSortPopoverOpen);
const handleChangeSort = (newOptions: Array<Item<SortType>>) => {
const handleChangeSort = (newOptions: Array<Item<SortField>>) => {
setSortOptions(newOptions);
setSortPopoverOpen(false);
onChangeSort(newOptions.find((o) => o.checked)?.type);
};
const handleChangeIndicatorTypeOptions = (newOptions: Array<Item<FilterType>>) => {
setIndicatorTypeOptions(newOptions);
onChangeIndicatorTypeFilter(
newOptions.filter((option) => option.checked === 'on').map((option) => option.type)
);
};
useEffect(() => {
if (selectedSort?.type === 'creationTime' || selectedSort?.type === 'indicatorType') {
onChangeSort(selectedSort.type);
}
}, [onChangeSort, selectedSort]);
return (
<EuiFlexGroup direction="row" gutterSize="s">
<EuiFlexItem grow>
@ -139,44 +92,7 @@ export function SloListSearchFilterSortBar({
/>
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ width: 200 }}>
<EuiFilterGroup>
<EuiPopover
button={
<EuiFilterButton
iconType="arrowDown"
onClick={handleToggleFilterButton}
isSelected={isFilterPopoverOpen}
numFilters={selectedIndicatorTypeFilter.length}
>
{i18n.translate('xpack.observability.slo.list.indicatorTypeFilter', {
defaultMessage: 'Indicator type',
})}
</EuiFilterButton>
}
isOpen={isFilterPopoverOpen}
closePopover={handleToggleFilterButton}
panelPaddingSize="none"
anchorPosition="downCenter"
>
<div style={{ width: 300 }}>
<EuiPopoverTitle paddingSize="s">
{i18n.translate('xpack.observability.slo.list.indicatorTypeFilter', {
defaultMessage: 'Indicator type',
})}
</EuiPopoverTitle>
<EuiSelectable<Item<FilterType>>
options={indicatorTypeOptions}
onChange={handleChangeIndicatorTypeOptions}
>
{(list) => list}
</EuiSelectable>
</div>
</EuiPopover>
</EuiFilterGroup>
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ width: 200 }}>
<EuiFlexItem grow={true} style={{ maxWidth: 280 }}>
<EuiFilterGroup>
<EuiPopover
button={
@ -202,7 +118,7 @@ export function SloListSearchFilterSortBar({
defaultMessage: 'Sort by',
})}
</EuiPopoverTitle>
<EuiSelectable<Item<SortType>>
<EuiSelectable<Item<SortField>>
singleSelection
options={sortOptions}
onChange={handleChangeSort}

View file

@ -103,9 +103,16 @@ export const getSLOSummaryMappingsTemplate = (name: string) => ({
errorBudgetRemaining: {
type: 'double',
},
status: {
errorBudgetEstimated: {
type: 'boolean',
},
statusCode: {
type: 'byte',
},
status: {
type: 'keyword',
ignore_above: 32,
},
},
},
},

View file

@ -29,5 +29,7 @@ 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_INGEST_PIPELINE_NAME = `${SLO_SUMMARY_INDEX_TEMPLATE_NAME}.pipeline`;
export const getSLOTransformId = (sloId: string, sloRevision: number) =>
`slo-${sloId}-${sloRevision}`;

View file

@ -0,0 +1,60 @@
/*
* 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_RESOURCES_VERSION } from '../constants';
export const getSLOSummaryPipelineTemplate = (id: string) => ({
id,
description: 'SLO summary ingest pipeline',
processors: [
{
split: {
description: 'Split comma separated list of tags into an array',
field: 'slo.tags',
separator: ',',
},
},
{
set: {
description: "if 'statusCode == 0', set status to NO_DATA",
if: 'ctx.statusCode == 0',
field: 'status',
value: 'NO_DATA',
},
},
{
set: {
description: "if 'statusCode == 1', set statusLabel to VIOLATED",
if: 'ctx.statusCode == 1',
field: 'status',
value: 'VIOLATED',
},
},
{
set: {
description: "if 'statusCode == 2', set status to DEGRADING",
if: 'ctx.statusCode == 2',
field: 'status',
value: 'DEGRADING',
},
},
{
set: {
description: "if 'statusCode == 4', set status to HEALTHY",
if: 'ctx.statusCode == 4',
field: 'status',
value: 'HEALTHY',
},
},
],
_meta: {
description: 'SLO summary ingest pipeline',
version: SLO_RESOURCES_VERSION,
managed: true,
managed_by: 'observability',
},
});

View file

@ -36,6 +36,7 @@ import { getGlobalDiagnosis, getSloDiagnosis } from '../../services/slo/get_diag
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,
@ -239,7 +240,7 @@ const findSLORoute = createObservabilityServerRoute({
tags: ['access:slo_read'],
},
params: findSLOParamsSchema,
handler: async ({ context, params }) => {
handler: async ({ context, params, logger }) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context);
if (!hasCorrectLicense) {
@ -249,8 +250,8 @@ const findSLORoute = createObservabilityServerRoute({
const soClient = (await context.core).savedObjects.client;
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
const repository = new KibanaSavedObjectsSLORepository(soClient);
const summaryClient = new DefaultSummaryClient(esClient);
const findSLO = new FindSLO(repository, summaryClient);
const summarySearchClient = new DefaultSummarySearchClient(esClient, logger);
const findSLO = new FindSLO(repository, summarySearchClient);
const response = await findSLO.execute(params?.query ?? {});

View file

@ -5,37 +5,45 @@
* 2.0.
*/
import { SLO, SLOId, Summary } from '../../domain/models';
import { SLO } from '../../domain/models';
import { FindSLO } from './find_slo';
import { createSLO, createPaginatedSLO } from './fixtures/slo';
import { createSummaryClientMock, createSLORepositoryMock } from './mocks';
import { SLORepository, SortField, SortDirection } from './slo_repository';
import { SummaryClient } from './summary_client';
import { createSLO } from './fixtures/slo';
import { createSLORepositoryMock, createSummarySearchClientMock } from './mocks';
import { SLORepository } from './slo_repository';
import { Paginated, SLOSummary, SummarySearchClient } from './summary_search_client';
describe('FindSLO', () => {
let mockRepository: jest.Mocked<SLORepository>;
let mockSummaryClient: jest.Mocked<SummaryClient>;
let mockSummarySearchClient: jest.Mocked<SummarySearchClient>;
let findSLO: FindSLO;
beforeEach(() => {
mockRepository = createSLORepositoryMock();
mockSummaryClient = createSummaryClientMock();
findSLO = new FindSLO(mockRepository, mockSummaryClient);
mockSummarySearchClient = createSummarySearchClientMock();
findSLO = new FindSLO(mockRepository, mockSummarySearchClient);
});
describe('happy path', () => {
it('returns the results with pagination', async () => {
const slo = createSLO();
mockRepository.find.mockResolvedValueOnce(createPaginatedSLO(slo));
mockSummaryClient.fetchSummary.mockResolvedValueOnce(someSummary(slo));
mockSummarySearchClient.search.mockResolvedValueOnce(summarySearchResult(slo));
mockRepository.findAllByIds.mockResolvedValueOnce([slo]);
const result = await findSLO.execute({});
expect(mockRepository.find).toHaveBeenCalledWith(
{ name: undefined },
{ field: SortField.CreationTime, direction: SortDirection.Asc },
{ page: 1, perPage: 25 }
);
expect(mockSummarySearchClient.search.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"",
Object {
"direction": "asc",
"field": "status",
},
Object {
"page": 1,
"perPage": 25,
},
]
`);
expect(result).toEqual({
page: 1,
@ -89,131 +97,65 @@ describe('FindSLO', () => {
});
});
it('calls the repository with the default criteria and pagination', async () => {
it('calls the repository with all the summary slo ids', async () => {
const slo = createSLO();
mockRepository.find.mockResolvedValueOnce(createPaginatedSLO(slo));
mockSummaryClient.fetchSummary.mockResolvedValueOnce(someSummary(slo));
mockSummarySearchClient.search.mockResolvedValueOnce(summarySearchResult(slo));
mockRepository.findAllByIds.mockResolvedValueOnce([slo]);
await findSLO.execute({});
expect(mockRepository.find).toHaveBeenCalledWith(
{ name: undefined },
{ field: SortField.CreationTime, direction: SortDirection.Asc },
{ page: 1, perPage: 25 }
);
expect(mockRepository.findAllByIds).toHaveBeenCalledWith([slo.id]);
});
it('calls the repository with the name filter criteria', async () => {
it('searches with the provided criteria', async () => {
const slo = createSLO();
mockRepository.find.mockResolvedValueOnce(createPaginatedSLO(slo));
mockSummaryClient.fetchSummary.mockResolvedValueOnce(someSummary(slo));
mockSummarySearchClient.search.mockResolvedValueOnce(summarySearchResult(slo));
mockRepository.findAllByIds.mockResolvedValueOnce([slo]);
await findSLO.execute({ name: 'Availability' });
await findSLO.execute({
kqlQuery: "slo.name:'Service*' and slo.indicator.type:'sli.kql.custom'",
page: '2',
perPage: '10',
sortBy: 'error_budget_consumed',
sortDirection: 'asc',
});
expect(mockRepository.find).toHaveBeenCalledWith(
{ name: 'Availability' },
{ field: SortField.CreationTime, direction: SortDirection.Asc },
{ page: 1, perPage: 25 }
);
});
it('calls the repository with the indicatorType filter criteria', async () => {
const slo = createSLO();
mockRepository.find.mockResolvedValueOnce(createPaginatedSLO(slo));
mockSummaryClient.fetchSummary.mockResolvedValueOnce(someSummary(slo));
await findSLO.execute({ indicatorTypes: ['sli.kql.custom'] });
expect(mockRepository.find).toHaveBeenCalledWith(
{ indicatorTypes: ['sli.kql.custom'] },
{ field: SortField.CreationTime, direction: SortDirection.Asc },
{ page: 1, perPage: 25 }
);
});
it('calls the repository with the pagination', async () => {
const slo = createSLO();
mockRepository.find.mockResolvedValueOnce(createPaginatedSLO(slo));
mockSummaryClient.fetchSummary.mockResolvedValueOnce(someSummary(slo));
await findSLO.execute({ name: 'My SLO*', page: '2', perPage: '100' });
expect(mockRepository.find).toHaveBeenCalledWith(
{ name: 'My SLO*' },
{ field: SortField.CreationTime, direction: SortDirection.Asc },
{ page: 2, perPage: 100 }
);
});
it('uses default pagination values when invalid', async () => {
const slo = createSLO();
mockRepository.find.mockResolvedValueOnce(createPaginatedSLO(slo));
mockSummaryClient.fetchSummary.mockResolvedValueOnce(someSummary(slo));
await findSLO.execute({ page: '-1', perPage: '0' });
expect(mockRepository.find).toHaveBeenCalledWith(
{ name: undefined },
{ field: SortField.CreationTime, direction: SortDirection.Asc },
{ page: 1, perPage: 25 }
);
});
it('sorts by name by default when not specified', async () => {
const slo = createSLO();
mockRepository.find.mockResolvedValueOnce(createPaginatedSLO(slo));
mockSummaryClient.fetchSummary.mockResolvedValueOnce(someSummary(slo));
await findSLO.execute({ sortBy: undefined });
expect(mockRepository.find).toHaveBeenCalledWith(
{ name: undefined },
{ field: SortField.CreationTime, direction: SortDirection.Asc },
{ page: 1, perPage: 25 }
);
});
it('sorts by indicator type', async () => {
const slo = createSLO();
mockRepository.find.mockResolvedValueOnce(createPaginatedSLO(slo));
mockSummaryClient.fetchSummary.mockResolvedValueOnce(someSummary(slo));
await findSLO.execute({ sortBy: 'indicatorType' });
expect(mockRepository.find).toHaveBeenCalledWith(
{ name: undefined },
{ field: SortField.IndicatorType, direction: SortDirection.Asc },
{ page: 1, perPage: 25 }
);
});
it('sorts by indicator type in descending order', async () => {
const slo = createSLO();
mockRepository.find.mockResolvedValueOnce(createPaginatedSLO(slo));
mockSummaryClient.fetchSummary.mockResolvedValueOnce(someSummary(slo));
await findSLO.execute({ sortBy: 'indicatorType', sortDirection: 'desc' });
expect(mockRepository.find).toHaveBeenCalledWith(
{ name: undefined },
{ field: SortField.IndicatorType, direction: SortDirection.Desc },
{ page: 1, perPage: 25 }
);
expect(mockSummarySearchClient.search.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"slo.name:'Service*' and slo.indicator.type:'sli.kql.custom'",
Object {
"direction": "asc",
"field": "error_budget_consumed",
},
Object {
"page": 2,
"perPage": 10,
},
]
`);
});
});
});
function someSummary(slo: SLO): Record<SLOId, Summary> {
function summarySearchResult(slo: SLO): Paginated<SLOSummary> {
return {
[slo.id]: {
status: 'HEALTHY',
sliValue: 0.9999,
errorBudget: {
initial: 0.001,
consumed: 0.1,
remaining: 0.9,
isEstimated: false,
total: 1,
perPage: 25,
page: 1,
results: [
{
id: slo.id,
summary: {
status: 'HEALTHY',
sliValue: 0.9999,
errorBudget: {
initial: 0.001,
consumed: 0.1,
remaining: 0.9,
isEstimated: false,
},
},
},
},
],
};
}

View file

@ -6,54 +6,44 @@
*/
import { FindSLOParams, FindSLOResponse, findSLOResponseSchema } from '@kbn/slo-schema';
import { SLO, SLOId, SLOWithSummary, Summary } from '../../domain/models';
import {
Criteria,
Paginated,
Pagination,
SLORepository,
Sort,
SortField,
SortDirection,
} from './slo_repository';
import { SummaryClient } from './summary_client';
import { SLO, SLOWithSummary } from '../../domain/models';
import { SLORepository } from './slo_repository';
import { Pagination, SLOSummary, Sort, SummarySearchClient } from './summary_search_client';
const DEFAULT_PAGE = 1;
const DEFAULT_PER_PAGE = 25;
export class FindSLO {
constructor(private repository: SLORepository, private summaryClient: SummaryClient) {}
constructor(
private repository: SLORepository,
private summarySearchClient: SummarySearchClient
) {}
public async execute(params: FindSLOParams): Promise<FindSLOResponse> {
const pagination: Pagination = toPagination(params);
const criteria: Criteria = toCriteria(params);
const sort: Sort = toSort(params);
const { results: sloList, ...resultMeta }: Paginated<SLO> = await this.repository.find(
criteria,
sort,
const sloSummaryList = await this.summarySearchClient.search(
params.kqlQuery ?? '',
toSort(params),
pagination
);
const summaryBySlo = await this.summaryClient.fetchSummary(sloList);
const sloListWithSummary = mergeSloWithSummary(sloList, summaryBySlo);
const sloList = await this.repository.findAllByIds(sloSummaryList.results.map((slo) => slo.id));
const sloListWithSummary = mergeSloWithSummary(sloList, sloSummaryList.results);
return findSLOResponseSchema.encode({
page: resultMeta.page,
perPage: resultMeta.perPage,
total: resultMeta.total,
page: sloSummaryList.page,
perPage: sloSummaryList.perPage,
total: sloSummaryList.total,
results: sloListWithSummary,
});
}
}
function mergeSloWithSummary(
sloList: SLO[],
summaryBySlo: Record<SLOId, Summary>
): SLOWithSummary[] {
return sloList.map((slo) => ({
...slo,
summary: summaryBySlo[slo.id],
function mergeSloWithSummary(sloList: SLO[], sloSummaryList: SLOSummary[]): SLOWithSummary[] {
return sloSummaryList.map((sloSummary) => ({
...sloList.find((s) => s.id === sloSummary.id)!,
summary: sloSummary.summary,
}));
}
@ -67,13 +57,9 @@ function toPagination(params: FindSLOParams): Pagination {
};
}
function toCriteria(params: FindSLOParams): Criteria {
return { name: params.name, indicatorTypes: params.indicatorTypes };
}
function toSort(params: FindSLOParams): Sort {
return {
field: params.sortBy === 'indicatorType' ? SortField.IndicatorType : SortField.CreationTime,
direction: params.sortDirection === 'desc' ? SortDirection.Desc : SortDirection.Asc,
field: params.sortBy ?? 'status',
direction: params.sortDirection ?? 'asc',
};
}

View file

@ -21,7 +21,6 @@ import {
StoredSLO,
} from '../../../domain/models';
import { SO_SLO_TYPE } from '../../../saved_objects';
import { Paginated } from '../slo_repository';
import { twoMinute } from './duration';
import { sevenDaysRolling, weeklyCalendarAligned } from './time_window';
@ -187,16 +186,3 @@ export const createSLOWithCalendarTimeWindow = (params: Partial<SLO> = {}): SLO
...params,
});
};
export const createPaginatedSLO = (
slo: SLO,
params: Partial<Paginated<SLO>> = {}
): Paginated<SLO> => {
return {
page: 1,
perPage: 25,
total: 1,
results: [slo],
...params,
};
};

View file

@ -9,6 +9,7 @@ import { ResourceInstaller } from '../resource_installer';
import { SLIClient } from '../sli_client';
import { SLORepository } from '../slo_repository';
import { SummaryClient } from '../summary_client';
import { SummarySearchClient } from '../summary_search_client';
import { TransformManager } from '../transform_manager';
const createResourceInstallerMock = (): jest.Mocked<ResourceInstaller> => {
@ -32,7 +33,6 @@ const createSLORepositoryMock = (): jest.Mocked<SLORepository> => {
findById: jest.fn(),
findAllByIds: jest.fn(),
deleteById: jest.fn(),
find: jest.fn(),
};
};
@ -42,6 +42,12 @@ const createSummaryClientMock = (): jest.Mocked<SummaryClient> => {
};
};
const createSummarySearchClientMock = (): jest.Mocked<SummarySearchClient> => {
return {
search: jest.fn(),
};
};
const createSLIClientMock = (): jest.Mocked<SLIClient> => {
return {
fetchSLIDataFrom: jest.fn(),
@ -53,5 +59,6 @@ export {
createTransformManagerMock,
createSLORepositoryMock,
createSummaryClientMock,
createSummarySearchClientMock,
createSLIClientMock,
};

View file

@ -16,6 +16,7 @@ import {
SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME,
SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME,
SLO_SUMMARY_INDEX_TEMPLATE_NAME,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
} from '../../assets/constants';
import { DefaultResourceInstaller } from './resource_installer';
@ -54,9 +55,16 @@ describe('resourceInstaller', () => {
2,
expect.objectContaining({ name: SLO_SUMMARY_INDEX_TEMPLATE_NAME })
);
expect(mockClusterClient.ingest.putPipeline).toHaveBeenCalledWith(
expect(mockClusterClient.ingest.putPipeline).toHaveBeenCalledTimes(2);
expect(mockClusterClient.ingest.putPipeline).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ id: SLO_INGEST_PIPELINE_NAME })
);
expect(mockClusterClient.ingest.putPipeline).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ id: SLO_SUMMARY_INGEST_PIPELINE_NAME })
);
});
});
@ -91,6 +99,10 @@ describe('resourceInstaller', () => {
// @ts-ignore _meta not typed properly
[SLO_INGEST_PIPELINE_NAME]: { _meta: { version: SLO_RESOURCES_VERSION } },
});
mockClusterClient.ingest.getPipeline.mockResponseOnce({
// @ts-ignore _meta not typed properly
[SLO_SUMMARY_INGEST_PIPELINE_NAME]: { _meta: { version: SLO_RESOURCES_VERSION } },
});
const installer = new DefaultResourceInstaller(mockClusterClient, loggerMock.create());
await installer.ensureCommonResourcesInstalled();

View file

@ -26,6 +26,7 @@ import {
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';
@ -35,6 +36,7 @@ import { getSLOSummaryMappingsTemplate } from '../../assets/component_templates/
import { getSLOSummarySettingsTemplate } from '../../assets/component_templates/slo_summary_settings_template';
import { getSLOSummaryIndexTemplate } from '../../assets/index_templates/slo_summary_index_templates';
import { retryTransientEsErrors } from '../../utils/retry';
import { getSLOSummaryPipelineTemplate } from '../../assets/ingest_templates/slo_summary_pipeline_template';
export interface ResourceInstaller {
ensureCommonResourcesInstalled(): Promise<void>;
@ -94,6 +96,10 @@ export class DefaultResourceInstaller implements ResourceInstaller {
await this.createOrUpdateIngestPipelineTemplate(
getSLOPipelineTemplate(SLO_INGEST_PIPELINE_NAME, SLO_INGEST_PIPELINE_INDEX_NAME_PREFIX)
);
await this.createOrUpdateIngestPipelineTemplate(
getSLOSummaryPipelineTemplate(SLO_SUMMARY_INGEST_PIPELINE_NAME)
);
} catch (err) {
this.logger.error(`Error installing resources shared for SLO: ${err.message}`);
throw err;
@ -149,7 +155,26 @@ export class DefaultResourceInstaller implements ResourceInstaller {
return false;
}
return indexTemplateExists && summaryIndexTemplateExists && ingestPipelineExists;
let summaryIngestPipelineExists = false;
try {
const pipeline = await this.execute(() =>
this.esClient.ingest.getPipeline({ id: SLO_SUMMARY_INGEST_PIPELINE_NAME })
);
summaryIngestPipelineExists =
pipeline &&
// @ts-ignore _meta is not defined on the type
pipeline[SLO_SUMMARY_INGEST_PIPELINE_NAME]._meta.version === SLO_RESOURCES_VERSION;
} catch (err) {
return false;
}
return (
indexTemplateExists &&
summaryIndexTemplateExists &&
ingestPipelineExists &&
summaryIngestPipelineExists
);
}
private async createOrUpdateComponentTemplate(template: ClusterPutComponentTemplateRequest) {

View file

@ -8,23 +8,16 @@
import { SavedObjectsClientContract, SavedObjectsFindResponse } from '@kbn/core/server';
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
import { sloSchema } from '@kbn/slo-schema';
import { SLO, StoredSLO } from '../../domain/models';
import { SO_SLO_TYPE } from '../../saved_objects';
import {
KibanaSavedObjectsSLORepository,
Pagination,
Sort,
SortDirection,
SortField,
} from './slo_repository';
import { createAPMTransactionDurationIndicator, createSLO, aStoredSLO } from './fixtures/slo';
import { SLOIdConflict, SLONotFound } from '../../errors';
import { SO_SLO_TYPE } from '../../saved_objects';
import { aStoredSLO, createAPMTransactionDurationIndicator, createSLO } from './fixtures/slo';
import { KibanaSavedObjectsSLORepository } from './slo_repository';
const SOME_SLO = createSLO({ indicator: createAPMTransactionDurationIndicator() });
const ANOTHER_SLO = createSLO();
function createFindResponse(sloList: SLO[]): SavedObjectsFindResponse<StoredSLO> {
function soFindResponse(sloList: SLO[]): SavedObjectsFindResponse<StoredSLO> {
return {
page: 1,
per_page: 25,
@ -48,7 +41,7 @@ describe('KibanaSavedObjectsSLORepository', () => {
describe('validation', () => {
it('findById throws when an SLO is not found', async () => {
soClientMock.find.mockResolvedValueOnce(createFindResponse([]));
soClientMock.find.mockResolvedValueOnce(soFindResponse([]));
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
await expect(repository.findById('inexistant-slo-id')).rejects.toThrowError(
@ -57,7 +50,7 @@ describe('KibanaSavedObjectsSLORepository', () => {
});
it('deleteById throws when an SLO is not found', async () => {
soClientMock.find.mockResolvedValueOnce(createFindResponse([]));
soClientMock.find.mockResolvedValueOnce(soFindResponse([]));
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
await expect(repository.deleteById('inexistant-slo-id')).rejects.toThrowError(
@ -69,7 +62,7 @@ describe('KibanaSavedObjectsSLORepository', () => {
describe('saving an SLO', () => {
it('saves the new SLO', async () => {
const slo = createSLO({ id: 'my-id' });
soClientMock.find.mockResolvedValueOnce(createFindResponse([]));
soClientMock.find.mockResolvedValueOnce(soFindResponse([]));
soClientMock.create.mockResolvedValueOnce(aStoredSLO(slo));
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
@ -90,7 +83,7 @@ describe('KibanaSavedObjectsSLORepository', () => {
it('throws when the SLO id already exists and "throwOnConflict" is true', async () => {
const slo = createSLO({ id: 'my-id' });
soClientMock.find.mockResolvedValueOnce(createFindResponse([slo]));
soClientMock.find.mockResolvedValueOnce(soFindResponse([slo]));
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
await expect(repository.save(slo, { throwOnConflict: true })).rejects.toThrowError(
@ -106,7 +99,7 @@ describe('KibanaSavedObjectsSLORepository', () => {
it('updates the existing SLO', async () => {
const slo = createSLO({ id: 'my-id' });
soClientMock.find.mockResolvedValueOnce(createFindResponse([slo]));
soClientMock.find.mockResolvedValueOnce(soFindResponse([slo]));
soClientMock.create.mockResolvedValueOnce(aStoredSLO(slo));
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
@ -128,7 +121,7 @@ describe('KibanaSavedObjectsSLORepository', () => {
it('finds an existing SLO', async () => {
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO]));
soClientMock.find.mockResolvedValueOnce(soFindResponse([SOME_SLO]));
const foundSLO = await repository.findById(SOME_SLO.id);
@ -143,7 +136,7 @@ describe('KibanaSavedObjectsSLORepository', () => {
it('finds all SLOs by ids', async () => {
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO, ANOTHER_SLO]));
soClientMock.find.mockResolvedValueOnce(soFindResponse([SOME_SLO, ANOTHER_SLO]));
const results = await repository.findAllByIds([SOME_SLO.id, ANOTHER_SLO.id]);
@ -158,7 +151,7 @@ describe('KibanaSavedObjectsSLORepository', () => {
it('deletes an SLO', async () => {
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO]));
soClientMock.find.mockResolvedValueOnce(soFindResponse([SOME_SLO]));
await repository.deleteById(SOME_SLO.id);
@ -170,238 +163,4 @@ describe('KibanaSavedObjectsSLORepository', () => {
});
expect(soClientMock.delete).toHaveBeenCalledWith(SO_SLO_TYPE, SOME_SLO.id);
});
describe('find', () => {
const DEFAULT_PAGINATION: Pagination = { page: 1, perPage: 25 };
const DEFAULT_SORTING: Sort = {
field: SortField.CreationTime,
direction: SortDirection.Asc,
};
describe('Name search', () => {
it('includes the search on name with wildcard when provided', async () => {
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO]));
const result = await repository.find(
{ name: 'availability*' },
DEFAULT_SORTING,
DEFAULT_PAGINATION
);
expect(result).toEqual({
page: 1,
perPage: 25,
total: 1,
results: [SOME_SLO],
});
expect(soClientMock.find).toHaveBeenCalledWith({
type: SO_SLO_TYPE,
page: 1,
perPage: 25,
filter: undefined,
search: '*availability*',
searchFields: ['name'],
sortField: 'created_at',
sortOrder: 'asc',
});
});
it('includes the search on name with added wildcard when not provided', async () => {
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO]));
const result = await repository.find(
{ name: 'availa' },
DEFAULT_SORTING,
DEFAULT_PAGINATION
);
expect(result).toEqual({
page: 1,
perPage: 25,
total: 1,
results: [SOME_SLO],
});
expect(soClientMock.find).toHaveBeenCalledWith({
type: SO_SLO_TYPE,
page: 1,
perPage: 25,
filter: undefined,
search: '*availa*',
searchFields: ['name'],
sortField: 'created_at',
sortOrder: 'asc',
});
});
});
describe('indicatorTypes filter', () => {
it('includes the filter on indicator types when provided', async () => {
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO]));
const result = await repository.find(
{ indicatorTypes: ['sli.kql.custom'] },
DEFAULT_SORTING,
DEFAULT_PAGINATION
);
expect(result).toEqual({
page: 1,
perPage: 25,
total: 1,
results: [SOME_SLO],
});
expect(soClientMock.find).toHaveBeenCalledWith({
type: SO_SLO_TYPE,
page: 1,
perPage: 25,
filter: `(slo.attributes.indicator.type: sli.kql.custom)`,
search: undefined,
searchFields: undefined,
sortField: 'created_at',
sortOrder: 'asc',
});
});
it('includes the filter on indicator types as logical OR when provided', async () => {
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO]));
const result = await repository.find(
{ indicatorTypes: ['sli.kql.custom', 'sli.apm.transactionDuration'] },
DEFAULT_SORTING,
DEFAULT_PAGINATION
);
expect(result).toEqual({
page: 1,
perPage: 25,
total: 1,
results: [SOME_SLO],
});
expect(soClientMock.find).toHaveBeenCalledWith({
type: SO_SLO_TYPE,
page: 1,
perPage: 25,
filter: `(slo.attributes.indicator.type: sli.kql.custom or slo.attributes.indicator.type: sli.apm.transactionDuration)`,
search: undefined,
searchFields: undefined,
sortField: 'created_at',
sortOrder: 'asc',
});
});
});
it('includes search on name and filter on indicator types', async () => {
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO]));
const result = await repository.find(
{ name: 'latency', indicatorTypes: ['sli.kql.custom', 'sli.apm.transactionDuration'] },
DEFAULT_SORTING,
DEFAULT_PAGINATION
);
expect(result).toEqual({
page: 1,
perPage: 25,
total: 1,
results: [SOME_SLO],
});
expect(soClientMock.find).toHaveBeenCalledWith({
type: SO_SLO_TYPE,
page: 1,
perPage: 25,
filter: `(slo.attributes.indicator.type: sli.kql.custom or slo.attributes.indicator.type: sli.apm.transactionDuration)`,
search: '*latency*',
searchFields: ['name'],
sortField: 'created_at',
sortOrder: 'asc',
});
});
it('does not include the search or filter when no criteria provided', async () => {
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO]));
const result = await repository.find({}, DEFAULT_SORTING, DEFAULT_PAGINATION);
expect(result).toEqual({
page: 1,
perPage: 25,
total: 1,
results: [SOME_SLO],
});
expect(soClientMock.find).toHaveBeenCalledWith({
type: SO_SLO_TYPE,
page: 1,
perPage: 25,
search: undefined,
searchFields: undefined,
sortField: 'created_at',
sortOrder: 'asc',
});
});
it('sorts by creation time ascending', async () => {
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO]));
await repository.find({}, DEFAULT_SORTING, DEFAULT_PAGINATION);
expect(soClientMock.find).toHaveBeenCalledWith({
type: SO_SLO_TYPE,
page: 1,
perPage: 25,
search: undefined,
searchFields: undefined,
sortField: 'created_at',
sortOrder: 'asc',
});
});
it('sorts by creation time descending', async () => {
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO]));
await repository.find(
{},
{ field: SortField.CreationTime, direction: SortDirection.Desc },
DEFAULT_PAGINATION
);
expect(soClientMock.find).toHaveBeenCalledWith({
type: SO_SLO_TYPE,
page: 1,
perPage: 25,
search: undefined,
searchFields: undefined,
sortField: 'created_at',
sortOrder: 'desc',
});
});
it('sorts by indicator type', async () => {
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO]));
await repository.find(
{},
{ field: SortField.IndicatorType, direction: SortDirection.Asc },
DEFAULT_PAGINATION
);
expect(soClientMock.find).toHaveBeenCalledWith({
type: SO_SLO_TYPE,
page: 1,
perPage: 25,
search: undefined,
searchFields: undefined,
sortField: 'indicator.type',
sortOrder: 'asc',
});
});
});
});

View file

@ -5,62 +5,21 @@
* 2.0.
*/
import * as t from 'io-ts';
import { fold } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/pipeable';
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server';
import { sloSchema } from '@kbn/slo-schema';
import { StoredSLO, SLO } from '../../domain/models';
import { SO_SLO_TYPE } from '../../saved_objects';
import { fold } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/pipeable';
import * as t from 'io-ts';
import { SLO, StoredSLO } from '../../domain/models';
import { SLOIdConflict, SLONotFound } from '../../errors';
type ObjectValues<T> = T[keyof T];
export interface Criteria {
name?: string;
indicatorTypes?: string[];
}
export interface Pagination {
page: number;
perPage: number;
}
export const SortDirection = {
Asc: 'Asc',
Desc: 'Desc',
} as const;
type SortDirection = ObjectValues<typeof SortDirection>;
export const SortField = {
CreationTime: 'CreationTime',
IndicatorType: 'IndicatorType',
};
type SortField = ObjectValues<typeof SortField>;
export interface Sort {
field: SortField;
direction: SortDirection;
}
export interface Paginated<T> {
page: number;
perPage: number;
total: number;
results: T[];
}
import { SO_SLO_TYPE } from '../../saved_objects';
export interface SLORepository {
save(slo: SLO, options?: { throwOnConflict: boolean }): Promise<SLO>;
findAllByIds(ids: string[]): Promise<SLO[]>;
findById(id: string): Promise<SLO>;
deleteById(id: string): Promise<void>;
find(criteria: Criteria, sort: Sort, pagination: Pagination): Promise<Paginated<SLO>>;
}
export class KibanaSavedObjectsSLORepository implements SLORepository {
@ -120,29 +79,6 @@ export class KibanaSavedObjectsSLORepository implements SLORepository {
await this.soClient.delete(SO_SLO_TYPE, response.saved_objects[0].id);
}
async find(criteria: Criteria, sort: Sort, pagination: Pagination): Promise<Paginated<SLO>> {
const { search, searchFields } = buildSearch(criteria);
const filterKuery = buildFilterKuery(criteria);
const { sortField, sortOrder } = buildSortQuery(sort);
const response = await this.soClient.find<StoredSLO>({
type: SO_SLO_TYPE,
page: pagination.page,
perPage: pagination.perPage,
search,
searchFields,
filter: filterKuery,
sortField,
sortOrder,
});
return {
total: response.total,
page: response.page,
perPage: response.per_page,
results: response.saved_objects.map((so) => toSLO(so.attributes)),
};
}
async findAllByIds(ids: string[]): Promise<SLO[]> {
if (ids.length === 0) return [];
@ -163,47 +99,6 @@ export class KibanaSavedObjectsSLORepository implements SLORepository {
}
}
function buildSearch(criteria: Criteria): {
search: string | undefined;
searchFields: string[] | undefined;
} {
if (!criteria.name) {
return { search: undefined, searchFields: undefined };
}
return { search: addWildcardsIfAbsent(criteria.name), searchFields: ['name'] };
}
function buildFilterKuery(criteria: Criteria): string | undefined {
const filters: string[] = [];
if (!!criteria.indicatorTypes) {
const indicatorTypesFilter: string[] = criteria.indicatorTypes.map(
(indicatorType) => `slo.attributes.indicator.type: ${indicatorType}`
);
filters.push(`(${indicatorTypesFilter.join(' or ')})`);
}
return filters.length > 0 ? filters.join(' and ') : undefined;
}
function buildSortQuery(sort: Sort): { sortField: string; sortOrder: 'asc' | 'desc' } {
let sortField: string;
switch (sort.field) {
case SortField.IndicatorType:
sortField = 'indicator.type';
break;
case SortField.CreationTime:
default:
sortField = 'created_at';
break;
}
return {
sortField,
sortOrder: sort.direction === SortDirection.Desc ? 'desc' : 'asc',
};
}
function toStoredSLO(slo: SLO): StoredSLO {
return sloSchema.encode(slo);
}
@ -216,17 +111,3 @@ function toSLO(storedSLO: StoredSLO): SLO {
}, t.identity)
);
}
const WILDCARD_CHAR = '*';
function addWildcardsIfAbsent(value: string): string {
let updatedValue = value;
if (updatedValue.substring(0, 1) !== WILDCARD_CHAR) {
updatedValue = `${WILDCARD_CHAR}${updatedValue}`;
}
if (value.substring(value.length - 1) !== WILDCARD_CHAR) {
updatedValue = `${updatedValue}${WILDCARD_CHAR}`;
}
return updatedValue;
}

View file

@ -0,0 +1,121 @@
/*
* 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, Logger } from '@kbn/core/server';
import { assertNever } from '@kbn/std';
import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../assets/constants';
import { SLOId, Status, Summary } from '../../domain/models';
import { getElastichsearchQueryOrThrow } from './transform_generators';
interface EsSummaryDocument {
slo: {
id: string;
revision: number;
};
sliValue: number;
errorBudgetConsumed: number;
errorBudgetRemaining: number;
errorBudgetInitial: number;
errorBudgetEstimated: boolean;
statusCode: number;
status: Status;
}
export interface Paginated<T> {
total: number;
page: number;
perPage: number;
results: T[];
}
export interface SLOSummary {
id: SLOId;
summary: Summary;
}
export type SortField = 'error_budget_consumed' | 'error_budget_remaining' | 'sli_value' | 'status';
export interface Sort {
field: SortField;
direction: 'asc' | 'desc';
}
export interface Pagination {
page: number;
perPage: number;
}
export interface SummarySearchClient {
search(kqlQuery: string, sort: Sort, pagination: Pagination): Promise<Paginated<SLOSummary>>;
}
export class DefaultSummarySearchClient implements SummarySearchClient {
constructor(private esClient: ElasticsearchClient, private logger: Logger) {}
async search(
kqlQuery: string,
sort: Sort,
pagination: Pagination
): Promise<Paginated<SLOSummary>> {
try {
const result = await this.esClient.search<EsSummaryDocument>({
index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
query: getElastichsearchQueryOrThrow(kqlQuery),
sort: {
[toDocumentSortField(sort.field)]: {
order: sort.direction,
},
},
from: (pagination.page - 1) * pagination.perPage,
size: pagination.perPage,
});
const total =
typeof result.hits.total === 'number' ? result.hits.total : result.hits.total?.value;
if (total === undefined || total === 0) {
return { total: 0, perPage: pagination.perPage, page: pagination.page, results: [] };
}
return {
total,
perPage: pagination.perPage,
page: pagination.page,
results: result.hits.hits.map((doc) => ({
id: doc._source!.slo.id,
summary: {
errorBudget: {
initial: doc._source!.errorBudgetInitial,
consumed: doc._source!.errorBudgetConsumed,
remaining: doc._source!.errorBudgetRemaining,
isEstimated: doc._source!.errorBudgetEstimated,
},
sliValue: doc._source!.sliValue,
status: doc._source!.status,
},
})),
};
} catch (err) {
this.logger.error(new Error('Summary search query error', { cause: err }));
return { total: 0, perPage: pagination.perPage, page: pagination.page, results: [] };
}
}
}
function toDocumentSortField(field: SortField) {
switch (field) {
case 'error_budget_consumed':
return 'errorBudgetConsumed';
case 'error_budget_remaining':
return 'errorBudgetRemaining';
case 'status':
return 'status';
case 'sli_value':
return 'sliValue';
default:
assertNever(field);
}
}

View file

@ -12,6 +12,7 @@ Array [
"description": "Summarize every SLO with timeslices budgeting method and a 7 days rolling time window",
"dest": Object {
"index": ".slo-observability.summary-v2",
"pipeline": ".slo-observability.summary.pipeline",
},
"frequency": "1m",
"pivot": Object {
@ -60,7 +61,7 @@ Array [
"script": "if (params.totalEvents == 0) { return -1 } else { return params.goodEvents / params.totalEvents }",
},
},
"status": Object {
"statusCode": Object {
"bucket_script": Object {
"buckets_path": Object {
"errorBudgetRemaining": "errorBudgetRemaining",
@ -79,6 +80,11 @@ Array [
},
},
"group_by": Object {
"errorBudgetEstimated": Object {
"terms": Object {
"field": "errorBudgetEstimated",
},
},
"service.environment": Object {
"terms": Object {
"field": "service.environment",
@ -189,6 +195,12 @@ Array [
],
},
},
"runtime_mappings": Object {
"errorBudgetEstimated": Object {
"script": "emit(false)",
"type": "boolean",
},
},
},
"sync": Object {
"time": Object {
@ -214,6 +226,7 @@ Array [
"description": "Summarize every SLO with timeslices budgeting method and a 30 days rolling time window",
"dest": Object {
"index": ".slo-observability.summary-v2",
"pipeline": ".slo-observability.summary.pipeline",
},
"frequency": "1m",
"pivot": Object {
@ -262,7 +275,7 @@ Array [
"script": "if (params.totalEvents == 0) { return -1 } else { return params.goodEvents / params.totalEvents }",
},
},
"status": Object {
"statusCode": Object {
"bucket_script": Object {
"buckets_path": Object {
"errorBudgetRemaining": "errorBudgetRemaining",
@ -281,6 +294,11 @@ Array [
},
},
"group_by": Object {
"errorBudgetEstimated": Object {
"terms": Object {
"field": "errorBudgetEstimated",
},
},
"service.environment": Object {
"terms": Object {
"field": "service.environment",
@ -391,6 +409,12 @@ Array [
],
},
},
"runtime_mappings": Object {
"errorBudgetEstimated": Object {
"script": "emit(false)",
"type": "boolean",
},
},
},
"sync": Object {
"time": Object {
@ -416,6 +440,7 @@ Array [
"description": "Summarize every SLO with timeslices budgeting method and a 90 days rolling time window",
"dest": Object {
"index": ".slo-observability.summary-v2",
"pipeline": ".slo-observability.summary.pipeline",
},
"frequency": "1m",
"pivot": Object {
@ -464,7 +489,7 @@ Array [
"script": "if (params.totalEvents == 0) { return -1 } else { return params.goodEvents / params.totalEvents }",
},
},
"status": Object {
"statusCode": Object {
"bucket_script": Object {
"buckets_path": Object {
"errorBudgetRemaining": "errorBudgetRemaining",
@ -483,6 +508,11 @@ Array [
},
},
"group_by": Object {
"errorBudgetEstimated": Object {
"terms": Object {
"field": "errorBudgetEstimated",
},
},
"service.environment": Object {
"terms": Object {
"field": "service.environment",
@ -593,6 +623,12 @@ Array [
],
},
},
"runtime_mappings": Object {
"errorBudgetEstimated": Object {
"script": "emit(false)",
"type": "boolean",
},
},
},
"sync": Object {
"time": Object {
@ -618,6 +654,7 @@ Array [
"description": "Summarize every SLO with timeslices budgeting method and a weekly calendar aligned time window",
"dest": Object {
"index": ".slo-observability.summary-v2",
"pipeline": ".slo-observability.summary.pipeline",
},
"frequency": "1m",
"pivot": Object {
@ -681,7 +718,7 @@ Array [
"script": "if (params.totalEvents == 0) { return -1 } else { return params.goodEvents / params.totalEvents }",
},
},
"status": Object {
"statusCode": Object {
"bucket_script": Object {
"buckets_path": Object {
"errorBudgetRemaining": "errorBudgetRemaining",
@ -698,6 +735,11 @@ Array [
},
},
"group_by": Object {
"errorBudgetEstimated": Object {
"terms": Object {
"field": "errorBudgetEstimated",
},
},
"service.environment": Object {
"terms": Object {
"field": "service.environment",
@ -808,6 +850,12 @@ Array [
],
},
},
"runtime_mappings": Object {
"errorBudgetEstimated": Object {
"script": "emit(false)",
"type": "boolean",
},
},
},
"sync": Object {
"time": Object {
@ -833,6 +881,7 @@ Array [
"description": "Summarize every SLO with timeslices budgeting method and a monthly calendar aligned time window",
"dest": Object {
"index": ".slo-observability.summary-v2",
"pipeline": ".slo-observability.summary.pipeline",
},
"frequency": "1m",
"pivot": Object {
@ -911,7 +960,7 @@ Array [
"script": "if (params.totalEvents == 0) { return -1 } else { return params.goodEvents / params.totalEvents }",
},
},
"status": Object {
"statusCode": Object {
"bucket_script": Object {
"buckets_path": Object {
"errorBudgetRemaining": "errorBudgetRemaining",
@ -928,6 +977,11 @@ Array [
},
},
"group_by": Object {
"errorBudgetEstimated": Object {
"terms": Object {
"field": "errorBudgetEstimated",
},
},
"service.environment": Object {
"terms": Object {
"field": "service.environment",
@ -1038,6 +1092,12 @@ Array [
],
},
},
"runtime_mappings": Object {
"errorBudgetEstimated": Object {
"script": "emit(false)",
"type": "boolean",
},
},
},
"sync": Object {
"time": Object {

View file

@ -56,6 +56,11 @@ export const groupBy = {
field: 'slo.timeWindow.type',
},
},
errorBudgetEstimated: {
terms: {
field: 'errorBudgetEstimated',
},
},
// optional fields: only specified for APM indicators. Must include missing_bucket:true
'service.name': {
terms: {

View file

@ -10,6 +10,7 @@ import {
SLO_DESTINATION_INDEX_PATTERN,
SLO_RESOURCES_VERSION,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
} from '../../../../assets/constants';
import { groupBy } from './common';
@ -18,9 +19,16 @@ export const SUMMARY_OCCURRENCES_30D_ROLLING: TransformPutTransformRequest = {
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}occurrences-30d-rolling`,
dest: {
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
runtime_mappings: {
errorBudgetEstimated: {
type: 'boolean',
script: 'emit(false)',
},
},
query: {
bool: {
filter: [
@ -105,7 +113,7 @@ export const SUMMARY_OCCURRENCES_30D_ROLLING: TransformPutTransformRequest = {
script: '1 - params.errorBudgetConsummed',
},
},
status: {
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',

View file

@ -10,6 +10,7 @@ import {
SLO_DESTINATION_INDEX_PATTERN,
SLO_RESOURCES_VERSION,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
} from '../../../../assets/constants';
import { groupBy } from './common';
@ -17,10 +18,17 @@ import { groupBy } from './common';
export const SUMMARY_OCCURRENCES_7D_ROLLING: TransformPutTransformRequest = {
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}occurrences-7d-rolling`,
dest: {
pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME,
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
runtime_mappings: {
errorBudgetEstimated: {
type: 'boolean',
script: 'emit(false)',
},
},
query: {
bool: {
filter: [
@ -105,7 +113,7 @@ export const SUMMARY_OCCURRENCES_7D_ROLLING: TransformPutTransformRequest = {
script: '1 - params.errorBudgetConsummed',
},
},
status: {
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',

View file

@ -10,6 +10,7 @@ import {
SLO_DESTINATION_INDEX_PATTERN,
SLO_RESOURCES_VERSION,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
} from '../../../../assets/constants';
import { groupBy } from './common';
@ -18,9 +19,16 @@ export const SUMMARY_OCCURRENCES_90D_ROLLING: TransformPutTransformRequest = {
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}occurrences-90d-rolling`,
dest: {
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
runtime_mappings: {
errorBudgetEstimated: {
type: 'boolean',
script: 'emit(false)',
},
},
query: {
bool: {
filter: [
@ -105,7 +113,7 @@ export const SUMMARY_OCCURRENCES_90D_ROLLING: TransformPutTransformRequest = {
script: '1 - params.errorBudgetConsummed',
},
},
status: {
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',

View file

@ -10,6 +10,7 @@ import {
SLO_DESTINATION_INDEX_PATTERN,
SLO_RESOURCES_VERSION,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
} from '../../../../assets/constants';
import { groupBy } from './common';
@ -18,9 +19,16 @@ export const SUMMARY_OCCURRENCES_MONTHLY_ALIGNED: TransformPutTransformRequest =
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}occurrences-monthly-aligned`,
dest: {
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
runtime_mappings: {
errorBudgetEstimated: {
type: 'boolean',
script: 'emit(true)',
},
},
query: {
bool: {
filter: [
@ -105,7 +113,7 @@ export const SUMMARY_OCCURRENCES_MONTHLY_ALIGNED: TransformPutTransformRequest =
script: '1 - params.errorBudgetConsumed',
},
},
status: {
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',

View file

@ -10,6 +10,7 @@ import {
SLO_DESTINATION_INDEX_PATTERN,
SLO_RESOURCES_VERSION,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
} from '../../../../assets/constants';
import { groupBy } from './common';
@ -18,9 +19,16 @@ export const SUMMARY_OCCURRENCES_WEEKLY_ALIGNED: TransformPutTransformRequest =
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}occurrences-weekly-aligned`,
dest: {
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
runtime_mappings: {
errorBudgetEstimated: {
type: 'boolean',
script: 'emit(true)',
},
},
query: {
bool: {
filter: [
@ -105,7 +113,7 @@ export const SUMMARY_OCCURRENCES_WEEKLY_ALIGNED: TransformPutTransformRequest =
script: '1 - params.errorBudgetConsumed',
},
},
status: {
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',

View file

@ -10,6 +10,7 @@ import {
SLO_DESTINATION_INDEX_PATTERN,
SLO_RESOURCES_VERSION,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
} from '../../../../assets/constants';
import { groupBy } from './common';
@ -18,9 +19,16 @@ export const SUMMARY_TIMESLICES_30D_ROLLING: TransformPutTransformRequest = {
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}timeslices-30d-rolling`,
dest: {
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
runtime_mappings: {
errorBudgetEstimated: {
type: 'boolean',
script: 'emit(false)',
},
},
query: {
bool: {
filter: [
@ -105,7 +113,7 @@ export const SUMMARY_TIMESLICES_30D_ROLLING: TransformPutTransformRequest = {
script: '1 - params.errorBudgetConsummed',
},
},
status: {
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',

View file

@ -10,6 +10,7 @@ import {
SLO_DESTINATION_INDEX_PATTERN,
SLO_RESOURCES_VERSION,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
} from '../../../../assets/constants';
import { groupBy } from './common';
@ -18,9 +19,16 @@ export const SUMMARY_TIMESLICES_7D_ROLLING: TransformPutTransformRequest = {
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}timeslices-7d-rolling`,
dest: {
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
runtime_mappings: {
errorBudgetEstimated: {
type: 'boolean',
script: 'emit(false)',
},
},
query: {
bool: {
filter: [
@ -105,7 +113,7 @@ export const SUMMARY_TIMESLICES_7D_ROLLING: TransformPutTransformRequest = {
script: '1 - params.errorBudgetConsummed',
},
},
status: {
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',

View file

@ -10,6 +10,7 @@ import {
SLO_DESTINATION_INDEX_PATTERN,
SLO_RESOURCES_VERSION,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
} from '../../../../assets/constants';
import { groupBy } from './common';
@ -18,9 +19,16 @@ export const SUMMARY_TIMESLICES_90D_ROLLING: TransformPutTransformRequest = {
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}timeslices-90d-rolling`,
dest: {
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
runtime_mappings: {
errorBudgetEstimated: {
type: 'boolean',
script: 'emit(false)',
},
},
query: {
bool: {
filter: [
@ -105,7 +113,7 @@ export const SUMMARY_TIMESLICES_90D_ROLLING: TransformPutTransformRequest = {
script: '1 - params.errorBudgetConsummed',
},
},
status: {
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',

View file

@ -10,6 +10,7 @@ import {
SLO_DESTINATION_INDEX_PATTERN,
SLO_RESOURCES_VERSION,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
} from '../../../../assets/constants';
import { groupBy } from './common';
@ -18,9 +19,16 @@ export const SUMMARY_TIMESLICES_MONTHLY_ALIGNED: TransformPutTransformRequest =
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}timeslices-monthly-aligned`,
dest: {
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
runtime_mappings: {
errorBudgetEstimated: {
type: 'boolean',
script: 'emit(false)',
},
},
query: {
bool: {
filter: [
@ -135,7 +143,7 @@ export const SUMMARY_TIMESLICES_MONTHLY_ALIGNED: TransformPutTransformRequest =
script: '1 - params.errorBudgetConsumed',
},
},
status: {
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',

View file

@ -10,6 +10,7 @@ import {
SLO_DESTINATION_INDEX_PATTERN,
SLO_RESOURCES_VERSION,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
} from '../../../../assets/constants';
import { groupBy } from './common';
@ -18,9 +19,16 @@ export const SUMMARY_TIMESLICES_WEEKLY_ALIGNED: TransformPutTransformRequest = {
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}timeslices-weekly-aligned`,
dest: {
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
runtime_mappings: {
errorBudgetEstimated: {
type: 'boolean',
script: 'emit(false)',
},
},
query: {
bool: {
filter: [
@ -120,7 +128,7 @@ export const SUMMARY_TIMESLICES_WEEKLY_ALIGNED: TransformPutTransformRequest = {
script: '1 - params.errorBudgetConsumed',
},
},
status: {
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',

View file

@ -27631,11 +27631,8 @@
"xpack.observability.slo.list.errorMessage": "Une erreur s'est produite lors du chargement des SLO. Contactez votre administrateur pour obtenir de l'aide.",
"xpack.observability.slo.list.errorNotification": "Un problème est survenu lors de la récupération des SLO",
"xpack.observability.slo.list.errorTitle": "Impossible de charger les SLO",
"xpack.observability.slo.list.indicatorTypeFilter": "Type dindicateur",
"xpack.observability.slo.list.search": "Recherche",
"xpack.observability.slo.list.sortBy": "Trier par",
"xpack.observability.slo.list.sortBy.creationTime": "Heure de création",
"xpack.observability.slo.list.sortBy.indicatorType": "Type dindicateur",
"xpack.observability.slo.rules.actionGroupSelectorLabel": "Groupe daction",
"xpack.observability.slo.rules.addWindowAriaLabel": "Ajouter une fenêtre",
"xpack.observability.slo.rules.addWIndowLabel": "Ajouter une fenêtre",

View file

@ -27631,11 +27631,9 @@
"xpack.observability.slo.list.errorMessage": "SLOオブジェクトの読み込みエラーが発生しました。ヘルプについては、管理者にお問い合わせください。",
"xpack.observability.slo.list.errorNotification": "SLOの取得中に問題が発生しました",
"xpack.observability.slo.list.errorTitle": "SLOを読み込めません",
"xpack.observability.slo.list.indicatorTypeFilter": "インジケータータイプ",
"xpack.observability.slo.list.search": "検索",
"xpack.observability.slo.list.sortBy": "並べ替え基準",
"xpack.observability.slo.list.sortBy.creationTime": "作成時刻",
"xpack.observability.slo.list.sortBy.indicatorType": "インジケータータイプ",
"xpack.observability.slo.rules.actionGroupSelectorLabel": "アクショングループ",
"xpack.observability.slo.rules.addWindowAriaLabel": "時間枠を追加",
"xpack.observability.slo.rules.addWIndowLabel": "時間枠を追加",

View file

@ -27629,11 +27629,8 @@
"xpack.observability.slo.list.errorMessage": "加载 SLO 时出错。请联系您的管理员寻求帮助。",
"xpack.observability.slo.list.errorNotification": "提取 SLO 时出现问题",
"xpack.observability.slo.list.errorTitle": "无法加载 SLO",
"xpack.observability.slo.list.indicatorTypeFilter": "指标类型",
"xpack.observability.slo.list.search": "搜索",
"xpack.observability.slo.list.sortBy": "排序依据",
"xpack.observability.slo.list.sortBy.creationTime": "创建时间",
"xpack.observability.slo.list.sortBy.indicatorType": "指标类型",
"xpack.observability.slo.rules.actionGroupSelectorLabel": "操作组",
"xpack.observability.slo.rules.addWindowAriaLabel": "添加窗口",
"xpack.observability.slo.rules.addWIndowLabel": "添加窗口",