mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
feat(slo): introduce new search capabilities (#162665)
This commit is contained in:
parent
1700e3e534
commit
757c881b9a
104 changed files with 7405 additions and 1429 deletions
|
@ -27,8 +27,6 @@ describe('Duration', () => {
|
|||
expect(new Duration(1, DurationUnit.Day).format()).toBe('1d');
|
||||
expect(new Duration(1, DurationUnit.Week).format()).toBe('1w');
|
||||
expect(new Duration(1, DurationUnit.Month).format()).toBe('1M');
|
||||
expect(new Duration(1, DurationUnit.Quarter).format()).toBe('1Q');
|
||||
expect(new Duration(1, DurationUnit.Year).format()).toBe('1Y');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -39,31 +37,25 @@ describe('Duration', () => {
|
|||
expect(short.isShorterThan(new Duration(1, DurationUnit.Day))).toBe(true);
|
||||
expect(short.isShorterThan(new Duration(1, DurationUnit.Week))).toBe(true);
|
||||
expect(short.isShorterThan(new Duration(1, DurationUnit.Month))).toBe(true);
|
||||
expect(short.isShorterThan(new Duration(1, DurationUnit.Quarter))).toBe(true);
|
||||
expect(short.isShorterThan(new Duration(1, DurationUnit.Year))).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when the current duration is longer (or equal) than the other duration', () => {
|
||||
const long = new Duration(1, DurationUnit.Year);
|
||||
const long = new Duration(1, DurationUnit.Month);
|
||||
expect(long.isShorterThan(new Duration(1, DurationUnit.Minute))).toBe(false);
|
||||
expect(long.isShorterThan(new Duration(1, DurationUnit.Hour))).toBe(false);
|
||||
expect(long.isShorterThan(new Duration(1, DurationUnit.Day))).toBe(false);
|
||||
expect(long.isShorterThan(new Duration(1, DurationUnit.Week))).toBe(false);
|
||||
expect(long.isShorterThan(new Duration(1, DurationUnit.Month))).toBe(false);
|
||||
expect(long.isShorterThan(new Duration(1, DurationUnit.Quarter))).toBe(false);
|
||||
expect(long.isShorterThan(new Duration(1, DurationUnit.Year))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLongerOrEqualThan', () => {
|
||||
it('returns true when the current duration is longer or equal than the other duration', () => {
|
||||
const long = new Duration(2, DurationUnit.Year);
|
||||
const long = new Duration(2, DurationUnit.Month);
|
||||
expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Hour))).toBe(true);
|
||||
expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Day))).toBe(true);
|
||||
expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Week))).toBe(true);
|
||||
expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Month))).toBe(true);
|
||||
expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Quarter))).toBe(true);
|
||||
expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Year))).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when the current duration is shorter than the other duration', () => {
|
||||
|
@ -73,8 +65,6 @@ describe('Duration', () => {
|
|||
expect(short.isLongerOrEqualThan(new Duration(1, DurationUnit.Day))).toBe(false);
|
||||
expect(short.isLongerOrEqualThan(new Duration(1, DurationUnit.Week))).toBe(false);
|
||||
expect(short.isLongerOrEqualThan(new Duration(1, DurationUnit.Month))).toBe(false);
|
||||
expect(short.isLongerOrEqualThan(new Duration(1, DurationUnit.Quarter))).toBe(false);
|
||||
expect(short.isLongerOrEqualThan(new Duration(1, DurationUnit.Year))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -14,8 +14,6 @@ enum DurationUnit {
|
|||
'Day' = 'd',
|
||||
'Week' = 'w',
|
||||
'Month' = 'M',
|
||||
'Quarter' = 'Q',
|
||||
'Year' = 'Y',
|
||||
}
|
||||
|
||||
class Duration {
|
||||
|
@ -55,6 +53,10 @@ class Duration {
|
|||
format(): string {
|
||||
return `${this.value}${this.unit}`;
|
||||
}
|
||||
|
||||
asSeconds(): number {
|
||||
return moment.duration(this.value, toMomentUnitOfTime(this.unit)).asSeconds();
|
||||
}
|
||||
}
|
||||
|
||||
const toDurationUnit = (unit: string): DurationUnit => {
|
||||
|
@ -69,10 +71,6 @@ const toDurationUnit = (unit: string): DurationUnit => {
|
|||
return DurationUnit.Week;
|
||||
case 'M':
|
||||
return DurationUnit.Month;
|
||||
case 'Q':
|
||||
return DurationUnit.Quarter;
|
||||
case 'y':
|
||||
return DurationUnit.Year;
|
||||
default:
|
||||
throw new Error('invalid duration unit');
|
||||
}
|
||||
|
@ -90,10 +88,6 @@ const toMomentUnitOfTime = (unit: DurationUnit): moment.unitOfTime.Diff => {
|
|||
return 'weeks';
|
||||
case DurationUnit.Month:
|
||||
return 'months';
|
||||
case DurationUnit.Quarter:
|
||||
return 'quarters';
|
||||
case DurationUnit.Year:
|
||||
return 'years';
|
||||
default:
|
||||
assertNever(unit);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -43,8 +43,6 @@ const summarySchema = t.type({
|
|||
errorBudget: errorBudgetSchema,
|
||||
});
|
||||
|
||||
type SummarySchema = t.TypeOf<typeof summarySchema>;
|
||||
|
||||
const historicalSummarySchema = t.intersection([
|
||||
t.type({
|
||||
date: dateType,
|
||||
|
@ -59,8 +57,6 @@ const previewDataSchema = t.type({
|
|||
|
||||
const dateRangeSchema = t.type({ from: dateType, to: dateType });
|
||||
|
||||
export type { SummarySchema };
|
||||
|
||||
export {
|
||||
ALL_VALUE,
|
||||
allOrAnyString,
|
||||
|
|
|
@ -136,27 +136,13 @@
|
|||
"$ref": "#/components/parameters/space_id"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"name": "kqlQuery",
|
||||
"in": "query",
|
||||
"description": "Filter by name",
|
||||
"description": "A valid kql query to filter the SLO with",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "awesome-service"
|
||||
},
|
||||
{
|
||||
"name": "indicatorTypes",
|
||||
"in": "query",
|
||||
"description": "Filter by indicator type",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
"sli.kql.custom"
|
||||
]
|
||||
"example": "slo.name:latency* and slo.tags : \"prod\""
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
|
@ -176,7 +162,7 @@
|
|||
"type": "integer",
|
||||
"default": 25
|
||||
},
|
||||
"example": 20
|
||||
"example": 25
|
||||
},
|
||||
{
|
||||
"name": "sortBy",
|
||||
|
@ -185,12 +171,14 @@
|
|||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"creationTime",
|
||||
"indicatorType"
|
||||
"sli_value",
|
||||
"status",
|
||||
"error_budget_consumed",
|
||||
"error_budget_remaining"
|
||||
],
|
||||
"default": "creationTime"
|
||||
"default": "status"
|
||||
},
|
||||
"example": "creationTime"
|
||||
"example": "status"
|
||||
},
|
||||
{
|
||||
"name": "sortDirection",
|
||||
|
@ -1333,7 +1321,8 @@
|
|||
"sli.apm.transactionErrorRate": "#/components/schemas/indicator_properties_apm_availability",
|
||||
"sli.kql.custom": "#/components/schemas/indicator_properties_custom_kql",
|
||||
"sli.apm.transactionDuration": "#/components/schemas/indicator_properties_apm_latency",
|
||||
"sli.apm.sli.metric.custom": "#/components/schemas/indicator_properties_custom_metric"
|
||||
"sli.metric.custom": "#/components/schemas/indicator_properties_custom_metric",
|
||||
"sli.histogram.custom": "#/components/schemas/indicator_properties_histogram"
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
|
|
|
@ -80,21 +80,12 @@ paths:
|
|||
parameters:
|
||||
- $ref: '#/components/parameters/kbn_xsrf'
|
||||
- $ref: '#/components/parameters/space_id'
|
||||
- name: name
|
||||
- name: kqlQuery
|
||||
in: query
|
||||
description: Filter by name
|
||||
description: A valid kql query to filter the SLO with
|
||||
schema:
|
||||
type: string
|
||||
example: awesome-service
|
||||
- name: indicatorTypes
|
||||
in: query
|
||||
description: Filter by indicator type
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
- sli.kql.custom
|
||||
example: 'slo.name:latency* and slo.tags : "prod"'
|
||||
- name: page
|
||||
in: query
|
||||
description: The page number to return
|
||||
|
@ -108,17 +99,19 @@ paths:
|
|||
schema:
|
||||
type: integer
|
||||
default: 25
|
||||
example: 20
|
||||
example: 25
|
||||
- name: sortBy
|
||||
in: query
|
||||
description: Sort by field
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- creationTime
|
||||
- indicatorType
|
||||
default: creationTime
|
||||
example: creationTime
|
||||
- sli_value
|
||||
- status
|
||||
- error_budget_consumed
|
||||
- error_budget_remaining
|
||||
default: status
|
||||
example: status
|
||||
- name: sortDirection
|
||||
in: query
|
||||
description: Sort order
|
||||
|
@ -920,7 +913,8 @@ components:
|
|||
sli.apm.transactionErrorRate: '#/components/schemas/indicator_properties_apm_availability'
|
||||
sli.kql.custom: '#/components/schemas/indicator_properties_custom_kql'
|
||||
sli.apm.transactionDuration: '#/components/schemas/indicator_properties_apm_latency'
|
||||
sli.apm.sli.metric.custom: '#/components/schemas/indicator_properties_custom_metric'
|
||||
sli.metric.custom: '#/components/schemas/indicator_properties_custom_metric'
|
||||
sli.histogram.custom: '#/components/schemas/indicator_properties_histogram'
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/indicator_properties_custom_kql'
|
||||
- $ref: '#/components/schemas/indicator_properties_apm_availability'
|
||||
|
|
|
@ -60,20 +60,12 @@ get:
|
|||
parameters:
|
||||
- $ref: ../components/headers/kbn_xsrf.yaml
|
||||
- $ref: ../components/parameters/space_id.yaml
|
||||
- name: name
|
||||
- name: kqlQuery
|
||||
in: query
|
||||
description: Filter by name
|
||||
description: A valid kql query to filter the SLO with
|
||||
schema:
|
||||
type: string
|
||||
example: awesome-service
|
||||
- name: indicatorTypes
|
||||
in: query
|
||||
description: Filter by indicator type
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: ['sli.kql.custom']
|
||||
example: 'slo.name:latency* and slo.tags : "prod"'
|
||||
- name: page
|
||||
in: query
|
||||
description: The page number to return
|
||||
|
@ -87,15 +79,15 @@ get:
|
|||
schema:
|
||||
type: integer
|
||||
default: 25
|
||||
example: 20
|
||||
example: 25
|
||||
- name: sortBy
|
||||
in: query
|
||||
description: Sort by field
|
||||
schema:
|
||||
type: string
|
||||
enum: [creationTime, indicatorType]
|
||||
default: creationTime
|
||||
example: creationTime
|
||||
enum: [sli_value, status, error_budget_consumed, error_budget_remaining]
|
||||
default: status
|
||||
example: status
|
||||
- name: sortDirection
|
||||
in: query
|
||||
description: Sort order
|
||||
|
|
|
@ -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*' });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiBadge, EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiBadge, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
|
||||
|
@ -19,11 +19,18 @@ export function SloStatusBadge({ slo }: SloStatusProps) {
|
|||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
{slo.summary.status === 'NO_DATA' && (
|
||||
<EuiBadge color="default">
|
||||
{i18n.translate('xpack.observability.slo.sloStatusBadge.noData', {
|
||||
defaultMessage: 'No data',
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={i18n.translate('xpack.observability.slo.sloStatusBadge.noDataTooltip', {
|
||||
defaultMessage: 'It may take some time before the data is aggregated and available.',
|
||||
})}
|
||||
</EuiBadge>
|
||||
>
|
||||
<EuiBadge color="default">
|
||||
{i18n.translate('xpack.observability.slo.sloStatusBadge.noData', {
|
||||
defaultMessage: 'No data',
|
||||
})}
|
||||
</EuiBadge>
|
||||
</EuiToolTip>
|
||||
)}
|
||||
|
||||
{slo.summary.status === 'HEALTHY' && (
|
||||
|
|
|
@ -8,10 +8,10 @@
|
|||
import type { Indicator } from '@kbn/slo-schema';
|
||||
|
||||
interface SloListFilter {
|
||||
name: string;
|
||||
kqlQuery: string;
|
||||
page: number;
|
||||
sortBy: string;
|
||||
indicatorTypes: string[];
|
||||
sortDirection: string;
|
||||
}
|
||||
|
||||
interface CompositeSloKeyFilter {
|
||||
|
|
|
@ -77,9 +77,6 @@ export function useCloneSlo() {
|
|||
})
|
||||
);
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: sloKeys.lists(), exact: false });
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ export function useCreateSlo() {
|
|||
|
||||
const [queryKey, previousData] = queriesData?.at(0) ?? [];
|
||||
|
||||
const newItem = { ...slo, id: uuidv1() };
|
||||
const newItem = { ...slo, id: uuidv1(), summary: undefined };
|
||||
|
||||
const optimisticUpdate = {
|
||||
page: previousData?.page ?? 1,
|
||||
|
@ -83,9 +83,6 @@ export function useCreateSlo() {
|
|||
http.basePath.prepend(paths.observability.sloCreateWithEncodedForm(encode(slo)))
|
||||
);
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: sloKeys.lists(), exact: false });
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -76,9 +76,6 @@ export function useDeleteSlo() {
|
|||
})
|
||||
);
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: sloKeys.lists(), exact: false });
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 = 'status',
|
||||
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,
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ import { rollingTimeWindowTypeSchema, SLOWithSummaryResponse } from '@kbn/slo-sc
|
|||
import { euiLightVars } from '@kbn/ui-theme';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { toMomentUnitOfTime } from '../../../../utils/slo/duration';
|
||||
import { toCalendarAlignedMomentUnitOfTime } from '../../../../utils/slo/duration';
|
||||
import { toDurationLabel } from '../../../../utils/slo/labels';
|
||||
|
||||
export interface Props {
|
||||
|
@ -34,11 +34,11 @@ export function SloTimeWindowBadge({ slo }: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
const unitMoment = toMomentUnitOfTime(unit);
|
||||
const unitMoment = toCalendarAlignedMomentUnitOfTime(unit);
|
||||
const now = moment.utc();
|
||||
|
||||
const periodStart = now.clone().startOf(unitMoment!);
|
||||
const periodEnd = now.clone().endOf(unitMoment!);
|
||||
const periodStart = now.clone().startOf(unitMoment);
|
||||
const periodEnd = now.clone().endOf(unitMoment);
|
||||
|
||||
const totalDurationInDays = periodEnd.diff(periodStart, 'days') + 1;
|
||||
const elapsedDurationInDays = now.diff(periodStart, 'days') + 1;
|
||||
|
|
|
@ -5,18 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPagination } from '@elastic/eui';
|
||||
import { debounce } from 'lodash';
|
||||
import { useIsMutating } from '@tanstack/react-query';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useFetchSloList } from '../../../hooks/slo/use_fetch_slo_list';
|
||||
import {
|
||||
FilterType,
|
||||
SloListSearchFilterSortBar,
|
||||
SortType,
|
||||
} from './slo_list_search_filter_sort_bar';
|
||||
import { SloListItems } from './slo_list_items';
|
||||
import { SloListSearchFilterSortBar, SortField } from './slo_list_search_filter_sort_bar';
|
||||
|
||||
export interface Props {
|
||||
autoRefresh: boolean;
|
||||
|
@ -24,16 +18,14 @@ export interface Props {
|
|||
|
||||
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>('status');
|
||||
|
||||
const { isInitialLoading, isLoading, isRefetching, isError, sloList, refetch } = useFetchSloList({
|
||||
page: activePage + 1,
|
||||
name: query,
|
||||
kqlQuery: query,
|
||||
sortBy: sort,
|
||||
indicatorTypes: indicatorTypeFilter,
|
||||
sortDirection: 'desc',
|
||||
shouldRefetch: autoRefresh,
|
||||
});
|
||||
|
||||
|
@ -49,20 +41,16 @@ export function SloList({ autoRefresh }: Props) {
|
|||
refetch();
|
||||
};
|
||||
|
||||
const handleChangeQuery = useMemo(
|
||||
() =>
|
||||
debounce((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setQuery(e.target.value);
|
||||
}, 300),
|
||||
[]
|
||||
);
|
||||
|
||||
const handleChangeSort = (newSort: SortType) => {
|
||||
setSort(newSort);
|
||||
const handleChangeQuery = (newQuery: string) => {
|
||||
setActivePage(0);
|
||||
setQuery(newQuery);
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleChangeIndicatorTypeFilter = (newFilter: FilterType[]) => {
|
||||
setIndicatorTypeFilter(newFilter);
|
||||
const handleChangeSort = (newSort: SortField | undefined) => {
|
||||
setActivePage(0);
|
||||
setSort(newSort);
|
||||
refetch();
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -80,7 +68,6 @@ export function SloList({ autoRefresh }: Props) {
|
|||
}
|
||||
onChangeQuery={handleChangeQuery}
|
||||
onChangeSort={handleChangeSort}
|
||||
onChangeIndicatorTypeFilter={handleChangeIndicatorTypeFilter}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiContextMenuItem,
|
||||
|
@ -19,27 +17,29 @@ import {
|
|||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { HistoricalSummaryResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import type { Rule } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import React, { useState } from 'react';
|
||||
import { rulesLocatorID, sloFeatureId } from '../../../../common';
|
||||
import { SLO_BURN_RATE_RULE_TYPE_ID } from '../../../../common/constants';
|
||||
import { sloKeys } from '../../../hooks/slo/query_key_factory';
|
||||
import { useCapabilities } from '../../../hooks/slo/use_capabilities';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { useCloneSlo } from '../../../hooks/slo/use_clone_slo';
|
||||
import { useGetFilteredRuleTypes } from '../../../hooks/use_get_filtered_rule_types';
|
||||
import { SloSummary } from './slo_summary';
|
||||
import { SloDeleteConfirmationModal } from './slo_delete_confirmation_modal';
|
||||
import { SloBadges } from './badges/slo_badges';
|
||||
import {
|
||||
transformSloResponseToCreateSloForm,
|
||||
transformCreateSLOFormToCreateSLOInput,
|
||||
} from '../../slo_edit/helpers/process_slo_form_values';
|
||||
import { SLO_BURN_RATE_RULE_TYPE_ID } from '../../../../common/constants';
|
||||
import { rulesLocatorID, sloFeatureId } from '../../../../common';
|
||||
import { paths } from '../../../routes/paths';
|
||||
import { useDeleteSlo } from '../../../hooks/slo/use_delete_slo';
|
||||
import type { ActiveAlerts } from '../../../hooks/slo/use_fetch_active_alerts';
|
||||
import type { SloRule } from '../../../hooks/slo/use_fetch_rules_for_slo';
|
||||
import { useGetFilteredRuleTypes } from '../../../hooks/use_get_filtered_rule_types';
|
||||
import type { RulesParams } from '../../../locators/rules';
|
||||
import { paths } from '../../../routes/paths';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import {
|
||||
transformCreateSLOFormToCreateSLOInput,
|
||||
transformSloResponseToCreateSloForm,
|
||||
} from '../../slo_edit/helpers/process_slo_form_values';
|
||||
import { SloBadges } from './badges/slo_badges';
|
||||
import { SloDeleteConfirmationModal } from './slo_delete_confirmation_modal';
|
||||
import { SloSummary } from './slo_summary';
|
||||
|
||||
export interface SloListItemProps {
|
||||
slo: SLOWithSummaryResponse;
|
||||
|
@ -47,7 +47,6 @@ export interface SloListItemProps {
|
|||
historicalSummary?: HistoricalSummaryResponse[];
|
||||
historicalSummaryLoading: boolean;
|
||||
activeAlerts?: ActiveAlerts;
|
||||
onConfirmDelete: (slo: SLOWithSummaryResponse) => void;
|
||||
}
|
||||
|
||||
export function SloListItem({
|
||||
|
@ -56,7 +55,6 @@ export function SloListItem({
|
|||
historicalSummary = [],
|
||||
historicalSummaryLoading,
|
||||
activeAlerts,
|
||||
onConfirmDelete,
|
||||
}: SloListItemProps) {
|
||||
const {
|
||||
application: { navigateToUrl },
|
||||
|
@ -72,6 +70,7 @@ export function SloListItem({
|
|||
const filteredRuleTypes = useGetFilteredRuleTypes();
|
||||
|
||||
const { mutate: cloneSlo } = useCloneSlo();
|
||||
const { mutate: deleteSlo } = useDeleteSlo();
|
||||
|
||||
const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false);
|
||||
const [isAddRuleFlyoutOpen, setIsAddRuleFlyoutOpen] = useState(false);
|
||||
|
@ -100,15 +99,7 @@ export function SloListItem({
|
|||
|
||||
const handleNavigateToRules = async () => {
|
||||
const locator = locators.get<RulesParams>(rulesLocatorID);
|
||||
|
||||
locator?.navigate(
|
||||
{
|
||||
params: { sloId: slo.id },
|
||||
},
|
||||
{
|
||||
replace: false,
|
||||
}
|
||||
);
|
||||
locator?.navigate({ params: { sloId: slo.id } }, { replace: false });
|
||||
};
|
||||
|
||||
const handleClone = () => {
|
||||
|
@ -127,7 +118,7 @@ export function SloListItem({
|
|||
|
||||
const handleDeleteConfirm = () => {
|
||||
setDeleteConfirmationModalOpen(false);
|
||||
onConfirmDelete(slo);
|
||||
deleteSlo({ id: slo.id, name: slo.name });
|
||||
};
|
||||
|
||||
const handleDeleteCancel = () => {
|
||||
|
|
|
@ -4,17 +4,15 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
import type { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import React from 'react';
|
||||
import { useFetchActiveAlerts } from '../../../hooks/slo/use_fetch_active_alerts';
|
||||
import { useFetchRulesForSlo } from '../../../hooks/slo/use_fetch_rules_for_slo';
|
||||
import { useFetchHistoricalSummary } from '../../../hooks/slo/use_fetch_historical_summary';
|
||||
import { useDeleteSlo } from '../../../hooks/slo/use_delete_slo';
|
||||
import { SloListItem } from './slo_list_item';
|
||||
import { useFetchRulesForSlo } from '../../../hooks/slo/use_fetch_rules_for_slo';
|
||||
import { SloListEmpty } from './slo_list_empty';
|
||||
import { SloListError } from './slo_list_error';
|
||||
import { SloListItem } from './slo_list_item';
|
||||
|
||||
export interface Props {
|
||||
sloList: SLOWithSummaryResponse[];
|
||||
|
@ -30,8 +28,6 @@ export function SloListItems({ sloList, loading, error }: Props) {
|
|||
const { isLoading: historicalSummaryLoading, data: historicalSummaryBySlo } =
|
||||
useFetchHistoricalSummary({ sloIds });
|
||||
|
||||
const { mutate: deleteSlo } = useDeleteSlo();
|
||||
|
||||
if (!loading && !error && sloList.length === 0) {
|
||||
return <SloListEmpty />;
|
||||
}
|
||||
|
@ -39,10 +35,6 @@ export function SloListItems({ sloList, loading, error }: Props) {
|
|||
return <SloListError />;
|
||||
}
|
||||
|
||||
const handleDelete = (slo: SLOWithSummaryResponse) => {
|
||||
deleteSlo({ id: slo.id, name: slo.name });
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
{sloList.map((slo) => (
|
||||
|
@ -53,7 +45,6 @@ export function SloListItems({ sloList, loading, error }: Props) {
|
|||
historicalSummary={historicalSummaryBySlo?.[slo.id]}
|
||||
historicalSummaryLoading={historicalSummaryLoading}
|
||||
slo={slo}
|
||||
onConfirmDelete={handleDelete}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
|
|
|
@ -28,7 +28,6 @@ const defaultProps: SloListSearchFilterSortBarProps = {
|
|||
loading: false,
|
||||
onChangeQuery: () => {},
|
||||
onChangeSort: () => {},
|
||||
onChangeIndicatorTypeFilter: () => {},
|
||||
};
|
||||
|
||||
export const SloListSearchFilterSortBar = Template.bind({});
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
EuiFieldSearch,
|
||||
EuiFilterButton,
|
||||
EuiFilterGroup,
|
||||
EuiFlexGroup,
|
||||
|
@ -18,29 +17,18 @@ 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 { QueryStringInput } from '@kbn/unified-search-plugin/public';
|
||||
import React, { useState } from 'react';
|
||||
import { useCreateDataView } from '../../../hooks/use_create_data_view';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
|
||||
export interface SloListSearchFilterSortBarProps {
|
||||
loading: boolean;
|
||||
onChangeQuery: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onChangeSort: (sort: SortType) => void;
|
||||
onChangeIndicatorTypeFilter: (filter: FilterType[]) => void;
|
||||
onChangeQuery: (query: string) => 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,42 +36,31 @@ 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',
|
||||
checked: 'on',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.observability.slo.list.sortBy.indicatorType', {
|
||||
defaultMessage: 'Indicator type',
|
||||
label: i18n.translate('xpack.observability.slo.list.sortBy.errorBudgetConsumed', {
|
||||
defaultMessage: 'Error budget consumed',
|
||||
}),
|
||||
type: 'indicatorType',
|
||||
},
|
||||
];
|
||||
|
||||
const INDICATOR_TYPE_OPTIONS: Array<Item<FilterType>> = [
|
||||
{
|
||||
label: INDICATOR_APM_LATENCY,
|
||||
type: 'sli.apm.transactionDuration',
|
||||
type: 'error_budget_consumed',
|
||||
},
|
||||
{
|
||||
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',
|
||||
label: i18n.translate('xpack.observability.slo.list.sortBy.errorBudgetRemaining', {
|
||||
defaultMessage: 'Error budget remaining',
|
||||
}),
|
||||
type: 'error_budget_remaining',
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -91,96 +68,60 @@ export function SloListSearchFilterSortBar({
|
|||
loading,
|
||||
onChangeQuery,
|
||||
onChangeSort,
|
||||
onChangeIndicatorTypeFilter,
|
||||
}: SloListSearchFilterSortBarProps) {
|
||||
const [isFilterPopoverOpen, setFilterPopoverOpen] = useState(false);
|
||||
const [isSortPopoverOpen, setSortPopoverOpen] = useState(false);
|
||||
const { data, dataViews, docLinks, http, notifications, storage, uiSettings, unifiedSearch } =
|
||||
useKibana().services;
|
||||
const { dataView } = useCreateDataView({ indexPatternString: '.slo-observability.summary-*' });
|
||||
|
||||
const [isSortPopoverOpen, setSortPopoverOpen] = useState(false);
|
||||
const [sortOptions, setSortOptions] = useState(SORT_OPTIONS);
|
||||
const [indicatorTypeOptions, setIndicatorTypeOptions] = useState(INDICATOR_TYPE_OPTIONS);
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
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>
|
||||
<EuiFieldSearch
|
||||
data-test-subj="o11ySloListSearchFilterSortBarFieldSearch"
|
||||
fullWidth
|
||||
isLoading={loading}
|
||||
onChange={onChangeQuery}
|
||||
<QueryStringInput
|
||||
appName="Observability"
|
||||
bubbleSubmitEvent={false}
|
||||
deps={{
|
||||
data,
|
||||
dataViews,
|
||||
docLinks,
|
||||
http,
|
||||
notifications,
|
||||
storage,
|
||||
uiSettings,
|
||||
unifiedSearch,
|
||||
}}
|
||||
disableAutoFocus
|
||||
onSubmit={() => onChangeQuery(query)}
|
||||
disableLanguageSwitcher
|
||||
isDisabled={loading}
|
||||
indexPatterns={dataView ? [dataView] : []}
|
||||
placeholder={i18n.translate('xpack.observability.slo.list.search', {
|
||||
defaultMessage: 'Search',
|
||||
defaultMessage: 'Search your SLOs...',
|
||||
})}
|
||||
query={{ query: String(query), language: 'kuery' }}
|
||||
size="s"
|
||||
onChange={(value) => setQuery(String(value.query))}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<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={
|
||||
<EuiFilterButton
|
||||
disabled={loading}
|
||||
iconType="arrowDown"
|
||||
onClick={handleToggleSortButton}
|
||||
isSelected={isSortPopoverOpen}
|
||||
|
@ -202,10 +143,11 @@ export function SloListSearchFilterSortBar({
|
|||
defaultMessage: 'Sort by',
|
||||
})}
|
||||
</EuiPopoverTitle>
|
||||
<EuiSelectable<Item<SortType>>
|
||||
<EuiSelectable<Item<SortField>>
|
||||
singleSelection
|
||||
options={sortOptions}
|
||||
onChange={handleChangeSort}
|
||||
isLoading={loading}
|
||||
>
|
||||
{(list) => list}
|
||||
</EuiSelectable>
|
||||
|
|
|
@ -69,6 +69,20 @@ const mockKibana = () => {
|
|||
services: {
|
||||
application: { navigateToUrl: mockNavigate },
|
||||
charts: chartPluginMock.createSetupContract(),
|
||||
data: {
|
||||
dataViews: {
|
||||
find: jest.fn().mockReturnValue([]),
|
||||
get: jest.fn().mockReturnValue([]),
|
||||
},
|
||||
},
|
||||
dataViews: {
|
||||
create: jest.fn().mockResolvedValue(42),
|
||||
},
|
||||
docLinks: {
|
||||
links: {
|
||||
query: {},
|
||||
},
|
||||
},
|
||||
http: {
|
||||
basePath: {
|
||||
prepend: (url: string) => url,
|
||||
|
@ -87,6 +101,9 @@ const mockKibana = () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
storage: {
|
||||
get: () => {},
|
||||
},
|
||||
triggersActionsUi: { getAddRuleFlyout: mockGetAddRuleFlyout },
|
||||
uiSettings: {
|
||||
get: (settings: string) => {
|
||||
|
@ -95,6 +112,11 @@ const mockKibana = () => {
|
|||
return '';
|
||||
},
|
||||
},
|
||||
unifiedSearch: {
|
||||
autocomplete: {
|
||||
hasQuerySuggestions: () => {},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -31,8 +31,7 @@ export function SlosPage() {
|
|||
const { hasAtLeast } = useLicense();
|
||||
|
||||
const { isInitialLoading, isLoading, isError, sloList } = useFetchSloList();
|
||||
|
||||
const { total } = sloList || {};
|
||||
const { total } = sloList || { total: 0 };
|
||||
|
||||
const [isAutoRefreshing, setIsAutoRefreshing] = useState<boolean>(true);
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { RuleTypeParams } from '@kbn/alerting-plugin/common';
|
||||
|
||||
type DurationUnit = 'm' | 'h' | 'd' | 'w' | 'M' | 'Y';
|
||||
type DurationUnit = 'm' | 'h' | 'd' | 'w' | 'M';
|
||||
|
||||
interface Duration {
|
||||
value: number;
|
||||
|
|
|
@ -28,24 +28,17 @@ export function toMinutes(duration: Duration) {
|
|||
return duration.value * 7 * 24 * 60;
|
||||
case 'M':
|
||||
return duration.value * 30 * 24 * 60;
|
||||
case 'Y':
|
||||
return duration.value * 365 * 24 * 60;
|
||||
default:
|
||||
assertNever(duration.unit);
|
||||
}
|
||||
|
||||
assertNever(duration.unit);
|
||||
}
|
||||
|
||||
export function toMomentUnitOfTime(unit: string): moment.unitOfTime.Diff | undefined {
|
||||
export function toCalendarAlignedMomentUnitOfTime(unit: string): moment.unitOfTime.StartOf {
|
||||
switch (unit) {
|
||||
case 'd':
|
||||
return 'days';
|
||||
default:
|
||||
case 'w':
|
||||
return 'weeks';
|
||||
return 'isoWeek';
|
||||
case 'M':
|
||||
return 'months';
|
||||
case 'Q':
|
||||
return 'quarters';
|
||||
case 'Y':
|
||||
return 'years';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -112,13 +112,6 @@ export function toDurationLabel(durationStr: string): string {
|
|||
duration: duration.value,
|
||||
},
|
||||
});
|
||||
case 'Y':
|
||||
return i18n.translate('xpack.observability.slo.duration.year', {
|
||||
defaultMessage: '{duration, plural, one {1 year} other {# years}}',
|
||||
values: {
|
||||
duration: duration.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,9 +139,5 @@ export function toDurationAdverbLabel(durationStr: string): string {
|
|||
return i18n.translate('xpack.observability.slo.duration.monthly', {
|
||||
defaultMessage: 'Monthly',
|
||||
});
|
||||
case 'Y':
|
||||
return i18n.translate('xpack.observability.slo.duration.yearly', {
|
||||
defaultMessage: 'Yearly',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SLO_RESOURCES_VERSION } from '../constants';
|
||||
|
||||
export const getSLOMappingsTemplate = (name: string) => ({
|
||||
name,
|
||||
template: {
|
||||
|
@ -14,6 +16,31 @@ export const getSLOMappingsTemplate = (name: string) => ({
|
|||
type: 'date',
|
||||
format: 'date_optional_time||epoch_millis',
|
||||
},
|
||||
// APM service and transaction specific fields
|
||||
service: {
|
||||
properties: {
|
||||
name: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
},
|
||||
environment: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
},
|
||||
},
|
||||
},
|
||||
transaction: {
|
||||
properties: {
|
||||
name: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
},
|
||||
type: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
},
|
||||
},
|
||||
},
|
||||
slo: {
|
||||
properties: {
|
||||
id: {
|
||||
|
@ -23,6 +50,53 @@ export const getSLOMappingsTemplate = (name: string) => ({
|
|||
revision: {
|
||||
type: 'long',
|
||||
},
|
||||
instanceId: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
},
|
||||
name: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
},
|
||||
description: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
},
|
||||
tags: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
},
|
||||
indicator: {
|
||||
properties: {
|
||||
type: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
},
|
||||
},
|
||||
},
|
||||
objective: {
|
||||
properties: {
|
||||
target: {
|
||||
type: 'double',
|
||||
},
|
||||
sliceDurationInSeconds: {
|
||||
type: 'long',
|
||||
},
|
||||
},
|
||||
},
|
||||
budgetingMethod: {
|
||||
type: 'keyword',
|
||||
},
|
||||
timeWindow: {
|
||||
properties: {
|
||||
duration: {
|
||||
type: 'keyword',
|
||||
},
|
||||
type: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
numerator: {
|
||||
type: 'long',
|
||||
},
|
||||
|
@ -32,9 +106,6 @@ export const getSLOMappingsTemplate = (name: string) => ({
|
|||
isGoodSlice: {
|
||||
type: 'byte',
|
||||
},
|
||||
context: {
|
||||
type: 'flattened',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -42,7 +113,7 @@ export const getSLOMappingsTemplate = (name: string) => ({
|
|||
},
|
||||
_meta: {
|
||||
description: 'Mappings for SLO rollup data',
|
||||
version: 1,
|
||||
version: SLO_RESOURCES_VERSION,
|
||||
managed: true,
|
||||
managed_by: 'observability',
|
||||
},
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SLO_RESOURCES_VERSION } from '../constants';
|
||||
|
||||
export const getSLOSettingsTemplate = (name: string) => ({
|
||||
name,
|
||||
template: {
|
||||
|
@ -14,7 +16,7 @@ export const getSLOSettingsTemplate = (name: string) => ({
|
|||
},
|
||||
_meta: {
|
||||
description: 'Settings for SLO rollup data',
|
||||
version: 1,
|
||||
version: SLO_RESOURCES_VERSION,
|
||||
managed: true,
|
||||
managed_by: 'observability',
|
||||
},
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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 getSLOSummaryMappingsTemplate = (name: string) => ({
|
||||
name,
|
||||
template: {
|
||||
mappings: {
|
||||
properties: {
|
||||
// APM service and transaction specific fields
|
||||
service: {
|
||||
properties: {
|
||||
name: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
},
|
||||
environment: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
},
|
||||
},
|
||||
},
|
||||
transaction: {
|
||||
properties: {
|
||||
name: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
},
|
||||
type: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
},
|
||||
},
|
||||
},
|
||||
slo: {
|
||||
properties: {
|
||||
id: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
},
|
||||
revision: {
|
||||
type: 'long',
|
||||
},
|
||||
instanceId: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
},
|
||||
name: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
},
|
||||
description: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
},
|
||||
tags: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
},
|
||||
indicator: {
|
||||
properties: {
|
||||
type: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
},
|
||||
},
|
||||
},
|
||||
budgetingMethod: {
|
||||
type: 'keyword',
|
||||
},
|
||||
timeWindow: {
|
||||
properties: {
|
||||
duration: {
|
||||
type: 'keyword',
|
||||
},
|
||||
type: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sliValue: {
|
||||
type: 'double',
|
||||
},
|
||||
goodEvents: {
|
||||
type: 'long',
|
||||
},
|
||||
totalEvents: {
|
||||
type: 'long',
|
||||
},
|
||||
errorBudgetInitial: {
|
||||
type: 'double',
|
||||
},
|
||||
errorBudgetConsumed: {
|
||||
type: 'double',
|
||||
},
|
||||
errorBudgetRemaining: {
|
||||
type: 'double',
|
||||
},
|
||||
errorBudgetEstimated: {
|
||||
type: 'boolean',
|
||||
},
|
||||
statusCode: {
|
||||
type: 'byte',
|
||||
},
|
||||
status: {
|
||||
type: 'keyword',
|
||||
ignore_above: 32,
|
||||
},
|
||||
isTempDoc: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
_meta: {
|
||||
description: 'SLO summary mappings template',
|
||||
version: SLO_RESOURCES_VERSION,
|
||||
managed: true,
|
||||
managed_by: 'observability',
|
||||
},
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 getSLOSummarySettingsTemplate = (name: string) => ({
|
||||
name,
|
||||
template: {
|
||||
settings: {
|
||||
hidden: true,
|
||||
},
|
||||
},
|
||||
_meta: {
|
||||
description: 'SLO summary settings template',
|
||||
version: SLO_RESOURCES_VERSION,
|
||||
managed: true,
|
||||
managed_by: 'observability',
|
||||
},
|
||||
});
|
|
@ -5,13 +5,32 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const SLO_RESOURCES_VERSION = 2;
|
||||
|
||||
export const SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME = '.slo-observability.sli-mappings';
|
||||
export const SLO_COMPONENT_TEMPLATE_SETTINGS_NAME = '.slo-observability.sli-settings';
|
||||
|
||||
export const SLO_INDEX_TEMPLATE_NAME = '.slo-observability.sli';
|
||||
export const SLO_RESOURCES_VERSION = 1;
|
||||
export const SLO_INGEST_PIPELINE_NAME = `${SLO_INDEX_TEMPLATE_NAME}.monthly`;
|
||||
export const SLO_DESTINATION_INDEX_NAME = `${SLO_INDEX_TEMPLATE_NAME}-v${SLO_RESOURCES_VERSION}`;
|
||||
export const SLO_DESTINATION_INDEX_PATTERN = `${SLO_DESTINATION_INDEX_NAME}*`;
|
||||
export const SLO_INDEX_TEMPLATE_PATTERN = `.slo-observability.sli-*`;
|
||||
|
||||
export const SLO_DESTINATION_INDEX_NAME = `.slo-observability.sli-v${SLO_RESOURCES_VERSION}`;
|
||||
export const SLO_DESTINATION_INDEX_PATTERN = `.slo-observability.sli-v${SLO_RESOURCES_VERSION}*`;
|
||||
|
||||
export const SLO_INGEST_PIPELINE_NAME = `.slo-observability.sli.pipeline`;
|
||||
// slo-observability.sli-v<version>.(YYYY-MM-DD)
|
||||
export const SLO_INGEST_PIPELINE_INDEX_NAME_PREFIX = `.slo-observability.sli-v${SLO_RESOURCES_VERSION}.`;
|
||||
|
||||
export const SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME = '.slo-observability.summary-mappings';
|
||||
export const SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME = '.slo-observability.summary-settings';
|
||||
export const SLO_SUMMARY_INDEX_TEMPLATE_NAME = '.slo-observability.summary';
|
||||
export const SLO_SUMMARY_INDEX_TEMPLATE_PATTERN = `.slo-observability.summary-*`;
|
||||
|
||||
export const SLO_SUMMARY_TRANSFORM_NAME_PREFIX = 'slo-summary-';
|
||||
export const SLO_SUMMARY_DESTINATION_INDEX_NAME = `.slo-observability.summary-v${SLO_RESOURCES_VERSION}`; // store the temporary summary document generated by transform
|
||||
export const SLO_SUMMARY_TEMP_INDEX_NAME = `.slo-observability.summary-v${SLO_RESOURCES_VERSION}.temp`; // store the temporary summary document
|
||||
export const SLO_SUMMARY_DESTINATION_INDEX_PATTERN = `.slo-observability.summary-v${SLO_RESOURCES_VERSION}*`; // include temp and non-temp summary indices
|
||||
|
||||
export const SLO_SUMMARY_INGEST_PIPELINE_NAME = `.slo-observability.summary.pipeline`;
|
||||
|
||||
export const getSLOTransformId = (sloId: string, sloRevision: number) =>
|
||||
`slo-${sloId}-${sloRevision}`;
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SLO_RESOURCES_VERSION } from '../constants';
|
||||
|
||||
export const getSLOIndexTemplate = (name: string, indexPattern: string, composedOf: string[]) => ({
|
||||
name,
|
||||
index_patterns: [indexPattern],
|
||||
|
@ -12,7 +14,7 @@ export const getSLOIndexTemplate = (name: string, indexPattern: string, composed
|
|||
priority: 500,
|
||||
_meta: {
|
||||
description: 'Template for SLO rollup data',
|
||||
version: 1,
|
||||
version: SLO_RESOURCES_VERSION,
|
||||
managed: true,
|
||||
managed_by: 'observability',
|
||||
},
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 getSLOSummaryIndexTemplate = (
|
||||
name: string,
|
||||
indexPattern: string,
|
||||
composedOf: string[]
|
||||
) => ({
|
||||
name,
|
||||
index_patterns: [indexPattern],
|
||||
composed_of: composedOf,
|
||||
priority: 500,
|
||||
_meta: {
|
||||
description: 'SLO summary index template',
|
||||
version: SLO_RESOURCES_VERSION,
|
||||
managed: true,
|
||||
managed_by: 'observability',
|
||||
},
|
||||
});
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SLO_RESOURCES_VERSION } from '../constants';
|
||||
|
||||
export const getSLOPipelineTemplate = (id: string, indexNamePrefix: string) => ({
|
||||
id,
|
||||
description: 'Monthly date-time index naming for SLO data',
|
||||
|
@ -19,7 +21,7 @@ export const getSLOPipelineTemplate = (id: string, indexNamePrefix: string) => (
|
|||
],
|
||||
_meta: {
|
||||
description: 'SLO ingest pipeline',
|
||||
version: 1,
|
||||
version: SLO_RESOURCES_VERSION,
|
||||
managed: true,
|
||||
managed_by: 'observability',
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
});
|
|
@ -12,6 +12,7 @@ import {
|
|||
TransformSource,
|
||||
TransformTimeSync,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
import { SLO_RESOURCES_VERSION } from '../constants';
|
||||
|
||||
export interface TransformSettings {
|
||||
frequency: TransformPutTransformRequest['frequency'];
|
||||
|
@ -47,7 +48,7 @@ export const getSLOTransformTemplate = (
|
|||
aggregations,
|
||||
},
|
||||
_meta: {
|
||||
version: 1,
|
||||
version: SLO_RESOURCES_VERSION,
|
||||
managed: true,
|
||||
managed_by: 'observability',
|
||||
},
|
||||
|
|
|
@ -5,10 +5,42 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
calendarAlignedTimeWindowSchema,
|
||||
rollingTimeWindowSchema,
|
||||
timeWindowSchema,
|
||||
} from '@kbn/slo-schema';
|
||||
import moment from 'moment';
|
||||
import * as t from 'io-ts';
|
||||
import { rollingTimeWindowSchema, timeWindowSchema } from '@kbn/slo-schema';
|
||||
|
||||
type TimeWindow = t.TypeOf<typeof timeWindowSchema>;
|
||||
type RollingTimeWindow = t.TypeOf<typeof rollingTimeWindowSchema>;
|
||||
type CalendarAlignedTimeWindow = t.TypeOf<typeof calendarAlignedTimeWindowSchema>;
|
||||
|
||||
export type { RollingTimeWindow, TimeWindow };
|
||||
export type { RollingTimeWindow, TimeWindow, CalendarAlignedTimeWindow };
|
||||
|
||||
export function toCalendarAlignedTimeWindowMomentUnit(
|
||||
timeWindow: CalendarAlignedTimeWindow
|
||||
): moment.unitOfTime.StartOf {
|
||||
const unit = timeWindow.duration.unit;
|
||||
switch (unit) {
|
||||
case 'w':
|
||||
return 'isoWeeks';
|
||||
case 'M':
|
||||
return 'months';
|
||||
default:
|
||||
throw new Error(`Invalid calendar aligned time window duration unit: ${unit}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function toRollingTimeWindowMomentUnit(
|
||||
timeWindow: RollingTimeWindow
|
||||
): moment.unitOfTime.Diff {
|
||||
const unit = timeWindow.duration.unit;
|
||||
switch (unit) {
|
||||
case 'd':
|
||||
return 'days';
|
||||
default:
|
||||
throw new Error(`Invalid rolling time window duration unit: ${unit}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { computeBurnRate } from './compute_burn_rate';
|
||||
import { toDateRange } from './date_range';
|
||||
import { createSLO } from '../../services/slo/fixtures/slo';
|
||||
import { sixHoursRolling } from '../../services/slo/fixtures/time_window';
|
||||
import { ninetyDaysRolling } from '../../services/slo/fixtures/time_window';
|
||||
|
||||
describe('computeBurnRate', () => {
|
||||
it('computes 0 when total is 0', () => {
|
||||
|
@ -16,7 +16,7 @@ describe('computeBurnRate', () => {
|
|||
computeBurnRate(createSLO(), {
|
||||
good: 10,
|
||||
total: 0,
|
||||
dateRange: toDateRange(sixHoursRolling()),
|
||||
dateRange: toDateRange(ninetyDaysRolling()),
|
||||
})
|
||||
).toEqual(0);
|
||||
});
|
||||
|
@ -26,7 +26,7 @@ describe('computeBurnRate', () => {
|
|||
computeBurnRate(createSLO(), {
|
||||
good: 9999,
|
||||
total: 1,
|
||||
dateRange: toDateRange(sixHoursRolling()),
|
||||
dateRange: toDateRange(ninetyDaysRolling()),
|
||||
})
|
||||
).toEqual(0);
|
||||
});
|
||||
|
@ -36,7 +36,7 @@ describe('computeBurnRate', () => {
|
|||
computeBurnRate(createSLO({ objective: { target: 0.9 } }), {
|
||||
good: 90,
|
||||
total: 100,
|
||||
dateRange: toDateRange(sixHoursRolling()),
|
||||
dateRange: toDateRange(ninetyDaysRolling()),
|
||||
})
|
||||
).toEqual(1);
|
||||
});
|
||||
|
@ -46,7 +46,7 @@ describe('computeBurnRate', () => {
|
|||
computeBurnRate(createSLO({ objective: { target: 0.99 } }), {
|
||||
good: 90,
|
||||
total: 100,
|
||||
dateRange: toDateRange(sixHoursRolling()),
|
||||
dateRange: toDateRange(ninetyDaysRolling()),
|
||||
})
|
||||
).toEqual(10);
|
||||
});
|
||||
|
@ -56,7 +56,7 @@ describe('computeBurnRate', () => {
|
|||
computeBurnRate(createSLO({ objective: { target: 0.8 } }), {
|
||||
good: 90,
|
||||
total: 100,
|
||||
dateRange: toDateRange(sixHoursRolling()),
|
||||
dateRange: toDateRange(ninetyDaysRolling()),
|
||||
})
|
||||
).toEqual(0.5);
|
||||
});
|
||||
|
|
|
@ -5,25 +5,29 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TimeWindow } from '../models/time_window';
|
||||
import { Duration } from '../models';
|
||||
import {
|
||||
monthlyCalendarAligned,
|
||||
ninetyDaysRolling,
|
||||
sevenDaysRolling,
|
||||
thirtyDaysRolling,
|
||||
weeklyCalendarAligned,
|
||||
} from '../../services/slo/fixtures/time_window';
|
||||
import { toDateRange } from './date_range';
|
||||
import { oneMonth, oneQuarter, oneWeek, thirtyDays } from '../../services/slo/fixtures/duration';
|
||||
|
||||
const NOW = new Date('2022-08-11T08:31:00.000Z');
|
||||
|
||||
describe('toDateRange', () => {
|
||||
describe('for calendar aligned time window', () => {
|
||||
it('computes the date range for weekly calendar', () => {
|
||||
const timeWindow = aCalendarTimeWindow(oneWeek());
|
||||
const timeWindow = weeklyCalendarAligned();
|
||||
expect(toDateRange(timeWindow, NOW)).toEqual({
|
||||
from: new Date('2022-08-07T00:00:00.000Z'),
|
||||
to: new Date('2022-08-13T23:59:59.999Z'),
|
||||
from: new Date('2022-08-08T00:00:00.000Z'),
|
||||
to: new Date('2022-08-14T23:59:59.999Z'),
|
||||
});
|
||||
});
|
||||
|
||||
it('computes the date range for monthly calendar', () => {
|
||||
const timeWindow = aCalendarTimeWindow(oneMonth());
|
||||
const timeWindow = monthlyCalendarAligned();
|
||||
expect(toDateRange(timeWindow, NOW)).toEqual({
|
||||
from: new Date('2022-08-01T00:00:00.000Z'),
|
||||
to: new Date('2022-08-31T23:59:59.999Z'),
|
||||
|
@ -33,42 +37,24 @@ describe('toDateRange', () => {
|
|||
|
||||
describe('for rolling time window', () => {
|
||||
it("computes the date range using a '30days' rolling window", () => {
|
||||
expect(toDateRange(aRollingTimeWindow(thirtyDays()), NOW)).toEqual({
|
||||
expect(toDateRange(thirtyDaysRolling(), NOW)).toEqual({
|
||||
from: new Date('2022-07-12T08:31:00.000Z'),
|
||||
to: new Date('2022-08-11T08:31:00.000Z'),
|
||||
});
|
||||
});
|
||||
|
||||
it("computes the date range using a 'weekly' rolling window", () => {
|
||||
expect(toDateRange(aRollingTimeWindow(oneWeek()), NOW)).toEqual({
|
||||
it("computes the date range using a '7days' rolling window", () => {
|
||||
expect(toDateRange(sevenDaysRolling(), NOW)).toEqual({
|
||||
from: new Date('2022-08-04T08:31:00.000Z'),
|
||||
to: new Date('2022-08-11T08:31:00.000Z'),
|
||||
});
|
||||
});
|
||||
|
||||
it("computes the date range using a 'monthly' rolling window", () => {
|
||||
expect(toDateRange(aRollingTimeWindow(oneMonth()), NOW)).toEqual({
|
||||
from: new Date('2022-07-11T08:31:00.000Z'),
|
||||
to: new Date('2022-08-11T08:31:00.000Z'),
|
||||
});
|
||||
});
|
||||
|
||||
it("computes the date range using a 'quarterly' rolling window", () => {
|
||||
expect(toDateRange(aRollingTimeWindow(oneQuarter()), NOW)).toEqual({
|
||||
from: new Date('2022-05-11T08:31:00.000Z'),
|
||||
it("computes the date range using a '90days' rolling window", () => {
|
||||
expect(toDateRange(ninetyDaysRolling(), NOW)).toEqual({
|
||||
from: new Date('2022-05-13T08:31:00.000Z'),
|
||||
to: new Date('2022-08-11T08:31:00.000Z'),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function aCalendarTimeWindow(duration: Duration): TimeWindow {
|
||||
return {
|
||||
duration,
|
||||
type: 'calendarAligned',
|
||||
};
|
||||
}
|
||||
|
||||
function aRollingTimeWindow(duration: Duration): TimeWindow {
|
||||
return { duration, type: 'rolling' };
|
||||
}
|
||||
|
|
|
@ -5,16 +5,19 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { calendarAlignedTimeWindowSchema, rollingTimeWindowSchema } from '@kbn/slo-schema';
|
||||
import { assertNever } from '@kbn/std';
|
||||
import moment from 'moment';
|
||||
import { calendarAlignedTimeWindowSchema, rollingTimeWindowSchema } from '@kbn/slo-schema';
|
||||
|
||||
import { DateRange, toMomentUnitOfTime } from '../models';
|
||||
import type { TimeWindow } from '../models/time_window';
|
||||
import { DateRange } from '../models';
|
||||
import {
|
||||
TimeWindow,
|
||||
toCalendarAlignedTimeWindowMomentUnit,
|
||||
toRollingTimeWindowMomentUnit,
|
||||
} from '../models/time_window';
|
||||
|
||||
export const toDateRange = (timeWindow: TimeWindow, currentDate: Date = new Date()): DateRange => {
|
||||
if (calendarAlignedTimeWindowSchema.is(timeWindow)) {
|
||||
const unit = toMomentUnitOfTime(timeWindow.duration.unit);
|
||||
const unit = toCalendarAlignedTimeWindowMomentUnit(timeWindow);
|
||||
const from = moment.utc(currentDate).startOf(unit);
|
||||
const to = moment.utc(currentDate).endOf(unit);
|
||||
|
||||
|
@ -22,12 +25,14 @@ export const toDateRange = (timeWindow: TimeWindow, currentDate: Date = new Date
|
|||
}
|
||||
|
||||
if (rollingTimeWindowSchema.is(timeWindow)) {
|
||||
const unit = toMomentUnitOfTime(timeWindow.duration.unit);
|
||||
const unit = toRollingTimeWindowMomentUnit(timeWindow);
|
||||
const now = moment.utc(currentDate).startOf('minute');
|
||||
const from = now.clone().subtract(timeWindow.duration.value, unit);
|
||||
const to = now.clone();
|
||||
|
||||
return {
|
||||
from: now.clone().subtract(timeWindow.duration.value, unit).toDate(),
|
||||
to: now.toDate(),
|
||||
from: from.toDate(),
|
||||
to: to.toDate(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { validateSLO } from '.';
|
||||
import { oneMinute, sixHours } from '../../services/slo/fixtures/duration';
|
||||
import { createSLO } from '../../services/slo/fixtures/slo';
|
||||
import { sevenDaysRolling } from '../../services/slo/fixtures/time_window';
|
||||
import { Duration, DurationUnit } from '../models';
|
||||
|
||||
describe('validateSLO', () => {
|
||||
|
@ -41,16 +42,12 @@ describe('validateSLO', () => {
|
|||
{ duration: new Duration(2, DurationUnit.Hour), shouldThrow: true },
|
||||
{ duration: new Duration(1, DurationUnit.Day), shouldThrow: true },
|
||||
{ duration: new Duration(7, DurationUnit.Day), shouldThrow: true },
|
||||
{ duration: new Duration(1, DurationUnit.Week), shouldThrow: false },
|
||||
{ duration: new Duration(2, DurationUnit.Week), shouldThrow: true },
|
||||
{ duration: new Duration(1, DurationUnit.Month), shouldThrow: false },
|
||||
{ duration: new Duration(2, DurationUnit.Month), shouldThrow: true },
|
||||
{ duration: new Duration(1, DurationUnit.Quarter), shouldThrow: true },
|
||||
{ duration: new Duration(3, DurationUnit.Quarter), shouldThrow: true },
|
||||
{ duration: new Duration(1, DurationUnit.Year), shouldThrow: true },
|
||||
{ duration: new Duration(3, DurationUnit.Year), shouldThrow: true },
|
||||
{ duration: new Duration(1, DurationUnit.Week), shouldThrow: false },
|
||||
{ duration: new Duration(1, DurationUnit.Month), shouldThrow: false },
|
||||
])(
|
||||
'throws when time window calendar aligned is not 1 week or 1 month',
|
||||
'throws when calendar aligned time window is not 1 week or 1 month',
|
||||
({ duration, shouldThrow }) => {
|
||||
if (shouldThrow) {
|
||||
expect(() =>
|
||||
|
@ -72,6 +69,34 @@ describe('validateSLO', () => {
|
|||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ duration: new Duration(7, DurationUnit.Day), shouldThrow: false },
|
||||
{ duration: new Duration(30, DurationUnit.Day), shouldThrow: false },
|
||||
{ duration: new Duration(90, DurationUnit.Day), shouldThrow: false },
|
||||
{ duration: new Duration(1, DurationUnit.Hour), shouldThrow: true },
|
||||
{ duration: new Duration(1, DurationUnit.Day), shouldThrow: true },
|
||||
{ duration: new Duration(1, DurationUnit.Week), shouldThrow: true },
|
||||
{ duration: new Duration(1, DurationUnit.Month), shouldThrow: true },
|
||||
])('throws when rolling time window is not 7, 30 or 90days', ({ duration, shouldThrow }) => {
|
||||
if (shouldThrow) {
|
||||
expect(() =>
|
||||
validateSLO(
|
||||
createSLO({
|
||||
timeWindow: { duration, type: 'rolling' },
|
||||
})
|
||||
)
|
||||
).toThrowError('Invalid time_window.duration');
|
||||
} else {
|
||||
expect(() =>
|
||||
validateSLO(
|
||||
createSLO({
|
||||
timeWindow: { duration, type: 'rolling' },
|
||||
})
|
||||
)
|
||||
).not.toThrowError();
|
||||
}
|
||||
});
|
||||
|
||||
describe('settings', () => {
|
||||
it("throws when frequency is longer or equal than '1h'", () => {
|
||||
const slo = createSLO({
|
||||
|
@ -173,25 +198,11 @@ describe('validateSLO', () => {
|
|||
objective: { ...slo.objective, timesliceWindow: new Duration(1, DurationUnit.Month) },
|
||||
})
|
||||
).toThrowError('Invalid objective.timeslice_window');
|
||||
|
||||
expect(() =>
|
||||
validateSLO({
|
||||
...slo,
|
||||
objective: { ...slo.objective, timesliceWindow: new Duration(1, DurationUnit.Quarter) },
|
||||
})
|
||||
).toThrowError('Invalid objective.timeslice_window');
|
||||
|
||||
expect(() =>
|
||||
validateSLO({
|
||||
...slo,
|
||||
objective: { ...slo.objective, timesliceWindow: new Duration(1, DurationUnit.Year) },
|
||||
})
|
||||
).toThrowError('Invalid objective.timeslice_window');
|
||||
});
|
||||
|
||||
it("throws when 'objective.timeslice_window' is longer than 'slo.time_window'", () => {
|
||||
const slo = createSLO({
|
||||
timeWindow: { duration: new Duration(1, DurationUnit.Week), type: 'rolling' },
|
||||
timeWindow: sevenDaysRolling(),
|
||||
budgetingMethod: 'timeslices',
|
||||
objective: {
|
||||
target: 0.95,
|
||||
|
|
|
@ -85,13 +85,8 @@ function isValidTargetNumber(value: number): boolean {
|
|||
}
|
||||
|
||||
function isValidRollingTimeWindowDuration(duration: Duration): boolean {
|
||||
return [
|
||||
DurationUnit.Day,
|
||||
DurationUnit.Week,
|
||||
DurationUnit.Month,
|
||||
DurationUnit.Quarter,
|
||||
DurationUnit.Year,
|
||||
].includes(duration.unit);
|
||||
// 7, 30 or 90days accepted
|
||||
return duration.unit === DurationUnit.Day && [7, 30, 90].includes(duration.value);
|
||||
}
|
||||
|
||||
function isValidCalendarAlignedTimeWindowDuration(duration: Duration): boolean {
|
||||
|
|
|
@ -5,47 +5,53 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
PluginInitializerContext,
|
||||
Plugin,
|
||||
CoreSetup,
|
||||
DEFAULT_APP_CATEGORIES,
|
||||
Logger,
|
||||
} from '@kbn/core/server';
|
||||
import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects';
|
||||
import { PluginSetupContract, PluginStartContract } from '@kbn/alerting-plugin/server';
|
||||
import { RuleRegistryPluginSetupContract } from '@kbn/rule-registry-plugin/server';
|
||||
import { PluginSetupContract as FeaturesSetup } from '@kbn/features-plugin/server';
|
||||
import {
|
||||
createUICapabilities as createCasesUICapabilities,
|
||||
getApiTags as getCasesApiTags,
|
||||
} from '@kbn/cases-plugin/common';
|
||||
import { SharePluginSetup } from '@kbn/share-plugin/server';
|
||||
import { SpacesPluginSetup } from '@kbn/spaces-plugin/server';
|
||||
import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server';
|
||||
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
|
||||
import { CloudSetup } from '@kbn/cloud-plugin/server';
|
||||
import {
|
||||
kubernetesGuideId,
|
||||
kubernetesGuideConfig,
|
||||
} from '../common/guided_onboarding/kubernetes_guide_config';
|
||||
CoreSetup,
|
||||
DEFAULT_APP_CATEGORIES,
|
||||
Logger,
|
||||
Plugin,
|
||||
PluginInitializerContext,
|
||||
} from '@kbn/core/server';
|
||||
import { PluginSetupContract as FeaturesSetup } from '@kbn/features-plugin/server';
|
||||
import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects';
|
||||
import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { RuleRegistryPluginSetupContract } from '@kbn/rule-registry-plugin/server';
|
||||
import { SharePluginSetup } from '@kbn/share-plugin/server';
|
||||
import { SpacesPluginSetup } from '@kbn/spaces-plugin/server';
|
||||
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
|
||||
import { ObservabilityConfig } from '.';
|
||||
import { casesFeatureId, observabilityFeatureId, sloFeatureId } from '../common';
|
||||
import { SLO_BURN_RATE_RULE_TYPE_ID } from '../common/constants';
|
||||
import {
|
||||
kubernetesGuideConfig,
|
||||
kubernetesGuideId,
|
||||
} from '../common/guided_onboarding/kubernetes_guide_config';
|
||||
import { AlertsLocatorDefinition } from '../common/locators/alerts';
|
||||
import {
|
||||
AnnotationsAPI,
|
||||
bootstrapAnnotations,
|
||||
ScopedAnnotationsClientFactory,
|
||||
AnnotationsAPI,
|
||||
} from './lib/annotations/bootstrap_annotations';
|
||||
import { uiSettings } from './ui_settings';
|
||||
import { registerRoutes } from './routes/register_routes';
|
||||
import { getObservabilityServerRouteRepository } from './routes/get_global_observability_server_route_repository';
|
||||
import { compositeSlo, slo, SO_COMPOSITE_SLO_TYPE, SO_SLO_TYPE } from './saved_objects';
|
||||
import { AlertsLocatorDefinition } from '../common/locators/alerts';
|
||||
import { casesFeatureId, observabilityFeatureId, sloFeatureId } from '../common';
|
||||
import { registerRuleTypes } from './lib/rules/register_rule_types';
|
||||
import { SLO_BURN_RATE_RULE_TYPE_ID } from '../common/constants';
|
||||
import { registerSloUsageCollector } from './lib/collectors/register';
|
||||
import { registerRuleTypes } from './lib/rules/register_rule_types';
|
||||
import { getObservabilityServerRouteRepository } from './routes/get_global_observability_server_route_repository';
|
||||
import { registerRoutes } from './routes/register_routes';
|
||||
import { compositeSlo, slo, SO_COMPOSITE_SLO_TYPE, SO_SLO_TYPE } from './saved_objects';
|
||||
import { threshold } from './saved_objects/threshold';
|
||||
import {
|
||||
DefaultResourceInstaller,
|
||||
DefaultSLOInstaller,
|
||||
DefaultSummaryTransformInstaller,
|
||||
} from './services/slo';
|
||||
|
||||
import { uiSettings } from './ui_settings';
|
||||
|
||||
export type ObservabilityPluginSetup = ReturnType<ObservabilityPlugin['setup']>;
|
||||
|
||||
|
@ -258,6 +264,20 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
|
|||
logger: this.logger,
|
||||
repository: getObservabilityServerRouteRepository(config),
|
||||
});
|
||||
|
||||
const esInternalClient = coreStart.elasticsearch.client.asInternalUser;
|
||||
|
||||
const sloResourceInstaller = new DefaultResourceInstaller(esInternalClient, this.logger);
|
||||
const sloSummaryInstaller = new DefaultSummaryTransformInstaller(
|
||||
esInternalClient,
|
||||
this.logger
|
||||
);
|
||||
const sloInstaller = new DefaultSLOInstaller(
|
||||
sloResourceInstaller,
|
||||
sloSummaryInstaller,
|
||||
this.logger
|
||||
);
|
||||
sloInstaller.install();
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,22 +5,22 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { forbidden, failedDependency } from '@hapi/boom';
|
||||
import { failedDependency, forbidden } from '@hapi/boom';
|
||||
import {
|
||||
createSLOParamsSchema,
|
||||
deleteSLOParamsSchema,
|
||||
fetchHistoricalSummaryParamsSchema,
|
||||
findSLOParamsSchema,
|
||||
getSLOBurnRatesParamsSchema,
|
||||
getPreviewDataParamsSchema,
|
||||
getSLOBurnRatesParamsSchema,
|
||||
getSLODiagnosisParamsSchema,
|
||||
getSLOParamsSchema,
|
||||
manageSLOParamsSchema,
|
||||
updateSLOParamsSchema,
|
||||
} from '@kbn/slo-schema';
|
||||
import type { IndicatorTypes } from '../../domain/models';
|
||||
import {
|
||||
CreateSLO,
|
||||
DefaultResourceInstaller,
|
||||
DefaultSummaryClient,
|
||||
DefaultTransformManager,
|
||||
DeleteSLO,
|
||||
|
@ -29,6 +29,13 @@ import {
|
|||
KibanaSavedObjectsSLORepository,
|
||||
UpdateSLO,
|
||||
} from '../../services/slo';
|
||||
import { FetchHistoricalSummary } from '../../services/slo/fetch_historical_summary';
|
||||
import { getBurnRates } from '../../services/slo/get_burn_rates';
|
||||
import { getGlobalDiagnosis, getSloDiagnosis } from '../../services/slo/get_diagnosis';
|
||||
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 {
|
||||
ApmTransactionDurationTransformGenerator,
|
||||
ApmTransactionErrorRateTransformGenerator,
|
||||
|
@ -37,15 +44,8 @@ import {
|
|||
MetricCustomTransformGenerator,
|
||||
TransformGenerator,
|
||||
} from '../../services/slo/transform_generators';
|
||||
import { createObservabilityServerRoute } from '../create_observability_server_route';
|
||||
import { DefaultHistoricalSummaryClient } from '../../services/slo/historical_summary_client';
|
||||
import { FetchHistoricalSummary } from '../../services/slo/fetch_historical_summary';
|
||||
import type { IndicatorTypes } from '../../domain/models';
|
||||
import type { ObservabilityRequestHandlerContext } from '../../types';
|
||||
import { ManageSLO } from '../../services/slo/manage_slo';
|
||||
import { getGlobalDiagnosis, getSloDiagnosis } from '../../services/slo/get_diagnosis';
|
||||
import { getBurnRates } from '../../services/slo/get_burn_rates';
|
||||
import { GetPreviewData } from '../../services/slo/get_preview_data';
|
||||
import { createObservabilityServerRoute } from '../create_observability_server_route';
|
||||
|
||||
const transformGenerators: Record<IndicatorTypes, TransformGenerator> = {
|
||||
'sli.apm.transactionDuration': new ApmTransactionDurationTransformGenerator(),
|
||||
|
@ -75,11 +75,9 @@ const createSLORoute = createObservabilityServerRoute({
|
|||
|
||||
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
|
||||
const resourceInstaller = new DefaultResourceInstaller(esClient, logger);
|
||||
const repository = new KibanaSavedObjectsSLORepository(soClient);
|
||||
const transformManager = new DefaultTransformManager(transformGenerators, esClient, logger);
|
||||
const createSLO = new CreateSLO(resourceInstaller, repository, transformManager);
|
||||
const createSLO = new CreateSLO(esClient, repository, transformManager);
|
||||
|
||||
const response = await createSLO.execute(params.body);
|
||||
|
||||
|
@ -228,7 +226,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) {
|
||||
|
@ -238,8 +236,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 ?? {});
|
||||
|
||||
|
@ -304,8 +302,9 @@ const getSloDiagnosisRoute = createObservabilityServerRoute({
|
|||
handler: async ({ context, params }) => {
|
||||
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
const repository = new KibanaSavedObjectsSLORepository(soClient);
|
||||
|
||||
return getSloDiagnosis(params.path.id, { esClient, soClient });
|
||||
return getSloDiagnosis(params.path.id, { esClient, repository });
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ Object {
|
|||
exports[`SummaryClient fetchSummary with a rolling and occurrences composite SLO returns the summary 2`] = `
|
||||
Array [
|
||||
Object {
|
||||
"index": ".slo-observability.sli-v1*",
|
||||
"index": ".slo-observability.sli-v2*",
|
||||
},
|
||||
Object {
|
||||
"aggs": Object {
|
||||
|
@ -108,7 +108,7 @@ Object {
|
|||
exports[`SummaryClient with rolling and timeslices SLO returns the summary 2`] = `
|
||||
Array [
|
||||
Object {
|
||||
"index": ".slo-observability.sli-v1*",
|
||||
"index": ".slo-observability.sli-v2*",
|
||||
},
|
||||
Object {
|
||||
"aggs": Object {
|
||||
|
|
46
x-pack/plugins/observability/server/services/slo/__snapshots__/create_slo.test.ts.snap
generated
Normal file
46
x-pack/plugins/observability/server/services/slo/__snapshots__/create_slo.test.ts.snap
generated
Normal file
|
@ -0,0 +1,46 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CreateSLO happy path calls the expected services 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"document": Object {
|
||||
"errorBudgetConsumed": 0,
|
||||
"errorBudgetEstimated": false,
|
||||
"errorBudgetInitial": 0.010000000000000009,
|
||||
"errorBudgetRemaining": 1,
|
||||
"goodEvents": 0,
|
||||
"isTempDoc": true,
|
||||
"service": Object {
|
||||
"environment": null,
|
||||
"name": null,
|
||||
},
|
||||
"sliValue": -1,
|
||||
"slo": Object {
|
||||
"budgetingMethod": "occurrences",
|
||||
"description": "irrelevant",
|
||||
"id": "unique-id",
|
||||
"indicator": Object {
|
||||
"type": "sli.apm.transactionErrorRate",
|
||||
},
|
||||
"instanceId": "*",
|
||||
"name": "irrelevant",
|
||||
"revision": 1,
|
||||
"tags": Array [],
|
||||
"timeWindow": Object {
|
||||
"duration": "7d",
|
||||
"type": "rolling",
|
||||
},
|
||||
},
|
||||
"status": "NO_DATA",
|
||||
"statusCode": 0,
|
||||
"totalEvents": 0,
|
||||
"transaction": Object {
|
||||
"name": null,
|
||||
"type": null,
|
||||
},
|
||||
},
|
||||
"id": "slo-unique-id",
|
||||
"index": ".slo-observability.summary-v2.temp",
|
||||
},
|
||||
]
|
||||
`;
|
106
x-pack/plugins/observability/server/services/slo/__snapshots__/summary_search_client.test.ts.snap
generated
Normal file
106
x-pack/plugins/observability/server/services/slo/__snapshots__/summary_search_client.test.ts.snap
generated
Normal file
|
@ -0,0 +1,106 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Summary Search Client returns the summary documents without duplicate temporary summary documents 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"index": ".slo-observability.summary-v2*",
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"terms": Object {
|
||||
"slo.id": Array [
|
||||
"slo-one",
|
||||
"slo_two",
|
||||
"slo-three",
|
||||
"slo-five",
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"isTempDoc": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"wait_for_completion": false,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Summary Search Client returns the summary documents without duplicate temporary summary documents 2`] = `
|
||||
Object {
|
||||
"page": 1,
|
||||
"perPage": 20,
|
||||
"results": Array [
|
||||
Object {
|
||||
"id": "slo-one",
|
||||
"summary": Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0.4,
|
||||
"initial": 0.02,
|
||||
"isEstimated": false,
|
||||
"remaining": 0.6,
|
||||
},
|
||||
"sliValue": 0.9,
|
||||
"status": "HEALTHY",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "slo_two",
|
||||
"summary": Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0.4,
|
||||
"initial": 0.02,
|
||||
"isEstimated": false,
|
||||
"remaining": 0.6,
|
||||
},
|
||||
"sliValue": 0.9,
|
||||
"status": "HEALTHY",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "slo-three",
|
||||
"summary": Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0.4,
|
||||
"initial": 0.02,
|
||||
"isEstimated": false,
|
||||
"remaining": 0.6,
|
||||
},
|
||||
"sliValue": 0.9,
|
||||
"status": "HEALTHY",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "slo-five",
|
||||
"summary": Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0.4,
|
||||
"initial": 0.02,
|
||||
"isEstimated": false,
|
||||
"remaining": 0.6,
|
||||
},
|
||||
"sliValue": 0.9,
|
||||
"status": "HEALTHY",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "slo-four",
|
||||
"summary": Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0.4,
|
||||
"initial": 0.02,
|
||||
"isEstimated": false,
|
||||
"remaining": 0.6,
|
||||
},
|
||||
"sliValue": 0.9,
|
||||
"status": "HEALTHY",
|
||||
},
|
||||
},
|
||||
],
|
||||
"total": 5,
|
||||
}
|
||||
`;
|
49
x-pack/plugins/observability/server/services/slo/__snapshots__/update_slo.test.ts.snap
generated
Normal file
49
x-pack/plugins/observability/server/services/slo/__snapshots__/update_slo.test.ts.snap
generated
Normal file
|
@ -0,0 +1,49 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`UpdateSLO index a temporary summary document 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"document": Object {
|
||||
"errorBudgetConsumed": 0,
|
||||
"errorBudgetEstimated": false,
|
||||
"errorBudgetInitial": 0.0010000000000000009,
|
||||
"errorBudgetRemaining": 1,
|
||||
"goodEvents": 0,
|
||||
"isTempDoc": true,
|
||||
"service": Object {
|
||||
"environment": null,
|
||||
"name": null,
|
||||
},
|
||||
"sliValue": -1,
|
||||
"slo": Object {
|
||||
"budgetingMethod": "occurrences",
|
||||
"description": "irrelevant",
|
||||
"id": "unique-id",
|
||||
"indicator": Object {
|
||||
"type": "sli.apm.transactionErrorRate",
|
||||
},
|
||||
"instanceId": "*",
|
||||
"name": "irrelevant",
|
||||
"revision": 2,
|
||||
"tags": Array [
|
||||
"critical",
|
||||
"k8s",
|
||||
],
|
||||
"timeWindow": Object {
|
||||
"duration": "7d",
|
||||
"type": "rolling",
|
||||
},
|
||||
},
|
||||
"status": "NO_DATA",
|
||||
"statusCode": 0,
|
||||
"totalEvents": 0,
|
||||
"transaction": Object {
|
||||
"name": null,
|
||||
"type": null,
|
||||
},
|
||||
},
|
||||
"id": "slo-unique-id",
|
||||
"index": ".slo-observability.summary-v2.temp",
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -5,43 +5,41 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ElasticsearchClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { CreateSLO } from './create_slo';
|
||||
import { fiveMinute, oneMinute } from './fixtures/duration';
|
||||
import { createAPMTransactionErrorRateIndicator, createSLOParams } from './fixtures/slo';
|
||||
import {
|
||||
createResourceInstallerMock,
|
||||
createSLORepositoryMock,
|
||||
createTransformManagerMock,
|
||||
} from './mocks';
|
||||
import { ResourceInstaller } from './resource_installer';
|
||||
import { createSLORepositoryMock, createTransformManagerMock } from './mocks';
|
||||
import { SLORepository } from './slo_repository';
|
||||
import { TransformManager } from './transform_manager';
|
||||
|
||||
describe('CreateSLO', () => {
|
||||
let mockResourceInstaller: jest.Mocked<ResourceInstaller>;
|
||||
let esClientMock: ElasticsearchClientMock;
|
||||
let mockRepository: jest.Mocked<SLORepository>;
|
||||
let mockTransformManager: jest.Mocked<TransformManager>;
|
||||
let createSLO: CreateSLO;
|
||||
|
||||
beforeEach(() => {
|
||||
mockResourceInstaller = createResourceInstallerMock();
|
||||
esClientMock = elasticsearchServiceMock.createElasticsearchClient();
|
||||
mockRepository = createSLORepositoryMock();
|
||||
mockTransformManager = createTransformManagerMock();
|
||||
createSLO = new CreateSLO(mockResourceInstaller, mockRepository, mockTransformManager);
|
||||
createSLO = new CreateSLO(esClientMock, mockRepository, mockTransformManager);
|
||||
});
|
||||
|
||||
describe('happy path', () => {
|
||||
it('calls the expected services', async () => {
|
||||
const sloParams = createSLOParams({ indicator: createAPMTransactionErrorRateIndicator() });
|
||||
const sloParams = createSLOParams({
|
||||
id: 'unique-id',
|
||||
indicator: createAPMTransactionErrorRateIndicator(),
|
||||
});
|
||||
mockTransformManager.install.mockResolvedValue('slo-transform-id');
|
||||
|
||||
const response = await createSLO.execute(sloParams);
|
||||
|
||||
expect(mockResourceInstaller.ensureCommonResourcesInstalled).toHaveBeenCalled();
|
||||
expect(mockRepository.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
...sloParams,
|
||||
id: expect.any(String),
|
||||
id: 'unique-id',
|
||||
settings: {
|
||||
syncDelay: oneMinute(),
|
||||
frequency: oneMinute(),
|
||||
|
@ -55,10 +53,11 @@ describe('CreateSLO', () => {
|
|||
{ throwOnConflict: true }
|
||||
);
|
||||
expect(mockTransformManager.install).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ ...sloParams, id: expect.any(String) })
|
||||
expect.objectContaining({ ...sloParams, id: 'unique-id' })
|
||||
);
|
||||
expect(mockTransformManager.start).toHaveBeenCalledWith('slo-transform-id');
|
||||
expect(response).toEqual(expect.objectContaining({ id: expect.any(String) }));
|
||||
expect(response).toEqual(expect.objectContaining({ id: 'unique-id' }));
|
||||
expect(esClientMock.index.mock.calls[0]).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('overrides the default values when provided', async () => {
|
||||
|
@ -73,7 +72,6 @@ describe('CreateSLO', () => {
|
|||
|
||||
await createSLO.execute(sloParams);
|
||||
|
||||
expect(mockResourceInstaller.ensureCommonResourcesInstalled).toHaveBeenCalled();
|
||||
expect(mockRepository.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
...sloParams,
|
||||
|
|
|
@ -5,19 +5,19 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { v1 as uuidv1 } from 'uuid';
|
||||
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { CreateSLOParams, CreateSLOResponse } from '@kbn/slo-schema';
|
||||
|
||||
import { v1 as uuidv1 } from 'uuid';
|
||||
import { SLO_SUMMARY_TEMP_INDEX_NAME } from '../../assets/constants';
|
||||
import { Duration, DurationUnit, SLO } from '../../domain/models';
|
||||
import { ResourceInstaller } from './resource_installer';
|
||||
import { SLORepository } from './slo_repository';
|
||||
import { TransformManager } from './transform_manager';
|
||||
import { validateSLO } from '../../domain/services';
|
||||
import { SLORepository } from './slo_repository';
|
||||
import { createTempSummaryDocument } from './summary_transform/helpers/create_temp_summary';
|
||||
import { TransformManager } from './transform_manager';
|
||||
|
||||
export class CreateSLO {
|
||||
constructor(
|
||||
private resourceInstaller: ResourceInstaller,
|
||||
private esClient: ElasticsearchClient,
|
||||
private repository: SLORepository,
|
||||
private transformManager: TransformManager
|
||||
) {}
|
||||
|
@ -26,9 +26,7 @@ export class CreateSLO {
|
|||
const slo = this.toSLO(params);
|
||||
validateSLO(slo);
|
||||
|
||||
await this.resourceInstaller.ensureCommonResourcesInstalled();
|
||||
await this.repository.save(slo, { throwOnConflict: true });
|
||||
|
||||
let sloTransformId;
|
||||
try {
|
||||
sloTransformId = await this.transformManager.install(slo);
|
||||
|
@ -48,6 +46,12 @@ export class CreateSLO {
|
|||
throw err;
|
||||
}
|
||||
|
||||
await this.esClient.index({
|
||||
index: SLO_SUMMARY_TEMP_INDEX_NAME,
|
||||
id: `slo-${slo.id}`,
|
||||
document: createTempSummaryDocument(slo),
|
||||
});
|
||||
|
||||
return this.toResponse(slo);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,11 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { RulesClientApi } from '@kbn/alerting-plugin/server/types';
|
||||
import { rulesClientMock } from '@kbn/alerting-plugin/server/rules_client.mock';
|
||||
import { RulesClientApi } from '@kbn/alerting-plugin/server/types';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { getSLOTransformId, SLO_INDEX_TEMPLATE_NAME } from '../../assets/constants';
|
||||
import {
|
||||
getSLOTransformId,
|
||||
SLO_DESTINATION_INDEX_PATTERN,
|
||||
SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
|
||||
} from '../../assets/constants';
|
||||
import { DeleteSLO } from './delete_slo';
|
||||
import { createAPMTransactionErrorRateIndicator, createSLO } from './fixtures/slo';
|
||||
import { createSLORepositoryMock, createTransformManagerMock } from './mocks';
|
||||
|
@ -45,9 +49,22 @@ describe('DeleteSLO', () => {
|
|||
expect(mockTransformManager.uninstall).toHaveBeenCalledWith(
|
||||
getSLOTransformId(slo.id, slo.revision)
|
||||
);
|
||||
expect(mockEsClient.deleteByQuery).toHaveBeenCalledWith(
|
||||
expect(mockEsClient.deleteByQuery).toHaveBeenCalledTimes(2);
|
||||
expect(mockEsClient.deleteByQuery).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
index: `${SLO_INDEX_TEMPLATE_NAME}*`,
|
||||
index: SLO_DESTINATION_INDEX_PATTERN,
|
||||
query: {
|
||||
match: {
|
||||
'slo.id': slo.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(mockEsClient.deleteByQuery).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
|
||||
query: {
|
||||
match: {
|
||||
'slo.id': slo.id,
|
||||
|
|
|
@ -7,8 +7,11 @@
|
|||
|
||||
import { RulesClientApi } from '@kbn/alerting-plugin/server/types';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { getSLOTransformId, SLO_INDEX_TEMPLATE_NAME } from '../../assets/constants';
|
||||
|
||||
import {
|
||||
getSLOTransformId,
|
||||
SLO_DESTINATION_INDEX_PATTERN,
|
||||
SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
|
||||
} from '../../assets/constants';
|
||||
import { SLORepository } from './slo_repository';
|
||||
import { TransformManager } from './transform_manager';
|
||||
|
||||
|
@ -28,13 +31,14 @@ export class DeleteSLO {
|
|||
await this.transformManager.uninstall(sloTransformId);
|
||||
|
||||
await this.deleteRollupData(slo.id);
|
||||
await this.deleteSummaryData(slo.id);
|
||||
await this.deleteAssociatedRules(slo.id);
|
||||
await this.repository.deleteById(slo.id);
|
||||
}
|
||||
|
||||
private async deleteRollupData(sloId: string): Promise<void> {
|
||||
await this.esClient.deleteByQuery({
|
||||
index: `${SLO_INDEX_TEMPLATE_NAME}*`,
|
||||
index: SLO_DESTINATION_INDEX_PATTERN,
|
||||
wait_for_completion: false,
|
||||
query: {
|
||||
match: {
|
||||
|
@ -44,6 +48,17 @@ export class DeleteSLO {
|
|||
});
|
||||
}
|
||||
|
||||
private async deleteSummaryData(sloId: string): Promise<void> {
|
||||
await this.esClient.deleteByQuery({
|
||||
index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
|
||||
wait_for_completion: false,
|
||||
query: {
|
||||
match: {
|
||||
'slo.id': sloId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
private async deleteAssociatedRules(sloId: string): Promise<void> {
|
||||
try {
|
||||
await this.rulesClient.bulkDeleteRules({
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,55 +6,45 @@
|
|||
*/
|
||||
|
||||
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,
|
||||
pagination
|
||||
const sloSummaryList = await this.summarySearchClient.search(
|
||||
params.kqlQuery ?? '',
|
||||
toSort(params),
|
||||
toPagination(params)
|
||||
);
|
||||
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
|
||||
.filter((sloSummary) => sloList.some((s) => s.id === sloSummary.id))
|
||||
.map((sloSummary) => ({
|
||||
...sloList.find((s) => s.id === sloSummary.id)!,
|
||||
summary: sloSummary.summary,
|
||||
}));
|
||||
}
|
||||
|
||||
function toPagination(params: FindSLOParams): Pagination {
|
||||
|
@ -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',
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,14 +7,14 @@
|
|||
|
||||
import { Duration, DurationUnit } from '../../../domain/models';
|
||||
|
||||
export function oneQuarter(): Duration {
|
||||
return new Duration(1, DurationUnit.Quarter);
|
||||
}
|
||||
|
||||
export function thirtyDays(): Duration {
|
||||
return new Duration(30, DurationUnit.Day);
|
||||
}
|
||||
|
||||
export function ninetyDays(): Duration {
|
||||
return new Duration(90, DurationUnit.Day);
|
||||
}
|
||||
|
||||
export function oneMonth(): Duration {
|
||||
return new Duration(1, DurationUnit.Month);
|
||||
}
|
||||
|
|
|
@ -5,12 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SavedObject } from '@kbn/core-saved-objects-server';
|
||||
import { CreateSLOParams, HistogramIndicator, sloSchema } from '@kbn/slo-schema';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { v1 as uuidv1 } from 'uuid';
|
||||
import { SavedObject } from '@kbn/core-saved-objects-server';
|
||||
import { sloSchema, CreateSLOParams, HistogramIndicator } from '@kbn/slo-schema';
|
||||
|
||||
import { SO_SLO_TYPE } from '../../../saved_objects';
|
||||
import {
|
||||
APMTransactionDurationIndicator,
|
||||
APMTransactionErrorRateIndicator,
|
||||
|
@ -22,9 +20,9 @@ import {
|
|||
SLO,
|
||||
StoredSLO,
|
||||
} from '../../../domain/models';
|
||||
import { Paginated } from '../slo_repository';
|
||||
import { oneWeek, twoMinute } from './duration';
|
||||
import { sevenDaysRolling } from './time_window';
|
||||
import { SO_SLO_TYPE } from '../../../saved_objects';
|
||||
import { twoMinute } from './duration';
|
||||
import { sevenDaysRolling, weeklyCalendarAligned } from './time_window';
|
||||
|
||||
export const createAPMTransactionErrorRateIndicator = (
|
||||
params: Partial<APMTransactionErrorRateIndicator['params']> = {}
|
||||
|
@ -184,23 +182,7 @@ export const createSLOWithTimeslicesBudgetingMethod = (params: Partial<SLO> = {}
|
|||
|
||||
export const createSLOWithCalendarTimeWindow = (params: Partial<SLO> = {}): SLO => {
|
||||
return createSLO({
|
||||
timeWindow: {
|
||||
duration: oneWeek(),
|
||||
type: 'calendarAligned',
|
||||
},
|
||||
timeWindow: weeklyCalendarAligned(),
|
||||
...params,
|
||||
});
|
||||
};
|
||||
|
||||
export const createPaginatedSLO = (
|
||||
slo: SLO,
|
||||
params: Partial<Paginated<SLO>> = {}
|
||||
): Paginated<SLO> => {
|
||||
return {
|
||||
page: 1,
|
||||
perPage: 25,
|
||||
total: 1,
|
||||
results: [slo],
|
||||
...params,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { v1 as uuidv1 } from 'uuid';
|
||||
|
||||
export const aSummaryDocument = ({
|
||||
id = uuidv1(),
|
||||
sliValue = 0.9,
|
||||
consumed = 0.4,
|
||||
isTempDoc = false,
|
||||
status = 'HEALTHY',
|
||||
} = {}) => {
|
||||
return {
|
||||
goodEvents: 96,
|
||||
totalEvents: 100,
|
||||
errorBudgetEstimated: false,
|
||||
errorBudgetRemaining: 1 - consumed,
|
||||
errorBudgetConsumed: consumed,
|
||||
isTempDoc,
|
||||
service: {
|
||||
environment: null,
|
||||
name: null,
|
||||
},
|
||||
slo: {
|
||||
indicator: {
|
||||
type: 'sli.kql.custom',
|
||||
},
|
||||
timeWindow: {
|
||||
duration: '30d',
|
||||
type: 'rolling',
|
||||
},
|
||||
instanceId: '*',
|
||||
name: 'irrelevant',
|
||||
description: '',
|
||||
id,
|
||||
budgetingMethod: 'occurrences',
|
||||
revision: 1,
|
||||
tags: ['tag-one', 'tag-two', 'irrelevant'],
|
||||
},
|
||||
errorBudgetInitial: 0.02,
|
||||
transaction: {
|
||||
name: null,
|
||||
type: null,
|
||||
},
|
||||
sliValue,
|
||||
statusCode: 4,
|
||||
status,
|
||||
};
|
||||
};
|
||||
|
||||
export const aHitFromSummaryIndex = (_source: any) => {
|
||||
return {
|
||||
_index: '.slo-observability.summary-v2',
|
||||
_id: uuidv1(),
|
||||
_score: 1,
|
||||
_source,
|
||||
};
|
||||
};
|
||||
|
||||
export const aHitFromTempSummaryIndex = (_source: any) => {
|
||||
return {
|
||||
_index: '.slo-observability.summary-v2.temp',
|
||||
_id: uuidv1(),
|
||||
_score: 1,
|
||||
_source,
|
||||
};
|
||||
};
|
|
@ -5,15 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { RollingTimeWindow, TimeWindow } from '../../../domain/models/time_window';
|
||||
import { oneWeek, sevenDays, sixHours, thirtyDays } from './duration';
|
||||
|
||||
export function sixHoursRolling(): TimeWindow {
|
||||
return {
|
||||
duration: sixHours(),
|
||||
type: 'rolling',
|
||||
};
|
||||
}
|
||||
import {
|
||||
CalendarAlignedTimeWindow,
|
||||
RollingTimeWindow,
|
||||
TimeWindow,
|
||||
} from '../../../domain/models/time_window';
|
||||
import { ninetyDays, oneMonth, oneWeek, sevenDays, thirtyDays } from './duration';
|
||||
|
||||
export function sevenDaysRolling(): RollingTimeWindow {
|
||||
return {
|
||||
|
@ -28,9 +25,23 @@ export function thirtyDaysRolling(): RollingTimeWindow {
|
|||
};
|
||||
}
|
||||
|
||||
export function weeklyCalendarAligned(): TimeWindow {
|
||||
export function ninetyDaysRolling(): TimeWindow {
|
||||
return {
|
||||
duration: ninetyDays(),
|
||||
type: 'rolling',
|
||||
};
|
||||
}
|
||||
|
||||
export function weeklyCalendarAligned(): CalendarAlignedTimeWindow {
|
||||
return {
|
||||
duration: oneWeek(),
|
||||
type: 'calendarAligned',
|
||||
};
|
||||
}
|
||||
|
||||
export function monthlyCalendarAligned(): CalendarAlignedTimeWindow {
|
||||
return {
|
||||
duration: oneMonth(),
|
||||
type: 'calendarAligned',
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,18 +6,20 @@
|
|||
*/
|
||||
|
||||
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
|
||||
import { LicensingApiRequestHandlerContext } from '@kbn/licensing-plugin/server';
|
||||
|
||||
import {
|
||||
getSLOTransformId,
|
||||
SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME,
|
||||
SLO_COMPONENT_TEMPLATE_SETTINGS_NAME,
|
||||
SLO_INDEX_TEMPLATE_NAME,
|
||||
SLO_INGEST_PIPELINE_NAME,
|
||||
SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME,
|
||||
SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME,
|
||||
SLO_SUMMARY_INDEX_TEMPLATE_NAME,
|
||||
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
|
||||
} from '../../assets/constants';
|
||||
import { StoredSLO } from '../../domain/models';
|
||||
import { SO_SLO_TYPE } from '../../saved_objects';
|
||||
import { SLO } from '../../domain/models';
|
||||
import { SLORepository } from './slo_repository';
|
||||
|
||||
const OK = 'OK';
|
||||
const NOT_OK = 'NOT_OK';
|
||||
|
@ -30,11 +32,19 @@ export async function getGlobalDiagnosis(
|
|||
const licenseInfo = licensing.license.toJSON();
|
||||
const userPrivileges = await esClient.security.getUserPrivileges();
|
||||
const sloResources = await getSloResourcesDiagnosis(esClient);
|
||||
const sloSummaryResources = await getSloSummaryResourcesDiagnosis(esClient);
|
||||
|
||||
const sloSummaryTransformsStats = await esClient.transform.getTransformStats({
|
||||
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}*`,
|
||||
allow_no_match: true,
|
||||
});
|
||||
|
||||
return {
|
||||
licenseAndFeatures: licenseInfo,
|
||||
userPrivileges,
|
||||
sloResources,
|
||||
sloSummaryResources,
|
||||
sloSummaryTransformsStats,
|
||||
};
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
@ -43,42 +53,36 @@ export async function getGlobalDiagnosis(
|
|||
|
||||
export async function getSloDiagnosis(
|
||||
sloId: string,
|
||||
services: { esClient: ElasticsearchClient; soClient: SavedObjectsClientContract }
|
||||
services: { esClient: ElasticsearchClient; repository: SLORepository }
|
||||
) {
|
||||
const { esClient, soClient } = services;
|
||||
const { esClient, repository } = services;
|
||||
|
||||
const sloResources = await getSloResourcesDiagnosis(esClient);
|
||||
const sloSummaryResources = await getSloSummaryResourcesDiagnosis(esClient);
|
||||
|
||||
let sloSavedObject;
|
||||
let slo: SLO | undefined;
|
||||
try {
|
||||
sloSavedObject = await soClient.get<StoredSLO>(SO_SLO_TYPE, sloId);
|
||||
slo = await repository.findById(sloId);
|
||||
} catch (err) {
|
||||
// noop
|
||||
}
|
||||
|
||||
const sloTransformStats = await esClient.transform.getTransformStats({
|
||||
transform_id: getSLOTransformId(sloId, sloSavedObject?.attributes.revision ?? 1),
|
||||
transform_id: getSLOTransformId(sloId, slo?.revision ?? 1),
|
||||
allow_no_match: true,
|
||||
});
|
||||
|
||||
let dataSample;
|
||||
if (sloSavedObject?.attributes.indicator.params.index) {
|
||||
const slo = sloSavedObject.attributes;
|
||||
const sortField =
|
||||
'timestampField' in slo.indicator.params
|
||||
? slo.indicator.params.timestampField ?? '@timestamp'
|
||||
: '@timestamp';
|
||||
dataSample = await esClient.search({
|
||||
index: slo.indicator.params.index,
|
||||
sort: { [sortField]: 'desc' },
|
||||
size: 5,
|
||||
});
|
||||
}
|
||||
const sloSummaryTransformsStats = await esClient.transform.getTransformStats({
|
||||
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}*`,
|
||||
allow_no_match: true,
|
||||
});
|
||||
|
||||
return {
|
||||
sloResources,
|
||||
sloSavedObject: sloSavedObject ?? NOT_OK,
|
||||
sloSummaryResources,
|
||||
slo: slo ?? NOT_OK,
|
||||
sloTransformStats,
|
||||
dataSample: dataSample ?? NOT_OK,
|
||||
sloSummaryTransformsStats,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -116,3 +120,29 @@ async function getSloResourcesDiagnosis(esClient: ElasticsearchClient) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getSloSummaryResourcesDiagnosis(esClient: ElasticsearchClient) {
|
||||
try {
|
||||
const indexTemplateExists = await esClient.indices.existsIndexTemplate({
|
||||
name: SLO_SUMMARY_INDEX_TEMPLATE_NAME,
|
||||
});
|
||||
|
||||
const mappingsTemplateExists = await esClient.cluster.existsComponentTemplate({
|
||||
name: SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME,
|
||||
});
|
||||
|
||||
const settingsTemplateExists = await esClient.cluster.existsComponentTemplate({
|
||||
name: SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME,
|
||||
});
|
||||
|
||||
return {
|
||||
[SLO_SUMMARY_INDEX_TEMPLATE_NAME]: indexTemplateExists ? OK : NOT_OK,
|
||||
[SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME]: mappingsTemplateExists ? OK : NOT_OK,
|
||||
[SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME]: settingsTemplateExists ? OK : NOT_OK,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err.meta.statusCode === 403) {
|
||||
throw new Error('Insufficient permissions to access Elasticsearch Cluster', { cause: err });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@ export * from './find_slo';
|
|||
export * from './get_slo';
|
||||
export * from './historical_summary_client';
|
||||
export * from './resource_installer';
|
||||
export * from './slo_installer';
|
||||
export * from './summary_transform/summary_transform_installer';
|
||||
export * from './sli_client';
|
||||
export * from './slo_repository';
|
||||
export * from './transform_manager';
|
||||
|
|
|
@ -9,6 +9,8 @@ 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 { SummaryTransformInstaller } from '../summary_transform/summary_transform_installer';
|
||||
import { TransformManager } from '../transform_manager';
|
||||
|
||||
const createResourceInstallerMock = (): jest.Mocked<ResourceInstaller> => {
|
||||
|
@ -17,6 +19,12 @@ const createResourceInstallerMock = (): jest.Mocked<ResourceInstaller> => {
|
|||
};
|
||||
};
|
||||
|
||||
const createSummaryTransformInstallerMock = (): jest.Mocked<SummaryTransformInstaller> => {
|
||||
return {
|
||||
installAndStart: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
const createTransformManagerMock = (): jest.Mocked<TransformManager> => {
|
||||
return {
|
||||
install: jest.fn(),
|
||||
|
@ -32,7 +40,6 @@ const createSLORepositoryMock = (): jest.Mocked<SLORepository> => {
|
|||
findById: jest.fn(),
|
||||
findAllByIds: jest.fn(),
|
||||
deleteById: jest.fn(),
|
||||
find: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -42,6 +49,12 @@ const createSummaryClientMock = (): jest.Mocked<SummaryClient> => {
|
|||
};
|
||||
};
|
||||
|
||||
const createSummarySearchClientMock = (): jest.Mocked<SummarySearchClient> => {
|
||||
return {
|
||||
search: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
const createSLIClientMock = (): jest.Mocked<SLIClient> => {
|
||||
return {
|
||||
fetchSLIDataFrom: jest.fn(),
|
||||
|
@ -50,8 +63,10 @@ const createSLIClientMock = (): jest.Mocked<SLIClient> => {
|
|||
|
||||
export {
|
||||
createResourceInstallerMock,
|
||||
createSummaryTransformInstallerMock,
|
||||
createTransformManagerMock,
|
||||
createSLORepositoryMock,
|
||||
createSummaryClientMock,
|
||||
createSummarySearchClientMock,
|
||||
createSLIClientMock,
|
||||
};
|
||||
|
|
|
@ -5,28 +5,31 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { IngestGetPipelineResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import {
|
||||
SLO_INGEST_PIPELINE_NAME,
|
||||
SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME,
|
||||
SLO_COMPONENT_TEMPLATE_SETTINGS_NAME,
|
||||
SLO_INDEX_TEMPLATE_NAME,
|
||||
SLO_INGEST_PIPELINE_NAME,
|
||||
SLO_RESOURCES_VERSION,
|
||||
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';
|
||||
|
||||
describe('resourceInstaller', () => {
|
||||
describe("when the common resources don't exist", () => {
|
||||
describe('when the common resources are not installed yet', () => {
|
||||
it('installs the common resources', async () => {
|
||||
const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient();
|
||||
mockClusterClient.indices.existsIndexTemplate.mockResponseOnce(false);
|
||||
mockClusterClient.indices.getIndexTemplate.mockResponseOnce({ index_templates: [] });
|
||||
const installer = new DefaultResourceInstaller(mockClusterClient, loggerMock.create());
|
||||
|
||||
await installer.ensureCommonResourcesInstalled();
|
||||
|
||||
expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(2);
|
||||
expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4);
|
||||
expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ name: SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME })
|
||||
|
@ -35,23 +38,71 @@ describe('resourceInstaller', () => {
|
|||
2,
|
||||
expect.objectContaining({ name: SLO_COMPONENT_TEMPLATE_SETTINGS_NAME })
|
||||
);
|
||||
expect(mockClusterClient.indices.putIndexTemplate).toHaveBeenCalledWith(
|
||||
expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect.objectContaining({ name: SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME })
|
||||
);
|
||||
expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
expect.objectContaining({ name: SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME })
|
||||
);
|
||||
expect(mockClusterClient.indices.putIndexTemplate).toHaveBeenCalledTimes(2);
|
||||
expect(mockClusterClient.indices.putIndexTemplate).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ name: SLO_INDEX_TEMPLATE_NAME })
|
||||
);
|
||||
expect(mockClusterClient.ingest.putPipeline).toHaveBeenCalledWith(
|
||||
expect(mockClusterClient.indices.putIndexTemplate).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({ name: SLO_SUMMARY_INDEX_TEMPLATE_NAME })
|
||||
);
|
||||
|
||||
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 })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the common resources exist', () => {
|
||||
it('does not install the common resources', async () => {
|
||||
describe('when the common resources are already installed', () => {
|
||||
it('skips the installation', async () => {
|
||||
const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient();
|
||||
mockClusterClient.indices.existsIndexTemplate.mockResponseOnce(true);
|
||||
mockClusterClient.indices.getIndexTemplate.mockResponseOnce({
|
||||
index_templates: [
|
||||
{
|
||||
name: SLO_INDEX_TEMPLATE_NAME,
|
||||
index_template: {
|
||||
index_patterns: [],
|
||||
composed_of: [],
|
||||
_meta: { version: SLO_RESOURCES_VERSION },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
mockClusterClient.indices.getIndexTemplate.mockResponseOnce({
|
||||
index_templates: [
|
||||
{
|
||||
name: SLO_SUMMARY_INDEX_TEMPLATE_NAME,
|
||||
index_template: {
|
||||
index_patterns: [],
|
||||
composed_of: [],
|
||||
_meta: { version: SLO_RESOURCES_VERSION },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
mockClusterClient.ingest.getPipeline.mockResponseOnce({
|
||||
// @ts-ignore _meta not typed properly
|
||||
[SLO_INGEST_PIPELINE_NAME]: { _meta: { version: SLO_RESOURCES_VERSION } },
|
||||
} as IngestGetPipelineResponse);
|
||||
});
|
||||
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();
|
||||
|
|
|
@ -11,18 +11,32 @@ import type {
|
|||
IngestPutPipelineRequest,
|
||||
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { ElasticsearchClient, Logger } from '@kbn/core/server';
|
||||
|
||||
import {
|
||||
SLO_INGEST_PIPELINE_NAME,
|
||||
SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME,
|
||||
SLO_COMPONENT_TEMPLATE_SETTINGS_NAME,
|
||||
SLO_INDEX_TEMPLATE_NAME,
|
||||
SLO_RESOURCES_VERSION,
|
||||
} from '../../assets/constants';
|
||||
import { getSLOMappingsTemplate } from '../../assets/component_templates/slo_mappings_template';
|
||||
import { getSLOSettingsTemplate } from '../../assets/component_templates/slo_settings_template';
|
||||
import { getSLOSummaryMappingsTemplate } from '../../assets/component_templates/slo_summary_mappings_template';
|
||||
import { getSLOSummarySettingsTemplate } from '../../assets/component_templates/slo_summary_settings_template';
|
||||
import {
|
||||
SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME,
|
||||
SLO_COMPONENT_TEMPLATE_SETTINGS_NAME,
|
||||
SLO_DESTINATION_INDEX_NAME,
|
||||
SLO_INDEX_TEMPLATE_NAME,
|
||||
SLO_INDEX_TEMPLATE_PATTERN,
|
||||
SLO_INGEST_PIPELINE_INDEX_NAME_PREFIX,
|
||||
SLO_INGEST_PIPELINE_NAME,
|
||||
SLO_RESOURCES_VERSION,
|
||||
SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME,
|
||||
SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME,
|
||||
SLO_SUMMARY_DESTINATION_INDEX_NAME,
|
||||
SLO_SUMMARY_INDEX_TEMPLATE_NAME,
|
||||
SLO_SUMMARY_INDEX_TEMPLATE_PATTERN,
|
||||
SLO_SUMMARY_INGEST_PIPELINE_NAME,
|
||||
SLO_SUMMARY_TEMP_INDEX_NAME,
|
||||
} from '../../assets/constants';
|
||||
import { getSLOIndexTemplate } from '../../assets/index_templates/slo_index_templates';
|
||||
import { getSLOSummaryIndexTemplate } from '../../assets/index_templates/slo_summary_index_templates';
|
||||
import { getSLOPipelineTemplate } from '../../assets/ingest_templates/slo_pipeline_template';
|
||||
import { getSLOSummaryPipelineTemplate } from '../../assets/ingest_templates/slo_summary_pipeline_template';
|
||||
import { retryTransientEsErrors } from '../../utils/retry';
|
||||
|
||||
export interface ResourceInstaller {
|
||||
ensureCommonResourcesInstalled(): Promise<void>;
|
||||
|
@ -35,13 +49,12 @@ export class DefaultResourceInstaller implements ResourceInstaller {
|
|||
const alreadyInstalled = await this.areResourcesAlreadyInstalled();
|
||||
|
||||
if (alreadyInstalled) {
|
||||
this.logger.debug(
|
||||
`Skipping installation of resources shared for SLO since they already exist`
|
||||
);
|
||||
this.logger.info('SLO resources already installed - skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.logger.info('Installing SLO shared resources');
|
||||
await Promise.all([
|
||||
this.createOrUpdateComponentTemplate(
|
||||
getSLOMappingsTemplate(SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME)
|
||||
|
@ -49,41 +62,90 @@ export class DefaultResourceInstaller implements ResourceInstaller {
|
|||
this.createOrUpdateComponentTemplate(
|
||||
getSLOSettingsTemplate(SLO_COMPONENT_TEMPLATE_SETTINGS_NAME)
|
||||
),
|
||||
this.createOrUpdateComponentTemplate(
|
||||
getSLOSummaryMappingsTemplate(SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME)
|
||||
),
|
||||
this.createOrUpdateComponentTemplate(
|
||||
getSLOSummarySettingsTemplate(SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME)
|
||||
),
|
||||
]);
|
||||
|
||||
await this.createOrUpdateIndexTemplate(
|
||||
getSLOIndexTemplate(SLO_INDEX_TEMPLATE_NAME, `${SLO_INDEX_TEMPLATE_NAME}-*`, [
|
||||
getSLOIndexTemplate(SLO_INDEX_TEMPLATE_NAME, SLO_INDEX_TEMPLATE_PATTERN, [
|
||||
SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME,
|
||||
SLO_COMPONENT_TEMPLATE_SETTINGS_NAME,
|
||||
])
|
||||
);
|
||||
|
||||
await this.createOrUpdateIngestPipelineTemplate(
|
||||
getSLOPipelineTemplate(
|
||||
SLO_INGEST_PIPELINE_NAME,
|
||||
this.getPipelinePrefix(SLO_RESOURCES_VERSION)
|
||||
await this.createOrUpdateIndexTemplate(
|
||||
getSLOSummaryIndexTemplate(
|
||||
SLO_SUMMARY_INDEX_TEMPLATE_NAME,
|
||||
SLO_SUMMARY_INDEX_TEMPLATE_PATTERN,
|
||||
[
|
||||
SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME,
|
||||
SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME,
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
await this.createIndex(SLO_DESTINATION_INDEX_NAME);
|
||||
await this.createIndex(SLO_SUMMARY_DESTINATION_INDEX_NAME);
|
||||
await this.createIndex(SLO_SUMMARY_TEMP_INDEX_NAME);
|
||||
|
||||
await this.createOrUpdateIngestPipelineTemplate(
|
||||
getSLOPipelineTemplate(SLO_INGEST_PIPELINE_NAME, SLO_INGEST_PIPELINE_INDEX_NAME_PREFIX)
|
||||
);
|
||||
|
||||
await this.createOrUpdateIngestPipelineTemplate(
|
||||
getSLOSummaryPipelineTemplate(SLO_SUMMARY_INGEST_PIPELINE_NAME)
|
||||
);
|
||||
} catch (err) {
|
||||
this.logger.error(`Error installing resources shared for SLO - ${err.message}`);
|
||||
this.logger.error(`Error installing resources shared for SLO: ${err.message}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private getPipelinePrefix(version: number): string {
|
||||
// Following https://www.elastic.co/blog/an-introduction-to-the-elastic-data-stream-naming-scheme
|
||||
// slo-observability.sli-<version>.<index-date>
|
||||
return `${SLO_INDEX_TEMPLATE_NAME}-v${version}.`;
|
||||
}
|
||||
|
||||
private async areResourcesAlreadyInstalled(): Promise<boolean> {
|
||||
const indexTemplateExists = await this.esClient.indices.existsIndexTemplate({
|
||||
name: SLO_INDEX_TEMPLATE_NAME,
|
||||
});
|
||||
let indexTemplateExists = false;
|
||||
try {
|
||||
const { index_templates: indexTemplates } = await this.execute(() =>
|
||||
this.esClient.indices.getIndexTemplate({
|
||||
name: SLO_INDEX_TEMPLATE_NAME,
|
||||
})
|
||||
);
|
||||
|
||||
const sloIndexTemplate = indexTemplates.find(
|
||||
(template) => template.name === SLO_INDEX_TEMPLATE_NAME
|
||||
);
|
||||
indexTemplateExists =
|
||||
!!sloIndexTemplate &&
|
||||
sloIndexTemplate.index_template._meta?.version === SLO_RESOURCES_VERSION;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let summaryIndexTemplateExists = false;
|
||||
try {
|
||||
const { index_templates: indexTemplates } = await this.execute(() =>
|
||||
this.esClient.indices.getIndexTemplate({
|
||||
name: SLO_SUMMARY_INDEX_TEMPLATE_NAME,
|
||||
})
|
||||
);
|
||||
const sloSummaryIndexTemplate = indexTemplates.find(
|
||||
(template) => template.name === SLO_SUMMARY_INDEX_TEMPLATE_NAME
|
||||
);
|
||||
summaryIndexTemplateExists =
|
||||
!!sloSummaryIndexTemplate &&
|
||||
sloSummaryIndexTemplate.index_template._meta?.version === SLO_RESOURCES_VERSION;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let ingestPipelineExists = false;
|
||||
try {
|
||||
const pipeline = await this.esClient.ingest.getPipeline({ id: SLO_INGEST_PIPELINE_NAME });
|
||||
const pipeline = await this.execute(() =>
|
||||
this.esClient.ingest.getPipeline({ id: SLO_INGEST_PIPELINE_NAME })
|
||||
);
|
||||
|
||||
ingestPipelineExists =
|
||||
// @ts-ignore _meta is not defined on the type
|
||||
|
@ -92,21 +154,54 @@ export class DefaultResourceInstaller implements ResourceInstaller {
|
|||
return false;
|
||||
}
|
||||
|
||||
return indexTemplateExists && 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) {
|
||||
this.logger.debug(`Installing SLO component template ${template.name}`);
|
||||
return this.esClient.cluster.putComponentTemplate(template);
|
||||
this.logger.info(`Installing SLO component template [${template.name}]`);
|
||||
return this.execute(() => this.esClient.cluster.putComponentTemplate(template));
|
||||
}
|
||||
|
||||
private async createOrUpdateIndexTemplate(template: IndicesPutIndexTemplateRequest) {
|
||||
this.logger.debug(`Installing SLO index template ${template.name}`);
|
||||
return this.esClient.indices.putIndexTemplate(template);
|
||||
this.logger.info(`Installing SLO index template [${template.name}]`);
|
||||
return this.execute(() => this.esClient.indices.putIndexTemplate(template));
|
||||
}
|
||||
|
||||
private async createOrUpdateIngestPipelineTemplate(template: IngestPutPipelineRequest) {
|
||||
this.logger.debug(`Installing SLO ingest pipeline template ${template.id}`);
|
||||
await this.esClient.ingest.putPipeline(template);
|
||||
this.logger.info(`Installing SLO ingest pipeline [${template.id}]`);
|
||||
return this.execute(() => this.esClient.ingest.putPipeline(template));
|
||||
}
|
||||
|
||||
private async createIndex(indexName: string) {
|
||||
try {
|
||||
await this.execute(() => this.esClient.indices.create({ index: indexName }));
|
||||
} catch (err) {
|
||||
if (err?.meta?.body?.error?.type !== 'resource_already_exists_exception') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async execute<T>(esCall: () => Promise<T>): Promise<T> {
|
||||
return await retryTransientEsErrors(esCall, { logger: this.logger });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,11 +13,15 @@ import {
|
|||
MsearchMultisearchBody,
|
||||
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { occurrencesBudgetingMethodSchema, timeslicesBudgetingMethodSchema } from '@kbn/slo-schema';
|
||||
import {
|
||||
occurrencesBudgetingMethodSchema,
|
||||
timeslicesBudgetingMethodSchema,
|
||||
toMomentUnitOfTime,
|
||||
} from '@kbn/slo-schema';
|
||||
import { assertNever } from '@kbn/std';
|
||||
import moment from 'moment';
|
||||
import { SLO_DESTINATION_INDEX_PATTERN } from '../../assets/constants';
|
||||
import { DateRange, Duration, IndicatorData, SLO } from '../../domain/models';
|
||||
import { toDateRange } from '../../domain/services/date_range';
|
||||
import { InternalQueryError } from '../../errors';
|
||||
|
||||
export interface SLIClient {
|
||||
|
@ -47,10 +51,7 @@ export class DefaultSLIClient implements SLIClient {
|
|||
a.duration.isShorterThan(b.duration) ? 1 : -1
|
||||
);
|
||||
const longestLookbackWindow = sortedLookbackWindows[0];
|
||||
const longestDateRange = toDateRange({
|
||||
duration: longestLookbackWindow.duration,
|
||||
type: 'rolling',
|
||||
});
|
||||
const longestDateRange = getLookbackDateRange(longestLookbackWindow.duration);
|
||||
|
||||
if (occurrencesBudgetingMethodSchema.is(slo.budgetingMethod)) {
|
||||
const result = await this.esClient.search<unknown, EsAggregations>({
|
||||
|
@ -179,3 +180,15 @@ function handleWindowedResult(
|
|||
|
||||
return indicatorDataPerLookbackWindow;
|
||||
}
|
||||
|
||||
function getLookbackDateRange(duration: Duration): { from: Date; to: Date } {
|
||||
const unit = toMomentUnitOfTime(duration.unit);
|
||||
const now = moment.utc().startOf('minute');
|
||||
const from = now.clone().subtract(duration.value, unit);
|
||||
const to = now.clone();
|
||||
|
||||
return {
|
||||
from: from.toDate(),
|
||||
to: to.toDate(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { MockedLogger } from '@kbn/logging-mocks';
|
||||
import { createResourceInstallerMock, createSummaryTransformInstallerMock } from './mocks';
|
||||
import { DefaultSLOInstaller } from './slo_installer';
|
||||
|
||||
describe('SLO Installer', () => {
|
||||
let loggerMock: jest.Mocked<MockedLogger>;
|
||||
|
||||
beforeEach(() => {
|
||||
loggerMock = loggingSystemMock.createLogger();
|
||||
});
|
||||
|
||||
it.skip('handles concurrent installation', async () => {
|
||||
const resourceInstaller = createResourceInstallerMock();
|
||||
const summaryTransformInstaller = createSummaryTransformInstallerMock();
|
||||
const service = new DefaultSLOInstaller(
|
||||
resourceInstaller,
|
||||
summaryTransformInstaller,
|
||||
loggerMock
|
||||
);
|
||||
|
||||
await Promise.all([service.install(), service.install()]);
|
||||
|
||||
expect(resourceInstaller.ensureCommonResourcesInstalled).toHaveBeenCalledTimes(1);
|
||||
expect(summaryTransformInstaller.installAndStart).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { ResourceInstaller, SummaryTransformInstaller } from '.';
|
||||
|
||||
export interface SLOInstaller {
|
||||
install(): Promise<void>;
|
||||
}
|
||||
|
||||
export class DefaultSLOInstaller implements SLOInstaller {
|
||||
private isInstalling: boolean = false;
|
||||
|
||||
constructor(
|
||||
private sloResourceInstaller: ResourceInstaller,
|
||||
private sloSummaryInstaller: SummaryTransformInstaller,
|
||||
private logger: Logger
|
||||
) {}
|
||||
|
||||
public async install() {
|
||||
if (this.isInstalling || process.env.CI) {
|
||||
return;
|
||||
}
|
||||
this.isInstalling = true;
|
||||
|
||||
let installTimeout;
|
||||
try {
|
||||
installTimeout = setTimeout(() => (this.isInstalling = false), 60000);
|
||||
|
||||
await this.sloResourceInstaller.ensureCommonResourcesInstalled();
|
||||
await this.sloSummaryInstaller.installAndStart();
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to install SLO common resources and summary transforms', {
|
||||
error,
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
this.isInstalling = false;
|
||||
clearTimeout(installTimeout);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ElasticsearchClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import {
|
||||
aHitFromSummaryIndex,
|
||||
aHitFromTempSummaryIndex,
|
||||
aSummaryDocument,
|
||||
} from './fixtures/summary_search_document';
|
||||
import {
|
||||
DefaultSummarySearchClient,
|
||||
Pagination,
|
||||
Sort,
|
||||
SummarySearchClient,
|
||||
} from './summary_search_client';
|
||||
|
||||
const defaultSort: Sort = {
|
||||
field: 'sli_value',
|
||||
direction: 'asc',
|
||||
};
|
||||
const defaultPagination: Pagination = {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
};
|
||||
|
||||
describe('Summary Search Client', () => {
|
||||
let esClientMock: ElasticsearchClientMock;
|
||||
let service: SummarySearchClient;
|
||||
|
||||
beforeEach(() => {
|
||||
esClientMock = elasticsearchServiceMock.createElasticsearchClient();
|
||||
service = new DefaultSummarySearchClient(esClientMock, loggerMock.create());
|
||||
});
|
||||
|
||||
it('returns an empty response on error', async () => {
|
||||
esClientMock.count.mockRejectedValue(new Error('Cannot reach es'));
|
||||
|
||||
await expect(service.search('', defaultSort, defaultPagination)).resolves
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"page": 1,
|
||||
"perPage": 20,
|
||||
"results": Array [],
|
||||
"total": 0,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns an empty response when the kql filter returns no document count', async () => {
|
||||
esClientMock.count.mockResolvedValue({
|
||||
count: 0,
|
||||
_shards: { failed: 0, successful: 1, total: 1 },
|
||||
});
|
||||
|
||||
await expect(service.search('', defaultSort, defaultPagination)).resolves
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"page": 1,
|
||||
"perPage": 20,
|
||||
"results": Array [],
|
||||
"total": 0,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns the summary documents without duplicate temporary summary documents', async () => {
|
||||
const SLO_ID1 = 'slo-one';
|
||||
const SLO_ID2 = 'slo_two';
|
||||
const SLO_ID3 = 'slo-three';
|
||||
const SLO_ID4 = 'slo-four';
|
||||
const SLO_ID5 = 'slo-five';
|
||||
esClientMock.count.mockResolvedValue({
|
||||
count: 8,
|
||||
_shards: { failed: 0, successful: 1, total: 1 },
|
||||
});
|
||||
esClientMock.search.mockResolvedValue({
|
||||
took: 0,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 2,
|
||||
successful: 2,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
},
|
||||
hits: {
|
||||
total: {
|
||||
value: 6,
|
||||
relation: 'eq',
|
||||
},
|
||||
max_score: 1,
|
||||
hits: [
|
||||
aHitFromSummaryIndex(aSummaryDocument({ id: SLO_ID1 })),
|
||||
aHitFromSummaryIndex(aSummaryDocument({ id: SLO_ID2 })),
|
||||
aHitFromSummaryIndex(aSummaryDocument({ id: SLO_ID3 })),
|
||||
aHitFromSummaryIndex(aSummaryDocument({ id: SLO_ID5 })), // no related temp doc
|
||||
aHitFromTempSummaryIndex(aSummaryDocument({ id: SLO_ID1, isTempDoc: true })), // removed as dup
|
||||
aHitFromTempSummaryIndex(aSummaryDocument({ id: SLO_ID2, isTempDoc: true })), // removed as dup
|
||||
aHitFromTempSummaryIndex(aSummaryDocument({ id: SLO_ID3, isTempDoc: true })), // removed as dup
|
||||
aHitFromTempSummaryIndex(aSummaryDocument({ id: SLO_ID4, isTempDoc: true })), // kept
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const results = await service.search('', defaultSort, defaultPagination);
|
||||
|
||||
expect(esClientMock.deleteByQuery).toHaveBeenCalled();
|
||||
expect(esClientMock.deleteByQuery.mock.calls[0]).toMatchSnapshot();
|
||||
expect(results).toMatchSnapshot();
|
||||
expect(results.total).toBe(5);
|
||||
});
|
||||
});
|
|
@ -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 { ElasticsearchClient, Logger } from '@kbn/core/server';
|
||||
import { assertNever } from '@kbn/std';
|
||||
import _ from 'lodash';
|
||||
import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../assets/constants';
|
||||
import { SLOId, Status, Summary } from '../../domain/models';
|
||||
import { toHighPrecision } from '../../utils/number';
|
||||
import { getElastichsearchQueryOrThrow } from './transform_generators';
|
||||
|
||||
interface EsSummaryDocument {
|
||||
slo: {
|
||||
id: string;
|
||||
revision: number;
|
||||
};
|
||||
sliValue: number;
|
||||
errorBudgetConsumed: number;
|
||||
errorBudgetRemaining: number;
|
||||
errorBudgetInitial: number;
|
||||
errorBudgetEstimated: boolean;
|
||||
statusCode: number;
|
||||
status: Status;
|
||||
isTempDoc: boolean;
|
||||
}
|
||||
|
||||
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 { count: total } = await this.esClient.count({
|
||||
index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
|
||||
query: getElastichsearchQueryOrThrow(kqlQuery),
|
||||
});
|
||||
|
||||
if (total === 0) {
|
||||
return { total: 0, perPage: pagination.perPage, page: pagination.page, results: [] };
|
||||
}
|
||||
|
||||
const summarySearch = await this.esClient.search<EsSummaryDocument>({
|
||||
index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
|
||||
query: getElastichsearchQueryOrThrow(kqlQuery),
|
||||
sort: {
|
||||
// non-temp first, then temp documents
|
||||
isTempDoc: {
|
||||
order: 'asc',
|
||||
},
|
||||
[toDocumentSortField(sort.field)]: {
|
||||
order: sort.direction,
|
||||
},
|
||||
},
|
||||
from: (pagination.page - 1) * pagination.perPage,
|
||||
size: pagination.perPage * 2, // twice as much as we return, in case they are all duplicate temp/non-temp summary
|
||||
});
|
||||
|
||||
const [tempSummaryDocuments, summaryDocuments] = _.partition(
|
||||
summarySearch.hits.hits,
|
||||
(doc) => !!doc._source?.isTempDoc
|
||||
);
|
||||
|
||||
// Always attempt to delete temporary summary documents with an existing non-temp summary document
|
||||
// The temp summary documents are _eventually_ removed as we get through the real summary documents
|
||||
const summarySloIds = summaryDocuments.map((doc) => doc._source?.slo.id);
|
||||
await this.esClient.deleteByQuery({
|
||||
index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
|
||||
wait_for_completion: false,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [{ terms: { 'slo.id': summarySloIds } }, { term: { isTempDoc: true } }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const tempSummaryDocumentsDeduped = tempSummaryDocuments.filter(
|
||||
(doc) => !summarySloIds.includes(doc._source?.slo.id)
|
||||
);
|
||||
|
||||
const finalResults = summaryDocuments
|
||||
.concat(tempSummaryDocumentsDeduped)
|
||||
.slice(0, pagination.perPage);
|
||||
|
||||
const finalTotal = total - (tempSummaryDocuments.length - tempSummaryDocumentsDeduped.length);
|
||||
return {
|
||||
total: finalTotal,
|
||||
perPage: pagination.perPage,
|
||||
page: pagination.page,
|
||||
results: finalResults.map((doc) => ({
|
||||
id: doc._source!.slo.id,
|
||||
summary: {
|
||||
errorBudget: {
|
||||
initial: toHighPrecision(doc._source!.errorBudgetInitial),
|
||||
consumed: toHighPrecision(doc._source!.errorBudgetConsumed),
|
||||
remaining: toHighPrecision(doc._source!.errorBudgetRemaining),
|
||||
isEstimated: doc._source!.errorBudgetEstimated,
|
||||
},
|
||||
sliValue: toHighPrecision(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);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SLO } from '../../../../domain/models';
|
||||
|
||||
export function createTempSummaryDocument(slo: SLO) {
|
||||
return {
|
||||
service: {
|
||||
environment: null,
|
||||
name: null,
|
||||
},
|
||||
transaction: {
|
||||
name: null,
|
||||
type: null,
|
||||
},
|
||||
slo: {
|
||||
indicator: {
|
||||
type: slo.indicator.type,
|
||||
},
|
||||
timeWindow: {
|
||||
duration: slo.timeWindow.duration.format(),
|
||||
type: slo.timeWindow.type,
|
||||
},
|
||||
instanceId: '*',
|
||||
name: slo.name,
|
||||
description: slo.description,
|
||||
id: slo.id,
|
||||
budgetingMethod: slo.budgetingMethod,
|
||||
revision: slo.revision,
|
||||
tags: slo.tags,
|
||||
},
|
||||
goodEvents: 0,
|
||||
totalEvents: 0,
|
||||
errorBudgetEstimated: false,
|
||||
errorBudgetRemaining: 1,
|
||||
errorBudgetConsumed: 0,
|
||||
errorBudgetInitial: 1 - slo.objective.target,
|
||||
sliValue: -1,
|
||||
statusCode: 0,
|
||||
status: 'NO_DATA',
|
||||
isTempDoc: true,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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,
|
||||
loggingSystemMock,
|
||||
} from '@kbn/core/server/mocks';
|
||||
import { MockedLogger } from '@kbn/logging-mocks';
|
||||
import { DefaultSummaryTransformInstaller } from './summary_transform_installer';
|
||||
import { ALL_TRANSFORM_TEMPLATES } from './templates';
|
||||
|
||||
describe('Summary Transform Installer', () => {
|
||||
let esClientMock: ElasticsearchClientMock;
|
||||
let loggerMock: jest.Mocked<MockedLogger>;
|
||||
|
||||
beforeEach(() => {
|
||||
esClientMock = elasticsearchServiceMock.createElasticsearchClient();
|
||||
loggerMock = loggingSystemMock.createLogger();
|
||||
});
|
||||
|
||||
it('skips the installation when latest version already installed', async () => {
|
||||
esClientMock.transform.getTransform.mockResolvedValue({
|
||||
count: ALL_TRANSFORM_TEMPLATES.length,
|
||||
// @ts-ignore
|
||||
transforms: ALL_TRANSFORM_TEMPLATES.map((transform) => ({
|
||||
id: transform.transform_id,
|
||||
_meta: transform._meta,
|
||||
})),
|
||||
});
|
||||
const installer = new DefaultSummaryTransformInstaller(esClientMock, loggerMock);
|
||||
|
||||
await installer.installAndStart();
|
||||
|
||||
expect(esClientMock.transform.stopTransform).not.toHaveBeenCalled();
|
||||
expect(esClientMock.transform.deleteTransform).not.toHaveBeenCalled();
|
||||
expect(esClientMock.transform.putTransform).not.toHaveBeenCalled();
|
||||
expect(esClientMock.transform.startTransform).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('installs every summary transforms when none are already installed', async () => {
|
||||
esClientMock.transform.getTransform.mockResolvedValue({ count: 0, transforms: [] });
|
||||
const installer = new DefaultSummaryTransformInstaller(esClientMock, loggerMock);
|
||||
|
||||
await installer.installAndStart();
|
||||
|
||||
const nbOfTransforms = ALL_TRANSFORM_TEMPLATES.length;
|
||||
|
||||
expect(esClientMock.transform.stopTransform).not.toHaveBeenCalled();
|
||||
expect(esClientMock.transform.deleteTransform).not.toHaveBeenCalled();
|
||||
expect(esClientMock.transform.putTransform).toHaveBeenCalledTimes(nbOfTransforms);
|
||||
expect(esClientMock.transform.startTransform).toHaveBeenCalledTimes(nbOfTransforms);
|
||||
});
|
||||
|
||||
it('desinstalls previous summary transforms prior to installing the new ones', async () => {
|
||||
esClientMock.transform.getTransform.mockResolvedValue({
|
||||
count: ALL_TRANSFORM_TEMPLATES.length,
|
||||
// @ts-ignore
|
||||
transforms: ALL_TRANSFORM_TEMPLATES.map((transform) => ({
|
||||
id: transform.transform_id,
|
||||
_meta: { ...transform._meta, version: -1 },
|
||||
})),
|
||||
});
|
||||
const installer = new DefaultSummaryTransformInstaller(esClientMock, loggerMock);
|
||||
|
||||
await installer.installAndStart();
|
||||
|
||||
const nbOfTransforms = ALL_TRANSFORM_TEMPLATES.length;
|
||||
|
||||
expect(esClientMock.transform.stopTransform).toHaveBeenCalledTimes(nbOfTransforms);
|
||||
expect(esClientMock.transform.deleteTransform).toHaveBeenCalledTimes(nbOfTransforms);
|
||||
expect(esClientMock.transform.putTransform).toHaveBeenCalledTimes(nbOfTransforms);
|
||||
expect(esClientMock.transform.startTransform).toHaveBeenCalledTimes(nbOfTransforms);
|
||||
});
|
||||
|
||||
it('installs only the missing summary transforms', async () => {
|
||||
const occurrencesSummaryTransforms = ALL_TRANSFORM_TEMPLATES.filter((transform) =>
|
||||
transform.transform_id.includes('-occurrences-')
|
||||
);
|
||||
esClientMock.transform.getTransform.mockResolvedValue({
|
||||
count: occurrencesSummaryTransforms.length,
|
||||
// @ts-ignore
|
||||
transforms: occurrencesSummaryTransforms.map((transform) => ({
|
||||
id: transform.transform_id,
|
||||
_meta: transform._meta,
|
||||
})),
|
||||
});
|
||||
const installer = new DefaultSummaryTransformInstaller(esClientMock, loggerMock);
|
||||
|
||||
await installer.installAndStart();
|
||||
|
||||
const nbOfTransforms = ALL_TRANSFORM_TEMPLATES.length - occurrencesSummaryTransforms.length;
|
||||
|
||||
expect(esClientMock.transform.stopTransform).not.toHaveBeenCalled();
|
||||
expect(esClientMock.transform.deleteTransform).not.toHaveBeenCalled();
|
||||
expect(esClientMock.transform.putTransform).toHaveBeenCalledTimes(nbOfTransforms);
|
||||
expect(esClientMock.transform.startTransform).toHaveBeenCalledTimes(nbOfTransforms);
|
||||
expect(esClientMock.transform.putTransform.mock.calls).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { ElasticsearchClient, Logger } from '@kbn/core/server';
|
||||
import {
|
||||
SLO_RESOURCES_VERSION,
|
||||
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
|
||||
} from '../../../assets/constants';
|
||||
import { retryTransientEsErrors } from '../../../utils/retry';
|
||||
import { ALL_TRANSFORM_TEMPLATES } from './templates';
|
||||
|
||||
export interface SummaryTransformInstaller {
|
||||
installAndStart(): Promise<void>;
|
||||
}
|
||||
|
||||
export class DefaultSummaryTransformInstaller implements SummaryTransformInstaller {
|
||||
constructor(private esClient: ElasticsearchClient, private logger: Logger) {}
|
||||
|
||||
public async installAndStart(): Promise<void> {
|
||||
const allTransformIds = ALL_TRANSFORM_TEMPLATES.map((transform) => transform.transform_id);
|
||||
const summaryTransforms = await this.execute(() =>
|
||||
this.esClient.transform.getTransform(
|
||||
{ transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}*`, allow_no_match: true },
|
||||
{ ignore: [404] }
|
||||
)
|
||||
);
|
||||
const alreadyInstalled =
|
||||
summaryTransforms.count === allTransformIds.length &&
|
||||
summaryTransforms.transforms.every(
|
||||
(transform) => transform._meta?.version === SLO_RESOURCES_VERSION
|
||||
) &&
|
||||
summaryTransforms.transforms.every((transform) => allTransformIds.includes(transform.id));
|
||||
|
||||
if (alreadyInstalled) {
|
||||
this.logger.info(`SLO summary transforms already installed - skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const transformTemplate of ALL_TRANSFORM_TEMPLATES) {
|
||||
const transformId = transformTemplate.transform_id;
|
||||
const transform = summaryTransforms.transforms.find((t) => t.id === transformId);
|
||||
|
||||
const transformAlreadyInstalled =
|
||||
!!transform && transform._meta?.version === SLO_RESOURCES_VERSION;
|
||||
const previousTransformAlreadyInstalled =
|
||||
!!transform && transform._meta?.version !== SLO_RESOURCES_VERSION;
|
||||
|
||||
if (transformAlreadyInstalled) {
|
||||
this.logger.info(`SLO summary transform [${transformId}] already installed - skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (previousTransformAlreadyInstalled) {
|
||||
await this.deletePreviousTransformVersion(transformId);
|
||||
}
|
||||
|
||||
await this.installTransform(transformId, transformTemplate);
|
||||
await this.startTransform(transformId);
|
||||
}
|
||||
|
||||
this.logger.info(`SLO summary transforms installed and started`);
|
||||
}
|
||||
|
||||
private async installTransform(
|
||||
transformId: string,
|
||||
transformTemplate: TransformPutTransformRequest
|
||||
) {
|
||||
this.logger.info(`Installing SLO summary transform [${transformId}]`);
|
||||
await this.execute(() =>
|
||||
this.esClient.transform.putTransform(transformTemplate, { ignore: [409] })
|
||||
);
|
||||
}
|
||||
|
||||
private async deletePreviousTransformVersion(transformId: string) {
|
||||
this.logger.info(`Deleting previous SLO summary transform [${transformId}]`);
|
||||
await this.execute(() =>
|
||||
this.esClient.transform.stopTransform(
|
||||
{ transform_id: transformId, allow_no_match: true, force: true },
|
||||
{ ignore: [409, 404] }
|
||||
)
|
||||
);
|
||||
await this.execute(() =>
|
||||
this.esClient.transform.deleteTransform(
|
||||
{ transform_id: transformId, force: true },
|
||||
{ ignore: [409, 404] }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async startTransform(transformId: string) {
|
||||
this.logger.info(`Starting SLO summary transform [${transformId}]`);
|
||||
await this.execute(() =>
|
||||
this.esClient.transform.startTransform({ transform_id: transformId }, { ignore: [409] })
|
||||
);
|
||||
}
|
||||
|
||||
private async execute<T>(esCall: () => Promise<T>): Promise<T> {
|
||||
return await retryTransientEsErrors(esCall, { logger: this.logger });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const groupBy = {
|
||||
'slo.id': {
|
||||
terms: {
|
||||
field: 'slo.id',
|
||||
},
|
||||
},
|
||||
'slo.revision': {
|
||||
terms: {
|
||||
field: 'slo.revision',
|
||||
},
|
||||
},
|
||||
'slo.instanceId': {
|
||||
terms: {
|
||||
field: 'slo.instanceId',
|
||||
},
|
||||
},
|
||||
'slo.name': {
|
||||
terms: {
|
||||
field: 'slo.name',
|
||||
},
|
||||
},
|
||||
'slo.description': {
|
||||
terms: {
|
||||
field: 'slo.description',
|
||||
},
|
||||
},
|
||||
'slo.tags': {
|
||||
terms: {
|
||||
field: 'slo.tags',
|
||||
},
|
||||
},
|
||||
'slo.indicator.type': {
|
||||
terms: {
|
||||
field: 'slo.indicator.type',
|
||||
},
|
||||
},
|
||||
'slo.budgetingMethod': {
|
||||
terms: {
|
||||
field: 'slo.budgetingMethod',
|
||||
},
|
||||
},
|
||||
'slo.timeWindow.duration': {
|
||||
terms: {
|
||||
field: 'slo.timeWindow.duration',
|
||||
},
|
||||
},
|
||||
'slo.timeWindow.type': {
|
||||
terms: {
|
||||
field: 'slo.timeWindow.type',
|
||||
},
|
||||
},
|
||||
errorBudgetEstimated: {
|
||||
terms: {
|
||||
field: 'errorBudgetEstimated',
|
||||
},
|
||||
},
|
||||
// Differentiate the temporary document from the summary one
|
||||
isTempDoc: {
|
||||
terms: {
|
||||
field: 'isTempDoc',
|
||||
},
|
||||
},
|
||||
// optional fields: only specified for APM indicators. Must include missing_bucket:true
|
||||
'service.name': {
|
||||
terms: {
|
||||
field: 'service.name',
|
||||
missing_bucket: true,
|
||||
},
|
||||
},
|
||||
'service.environment': {
|
||||
terms: {
|
||||
field: 'service.environment',
|
||||
missing_bucket: true,
|
||||
},
|
||||
},
|
||||
'transaction.name': {
|
||||
terms: {
|
||||
field: 'transaction.name',
|
||||
missing_bucket: true,
|
||||
},
|
||||
},
|
||||
'transaction.type': {
|
||||
terms: {
|
||||
field: 'transaction.type',
|
||||
missing_bucket: true,
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { SUMMARY_OCCURRENCES_7D_ROLLING } from './summary_occurrences_7d_rolling';
|
||||
import { SUMMARY_OCCURRENCES_30D_ROLLING } from './summary_occurrences_30d_rolling';
|
||||
import { SUMMARY_OCCURRENCES_90D_ROLLING } from './summary_occurrences_90d_rolling';
|
||||
import { SUMMARY_TIMESLICES_7D_ROLLING } from './summary_timeslices_7d_rolling';
|
||||
import { SUMMARY_TIMESLICES_30D_ROLLING } from './summary_timeslices_30d_rolling';
|
||||
import { SUMMARY_TIMESLICES_90D_ROLLING } from './summary_timeslices_90d_rolling';
|
||||
import { SUMMARY_OCCURRENCES_WEEKLY_ALIGNED } from './summary_occurrences_weekly_aligned';
|
||||
import { SUMMARY_OCCURRENCES_MONTHLY_ALIGNED } from './summary_occurrences_monthly_aligned';
|
||||
import { SUMMARY_TIMESLICES_WEEKLY_ALIGNED } from './summary_timeslices_weekly_aligned';
|
||||
import { SUMMARY_TIMESLICES_MONTHLY_ALIGNED } from './summary_timeslices_monthly_aligned';
|
||||
|
||||
export const ALL_TRANSFORM_TEMPLATES = [
|
||||
SUMMARY_OCCURRENCES_7D_ROLLING,
|
||||
SUMMARY_OCCURRENCES_30D_ROLLING,
|
||||
SUMMARY_OCCURRENCES_90D_ROLLING,
|
||||
SUMMARY_OCCURRENCES_WEEKLY_ALIGNED,
|
||||
SUMMARY_OCCURRENCES_MONTHLY_ALIGNED,
|
||||
SUMMARY_TIMESLICES_7D_ROLLING,
|
||||
SUMMARY_TIMESLICES_30D_ROLLING,
|
||||
SUMMARY_TIMESLICES_90D_ROLLING,
|
||||
SUMMARY_TIMESLICES_WEEKLY_ALIGNED,
|
||||
SUMMARY_TIMESLICES_MONTHLY_ALIGNED,
|
||||
];
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
import {
|
||||
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';
|
||||
|
||||
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)',
|
||||
},
|
||||
isTempDoc: {
|
||||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: 'now-30d/m',
|
||||
lte: 'now/m',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.budgetingMethod': 'occurrences',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.timeWindow.type': 'rolling',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.timeWindow.duration': '30d',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
pivot: {
|
||||
group_by: groupBy,
|
||||
aggregations: {
|
||||
goodEvents: {
|
||||
sum: {
|
||||
field: 'slo.numerator',
|
||||
},
|
||||
},
|
||||
totalEvents: {
|
||||
sum: {
|
||||
field: 'slo.denominator',
|
||||
},
|
||||
},
|
||||
_objectiveTarget: {
|
||||
max: {
|
||||
field: 'slo.objective.target',
|
||||
},
|
||||
},
|
||||
sliValue: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
goodEvents: 'goodEvents',
|
||||
totalEvents: 'totalEvents',
|
||||
},
|
||||
script:
|
||||
'if (params.totalEvents == 0) { return -1 } else { return params.goodEvents / params.totalEvents }',
|
||||
},
|
||||
},
|
||||
errorBudgetInitial: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
objectiveTarget: '_objectiveTarget',
|
||||
},
|
||||
script: '1 - params.objectiveTarget',
|
||||
},
|
||||
},
|
||||
errorBudgetConsumed: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
sliValue: 'sliValue',
|
||||
errorBudgetInitial: 'errorBudgetInitial',
|
||||
},
|
||||
script:
|
||||
'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }',
|
||||
},
|
||||
},
|
||||
errorBudgetRemaining: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
errorBudgetConsummed: 'errorBudgetConsumed',
|
||||
},
|
||||
script: '1 - params.errorBudgetConsummed',
|
||||
},
|
||||
},
|
||||
statusCode: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
sliValue: 'sliValue',
|
||||
objectiveTarget: '_objectiveTarget',
|
||||
errorBudgetRemaining: 'errorBudgetRemaining',
|
||||
},
|
||||
script: {
|
||||
source:
|
||||
'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objectiveTarget) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
description:
|
||||
'Summarize every SLO with occurrences budgeting method and a 30 days rolling time window',
|
||||
frequency: '1m',
|
||||
sync: {
|
||||
time: {
|
||||
field: '@timestamp',
|
||||
delay: '125s',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
deduce_mappings: false,
|
||||
max_page_search_size: 8000,
|
||||
},
|
||||
_meta: {
|
||||
version: SLO_RESOURCES_VERSION,
|
||||
managed: true,
|
||||
managed_by: 'observability',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
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';
|
||||
|
||||
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)',
|
||||
},
|
||||
isTempDoc: {
|
||||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: 'now-7d/m',
|
||||
lte: 'now/m',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.budgetingMethod': 'occurrences',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.timeWindow.type': 'rolling',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.timeWindow.duration': '7d',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
pivot: {
|
||||
group_by: groupBy,
|
||||
aggregations: {
|
||||
goodEvents: {
|
||||
sum: {
|
||||
field: 'slo.numerator',
|
||||
},
|
||||
},
|
||||
totalEvents: {
|
||||
sum: {
|
||||
field: 'slo.denominator',
|
||||
},
|
||||
},
|
||||
_objectiveTarget: {
|
||||
max: {
|
||||
field: 'slo.objective.target',
|
||||
},
|
||||
},
|
||||
sliValue: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
goodEvents: 'goodEvents',
|
||||
totalEvents: 'totalEvents',
|
||||
},
|
||||
script:
|
||||
'if (params.totalEvents == 0) { return -1 } else { return params.goodEvents / params.totalEvents }',
|
||||
},
|
||||
},
|
||||
errorBudgetInitial: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
objectiveTarget: '_objectiveTarget',
|
||||
},
|
||||
script: '1 - params.objectiveTarget',
|
||||
},
|
||||
},
|
||||
errorBudgetConsumed: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
sliValue: 'sliValue',
|
||||
errorBudgetInitial: 'errorBudgetInitial',
|
||||
},
|
||||
script:
|
||||
'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }',
|
||||
},
|
||||
},
|
||||
errorBudgetRemaining: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
errorBudgetConsummed: 'errorBudgetConsumed',
|
||||
},
|
||||
script: '1 - params.errorBudgetConsummed',
|
||||
},
|
||||
},
|
||||
statusCode: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
sliValue: 'sliValue',
|
||||
objectiveTarget: '_objectiveTarget',
|
||||
errorBudgetRemaining: 'errorBudgetRemaining',
|
||||
},
|
||||
script: {
|
||||
source:
|
||||
'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objectiveTarget) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
description:
|
||||
'Summarize every SLO with occurrences budgeting method and a 7 days rolling time window',
|
||||
frequency: '1m',
|
||||
sync: {
|
||||
time: {
|
||||
field: '@timestamp',
|
||||
delay: '125s',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
deduce_mappings: false,
|
||||
},
|
||||
_meta: {
|
||||
version: SLO_RESOURCES_VERSION,
|
||||
managed: true,
|
||||
managed_by: 'observability',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
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';
|
||||
|
||||
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)',
|
||||
},
|
||||
isTempDoc: {
|
||||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: 'now-90d/m',
|
||||
lte: 'now/m',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.budgetingMethod': 'occurrences',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.timeWindow.type': 'rolling',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.timeWindow.duration': '90d',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
pivot: {
|
||||
group_by: groupBy,
|
||||
aggregations: {
|
||||
goodEvents: {
|
||||
sum: {
|
||||
field: 'slo.numerator',
|
||||
},
|
||||
},
|
||||
totalEvents: {
|
||||
sum: {
|
||||
field: 'slo.denominator',
|
||||
},
|
||||
},
|
||||
_objectiveTarget: {
|
||||
max: {
|
||||
field: 'slo.objective.target',
|
||||
},
|
||||
},
|
||||
sliValue: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
goodEvents: 'goodEvents',
|
||||
totalEvents: 'totalEvents',
|
||||
},
|
||||
script:
|
||||
'if (params.totalEvents == 0) { return -1 } else { return params.goodEvents / params.totalEvents }',
|
||||
},
|
||||
},
|
||||
errorBudgetInitial: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
objectiveTarget: '_objectiveTarget',
|
||||
},
|
||||
script: '1 - params.objectiveTarget',
|
||||
},
|
||||
},
|
||||
errorBudgetConsumed: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
sliValue: 'sliValue',
|
||||
errorBudgetInitial: 'errorBudgetInitial',
|
||||
},
|
||||
script:
|
||||
'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }',
|
||||
},
|
||||
},
|
||||
errorBudgetRemaining: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
errorBudgetConsummed: 'errorBudgetConsumed',
|
||||
},
|
||||
script: '1 - params.errorBudgetConsummed',
|
||||
},
|
||||
},
|
||||
statusCode: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
sliValue: 'sliValue',
|
||||
objectiveTarget: '_objectiveTarget',
|
||||
errorBudgetRemaining: 'errorBudgetRemaining',
|
||||
},
|
||||
script: {
|
||||
source:
|
||||
'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objectiveTarget) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
description:
|
||||
'Summarize every SLO with occurrences budgeting method and a 90 days rolling time window',
|
||||
frequency: '1m',
|
||||
sync: {
|
||||
time: {
|
||||
field: '@timestamp',
|
||||
delay: '125s',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
deduce_mappings: false,
|
||||
},
|
||||
_meta: {
|
||||
version: SLO_RESOURCES_VERSION,
|
||||
managed: true,
|
||||
managed_by: 'observability',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
import {
|
||||
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';
|
||||
|
||||
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)',
|
||||
},
|
||||
isTempDoc: {
|
||||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: 'now/M',
|
||||
lte: 'now/m',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.budgetingMethod': 'occurrences',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.timeWindow.type': 'calendarAligned',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.timeWindow.duration': '1M',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
pivot: {
|
||||
group_by: groupBy,
|
||||
aggregations: {
|
||||
_objectiveTarget: {
|
||||
max: {
|
||||
field: 'slo.objective.target',
|
||||
},
|
||||
},
|
||||
goodEvents: {
|
||||
sum: {
|
||||
field: 'slo.numerator',
|
||||
},
|
||||
},
|
||||
totalEvents: {
|
||||
sum: {
|
||||
field: 'slo.denominator',
|
||||
},
|
||||
},
|
||||
sliValue: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
goodEvents: 'goodEvents',
|
||||
totalEvents: 'totalEvents',
|
||||
},
|
||||
script:
|
||||
'if (params.totalEvents == 0) { return -1 } else { return params.goodEvents / params.totalEvents }',
|
||||
},
|
||||
},
|
||||
errorBudgetInitial: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
objective: '_objectiveTarget',
|
||||
},
|
||||
script: '1 - params.objective',
|
||||
},
|
||||
},
|
||||
errorBudgetConsumed: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
sliValue: 'sliValue',
|
||||
errorBudgetInitial: 'errorBudgetInitial',
|
||||
},
|
||||
script:
|
||||
'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }',
|
||||
},
|
||||
},
|
||||
errorBudgetRemaining: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
errorBudgetConsumed: 'errorBudgetConsumed',
|
||||
},
|
||||
script: '1 - params.errorBudgetConsumed',
|
||||
},
|
||||
},
|
||||
statusCode: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
sliValue: 'sliValue',
|
||||
objective: '_objectiveTarget',
|
||||
errorBudgetRemaining: 'errorBudgetRemaining',
|
||||
},
|
||||
script:
|
||||
'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objective) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
description:
|
||||
'Summarize every SLO with occurrences budgeting method and a monthly calendar aligned time window',
|
||||
frequency: '1m',
|
||||
sync: {
|
||||
time: {
|
||||
field: '@timestamp',
|
||||
delay: '125s',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
deduce_mappings: false,
|
||||
},
|
||||
_meta: {
|
||||
version: SLO_RESOURCES_VERSION,
|
||||
managed: true,
|
||||
managed_by: 'observability',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
import {
|
||||
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';
|
||||
|
||||
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)',
|
||||
},
|
||||
isTempDoc: {
|
||||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: 'now/w',
|
||||
lte: 'now/m',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.budgetingMethod': 'occurrences',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.timeWindow.type': 'calendarAligned',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.timeWindow.duration': '1w',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
pivot: {
|
||||
group_by: groupBy,
|
||||
aggregations: {
|
||||
_objectiveTarget: {
|
||||
max: {
|
||||
field: 'slo.objective.target',
|
||||
},
|
||||
},
|
||||
goodEvents: {
|
||||
sum: {
|
||||
field: 'slo.numerator',
|
||||
},
|
||||
},
|
||||
totalEvents: {
|
||||
sum: {
|
||||
field: 'slo.denominator',
|
||||
},
|
||||
},
|
||||
sliValue: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
goodEvents: 'goodEvents',
|
||||
totalEvents: 'totalEvents',
|
||||
},
|
||||
script:
|
||||
'if (params.totalEvents == 0) { return -1 } else { return params.goodEvents / params.totalEvents }',
|
||||
},
|
||||
},
|
||||
errorBudgetInitial: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
objective: '_objectiveTarget',
|
||||
},
|
||||
script: '1 - params.objective',
|
||||
},
|
||||
},
|
||||
errorBudgetConsumed: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
sliValue: 'sliValue',
|
||||
errorBudgetInitial: 'errorBudgetInitial',
|
||||
},
|
||||
script:
|
||||
'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }',
|
||||
},
|
||||
},
|
||||
errorBudgetRemaining: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
errorBudgetConsumed: 'errorBudgetConsumed',
|
||||
},
|
||||
script: '1 - params.errorBudgetConsumed',
|
||||
},
|
||||
},
|
||||
statusCode: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
sliValue: 'sliValue',
|
||||
objective: '_objectiveTarget',
|
||||
errorBudgetRemaining: 'errorBudgetRemaining',
|
||||
},
|
||||
script:
|
||||
'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objective) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
description:
|
||||
'Summarize every SLO with occurrences budgeting method and a weekly calendar aligned time window',
|
||||
frequency: '1m',
|
||||
sync: {
|
||||
time: {
|
||||
field: '@timestamp',
|
||||
delay: '125s',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
deduce_mappings: false,
|
||||
},
|
||||
_meta: {
|
||||
version: SLO_RESOURCES_VERSION,
|
||||
managed: true,
|
||||
managed_by: 'observability',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
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';
|
||||
|
||||
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)',
|
||||
},
|
||||
isTempDoc: {
|
||||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: 'now-30d/m',
|
||||
lte: 'now/m',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.budgetingMethod': 'timeslices',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.timeWindow.type': 'rolling',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.timeWindow.duration': '30d',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
pivot: {
|
||||
group_by: groupBy,
|
||||
aggregations: {
|
||||
goodEvents: {
|
||||
sum: {
|
||||
field: 'slo.isGoodSlice',
|
||||
},
|
||||
},
|
||||
totalEvents: {
|
||||
value_count: {
|
||||
field: 'slo.isGoodSlice',
|
||||
},
|
||||
},
|
||||
_objectiveTarget: {
|
||||
max: {
|
||||
field: 'slo.objective.target',
|
||||
},
|
||||
},
|
||||
sliValue: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
goodEvents: 'goodEvents',
|
||||
totalEvents: 'totalEvents',
|
||||
},
|
||||
script:
|
||||
'if (params.totalEvents == 0) { return -1 } else { return params.goodEvents / params.totalEvents }',
|
||||
},
|
||||
},
|
||||
errorBudgetInitial: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
objectiveTarget: '_objectiveTarget',
|
||||
},
|
||||
script: '1 - params.objectiveTarget',
|
||||
},
|
||||
},
|
||||
errorBudgetConsumed: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
sliValue: 'sliValue',
|
||||
errorBudgetInitial: 'errorBudgetInitial',
|
||||
},
|
||||
script:
|
||||
'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }',
|
||||
},
|
||||
},
|
||||
errorBudgetRemaining: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
errorBudgetConsummed: 'errorBudgetConsumed',
|
||||
},
|
||||
script: '1 - params.errorBudgetConsummed',
|
||||
},
|
||||
},
|
||||
statusCode: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
sliValue: 'sliValue',
|
||||
objectiveTarget: '_objectiveTarget',
|
||||
errorBudgetRemaining: 'errorBudgetRemaining',
|
||||
},
|
||||
script: {
|
||||
source:
|
||||
'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objectiveTarget) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
description:
|
||||
'Summarize every SLO with timeslices budgeting method and a 30 days rolling time window',
|
||||
frequency: '1m',
|
||||
sync: {
|
||||
time: {
|
||||
field: '@timestamp',
|
||||
delay: '125s',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
deduce_mappings: false,
|
||||
},
|
||||
_meta: {
|
||||
version: SLO_RESOURCES_VERSION,
|
||||
managed: true,
|
||||
managed_by: 'observability',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
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';
|
||||
|
||||
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)',
|
||||
},
|
||||
isTempDoc: {
|
||||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: 'now-7d/m',
|
||||
lte: 'now/m',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.budgetingMethod': 'timeslices',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.timeWindow.type': 'rolling',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.timeWindow.duration': '7d',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
pivot: {
|
||||
group_by: groupBy,
|
||||
aggregations: {
|
||||
goodEvents: {
|
||||
sum: {
|
||||
field: 'slo.isGoodSlice',
|
||||
},
|
||||
},
|
||||
totalEvents: {
|
||||
value_count: {
|
||||
field: 'slo.isGoodSlice',
|
||||
},
|
||||
},
|
||||
_objectiveTarget: {
|
||||
max: {
|
||||
field: 'slo.objective.target',
|
||||
},
|
||||
},
|
||||
sliValue: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
goodEvents: 'goodEvents',
|
||||
totalEvents: 'totalEvents',
|
||||
},
|
||||
script:
|
||||
'if (params.totalEvents == 0) { return -1 } else { return params.goodEvents / params.totalEvents }',
|
||||
},
|
||||
},
|
||||
errorBudgetInitial: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
objectiveTarget: '_objectiveTarget',
|
||||
},
|
||||
script: '1 - params.objectiveTarget',
|
||||
},
|
||||
},
|
||||
errorBudgetConsumed: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
sliValue: 'sliValue',
|
||||
errorBudgetInitial: 'errorBudgetInitial',
|
||||
},
|
||||
script:
|
||||
'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }',
|
||||
},
|
||||
},
|
||||
errorBudgetRemaining: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
errorBudgetConsummed: 'errorBudgetConsumed',
|
||||
},
|
||||
script: '1 - params.errorBudgetConsummed',
|
||||
},
|
||||
},
|
||||
statusCode: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
sliValue: 'sliValue',
|
||||
objectiveTarget: '_objectiveTarget',
|
||||
errorBudgetRemaining: 'errorBudgetRemaining',
|
||||
},
|
||||
script: {
|
||||
source:
|
||||
'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objectiveTarget) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
description:
|
||||
'Summarize every SLO with timeslices budgeting method and a 7 days rolling time window',
|
||||
frequency: '1m',
|
||||
sync: {
|
||||
time: {
|
||||
field: '@timestamp',
|
||||
delay: '125s',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
deduce_mappings: false,
|
||||
},
|
||||
_meta: {
|
||||
version: SLO_RESOURCES_VERSION,
|
||||
managed: true,
|
||||
managed_by: 'observability',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
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';
|
||||
|
||||
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)',
|
||||
},
|
||||
isTempDoc: {
|
||||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: 'now-90d/m',
|
||||
lte: 'now/m',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.budgetingMethod': 'timeslices',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.timeWindow.type': 'rolling',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.timeWindow.duration': '90d',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
pivot: {
|
||||
group_by: groupBy,
|
||||
aggregations: {
|
||||
goodEvents: {
|
||||
sum: {
|
||||
field: 'slo.isGoodSlice',
|
||||
},
|
||||
},
|
||||
totalEvents: {
|
||||
value_count: {
|
||||
field: 'slo.isGoodSlice',
|
||||
},
|
||||
},
|
||||
_objectiveTarget: {
|
||||
max: {
|
||||
field: 'slo.objective.target',
|
||||
},
|
||||
},
|
||||
sliValue: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
goodEvents: 'goodEvents',
|
||||
totalEvents: 'totalEvents',
|
||||
},
|
||||
script:
|
||||
'if (params.totalEvents == 0) { return -1 } else { return params.goodEvents / params.totalEvents }',
|
||||
},
|
||||
},
|
||||
errorBudgetInitial: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
objectiveTarget: '_objectiveTarget',
|
||||
},
|
||||
script: '1 - params.objectiveTarget',
|
||||
},
|
||||
},
|
||||
errorBudgetConsumed: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
sliValue: 'sliValue',
|
||||
errorBudgetInitial: 'errorBudgetInitial',
|
||||
},
|
||||
script:
|
||||
'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }',
|
||||
},
|
||||
},
|
||||
errorBudgetRemaining: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
errorBudgetConsummed: 'errorBudgetConsumed',
|
||||
},
|
||||
script: '1 - params.errorBudgetConsummed',
|
||||
},
|
||||
},
|
||||
statusCode: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
sliValue: 'sliValue',
|
||||
objectiveTarget: '_objectiveTarget',
|
||||
errorBudgetRemaining: 'errorBudgetRemaining',
|
||||
},
|
||||
script: {
|
||||
source:
|
||||
'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objectiveTarget) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
description:
|
||||
'Summarize every SLO with timeslices budgeting method and a 90 days rolling time window',
|
||||
frequency: '1m',
|
||||
sync: {
|
||||
time: {
|
||||
field: '@timestamp',
|
||||
delay: '125s',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
deduce_mappings: false,
|
||||
},
|
||||
_meta: {
|
||||
version: SLO_RESOURCES_VERSION,
|
||||
managed: true,
|
||||
managed_by: 'observability',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
import {
|
||||
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';
|
||||
|
||||
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)',
|
||||
},
|
||||
isTempDoc: {
|
||||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: 'now/M',
|
||||
lte: 'now/m',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.budgetingMethod': 'timeslices',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.timeWindow.type': 'calendarAligned',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.timeWindow.duration': '1M',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
pivot: {
|
||||
group_by: groupBy,
|
||||
aggregations: {
|
||||
_sliceDurationInSeconds: {
|
||||
max: {
|
||||
field: 'slo.objective.sliceDurationInSeconds',
|
||||
},
|
||||
},
|
||||
_totalSlicesInPeriod: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
sliceDurationInSeconds: '_sliceDurationInSeconds',
|
||||
},
|
||||
script: {
|
||||
source: `
|
||||
Date d = new Date();
|
||||
Instant instant = Instant.ofEpochMilli(d.getTime());
|
||||
LocalDateTime now = LocalDateTime.ofInstant(instant, ZoneOffset.UTC);
|
||||
LocalDateTime startOfMonth = now
|
||||
.withDayOfMonth(1)
|
||||
.withHour(0)
|
||||
.withMinute(0)
|
||||
.withSecond(0);
|
||||
LocalDateTime startOfNextMonth = startOfMonth.plusMonths(1);
|
||||
double sliceDurationInMinutes = params.sliceDurationInSeconds / 60;
|
||||
|
||||
return Math.ceil(Duration.between(startOfMonth, startOfNextMonth).toMinutes() / sliceDurationInMinutes);
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
_objectiveTarget: {
|
||||
max: {
|
||||
field: 'slo.objective.target',
|
||||
},
|
||||
},
|
||||
goodEvents: {
|
||||
sum: {
|
||||
field: 'slo.isGoodSlice',
|
||||
},
|
||||
},
|
||||
totalEvents: {
|
||||
value_count: {
|
||||
field: 'slo.isGoodSlice',
|
||||
},
|
||||
},
|
||||
sliValue: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
goodEvents: 'goodEvents',
|
||||
totalEvents: 'totalEvents',
|
||||
},
|
||||
script:
|
||||
'if (params.totalEvents == 0) { return -1 } else { return params.goodEvents / params.totalEvents }',
|
||||
},
|
||||
},
|
||||
errorBudgetInitial: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
objective: '_objectiveTarget',
|
||||
},
|
||||
script: '1 - params.objective',
|
||||
},
|
||||
},
|
||||
errorBudgetConsumed: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
goodEvents: 'goodEvents',
|
||||
totalEvents: 'totalEvents',
|
||||
totalSlicesInPeriod: '_totalSlicesInPeriod',
|
||||
errorBudgetInitial: 'errorBudgetInitial',
|
||||
},
|
||||
script:
|
||||
'if (params.totalEvents == 0) { return 0 } else { return (params.totalEvents - params.goodEvents) / (params.totalSlicesInPeriod * params.errorBudgetInitial) }',
|
||||
},
|
||||
},
|
||||
errorBudgetRemaining: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
errorBudgetConsumed: 'errorBudgetConsumed',
|
||||
},
|
||||
script: '1 - params.errorBudgetConsumed',
|
||||
},
|
||||
},
|
||||
statusCode: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
sliValue: 'sliValue',
|
||||
objective: '_objectiveTarget',
|
||||
errorBudgetRemaining: 'errorBudgetRemaining',
|
||||
},
|
||||
script:
|
||||
'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objective) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
description:
|
||||
'Summarize every SLO with timeslices budgeting method and a monthly calendar aligned time window',
|
||||
frequency: '1m',
|
||||
sync: {
|
||||
time: {
|
||||
field: '@timestamp',
|
||||
delay: '125s',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
deduce_mappings: false,
|
||||
},
|
||||
_meta: {
|
||||
version: SLO_RESOURCES_VERSION,
|
||||
managed: true,
|
||||
managed_by: 'observability',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
import {
|
||||
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';
|
||||
|
||||
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)',
|
||||
},
|
||||
isTempDoc: {
|
||||
type: 'boolean',
|
||||
script: 'emit(false)',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: 'now/w',
|
||||
lte: 'now/m',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.budgetingMethod': 'timeslices',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.timeWindow.type': 'calendarAligned',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'slo.timeWindow.duration': '1w',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
pivot: {
|
||||
group_by: groupBy,
|
||||
aggregations: {
|
||||
_sliceDurationInSeconds: {
|
||||
max: {
|
||||
field: 'slo.objective.sliceDurationInSeconds',
|
||||
},
|
||||
},
|
||||
_totalSlicesInPeriod: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
sliceDurationInSeconds: '_sliceDurationInSeconds',
|
||||
},
|
||||
script: 'Math.ceil(7 * 24 * 60 * 60 / params.sliceDurationInSeconds)',
|
||||
},
|
||||
},
|
||||
_objectiveTarget: {
|
||||
max: {
|
||||
field: 'slo.objective.target',
|
||||
},
|
||||
},
|
||||
goodEvents: {
|
||||
sum: {
|
||||
field: 'slo.isGoodSlice',
|
||||
},
|
||||
},
|
||||
totalEvents: {
|
||||
value_count: {
|
||||
field: 'slo.isGoodSlice',
|
||||
},
|
||||
},
|
||||
sliValue: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
goodEvents: 'goodEvents',
|
||||
totalEvents: 'totalEvents',
|
||||
},
|
||||
script:
|
||||
'if (params.totalEvents == 0) { return -1 } else { return params.goodEvents / params.totalEvents }',
|
||||
},
|
||||
},
|
||||
errorBudgetInitial: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
objective: '_objectiveTarget',
|
||||
},
|
||||
script: '1 - params.objective',
|
||||
},
|
||||
},
|
||||
errorBudgetConsumed: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
goodEvents: 'goodEvents',
|
||||
totalEvents: 'totalEvents',
|
||||
totalSlicesInPeriod: '_totalSlicesInPeriod',
|
||||
errorBudgetInitial: 'errorBudgetInitial',
|
||||
},
|
||||
script:
|
||||
'if (params.totalEvents == 0) { return 0 } else { return (params.totalEvents - params.goodEvents) / (params.totalSlicesInPeriod * params.errorBudgetInitial) }',
|
||||
},
|
||||
},
|
||||
errorBudgetRemaining: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
errorBudgetConsumed: 'errorBudgetConsumed',
|
||||
},
|
||||
script: '1 - params.errorBudgetConsumed',
|
||||
},
|
||||
},
|
||||
statusCode: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
sliValue: 'sliValue',
|
||||
objective: '_objectiveTarget',
|
||||
errorBudgetRemaining: 'errorBudgetRemaining',
|
||||
},
|
||||
script:
|
||||
'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objective) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
description:
|
||||
'Summarize every SLO with timeslices budgeting method and a weekly calendar aligned time window',
|
||||
frequency: '1m',
|
||||
sync: {
|
||||
time: {
|
||||
field: '@timestamp',
|
||||
delay: '125s',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
deduce_mappings: false,
|
||||
},
|
||||
_meta: {
|
||||
version: SLO_RESOURCES_VERSION,
|
||||
managed: true,
|
||||
managed_by: 'observability',
|
||||
},
|
||||
};
|
|
@ -139,17 +139,453 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`APM Transaction Duration Transform Generator groups by the 'service.environment' 1`] = `
|
||||
Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"terms": Object {
|
||||
"processor.event": Array [
|
||||
"metric",
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"metricset.name": "transaction",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "transaction.duration.histogram",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
"gte": "now-7d",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"match": Object {
|
||||
"service.environment": "production",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`APM Transaction Duration Transform Generator groups by the 'service.environment' 2`] = `
|
||||
Object {
|
||||
"@timestamp": Object {
|
||||
"date_histogram": Object {
|
||||
"field": "@timestamp",
|
||||
"fixed_interval": "1m",
|
||||
},
|
||||
},
|
||||
"service.environment": Object {
|
||||
"terms": Object {
|
||||
"field": "service.environment",
|
||||
},
|
||||
},
|
||||
"slo.budgetingMethod": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.budgetingMethod",
|
||||
},
|
||||
},
|
||||
"slo.description": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
},
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.indicator.type",
|
||||
},
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.instanceId",
|
||||
},
|
||||
},
|
||||
"slo.name": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.name",
|
||||
},
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.target",
|
||||
},
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.revision",
|
||||
},
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.tags",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.duration",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.type",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`APM Transaction Duration Transform Generator groups by the 'service.name' 1`] = `
|
||||
Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"terms": Object {
|
||||
"processor.event": Array [
|
||||
"metric",
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"metricset.name": "transaction",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "transaction.duration.histogram",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
"gte": "now-7d",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"match": Object {
|
||||
"service.name": "my-service",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`APM Transaction Duration Transform Generator groups by the 'service.name' 2`] = `
|
||||
Object {
|
||||
"@timestamp": Object {
|
||||
"date_histogram": Object {
|
||||
"field": "@timestamp",
|
||||
"fixed_interval": "1m",
|
||||
},
|
||||
},
|
||||
"service.name": Object {
|
||||
"terms": Object {
|
||||
"field": "service.name",
|
||||
},
|
||||
},
|
||||
"slo.budgetingMethod": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.budgetingMethod",
|
||||
},
|
||||
},
|
||||
"slo.description": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
},
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.indicator.type",
|
||||
},
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.instanceId",
|
||||
},
|
||||
},
|
||||
"slo.name": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.name",
|
||||
},
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.target",
|
||||
},
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.revision",
|
||||
},
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.tags",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.duration",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.type",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`APM Transaction Duration Transform Generator groups by the 'transaction.name' 1`] = `
|
||||
Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"terms": Object {
|
||||
"processor.event": Array [
|
||||
"metric",
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"metricset.name": "transaction",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "transaction.duration.histogram",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
"gte": "now-7d",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"match": Object {
|
||||
"transaction.name": "GET /foo",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`APM Transaction Duration Transform Generator groups by the 'transaction.name' 2`] = `
|
||||
Object {
|
||||
"@timestamp": Object {
|
||||
"date_histogram": Object {
|
||||
"field": "@timestamp",
|
||||
"fixed_interval": "1m",
|
||||
},
|
||||
},
|
||||
"slo.budgetingMethod": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.budgetingMethod",
|
||||
},
|
||||
},
|
||||
"slo.description": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
},
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.indicator.type",
|
||||
},
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.instanceId",
|
||||
},
|
||||
},
|
||||
"slo.name": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.name",
|
||||
},
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.target",
|
||||
},
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.revision",
|
||||
},
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.tags",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.duration",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.type",
|
||||
},
|
||||
},
|
||||
"transaction.name": Object {
|
||||
"terms": Object {
|
||||
"field": "transaction.name",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`APM Transaction Duration Transform Generator groups by the 'transaction.type' 1`] = `
|
||||
Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"terms": Object {
|
||||
"processor.event": Array [
|
||||
"metric",
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"metricset.name": "transaction",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "transaction.duration.histogram",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
"gte": "now-7d",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"match": Object {
|
||||
"transaction.type": "request",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`APM Transaction Duration Transform Generator groups by the 'transaction.type' 2`] = `
|
||||
Object {
|
||||
"@timestamp": Object {
|
||||
"date_histogram": Object {
|
||||
"field": "@timestamp",
|
||||
"fixed_interval": "1m",
|
||||
},
|
||||
},
|
||||
"slo.budgetingMethod": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.budgetingMethod",
|
||||
},
|
||||
},
|
||||
"slo.description": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
},
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.indicator.type",
|
||||
},
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.instanceId",
|
||||
},
|
||||
},
|
||||
"slo.name": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.name",
|
||||
},
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.target",
|
||||
},
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.revision",
|
||||
},
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.tags",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.duration",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.type",
|
||||
},
|
||||
},
|
||||
"transaction.type": Object {
|
||||
"terms": Object {
|
||||
"field": "transaction.type",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`APM Transaction Duration Transform Generator returns the expected transform params for timeslices slo 1`] = `
|
||||
Object {
|
||||
"_meta": Object {
|
||||
"managed": true,
|
||||
"managed_by": "observability",
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
},
|
||||
"description": "Rolled-up SLI data for SLO: irrelevant",
|
||||
"dest": Object {
|
||||
"index": ".slo-observability.sli-v1",
|
||||
"pipeline": ".slo-observability.sli.monthly",
|
||||
"index": ".slo-observability.sli-v2",
|
||||
"pipeline": ".slo-observability.sli.pipeline",
|
||||
},
|
||||
"frequency": "1m",
|
||||
"pivot": Object {
|
||||
|
@ -194,16 +630,86 @@ Object {
|
|||
"fixed_interval": "2m",
|
||||
},
|
||||
},
|
||||
"service.environment": Object {
|
||||
"terms": Object {
|
||||
"field": "service.environment",
|
||||
},
|
||||
},
|
||||
"service.name": Object {
|
||||
"terms": Object {
|
||||
"field": "service.name",
|
||||
},
|
||||
},
|
||||
"slo.budgetingMethod": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.budgetingMethod",
|
||||
},
|
||||
},
|
||||
"slo.description": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
},
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.indicator.type",
|
||||
},
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.instanceId",
|
||||
},
|
||||
},
|
||||
"slo.name": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.name",
|
||||
},
|
||||
},
|
||||
"slo.objective.sliceDurationInSeconds": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.sliceDurationInSeconds",
|
||||
},
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.target",
|
||||
},
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.revision",
|
||||
},
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.tags",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.duration",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.type",
|
||||
},
|
||||
},
|
||||
"transaction.name": Object {
|
||||
"terms": Object {
|
||||
"field": "transaction.name",
|
||||
},
|
||||
},
|
||||
"transaction.type": Object {
|
||||
"terms": Object {
|
||||
"field": "transaction.type",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"settings": Object {
|
||||
|
@ -262,18 +768,78 @@ Object {
|
|||
},
|
||||
},
|
||||
"runtime_mappings": Object {
|
||||
"slo.budgetingMethod": Object {
|
||||
"script": Object {
|
||||
"source": "emit('timeslices')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.description": Object {
|
||||
"script": Object {
|
||||
"source": "emit('irrelevant')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.id": Object {
|
||||
"script": Object {
|
||||
"source": Any<String>,
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"script": Object {
|
||||
"source": "emit('sli.apm.transactionDuration')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"script": Object {
|
||||
"source": "emit('*')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.name": Object {
|
||||
"script": Object {
|
||||
"source": "emit('irrelevant')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.objective.sliceDurationInSeconds": Object {
|
||||
"script": Object {
|
||||
"source": "emit(120)",
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"script": Object {
|
||||
"source": "emit(0.98)",
|
||||
},
|
||||
"type": "double",
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"script": Object {
|
||||
"source": "emit(1)",
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"script": Object {
|
||||
"source": "emit('critical,k8s')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"script": Object {
|
||||
"source": "emit('7d')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"script": Object {
|
||||
"source": "emit('rolling')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
},
|
||||
},
|
||||
"sync": Object {
|
||||
|
@ -291,12 +857,12 @@ Object {
|
|||
"_meta": Object {
|
||||
"managed": true,
|
||||
"managed_by": "observability",
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
},
|
||||
"description": "Rolled-up SLI data for SLO: irrelevant",
|
||||
"dest": Object {
|
||||
"index": ".slo-observability.sli-v1",
|
||||
"pipeline": ".slo-observability.sli.monthly",
|
||||
"index": ".slo-observability.sli-v2",
|
||||
"pipeline": ".slo-observability.sli.pipeline",
|
||||
},
|
||||
"frequency": "1m",
|
||||
"pivot": Object {
|
||||
|
@ -332,16 +898,81 @@ Object {
|
|||
"fixed_interval": "1m",
|
||||
},
|
||||
},
|
||||
"service.environment": Object {
|
||||
"terms": Object {
|
||||
"field": "service.environment",
|
||||
},
|
||||
},
|
||||
"service.name": Object {
|
||||
"terms": Object {
|
||||
"field": "service.name",
|
||||
},
|
||||
},
|
||||
"slo.budgetingMethod": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.budgetingMethod",
|
||||
},
|
||||
},
|
||||
"slo.description": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
},
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.indicator.type",
|
||||
},
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.instanceId",
|
||||
},
|
||||
},
|
||||
"slo.name": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.name",
|
||||
},
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.target",
|
||||
},
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.revision",
|
||||
},
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.tags",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.duration",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.type",
|
||||
},
|
||||
},
|
||||
"transaction.name": Object {
|
||||
"terms": Object {
|
||||
"field": "transaction.name",
|
||||
},
|
||||
},
|
||||
"transaction.type": Object {
|
||||
"terms": Object {
|
||||
"field": "transaction.type",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"settings": Object {
|
||||
|
@ -400,18 +1031,72 @@ Object {
|
|||
},
|
||||
},
|
||||
"runtime_mappings": Object {
|
||||
"slo.budgetingMethod": Object {
|
||||
"script": Object {
|
||||
"source": "emit('occurrences')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.description": Object {
|
||||
"script": Object {
|
||||
"source": "emit('irrelevant')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.id": Object {
|
||||
"script": Object {
|
||||
"source": Any<String>,
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"script": Object {
|
||||
"source": "emit('sli.apm.transactionDuration')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"script": Object {
|
||||
"source": "emit('*')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.name": Object {
|
||||
"script": Object {
|
||||
"source": "emit('irrelevant')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"script": Object {
|
||||
"source": "emit(0.999)",
|
||||
},
|
||||
"type": "double",
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"script": Object {
|
||||
"source": "emit(1)",
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"script": Object {
|
||||
"source": "emit('critical,k8s')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"script": Object {
|
||||
"source": "emit('7d')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"script": Object {
|
||||
"source": "emit('rolling')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
},
|
||||
},
|
||||
"sync": Object {
|
||||
|
|
|
@ -131,17 +131,437 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`APM Transaction Error Rate Transform Generator groups by the 'service.environment' 1`] = `
|
||||
Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"term": Object {
|
||||
"metricset.name": "transaction",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"terms": Object {
|
||||
"event.outcome": Array [
|
||||
"success",
|
||||
"failure",
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
"gte": "now-7d",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"match": Object {
|
||||
"service.environment": "production",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`APM Transaction Error Rate Transform Generator groups by the 'service.environment' 2`] = `
|
||||
Object {
|
||||
"@timestamp": Object {
|
||||
"date_histogram": Object {
|
||||
"field": "@timestamp",
|
||||
"fixed_interval": "1m",
|
||||
},
|
||||
},
|
||||
"service.environment": Object {
|
||||
"terms": Object {
|
||||
"field": "service.environment",
|
||||
},
|
||||
},
|
||||
"slo.budgetingMethod": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.budgetingMethod",
|
||||
},
|
||||
},
|
||||
"slo.description": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
},
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.indicator.type",
|
||||
},
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.instanceId",
|
||||
},
|
||||
},
|
||||
"slo.name": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.name",
|
||||
},
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.target",
|
||||
},
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.revision",
|
||||
},
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.tags",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.duration",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.type",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`APM Transaction Error Rate Transform Generator groups by the 'service.name' 1`] = `
|
||||
Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"term": Object {
|
||||
"metricset.name": "transaction",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"terms": Object {
|
||||
"event.outcome": Array [
|
||||
"success",
|
||||
"failure",
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
"gte": "now-7d",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"match": Object {
|
||||
"service.name": "my-service",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`APM Transaction Error Rate Transform Generator groups by the 'service.name' 2`] = `
|
||||
Object {
|
||||
"@timestamp": Object {
|
||||
"date_histogram": Object {
|
||||
"field": "@timestamp",
|
||||
"fixed_interval": "1m",
|
||||
},
|
||||
},
|
||||
"service.name": Object {
|
||||
"terms": Object {
|
||||
"field": "service.name",
|
||||
},
|
||||
},
|
||||
"slo.budgetingMethod": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.budgetingMethod",
|
||||
},
|
||||
},
|
||||
"slo.description": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
},
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.indicator.type",
|
||||
},
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.instanceId",
|
||||
},
|
||||
},
|
||||
"slo.name": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.name",
|
||||
},
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.target",
|
||||
},
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.revision",
|
||||
},
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.tags",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.duration",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.type",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`APM Transaction Error Rate Transform Generator groups by the 'transaction.name' 1`] = `
|
||||
Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"term": Object {
|
||||
"metricset.name": "transaction",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"terms": Object {
|
||||
"event.outcome": Array [
|
||||
"success",
|
||||
"failure",
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
"gte": "now-7d",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"match": Object {
|
||||
"transaction.name": "GET /foo",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`APM Transaction Error Rate Transform Generator groups by the 'transaction.name' 2`] = `
|
||||
Object {
|
||||
"@timestamp": Object {
|
||||
"date_histogram": Object {
|
||||
"field": "@timestamp",
|
||||
"fixed_interval": "1m",
|
||||
},
|
||||
},
|
||||
"slo.budgetingMethod": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.budgetingMethod",
|
||||
},
|
||||
},
|
||||
"slo.description": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
},
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.indicator.type",
|
||||
},
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.instanceId",
|
||||
},
|
||||
},
|
||||
"slo.name": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.name",
|
||||
},
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.target",
|
||||
},
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.revision",
|
||||
},
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.tags",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.duration",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.type",
|
||||
},
|
||||
},
|
||||
"transaction.name": Object {
|
||||
"terms": Object {
|
||||
"field": "transaction.name",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`APM Transaction Error Rate Transform Generator groups by the 'transaction.type' 1`] = `
|
||||
Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"term": Object {
|
||||
"metricset.name": "transaction",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"terms": Object {
|
||||
"event.outcome": Array [
|
||||
"success",
|
||||
"failure",
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
"gte": "now-7d",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"match": Object {
|
||||
"transaction.type": "request",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`APM Transaction Error Rate Transform Generator groups by the 'transaction.type' 2`] = `
|
||||
Object {
|
||||
"@timestamp": Object {
|
||||
"date_histogram": Object {
|
||||
"field": "@timestamp",
|
||||
"fixed_interval": "1m",
|
||||
},
|
||||
},
|
||||
"slo.budgetingMethod": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.budgetingMethod",
|
||||
},
|
||||
},
|
||||
"slo.description": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
},
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.indicator.type",
|
||||
},
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.instanceId",
|
||||
},
|
||||
},
|
||||
"slo.name": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.name",
|
||||
},
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.target",
|
||||
},
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.revision",
|
||||
},
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.tags",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.duration",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.type",
|
||||
},
|
||||
},
|
||||
"transaction.type": Object {
|
||||
"terms": Object {
|
||||
"field": "transaction.type",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`APM Transaction Error Rate Transform Generator returns the expected transform params for timeslices slo 1`] = `
|
||||
Object {
|
||||
"_meta": Object {
|
||||
"managed": true,
|
||||
"managed_by": "observability",
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
},
|
||||
"description": "Rolled-up SLI data for SLO: irrelevant",
|
||||
"dest": Object {
|
||||
"index": ".slo-observability.sli-v1",
|
||||
"pipeline": ".slo-observability.sli.monthly",
|
||||
"index": ".slo-observability.sli-v2",
|
||||
"pipeline": ".slo-observability.sli.pipeline",
|
||||
},
|
||||
"frequency": "1m",
|
||||
"pivot": Object {
|
||||
|
@ -179,16 +599,86 @@ Object {
|
|||
"fixed_interval": "2m",
|
||||
},
|
||||
},
|
||||
"service.environment": Object {
|
||||
"terms": Object {
|
||||
"field": "service.environment",
|
||||
},
|
||||
},
|
||||
"service.name": Object {
|
||||
"terms": Object {
|
||||
"field": "service.name",
|
||||
},
|
||||
},
|
||||
"slo.budgetingMethod": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.budgetingMethod",
|
||||
},
|
||||
},
|
||||
"slo.description": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
},
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.indicator.type",
|
||||
},
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.instanceId",
|
||||
},
|
||||
},
|
||||
"slo.name": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.name",
|
||||
},
|
||||
},
|
||||
"slo.objective.sliceDurationInSeconds": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.sliceDurationInSeconds",
|
||||
},
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.target",
|
||||
},
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.revision",
|
||||
},
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.tags",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.duration",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.type",
|
||||
},
|
||||
},
|
||||
"transaction.name": Object {
|
||||
"terms": Object {
|
||||
"field": "transaction.name",
|
||||
},
|
||||
},
|
||||
"transaction.type": Object {
|
||||
"terms": Object {
|
||||
"field": "transaction.type",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"settings": Object {
|
||||
|
@ -243,18 +733,78 @@ Object {
|
|||
},
|
||||
},
|
||||
"runtime_mappings": Object {
|
||||
"slo.budgetingMethod": Object {
|
||||
"script": Object {
|
||||
"source": "emit('timeslices')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.description": Object {
|
||||
"script": Object {
|
||||
"source": "emit('irrelevant')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.id": Object {
|
||||
"script": Object {
|
||||
"source": Any<String>,
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"script": Object {
|
||||
"source": "emit('sli.apm.transactionErrorRate')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"script": Object {
|
||||
"source": "emit('*')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.name": Object {
|
||||
"script": Object {
|
||||
"source": "emit('irrelevant')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.objective.sliceDurationInSeconds": Object {
|
||||
"script": Object {
|
||||
"source": "emit(120)",
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"script": Object {
|
||||
"source": "emit(0.98)",
|
||||
},
|
||||
"type": "double",
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"script": Object {
|
||||
"source": "emit(1)",
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"script": Object {
|
||||
"source": "emit('critical,k8s')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"script": Object {
|
||||
"source": "emit('7d')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"script": Object {
|
||||
"source": "emit('rolling')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
},
|
||||
},
|
||||
"sync": Object {
|
||||
|
@ -272,12 +822,12 @@ Object {
|
|||
"_meta": Object {
|
||||
"managed": true,
|
||||
"managed_by": "observability",
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
},
|
||||
"description": "Rolled-up SLI data for SLO: irrelevant",
|
||||
"dest": Object {
|
||||
"index": ".slo-observability.sli-v1",
|
||||
"pipeline": ".slo-observability.sli.monthly",
|
||||
"index": ".slo-observability.sli-v2",
|
||||
"pipeline": ".slo-observability.sli.pipeline",
|
||||
},
|
||||
"frequency": "1m",
|
||||
"pivot": Object {
|
||||
|
@ -306,16 +856,81 @@ Object {
|
|||
"fixed_interval": "1m",
|
||||
},
|
||||
},
|
||||
"service.environment": Object {
|
||||
"terms": Object {
|
||||
"field": "service.environment",
|
||||
},
|
||||
},
|
||||
"service.name": Object {
|
||||
"terms": Object {
|
||||
"field": "service.name",
|
||||
},
|
||||
},
|
||||
"slo.budgetingMethod": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.budgetingMethod",
|
||||
},
|
||||
},
|
||||
"slo.description": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
},
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.indicator.type",
|
||||
},
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.instanceId",
|
||||
},
|
||||
},
|
||||
"slo.name": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.name",
|
||||
},
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.target",
|
||||
},
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.revision",
|
||||
},
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.tags",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.duration",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.type",
|
||||
},
|
||||
},
|
||||
"transaction.name": Object {
|
||||
"terms": Object {
|
||||
"field": "transaction.name",
|
||||
},
|
||||
},
|
||||
"transaction.type": Object {
|
||||
"terms": Object {
|
||||
"field": "transaction.type",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"settings": Object {
|
||||
|
@ -370,18 +985,72 @@ Object {
|
|||
},
|
||||
},
|
||||
"runtime_mappings": Object {
|
||||
"slo.budgetingMethod": Object {
|
||||
"script": Object {
|
||||
"source": "emit('occurrences')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.description": Object {
|
||||
"script": Object {
|
||||
"source": "emit('irrelevant')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.id": Object {
|
||||
"script": Object {
|
||||
"source": Any<String>,
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"script": Object {
|
||||
"source": "emit('sli.apm.transactionErrorRate')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"script": Object {
|
||||
"source": "emit('*')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.name": Object {
|
||||
"script": Object {
|
||||
"source": "emit('irrelevant')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"script": Object {
|
||||
"source": "emit(0.999)",
|
||||
},
|
||||
"type": "double",
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"script": Object {
|
||||
"source": "emit(1)",
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"script": Object {
|
||||
"source": "emit('critical,k8s')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"script": Object {
|
||||
"source": "emit('7d')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"script": Object {
|
||||
"source": "emit('rolling')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
},
|
||||
},
|
||||
"sync": Object {
|
||||
|
|
|
@ -64,12 +64,12 @@ Object {
|
|||
"_meta": Object {
|
||||
"managed": true,
|
||||
"managed_by": "observability",
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
},
|
||||
"description": "Rolled-up SLI data for SLO: irrelevant",
|
||||
"dest": Object {
|
||||
"index": ".slo-observability.sli-v1",
|
||||
"pipeline": ".slo-observability.sli.monthly",
|
||||
"index": ".slo-observability.sli-v2",
|
||||
"pipeline": ".slo-observability.sli.pipeline",
|
||||
},
|
||||
"frequency": "1m",
|
||||
"pivot": Object {
|
||||
|
@ -138,16 +138,66 @@ Object {
|
|||
"fixed_interval": "2m",
|
||||
},
|
||||
},
|
||||
"slo.budgetingMethod": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.budgetingMethod",
|
||||
},
|
||||
},
|
||||
"slo.description": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
},
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.indicator.type",
|
||||
},
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.instanceId",
|
||||
},
|
||||
},
|
||||
"slo.name": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.name",
|
||||
},
|
||||
},
|
||||
"slo.objective.sliceDurationInSeconds": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.sliceDurationInSeconds",
|
||||
},
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.target",
|
||||
},
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.revision",
|
||||
},
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.tags",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.duration",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.type",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"settings": Object {
|
||||
|
@ -168,18 +218,78 @@ Object {
|
|||
},
|
||||
},
|
||||
"runtime_mappings": Object {
|
||||
"slo.budgetingMethod": Object {
|
||||
"script": Object {
|
||||
"source": "emit('timeslices')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.description": Object {
|
||||
"script": Object {
|
||||
"source": "emit('irrelevant')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.id": Object {
|
||||
"script": Object {
|
||||
"source": Any<String>,
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"script": Object {
|
||||
"source": "emit('sli.histogram.custom')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"script": Object {
|
||||
"source": "emit('*')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.name": Object {
|
||||
"script": Object {
|
||||
"source": "emit('irrelevant')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.objective.sliceDurationInSeconds": Object {
|
||||
"script": Object {
|
||||
"source": "emit(120)",
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"script": Object {
|
||||
"source": "emit(0.98)",
|
||||
},
|
||||
"type": "double",
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"script": Object {
|
||||
"source": "emit(1)",
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"script": Object {
|
||||
"source": "emit('critical,k8s')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"script": Object {
|
||||
"source": "emit('7d')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"script": Object {
|
||||
"source": "emit('rolling')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
},
|
||||
},
|
||||
"sync": Object {
|
||||
|
@ -197,12 +307,12 @@ Object {
|
|||
"_meta": Object {
|
||||
"managed": true,
|
||||
"managed_by": "observability",
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
},
|
||||
"description": "Rolled-up SLI data for SLO: irrelevant",
|
||||
"dest": Object {
|
||||
"index": ".slo-observability.sli-v1",
|
||||
"pipeline": ".slo-observability.sli.monthly",
|
||||
"index": ".slo-observability.sli-v2",
|
||||
"pipeline": ".slo-observability.sli.pipeline",
|
||||
},
|
||||
"frequency": "1m",
|
||||
"pivot": Object {
|
||||
|
@ -262,16 +372,61 @@ Object {
|
|||
"fixed_interval": "1m",
|
||||
},
|
||||
},
|
||||
"slo.budgetingMethod": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.budgetingMethod",
|
||||
},
|
||||
},
|
||||
"slo.description": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
},
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.indicator.type",
|
||||
},
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.instanceId",
|
||||
},
|
||||
},
|
||||
"slo.name": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.name",
|
||||
},
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.target",
|
||||
},
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.revision",
|
||||
},
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.tags",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.duration",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.type",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"settings": Object {
|
||||
|
@ -292,18 +447,72 @@ Object {
|
|||
},
|
||||
},
|
||||
"runtime_mappings": Object {
|
||||
"slo.budgetingMethod": Object {
|
||||
"script": Object {
|
||||
"source": "emit('occurrences')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.description": Object {
|
||||
"script": Object {
|
||||
"source": "emit('irrelevant')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.id": Object {
|
||||
"script": Object {
|
||||
"source": Any<String>,
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"script": Object {
|
||||
"source": "emit('sli.histogram.custom')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"script": Object {
|
||||
"source": "emit('*')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.name": Object {
|
||||
"script": Object {
|
||||
"source": "emit('irrelevant')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"script": Object {
|
||||
"source": "emit(0.999)",
|
||||
},
|
||||
"type": "double",
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"script": Object {
|
||||
"source": "emit(1)",
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"script": Object {
|
||||
"source": "emit('critical,k8s')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"script": Object {
|
||||
"source": "emit('7d')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"script": Object {
|
||||
"source": "emit('rolling')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
},
|
||||
},
|
||||
"sync": Object {
|
||||
|
|
|
@ -105,12 +105,12 @@ Object {
|
|||
"_meta": Object {
|
||||
"managed": true,
|
||||
"managed_by": "observability",
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
},
|
||||
"description": "Rolled-up SLI data for SLO: irrelevant",
|
||||
"dest": Object {
|
||||
"index": ".slo-observability.sli-v1",
|
||||
"pipeline": ".slo-observability.sli.monthly",
|
||||
"index": ".slo-observability.sli-v2",
|
||||
"pipeline": ".slo-observability.sli.pipeline",
|
||||
},
|
||||
"frequency": "1m",
|
||||
"pivot": Object {
|
||||
|
@ -153,16 +153,66 @@ Object {
|
|||
"fixed_interval": "2m",
|
||||
},
|
||||
},
|
||||
"slo.budgetingMethod": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.budgetingMethod",
|
||||
},
|
||||
},
|
||||
"slo.description": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
},
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.indicator.type",
|
||||
},
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.instanceId",
|
||||
},
|
||||
},
|
||||
"slo.name": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.name",
|
||||
},
|
||||
},
|
||||
"slo.objective.sliceDurationInSeconds": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.sliceDurationInSeconds",
|
||||
},
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.target",
|
||||
},
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.revision",
|
||||
},
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.tags",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.duration",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.type",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"settings": Object {
|
||||
|
@ -183,18 +233,78 @@ Object {
|
|||
},
|
||||
},
|
||||
"runtime_mappings": Object {
|
||||
"slo.budgetingMethod": Object {
|
||||
"script": Object {
|
||||
"source": "emit('timeslices')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.description": Object {
|
||||
"script": Object {
|
||||
"source": "emit('irrelevant')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.id": Object {
|
||||
"script": Object {
|
||||
"source": Any<String>,
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"script": Object {
|
||||
"source": "emit('sli.kql.custom')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"script": Object {
|
||||
"source": "emit('*')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.name": Object {
|
||||
"script": Object {
|
||||
"source": "emit('irrelevant')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.objective.sliceDurationInSeconds": Object {
|
||||
"script": Object {
|
||||
"source": "emit(120)",
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"script": Object {
|
||||
"source": "emit(0.98)",
|
||||
},
|
||||
"type": "double",
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"script": Object {
|
||||
"source": "emit(1)",
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"script": Object {
|
||||
"source": "emit('critical,k8s')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"script": Object {
|
||||
"source": "emit('7d')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"script": Object {
|
||||
"source": "emit('rolling')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
},
|
||||
},
|
||||
"sync": Object {
|
||||
|
@ -212,12 +322,12 @@ Object {
|
|||
"_meta": Object {
|
||||
"managed": true,
|
||||
"managed_by": "observability",
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
},
|
||||
"description": "Rolled-up SLI data for SLO: irrelevant",
|
||||
"dest": Object {
|
||||
"index": ".slo-observability.sli-v1",
|
||||
"pipeline": ".slo-observability.sli.monthly",
|
||||
"index": ".slo-observability.sli-v2",
|
||||
"pipeline": ".slo-observability.sli.pipeline",
|
||||
},
|
||||
"frequency": "1m",
|
||||
"pivot": Object {
|
||||
|
@ -251,16 +361,61 @@ Object {
|
|||
"fixed_interval": "1m",
|
||||
},
|
||||
},
|
||||
"slo.budgetingMethod": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.budgetingMethod",
|
||||
},
|
||||
},
|
||||
"slo.description": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
},
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.indicator.type",
|
||||
},
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.instanceId",
|
||||
},
|
||||
},
|
||||
"slo.name": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.name",
|
||||
},
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.target",
|
||||
},
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.revision",
|
||||
},
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.tags",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.duration",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.type",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"settings": Object {
|
||||
|
@ -281,18 +436,72 @@ Object {
|
|||
},
|
||||
},
|
||||
"runtime_mappings": Object {
|
||||
"slo.budgetingMethod": Object {
|
||||
"script": Object {
|
||||
"source": "emit('occurrences')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.description": Object {
|
||||
"script": Object {
|
||||
"source": "emit('irrelevant')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.id": Object {
|
||||
"script": Object {
|
||||
"source": Any<String>,
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"script": Object {
|
||||
"source": "emit('sli.kql.custom')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"script": Object {
|
||||
"source": "emit('*')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.name": Object {
|
||||
"script": Object {
|
||||
"source": "emit('irrelevant')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"script": Object {
|
||||
"source": "emit(0.999)",
|
||||
},
|
||||
"type": "double",
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"script": Object {
|
||||
"source": "emit(1)",
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"script": Object {
|
||||
"source": "emit('critical,k8s')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"script": Object {
|
||||
"source": "emit('7d')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"script": Object {
|
||||
"source": "emit('rolling')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
},
|
||||
},
|
||||
"sync": Object {
|
||||
|
|
|
@ -76,12 +76,12 @@ Object {
|
|||
"_meta": Object {
|
||||
"managed": true,
|
||||
"managed_by": "observability",
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
},
|
||||
"description": "Rolled-up SLI data for SLO: irrelevant",
|
||||
"dest": Object {
|
||||
"index": ".slo-observability.sli-v1",
|
||||
"pipeline": ".slo-observability.sli.monthly",
|
||||
"index": ".slo-observability.sli-v2",
|
||||
"pipeline": ".slo-observability.sli.pipeline",
|
||||
},
|
||||
"frequency": "1m",
|
||||
"pivot": Object {
|
||||
|
@ -162,16 +162,66 @@ Object {
|
|||
"fixed_interval": "2m",
|
||||
},
|
||||
},
|
||||
"slo.budgetingMethod": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.budgetingMethod",
|
||||
},
|
||||
},
|
||||
"slo.description": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
},
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.indicator.type",
|
||||
},
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.instanceId",
|
||||
},
|
||||
},
|
||||
"slo.name": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.name",
|
||||
},
|
||||
},
|
||||
"slo.objective.sliceDurationInSeconds": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.sliceDurationInSeconds",
|
||||
},
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.target",
|
||||
},
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.revision",
|
||||
},
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.tags",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.duration",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.type",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"settings": Object {
|
||||
|
@ -192,18 +242,78 @@ Object {
|
|||
},
|
||||
},
|
||||
"runtime_mappings": Object {
|
||||
"slo.budgetingMethod": Object {
|
||||
"script": Object {
|
||||
"source": "emit('timeslices')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.description": Object {
|
||||
"script": Object {
|
||||
"source": "emit('irrelevant')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.id": Object {
|
||||
"script": Object {
|
||||
"source": Any<String>,
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"script": Object {
|
||||
"source": "emit('sli.metric.custom')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"script": Object {
|
||||
"source": "emit('*')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.name": Object {
|
||||
"script": Object {
|
||||
"source": "emit('irrelevant')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.objective.sliceDurationInSeconds": Object {
|
||||
"script": Object {
|
||||
"source": "emit(120)",
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"script": Object {
|
||||
"source": "emit(0.98)",
|
||||
},
|
||||
"type": "double",
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"script": Object {
|
||||
"source": "emit(1)",
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"script": Object {
|
||||
"source": "emit('critical,k8s')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"script": Object {
|
||||
"source": "emit('7d')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"script": Object {
|
||||
"source": "emit('rolling')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
},
|
||||
},
|
||||
"sync": Object {
|
||||
|
@ -221,12 +331,12 @@ Object {
|
|||
"_meta": Object {
|
||||
"managed": true,
|
||||
"managed_by": "observability",
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
},
|
||||
"description": "Rolled-up SLI data for SLO: irrelevant",
|
||||
"dest": Object {
|
||||
"index": ".slo-observability.sli-v1",
|
||||
"pipeline": ".slo-observability.sli.monthly",
|
||||
"index": ".slo-observability.sli-v2",
|
||||
"pipeline": ".slo-observability.sli.pipeline",
|
||||
},
|
||||
"frequency": "1m",
|
||||
"pivot": Object {
|
||||
|
@ -298,16 +408,61 @@ Object {
|
|||
"fixed_interval": "1m",
|
||||
},
|
||||
},
|
||||
"slo.budgetingMethod": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.budgetingMethod",
|
||||
},
|
||||
},
|
||||
"slo.description": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
},
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.indicator.type",
|
||||
},
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.instanceId",
|
||||
},
|
||||
},
|
||||
"slo.name": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.name",
|
||||
},
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.objective.target",
|
||||
},
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.revision",
|
||||
},
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.tags",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.duration",
|
||||
},
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.timeWindow.type",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"settings": Object {
|
||||
|
@ -328,18 +483,72 @@ Object {
|
|||
},
|
||||
},
|
||||
"runtime_mappings": Object {
|
||||
"slo.budgetingMethod": Object {
|
||||
"script": Object {
|
||||
"source": "emit('occurrences')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.description": Object {
|
||||
"script": Object {
|
||||
"source": "emit('irrelevant')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.id": Object {
|
||||
"script": Object {
|
||||
"source": Any<String>,
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.indicator.type": Object {
|
||||
"script": Object {
|
||||
"source": "emit('sli.metric.custom')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.instanceId": Object {
|
||||
"script": Object {
|
||||
"source": "emit('*')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.name": Object {
|
||||
"script": Object {
|
||||
"source": "emit('irrelevant')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.objective.target": Object {
|
||||
"script": Object {
|
||||
"source": "emit(0.999)",
|
||||
},
|
||||
"type": "double",
|
||||
},
|
||||
"slo.revision": Object {
|
||||
"script": Object {
|
||||
"source": "emit(1)",
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"slo.tags": Object {
|
||||
"script": Object {
|
||||
"source": "emit('critical,k8s')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.timeWindow.duration": Object {
|
||||
"script": Object {
|
||||
"source": "emit('7d')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.timeWindow.type": Object {
|
||||
"script": Object {
|
||||
"source": "emit('rolling')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
},
|
||||
},
|
||||
"sync": Object {
|
||||
|
|
|
@ -15,28 +15,28 @@ import { ApmTransactionDurationTransformGenerator } from './apm_transaction_dura
|
|||
const generator = new ApmTransactionDurationTransformGenerator();
|
||||
|
||||
describe('APM Transaction Duration Transform Generator', () => {
|
||||
it('returns the expected transform params with every specified indicator params', async () => {
|
||||
const anSLO = createSLO({ indicator: createAPMTransactionDurationIndicator() });
|
||||
const transform = generator.getTransformParams(anSLO);
|
||||
it('returns the expected transform params with every specified indicator params', () => {
|
||||
const slo = createSLO({ indicator: createAPMTransactionDurationIndicator() });
|
||||
const transform = generator.getTransformParams(slo);
|
||||
|
||||
expect(transform).toMatchSnapshot({
|
||||
transform_id: expect.any(String),
|
||||
source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } },
|
||||
});
|
||||
expect(transform.transform_id).toEqual(`slo-${anSLO.id}-${anSLO.revision}`);
|
||||
expect(transform.transform_id).toEqual(`slo-${slo.id}-${slo.revision}`);
|
||||
expect(transform.source.runtime_mappings!['slo.id']).toMatchObject({
|
||||
script: { source: `emit('${anSLO.id}')` },
|
||||
script: { source: `emit('${slo.id}')` },
|
||||
});
|
||||
expect(transform.source.runtime_mappings!['slo.revision']).toMatchObject({
|
||||
script: { source: `emit(${anSLO.revision})` },
|
||||
script: { source: `emit(${slo.revision})` },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the expected transform params for timeslices slo', async () => {
|
||||
const anSLO = createSLOWithTimeslicesBudgetingMethod({
|
||||
it('returns the expected transform params for timeslices slo', () => {
|
||||
const slo = createSLOWithTimeslicesBudgetingMethod({
|
||||
indicator: createAPMTransactionDurationIndicator(),
|
||||
});
|
||||
const transform = generator.getTransformParams(anSLO);
|
||||
const transform = generator.getTransformParams(slo);
|
||||
|
||||
expect(transform).toMatchSnapshot({
|
||||
transform_id: expect.any(String),
|
||||
|
@ -44,8 +44,8 @@ describe('APM Transaction Duration Transform Generator', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("does not include the query filter when params are '*'", async () => {
|
||||
const anSLO = createSLO({
|
||||
it("does not include the query filter when params are '*'", () => {
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionDurationIndicator({
|
||||
environment: '*',
|
||||
service: '*',
|
||||
|
@ -53,32 +53,96 @@ describe('APM Transaction Duration Transform Generator', () => {
|
|||
transactionType: '*',
|
||||
}),
|
||||
});
|
||||
const transform = generator.getTransformParams(anSLO);
|
||||
const transform = generator.getTransformParams(slo);
|
||||
|
||||
expect(transform.source.query).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('uses the provided index params as source index', async () => {
|
||||
it('uses the provided index params as source index', () => {
|
||||
const index = 'my-custom-apm-index*';
|
||||
const anSLO = createSLO({
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionDurationIndicator({
|
||||
index,
|
||||
}),
|
||||
});
|
||||
const transform = generator.getTransformParams(anSLO);
|
||||
const transform = generator.getTransformParams(slo);
|
||||
|
||||
expect(transform.source.index).toEqual(index);
|
||||
});
|
||||
|
||||
it('adds the custom kql filter to the query', async () => {
|
||||
it('adds the custom kql filter to the query', () => {
|
||||
const filter = `"my.field" : "value" and ("foo" >= 12 or "bar" <= 100)`;
|
||||
const anSLO = createSLO({
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionDurationIndicator({
|
||||
filter,
|
||||
}),
|
||||
});
|
||||
const transform = generator.getTransformParams(anSLO);
|
||||
const transform = generator.getTransformParams(slo);
|
||||
|
||||
expect(transform.source.query).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("groups by the 'service.name'", () => {
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionDurationIndicator({
|
||||
service: 'my-service',
|
||||
environment: '*',
|
||||
transactionName: '*',
|
||||
transactionType: '*',
|
||||
}),
|
||||
});
|
||||
|
||||
const transform = generator.getTransformParams(slo);
|
||||
|
||||
expect(transform.source.query).toMatchSnapshot();
|
||||
expect(transform.pivot?.group_by).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("groups by the 'service.environment'", () => {
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionDurationIndicator({
|
||||
service: '*',
|
||||
environment: 'production',
|
||||
transactionName: '*',
|
||||
transactionType: '*',
|
||||
}),
|
||||
});
|
||||
|
||||
const transform = generator.getTransformParams(slo);
|
||||
|
||||
expect(transform.source.query).toMatchSnapshot();
|
||||
expect(transform.pivot?.group_by).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("groups by the 'transaction.name'", () => {
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionDurationIndicator({
|
||||
service: '*',
|
||||
environment: '*',
|
||||
transactionName: 'GET /foo',
|
||||
transactionType: '*',
|
||||
}),
|
||||
});
|
||||
|
||||
const transform = generator.getTransformParams(slo);
|
||||
|
||||
expect(transform.source.query).toMatchSnapshot();
|
||||
expect(transform.pivot?.group_by).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("groups by the 'transaction.type'", () => {
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionDurationIndicator({
|
||||
service: '*',
|
||||
environment: '*',
|
||||
transactionName: '*',
|
||||
transactionType: 'request',
|
||||
}),
|
||||
});
|
||||
|
||||
const transform = generator.getTransformParams(slo);
|
||||
|
||||
expect(transform.source.query).toMatchSnapshot();
|
||||
expect(transform.pivot?.group_by).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,17 +11,17 @@ import {
|
|||
apmTransactionDurationIndicatorSchema,
|
||||
timeslicesBudgetingMethodSchema,
|
||||
} from '@kbn/slo-schema';
|
||||
import { InvalidTransformError } from '../../../errors';
|
||||
import { getElastichsearchQueryOrThrow, TransformGenerator } from '.';
|
||||
import {
|
||||
getSLOTransformId,
|
||||
SLO_DESTINATION_INDEX_NAME,
|
||||
SLO_INGEST_PIPELINE_NAME,
|
||||
getSLOTransformId,
|
||||
} from '../../../assets/constants';
|
||||
import { getSLOTransformTemplate } from '../../../assets/transform_templates/slo_transform_template';
|
||||
import { SLO, APMTransactionDurationIndicator } from '../../../domain/models';
|
||||
import { getElastichsearchQueryOrThrow, TransformGenerator } from '.';
|
||||
import { Query } from './types';
|
||||
import { APMTransactionDurationIndicator, SLO } from '../../../domain/models';
|
||||
import { InvalidTransformError } from '../../../errors';
|
||||
import { parseIndex } from './common';
|
||||
import { Query } from './types';
|
||||
|
||||
export class ApmTransactionDurationTransformGenerator extends TransformGenerator {
|
||||
public getTransformParams(slo: SLO): TransformPutTransformRequest {
|
||||
|
@ -34,7 +34,7 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator
|
|||
this.buildDescription(slo),
|
||||
this.buildSource(slo, slo.indicator),
|
||||
this.buildDestination(),
|
||||
this.buildGroupBy(slo),
|
||||
this.buildGroupBy(slo, slo.indicator),
|
||||
this.buildAggregations(slo, slo.indicator),
|
||||
this.buildSettings(slo)
|
||||
);
|
||||
|
@ -44,6 +44,29 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator
|
|||
return getSLOTransformId(slo.id, slo.revision);
|
||||
}
|
||||
|
||||
private buildGroupBy(slo: SLO, indicator: APMTransactionDurationIndicator) {
|
||||
// These groupBy fields must match the fields from the source query, otherwise
|
||||
// the transform will create permutations for each value present in the source.
|
||||
// E.g. if environment is not specified in the source query, but we include it in the groupBy,
|
||||
// we'll output documents for each environment value
|
||||
const extraGroupByFields = {
|
||||
...(indicator.params.service !== ALL_VALUE && {
|
||||
'service.name': { terms: { field: 'service.name' } },
|
||||
}),
|
||||
...(indicator.params.environment !== ALL_VALUE && {
|
||||
'service.environment': { terms: { field: 'service.environment' } },
|
||||
}),
|
||||
...(indicator.params.transactionName !== ALL_VALUE && {
|
||||
'transaction.name': { terms: { field: 'transaction.name' } },
|
||||
}),
|
||||
...(indicator.params.transactionType !== ALL_VALUE && {
|
||||
'transaction.type': { terms: { field: 'transaction.type' } },
|
||||
}),
|
||||
};
|
||||
|
||||
return this.buildCommonGroupBy(slo, '@timestamp', extraGroupByFields);
|
||||
}
|
||||
|
||||
private buildSource(slo: SLO, indicator: APMTransactionDurationIndicator) {
|
||||
const queryFilter: Query[] = [
|
||||
{
|
||||
|
@ -54,6 +77,7 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator
|
|||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (indicator.params.service !== ALL_VALUE) {
|
||||
queryFilter.push({
|
||||
match: {
|
||||
|
|
|
@ -16,27 +16,27 @@ const generator = new ApmTransactionErrorRateTransformGenerator();
|
|||
|
||||
describe('APM Transaction Error Rate Transform Generator', () => {
|
||||
it('returns the expected transform params with every specified indicator params', async () => {
|
||||
const anSLO = createSLO({ indicator: createAPMTransactionErrorRateIndicator() });
|
||||
const transform = generator.getTransformParams(anSLO);
|
||||
const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator() });
|
||||
const transform = generator.getTransformParams(slo);
|
||||
|
||||
expect(transform).toMatchSnapshot({
|
||||
transform_id: expect.any(String),
|
||||
source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } },
|
||||
});
|
||||
expect(transform.transform_id).toEqual(`slo-${anSLO.id}-${anSLO.revision}`);
|
||||
expect(transform.transform_id).toEqual(`slo-${slo.id}-${slo.revision}`);
|
||||
expect(transform.source.runtime_mappings!['slo.id']).toMatchObject({
|
||||
script: { source: `emit('${anSLO.id}')` },
|
||||
script: { source: `emit('${slo.id}')` },
|
||||
});
|
||||
expect(transform.source.runtime_mappings!['slo.revision']).toMatchObject({
|
||||
script: { source: `emit(${anSLO.revision})` },
|
||||
script: { source: `emit(${slo.revision})` },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the expected transform params for timeslices slo', async () => {
|
||||
const anSLO = createSLOWithTimeslicesBudgetingMethod({
|
||||
const slo = createSLOWithTimeslicesBudgetingMethod({
|
||||
indicator: createAPMTransactionErrorRateIndicator(),
|
||||
});
|
||||
const transform = generator.getTransformParams(anSLO);
|
||||
const transform = generator.getTransformParams(slo);
|
||||
|
||||
expect(transform).toMatchSnapshot({
|
||||
transform_id: expect.any(String),
|
||||
|
@ -45,7 +45,7 @@ describe('APM Transaction Error Rate Transform Generator', () => {
|
|||
});
|
||||
|
||||
it("does not include the query filter when params are '*'", async () => {
|
||||
const anSLO = createSLO({
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionErrorRateIndicator({
|
||||
environment: '*',
|
||||
service: '*',
|
||||
|
@ -53,32 +53,96 @@ describe('APM Transaction Error Rate Transform Generator', () => {
|
|||
transactionType: '*',
|
||||
}),
|
||||
});
|
||||
const transform = generator.getTransformParams(anSLO);
|
||||
const transform = generator.getTransformParams(slo);
|
||||
|
||||
expect(transform.source.query).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('uses the provided index params as source index', async () => {
|
||||
const index = 'my-custom-apm-index*';
|
||||
const anSLO = createSLO({
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionErrorRateIndicator({
|
||||
index,
|
||||
}),
|
||||
});
|
||||
const transform = generator.getTransformParams(anSLO);
|
||||
const transform = generator.getTransformParams(slo);
|
||||
|
||||
expect(transform.source.index).toEqual(index);
|
||||
});
|
||||
|
||||
it('adds the custom kql filter to the query', async () => {
|
||||
const filter = `"my.field" : "value" and ("foo" >= 12 or "bar" <= 100)`;
|
||||
const anSLO = createSLO({
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionErrorRateIndicator({
|
||||
filter,
|
||||
}),
|
||||
});
|
||||
const transform = generator.getTransformParams(anSLO);
|
||||
const transform = generator.getTransformParams(slo);
|
||||
|
||||
expect(transform.source.query).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("groups by the 'service.name'", () => {
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionErrorRateIndicator({
|
||||
service: 'my-service',
|
||||
environment: '*',
|
||||
transactionName: '*',
|
||||
transactionType: '*',
|
||||
}),
|
||||
});
|
||||
|
||||
const transform = generator.getTransformParams(slo);
|
||||
|
||||
expect(transform.source.query).toMatchSnapshot();
|
||||
expect(transform.pivot?.group_by).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("groups by the 'service.environment'", () => {
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionErrorRateIndicator({
|
||||
service: '*',
|
||||
environment: 'production',
|
||||
transactionName: '*',
|
||||
transactionType: '*',
|
||||
}),
|
||||
});
|
||||
|
||||
const transform = generator.getTransformParams(slo);
|
||||
|
||||
expect(transform.source.query).toMatchSnapshot();
|
||||
expect(transform.pivot?.group_by).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("groups by the 'transaction.name'", () => {
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionErrorRateIndicator({
|
||||
service: '*',
|
||||
environment: '*',
|
||||
transactionName: 'GET /foo',
|
||||
transactionType: '*',
|
||||
}),
|
||||
});
|
||||
|
||||
const transform = generator.getTransformParams(slo);
|
||||
|
||||
expect(transform.source.query).toMatchSnapshot();
|
||||
expect(transform.pivot?.group_by).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("groups by the 'transaction.type'", () => {
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionErrorRateIndicator({
|
||||
service: '*',
|
||||
environment: '*',
|
||||
transactionName: '*',
|
||||
transactionType: 'request',
|
||||
}),
|
||||
});
|
||||
|
||||
const transform = generator.getTransformParams(slo);
|
||||
|
||||
expect(transform.source.query).toMatchSnapshot();
|
||||
expect(transform.pivot?.group_by).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,18 +11,17 @@ import {
|
|||
apmTransactionErrorRateIndicatorSchema,
|
||||
timeslicesBudgetingMethodSchema,
|
||||
} from '@kbn/slo-schema';
|
||||
|
||||
import { InvalidTransformError } from '../../../errors';
|
||||
import { getSLOTransformTemplate } from '../../../assets/transform_templates/slo_transform_template';
|
||||
import { getElastichsearchQueryOrThrow, TransformGenerator } from '.';
|
||||
import {
|
||||
getSLOTransformId,
|
||||
SLO_DESTINATION_INDEX_NAME,
|
||||
SLO_INGEST_PIPELINE_NAME,
|
||||
getSLOTransformId,
|
||||
} from '../../../assets/constants';
|
||||
import { getSLOTransformTemplate } from '../../../assets/transform_templates/slo_transform_template';
|
||||
import { APMTransactionErrorRateIndicator, SLO } from '../../../domain/models';
|
||||
import { Query } from './types';
|
||||
import { InvalidTransformError } from '../../../errors';
|
||||
import { parseIndex } from './common';
|
||||
import { Query } from './types';
|
||||
|
||||
export class ApmTransactionErrorRateTransformGenerator extends TransformGenerator {
|
||||
public getTransformParams(slo: SLO): TransformPutTransformRequest {
|
||||
|
@ -35,7 +34,7 @@ export class ApmTransactionErrorRateTransformGenerator extends TransformGenerato
|
|||
this.buildDescription(slo),
|
||||
this.buildSource(slo, slo.indicator),
|
||||
this.buildDestination(),
|
||||
this.buildGroupBy(slo),
|
||||
this.buildGroupBy(slo, slo.indicator),
|
||||
this.buildAggregations(slo),
|
||||
this.buildSettings(slo)
|
||||
);
|
||||
|
@ -45,6 +44,29 @@ export class ApmTransactionErrorRateTransformGenerator extends TransformGenerato
|
|||
return getSLOTransformId(slo.id, slo.revision);
|
||||
}
|
||||
|
||||
private buildGroupBy(slo: SLO, indicator: APMTransactionErrorRateIndicator) {
|
||||
// These groupBy fields must match the fields from the source query, otherwise
|
||||
// the transform will create permutations for each value present in the source.
|
||||
// E.g. if environment is not specified in the source query, but we include it in the groupBy,
|
||||
// we'll output documents for each environment value
|
||||
const extraGroupByFields = {
|
||||
...(indicator.params.service !== ALL_VALUE && {
|
||||
'service.name': { terms: { field: 'service.name' } },
|
||||
}),
|
||||
...(indicator.params.environment !== ALL_VALUE && {
|
||||
'service.environment': { terms: { field: 'service.environment' } },
|
||||
}),
|
||||
...(indicator.params.transactionName !== ALL_VALUE && {
|
||||
'transaction.name': { terms: { field: 'transaction.name' } },
|
||||
}),
|
||||
...(indicator.params.transactionType !== ALL_VALUE && {
|
||||
'transaction.type': { terms: { field: 'transaction.type' } },
|
||||
}),
|
||||
};
|
||||
|
||||
return this.buildCommonGroupBy(slo, '@timestamp', extraGroupByFields);
|
||||
}
|
||||
|
||||
private buildSource(slo: SLO, indicator: APMTransactionErrorRateIndicator) {
|
||||
const queryFilter: Query[] = [
|
||||
{
|
||||
|
|
|
@ -34,7 +34,7 @@ export class HistogramTransformGenerator extends TransformGenerator {
|
|||
this.buildDescription(slo),
|
||||
this.buildSource(slo, slo.indicator),
|
||||
this.buildDestination(),
|
||||
this.buildGroupBy(slo, slo.indicator.params.timestampField),
|
||||
this.buildCommonGroupBy(slo, slo.indicator.params.timestampField),
|
||||
this.buildAggregations(slo, slo.indicator),
|
||||
this.buildSettings(slo, slo.indicator.params.timestampField)
|
||||
);
|
||||
|
|
|
@ -29,7 +29,7 @@ export class KQLCustomTransformGenerator extends TransformGenerator {
|
|||
this.buildDescription(slo),
|
||||
this.buildSource(slo, slo.indicator),
|
||||
this.buildDestination(),
|
||||
this.buildGroupBy(slo, slo.indicator.params.timestampField),
|
||||
this.buildCommonGroupBy(slo, slo.indicator.params.timestampField),
|
||||
this.buildAggregations(slo, slo.indicator),
|
||||
this.buildSettings(slo, slo.indicator.params.timestampField)
|
||||
);
|
||||
|
|
|
@ -32,7 +32,7 @@ export class MetricCustomTransformGenerator extends TransformGenerator {
|
|||
this.buildDescription(slo),
|
||||
this.buildSource(slo, slo.indicator),
|
||||
this.buildDestination(),
|
||||
this.buildGroupBy(slo, slo.indicator.params.timestampField),
|
||||
this.buildCommonGroupBy(slo, slo.indicator.params.timestampField),
|
||||
this.buildAggregations(slo, slo.indicator),
|
||||
this.buildSettings(slo, slo.indicator.params.timestampField)
|
||||
);
|
||||
|
|
|
@ -5,30 +5,93 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { MappingRuntimeFieldType } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { timeslicesBudgetingMethodSchema } from '@kbn/slo-schema';
|
||||
|
||||
import {
|
||||
MappingRuntimeFields,
|
||||
TransformPutTransformRequest,
|
||||
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { ALL_VALUE, timeslicesBudgetingMethodSchema } from '@kbn/slo-schema';
|
||||
import { TransformSettings } from '../../../assets/transform_templates/slo_transform_template';
|
||||
import { SLO } from '../../../domain/models';
|
||||
|
||||
export abstract class TransformGenerator {
|
||||
public abstract getTransformParams(slo: SLO): TransformPutTransformRequest;
|
||||
|
||||
public buildCommonRuntimeMappings(slo: SLO) {
|
||||
public buildCommonRuntimeMappings(slo: SLO): MappingRuntimeFields {
|
||||
return {
|
||||
'slo.id': {
|
||||
type: 'keyword' as MappingRuntimeFieldType,
|
||||
type: 'keyword',
|
||||
script: {
|
||||
source: `emit('${slo.id}')`,
|
||||
},
|
||||
},
|
||||
'slo.revision': {
|
||||
type: 'long' as MappingRuntimeFieldType,
|
||||
type: 'long',
|
||||
script: {
|
||||
source: `emit(${slo.revision})`,
|
||||
},
|
||||
},
|
||||
'slo.instanceId': {
|
||||
type: 'keyword',
|
||||
script: {
|
||||
source: `emit('${ALL_VALUE}')`,
|
||||
},
|
||||
},
|
||||
'slo.name': {
|
||||
type: 'keyword',
|
||||
script: {
|
||||
source: `emit('${slo.name}')`,
|
||||
},
|
||||
},
|
||||
'slo.description': {
|
||||
type: 'keyword',
|
||||
script: {
|
||||
source: `emit('${slo.description}')`,
|
||||
},
|
||||
},
|
||||
'slo.tags': {
|
||||
type: 'keyword',
|
||||
script: {
|
||||
source: `emit('${slo.tags}')`,
|
||||
},
|
||||
},
|
||||
'slo.indicator.type': {
|
||||
type: 'keyword',
|
||||
script: {
|
||||
source: `emit('${slo.indicator.type}')`,
|
||||
},
|
||||
},
|
||||
'slo.objective.target': {
|
||||
type: 'double',
|
||||
script: {
|
||||
source: `emit(${slo.objective.target})`,
|
||||
},
|
||||
},
|
||||
...(slo.objective.timesliceWindow && {
|
||||
'slo.objective.sliceDurationInSeconds': {
|
||||
type: 'long',
|
||||
script: {
|
||||
source: `emit(${slo.objective.timesliceWindow!.asSeconds()})`,
|
||||
},
|
||||
},
|
||||
}),
|
||||
'slo.budgetingMethod': {
|
||||
type: 'keyword',
|
||||
script: {
|
||||
source: `emit('${slo.budgetingMethod}')`,
|
||||
},
|
||||
},
|
||||
'slo.timeWindow.duration': {
|
||||
type: 'keyword',
|
||||
script: {
|
||||
source: `emit('${slo.timeWindow.duration.format()}')`,
|
||||
},
|
||||
},
|
||||
'slo.timeWindow.type': {
|
||||
type: 'keyword',
|
||||
script: {
|
||||
source: `emit('${slo.timeWindow.type}')`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -36,24 +99,35 @@ export abstract class TransformGenerator {
|
|||
return `Rolled-up SLI data for SLO: ${slo.name}`;
|
||||
}
|
||||
|
||||
public buildGroupBy(slo: SLO, sourceIndexTimestampField: string | undefined = '@timestamp') {
|
||||
public buildCommonGroupBy(
|
||||
slo: SLO,
|
||||
sourceIndexTimestampField: string | undefined = '@timestamp',
|
||||
extraGroupByFields = {}
|
||||
) {
|
||||
let fixedInterval = '1m';
|
||||
if (timeslicesBudgetingMethodSchema.is(slo.budgetingMethod)) {
|
||||
fixedInterval = slo.objective.timesliceWindow!.format();
|
||||
}
|
||||
|
||||
return {
|
||||
'slo.id': {
|
||||
terms: {
|
||||
field: 'slo.id',
|
||||
'slo.id': { terms: { field: 'slo.id' } },
|
||||
'slo.revision': { terms: { field: 'slo.revision' } },
|
||||
'slo.instanceId': { terms: { field: 'slo.instanceId' } },
|
||||
'slo.name': { terms: { field: 'slo.name' } },
|
||||
'slo.description': { terms: { field: 'slo.description' } },
|
||||
'slo.tags': { terms: { field: 'slo.tags' } },
|
||||
'slo.indicator.type': { terms: { field: 'slo.indicator.type' } },
|
||||
'slo.objective.target': { terms: { field: 'slo.objective.target' } },
|
||||
...(slo.objective.timesliceWindow && {
|
||||
'slo.objective.sliceDurationInSeconds': {
|
||||
terms: { field: 'slo.objective.sliceDurationInSeconds' },
|
||||
},
|
||||
},
|
||||
'slo.revision': {
|
||||
terms: {
|
||||
field: 'slo.revision',
|
||||
},
|
||||
},
|
||||
// timestamp field defined in the destination index
|
||||
}),
|
||||
'slo.budgetingMethod': { terms: { field: 'slo.budgetingMethod' } },
|
||||
'slo.timeWindow.duration': { terms: { field: 'slo.timeWindow.duration' } },
|
||||
'slo.timeWindow.type': { terms: { field: 'slo.timeWindow.type' } },
|
||||
...extraGroupByFields,
|
||||
// @timestamp field defined in the destination index
|
||||
'@timestamp': {
|
||||
date_histogram: {
|
||||
field: sourceIndexTimestampField, // timestamp field defined in the source index
|
||||
|
|
|
@ -8,7 +8,11 @@
|
|||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
|
||||
import { getSLOTransformId } from '../../assets/constants';
|
||||
import {
|
||||
getSLOTransformId,
|
||||
SLO_DESTINATION_INDEX_PATTERN,
|
||||
SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
|
||||
} from '../../assets/constants';
|
||||
import { SLO } from '../../domain/models';
|
||||
import { fiveMinute, oneMinute } from './fixtures/duration';
|
||||
import {
|
||||
|
@ -34,131 +38,107 @@ describe('UpdateSLO', () => {
|
|||
updateSLO = new UpdateSLO(mockRepository, mockTransformManager, mockEsClient);
|
||||
});
|
||||
|
||||
describe('without breaking changes', () => {
|
||||
it('updates the SLO saved object without revision bump', async () => {
|
||||
const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator() });
|
||||
mockRepository.findById.mockResolvedValueOnce(slo);
|
||||
it('updates the settings correctly', async () => {
|
||||
const slo = createSLO();
|
||||
mockRepository.findById.mockResolvedValueOnce(slo);
|
||||
|
||||
const newName = 'new slo name';
|
||||
const newTags = ['other', 'tags'];
|
||||
const response = await updateSLO.execute(slo.id, { name: newName, tags: newTags });
|
||||
const newSettings = { ...slo.settings, timestamp_field: 'newField' };
|
||||
await updateSLO.execute(slo.id, { settings: newSettings });
|
||||
|
||||
expectTransformManagerNeverCalled();
|
||||
expect(mockEsClient.deleteByQuery).not.toBeCalled();
|
||||
expect(mockRepository.save).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
...slo,
|
||||
name: newName,
|
||||
tags: newTags,
|
||||
updatedAt: expect.anything(),
|
||||
})
|
||||
);
|
||||
expect(slo.name).not.toEqual(newName);
|
||||
expect(response.name).toEqual(newName);
|
||||
expect(response.updatedAt).not.toEqual(slo.updatedAt);
|
||||
expect(response.revision).toEqual(slo.revision);
|
||||
expect(response.tags).toEqual(newTags);
|
||||
expect(slo.tags).not.toEqual(newTags);
|
||||
});
|
||||
expectDeletionOfObsoleteSLOData(slo);
|
||||
expect(mockRepository.save).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
...slo,
|
||||
settings: newSettings,
|
||||
revision: 2,
|
||||
updatedAt: expect.anything(),
|
||||
})
|
||||
);
|
||||
expectInstallationOfNewSLOTransform();
|
||||
});
|
||||
|
||||
describe('with breaking changes', () => {
|
||||
it('consideres settings as a breaking change', async () => {
|
||||
const slo = createSLO();
|
||||
mockRepository.findById.mockResolvedValueOnce(slo);
|
||||
it('updates the budgeting method correctly', async () => {
|
||||
const slo = createSLO({ budgetingMethod: 'occurrences' });
|
||||
mockRepository.findById.mockResolvedValueOnce(slo);
|
||||
|
||||
const newSettings = { ...slo.settings, timestamp_field: 'newField' };
|
||||
await updateSLO.execute(slo.id, { settings: newSettings });
|
||||
|
||||
expectDeletionOfObsoleteSLOData(slo);
|
||||
expect(mockRepository.save).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
...slo,
|
||||
settings: newSettings,
|
||||
revision: 2,
|
||||
updatedAt: expect.anything(),
|
||||
})
|
||||
);
|
||||
expectInstallationOfNewSLOTransform();
|
||||
await updateSLO.execute(slo.id, {
|
||||
budgetingMethod: 'timeslices',
|
||||
objective: {
|
||||
target: slo.objective.target,
|
||||
timesliceTarget: 0.9,
|
||||
timesliceWindow: oneMinute(),
|
||||
},
|
||||
});
|
||||
|
||||
it('consideres a budgeting method change as a breaking change', async () => {
|
||||
const slo = createSLO({ budgetingMethod: 'occurrences' });
|
||||
mockRepository.findById.mockResolvedValueOnce(slo);
|
||||
|
||||
await updateSLO.execute(slo.id, {
|
||||
budgetingMethod: 'timeslices',
|
||||
objective: {
|
||||
target: slo.objective.target,
|
||||
timesliceTarget: 0.9,
|
||||
timesliceWindow: oneMinute(),
|
||||
},
|
||||
});
|
||||
|
||||
expectDeletionOfObsoleteSLOData(slo);
|
||||
expectInstallationOfNewSLOTransform();
|
||||
});
|
||||
|
||||
it('consideres a timeslice target change as a breaking change', async () => {
|
||||
const slo = createSLOWithTimeslicesBudgetingMethod();
|
||||
mockRepository.findById.mockResolvedValueOnce(slo);
|
||||
|
||||
await updateSLO.execute(slo.id, {
|
||||
objective: {
|
||||
target: slo.objective.target,
|
||||
timesliceTarget: 0.1,
|
||||
timesliceWindow: slo.objective.timesliceWindow,
|
||||
},
|
||||
});
|
||||
|
||||
expectDeletionOfObsoleteSLOData(slo);
|
||||
expectInstallationOfNewSLOTransform();
|
||||
});
|
||||
|
||||
it('consideres a timeslice window change as a breaking change', async () => {
|
||||
const slo = createSLOWithTimeslicesBudgetingMethod();
|
||||
mockRepository.findById.mockResolvedValueOnce(slo);
|
||||
|
||||
await updateSLO.execute(slo.id, {
|
||||
objective: {
|
||||
target: slo.objective.target,
|
||||
timesliceTarget: slo.objective.timesliceTarget,
|
||||
timesliceWindow: fiveMinute(),
|
||||
},
|
||||
});
|
||||
|
||||
expectDeletionOfObsoleteSLOData(slo);
|
||||
expectInstallationOfNewSLOTransform();
|
||||
});
|
||||
|
||||
it('removes the obsolete data from the SLO previous revision', async () => {
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionErrorRateIndicator({ environment: 'development' }),
|
||||
});
|
||||
mockRepository.findById.mockResolvedValueOnce(slo);
|
||||
|
||||
const newIndicator = createAPMTransactionErrorRateIndicator({ environment: 'production' });
|
||||
await updateSLO.execute(slo.id, { indicator: newIndicator });
|
||||
|
||||
expectDeletionOfObsoleteSLOData(slo);
|
||||
expect(mockRepository.save).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
...slo,
|
||||
indicator: newIndicator,
|
||||
revision: 2,
|
||||
updatedAt: expect.anything(),
|
||||
})
|
||||
);
|
||||
expectInstallationOfNewSLOTransform();
|
||||
});
|
||||
expectDeletionOfObsoleteSLOData(slo);
|
||||
expectInstallationOfNewSLOTransform();
|
||||
});
|
||||
|
||||
function expectTransformManagerNeverCalled() {
|
||||
expect(mockTransformManager.stop).not.toBeCalled();
|
||||
expect(mockTransformManager.uninstall).not.toBeCalled();
|
||||
expect(mockTransformManager.start).not.toBeCalled();
|
||||
expect(mockTransformManager.install).not.toBeCalled();
|
||||
}
|
||||
it('updates the timeslice target correctly', async () => {
|
||||
const slo = createSLOWithTimeslicesBudgetingMethod();
|
||||
mockRepository.findById.mockResolvedValueOnce(slo);
|
||||
|
||||
await updateSLO.execute(slo.id, {
|
||||
objective: {
|
||||
target: slo.objective.target,
|
||||
timesliceTarget: 0.1,
|
||||
timesliceWindow: slo.objective.timesliceWindow,
|
||||
},
|
||||
});
|
||||
|
||||
expectDeletionOfObsoleteSLOData(slo);
|
||||
expectInstallationOfNewSLOTransform();
|
||||
});
|
||||
|
||||
it('consideres a timeslice window change as a breaking change', async () => {
|
||||
const slo = createSLOWithTimeslicesBudgetingMethod();
|
||||
mockRepository.findById.mockResolvedValueOnce(slo);
|
||||
|
||||
await updateSLO.execute(slo.id, {
|
||||
objective: {
|
||||
target: slo.objective.target,
|
||||
timesliceTarget: slo.objective.timesliceTarget,
|
||||
timesliceWindow: fiveMinute(),
|
||||
},
|
||||
});
|
||||
|
||||
expectDeletionOfObsoleteSLOData(slo);
|
||||
expectInstallationOfNewSLOTransform();
|
||||
});
|
||||
|
||||
it('index a temporary summary document', async () => {
|
||||
const slo = createSLO({
|
||||
id: 'unique-id',
|
||||
indicator: createAPMTransactionErrorRateIndicator({ environment: 'development' }),
|
||||
});
|
||||
mockRepository.findById.mockResolvedValueOnce(slo);
|
||||
|
||||
const newIndicator = createAPMTransactionErrorRateIndicator({ environment: 'production' });
|
||||
await updateSLO.execute(slo.id, { indicator: newIndicator });
|
||||
|
||||
expect(mockEsClient.index.mock.calls[0]).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('removes the obsolete data from the SLO previous revision', async () => {
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionErrorRateIndicator({ environment: 'development' }),
|
||||
});
|
||||
mockRepository.findById.mockResolvedValueOnce(slo);
|
||||
|
||||
const newIndicator = createAPMTransactionErrorRateIndicator({ environment: 'production' });
|
||||
await updateSLO.execute(slo.id, { indicator: newIndicator });
|
||||
|
||||
expectDeletionOfObsoleteSLOData(slo);
|
||||
expect(mockRepository.save).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
...slo,
|
||||
indicator: newIndicator,
|
||||
revision: 2,
|
||||
updatedAt: expect.anything(),
|
||||
})
|
||||
);
|
||||
expectInstallationOfNewSLOTransform();
|
||||
});
|
||||
|
||||
function expectInstallationOfNewSLOTransform() {
|
||||
expect(mockTransformManager.start).toBeCalled();
|
||||
|
@ -169,8 +149,26 @@ describe('UpdateSLO', () => {
|
|||
const transformId = getSLOTransformId(originalSlo.id, originalSlo.revision);
|
||||
expect(mockTransformManager.stop).toBeCalledWith(transformId);
|
||||
expect(mockTransformManager.uninstall).toBeCalledWith(transformId);
|
||||
expect(mockEsClient.deleteByQuery).toBeCalledWith(
|
||||
expect(mockEsClient.deleteByQuery).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(mockEsClient.deleteByQuery).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
index: SLO_DESTINATION_INDEX_PATTERN,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { 'slo.id': originalSlo.id } },
|
||||
{ term: { 'slo.revision': originalSlo.revision } },
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(mockEsClient.deleteByQuery).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue