mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
feat(slo): Introduce group by (#163008)
This commit is contained in:
parent
1a3aefe6ec
commit
7d3fe32976
60 changed files with 979 additions and 315 deletions
|
@ -7,6 +7,7 @@
|
|||
|
||||
import * as t from 'io-ts';
|
||||
import {
|
||||
allOrAnyString,
|
||||
apmTransactionDurationIndicatorSchema,
|
||||
apmTransactionErrorRateIndicatorSchema,
|
||||
budgetingMethodSchema,
|
||||
|
@ -39,7 +40,12 @@ const createSLOParamsSchema = t.type({
|
|||
budgetingMethod: budgetingMethodSchema,
|
||||
objective: objectiveSchema,
|
||||
}),
|
||||
t.partial({ id: sloIdSchema, settings: optionalSettingsSchema, tags: tagsSchema }),
|
||||
t.partial({
|
||||
id: sloIdSchema,
|
||||
settings: optionalSettingsSchema,
|
||||
tags: tagsSchema,
|
||||
groupBy: allOrAnyString,
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
|
@ -61,12 +67,6 @@ const deleteSLOParamsSchema = t.type({
|
|||
}),
|
||||
});
|
||||
|
||||
const getSLOParamsSchema = t.type({
|
||||
path: t.type({
|
||||
id: sloIdSchema,
|
||||
}),
|
||||
});
|
||||
|
||||
const sortDirectionSchema = t.union([t.literal('asc'), t.literal('desc')]);
|
||||
const sortBySchema = t.union([
|
||||
t.literal('error_budget_consumed'),
|
||||
|
@ -85,27 +85,47 @@ const findSLOParamsSchema = t.partial({
|
|||
}),
|
||||
});
|
||||
|
||||
const sloResponseSchema = t.type({
|
||||
id: sloIdSchema,
|
||||
name: t.string,
|
||||
description: t.string,
|
||||
indicator: indicatorSchema,
|
||||
timeWindow: timeWindowSchema,
|
||||
budgetingMethod: budgetingMethodSchema,
|
||||
objective: objectiveSchema,
|
||||
revision: t.number,
|
||||
settings: settingsSchema,
|
||||
enabled: t.boolean,
|
||||
tags: tagsSchema,
|
||||
createdAt: dateType,
|
||||
updatedAt: dateType,
|
||||
});
|
||||
const sloResponseSchema = t.intersection([
|
||||
t.type({
|
||||
id: sloIdSchema,
|
||||
name: t.string,
|
||||
description: t.string,
|
||||
indicator: indicatorSchema,
|
||||
timeWindow: timeWindowSchema,
|
||||
budgetingMethod: budgetingMethodSchema,
|
||||
objective: objectiveSchema,
|
||||
revision: t.number,
|
||||
settings: settingsSchema,
|
||||
enabled: t.boolean,
|
||||
tags: tagsSchema,
|
||||
groupBy: allOrAnyString,
|
||||
createdAt: dateType,
|
||||
updatedAt: dateType,
|
||||
}),
|
||||
t.partial({
|
||||
instanceId: allOrAnyString,
|
||||
}),
|
||||
]);
|
||||
|
||||
const sloWithSummaryResponseSchema = t.intersection([
|
||||
sloResponseSchema,
|
||||
t.type({ summary: summarySchema }),
|
||||
]);
|
||||
|
||||
const getSLOQuerySchema = t.partial({
|
||||
query: t.partial({
|
||||
instanceId: allOrAnyString,
|
||||
}),
|
||||
});
|
||||
const getSLOParamsSchema = t.intersection([
|
||||
t.type({
|
||||
path: t.type({
|
||||
id: sloIdSchema,
|
||||
}),
|
||||
}),
|
||||
getSLOQuerySchema,
|
||||
]);
|
||||
|
||||
const getSLOResponseSchema = sloWithSummaryResponseSchema;
|
||||
|
||||
const updateSLOParamsSchema = t.type({
|
||||
|
@ -121,6 +141,7 @@ const updateSLOParamsSchema = t.type({
|
|||
objective: objectiveSchema,
|
||||
settings: optionalSettingsSchema,
|
||||
tags: tagsSchema,
|
||||
groupBy: allOrAnyString,
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -171,6 +192,15 @@ const getSLOBurnRatesParamsSchema = t.type({
|
|||
}),
|
||||
});
|
||||
|
||||
const getSLOInstancesParamsSchema = t.type({
|
||||
path: t.type({ id: t.string }),
|
||||
});
|
||||
|
||||
const getSLOInstancesResponseSchema = t.type({
|
||||
groupBy: t.string,
|
||||
instances: t.array(t.string),
|
||||
});
|
||||
|
||||
type SLOResponse = t.OutputOf<typeof sloResponseSchema>;
|
||||
type SLOWithSummaryResponse = t.OutputOf<typeof sloWithSummaryResponseSchema>;
|
||||
|
||||
|
@ -178,6 +208,7 @@ type CreateSLOInput = t.OutputOf<typeof createSLOParamsSchema.props.body>; // Ra
|
|||
type CreateSLOParams = t.TypeOf<typeof createSLOParamsSchema.props.body>; // Parsed payload used by the backend
|
||||
type CreateSLOResponse = t.TypeOf<typeof createSLOResponseSchema>; // Raw response sent to the frontend
|
||||
|
||||
type GetSLOParams = t.TypeOf<typeof getSLOQuerySchema.props.query>;
|
||||
type GetSLOResponse = t.OutputOf<typeof getSLOResponseSchema>;
|
||||
|
||||
type ManageSLOParams = t.TypeOf<typeof manageSLOParamsSchema.props.path>;
|
||||
|
@ -196,6 +227,8 @@ type HistoricalSummaryResponse = t.OutputOf<typeof historicalSummarySchema>;
|
|||
type GetPreviewDataParams = t.TypeOf<typeof getPreviewDataParamsSchema.props.body>;
|
||||
type GetPreviewDataResponse = t.OutputOf<typeof getPreviewDataResponseSchema>;
|
||||
|
||||
type GetSLOInstancesResponse = t.OutputOf<typeof getSLOInstancesResponseSchema>;
|
||||
|
||||
type GetSLOBurnRatesResponse = t.OutputOf<typeof getSLOBurnRatesResponseSchema>;
|
||||
type BudgetingMethod = t.OutputOf<typeof budgetingMethodSchema>;
|
||||
type TimeWindow = t.OutputOf<typeof timeWindowTypeSchema>;
|
||||
|
@ -226,6 +259,8 @@ export {
|
|||
updateSLOResponseSchema,
|
||||
getSLOBurnRatesParamsSchema,
|
||||
getSLOBurnRatesResponseSchema,
|
||||
getSLOInstancesParamsSchema,
|
||||
getSLOInstancesResponseSchema,
|
||||
};
|
||||
export type {
|
||||
BudgetingMethod,
|
||||
|
@ -236,6 +271,7 @@ export type {
|
|||
FindSLOResponse,
|
||||
GetPreviewDataParams,
|
||||
GetPreviewDataResponse,
|
||||
GetSLOParams,
|
||||
GetSLOResponse,
|
||||
FetchHistoricalSummaryParams,
|
||||
FetchHistoricalSummaryResponse,
|
||||
|
@ -249,6 +285,7 @@ export type {
|
|||
APMTransactionDurationIndicator,
|
||||
APMTransactionErrorRateIndicator,
|
||||
GetSLOBurnRatesResponse,
|
||||
GetSLOInstancesResponse,
|
||||
IndicatorType,
|
||||
Indicator,
|
||||
MetricCustomIndicator,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { dateType, summarySchema } from './common';
|
||||
import { allOrAnyString, dateType, summarySchema } from './common';
|
||||
import { durationType } from './duration';
|
||||
import { indicatorSchema } from './indicators';
|
||||
import { timeWindowSchema } from './time_window';
|
||||
|
@ -32,9 +32,7 @@ const settingsSchema = t.type({
|
|||
});
|
||||
|
||||
const optionalSettingsSchema = t.partial({ ...settingsSchema.props });
|
||||
|
||||
const tagsSchema = t.array(t.string);
|
||||
|
||||
const sloIdSchema = t.string;
|
||||
|
||||
const sloSchema = t.type({
|
||||
|
@ -51,6 +49,7 @@ const sloSchema = t.type({
|
|||
tags: tagsSchema,
|
||||
createdAt: dateType,
|
||||
updatedAt: dateType,
|
||||
groupBy: allOrAnyString,
|
||||
});
|
||||
|
||||
const sloWithSummarySchema = t.intersection([sloSchema, t.type({ summary: summarySchema })]);
|
||||
|
|
|
@ -266,6 +266,15 @@
|
|||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/slo_id"
|
||||
},
|
||||
{
|
||||
"name": "instanceId",
|
||||
"in": "query",
|
||||
"description": "the specific instanceId used by the summary calculation",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "host-abcde"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
@ -1368,6 +1377,16 @@
|
|||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"groupBy": {
|
||||
"description": "optional group by field to use to generate an SLO per distinct value",
|
||||
"type": "string",
|
||||
"example": "some.field"
|
||||
},
|
||||
"instanceId": {
|
||||
"description": "the value derived from the groupBy field, if present, otherwise '*'",
|
||||
"type": "string",
|
||||
"example": "host-abcde"
|
||||
},
|
||||
"createdAt": {
|
||||
"description": "The creation date",
|
||||
"type": "string",
|
||||
|
@ -1553,6 +1572,11 @@
|
|||
},
|
||||
"settings": {
|
||||
"$ref": "#/components/schemas/settings"
|
||||
},
|
||||
"groupBy": {
|
||||
"description": "optional group by field to use to generate an SLO per distinct value",
|
||||
"type": "string",
|
||||
"example": "some.field"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -165,6 +165,12 @@ paths:
|
|||
- $ref: '#/components/parameters/kbn_xsrf'
|
||||
- $ref: '#/components/parameters/space_id'
|
||||
- $ref: '#/components/parameters/slo_id'
|
||||
- name: instanceId
|
||||
in: query
|
||||
description: the specific instanceId used by the summary calculation
|
||||
schema:
|
||||
type: string
|
||||
example: host-abcde
|
||||
responses:
|
||||
'200':
|
||||
description: Successful request
|
||||
|
@ -939,6 +945,14 @@ components:
|
|||
description: Indicate if the SLO is enabled
|
||||
type: boolean
|
||||
example: true
|
||||
groupBy:
|
||||
description: optional group by field to use to generate an SLO per distinct value
|
||||
type: string
|
||||
example: some.field
|
||||
instanceId:
|
||||
description: the value derived from the groupBy field, if present, otherwise '*'
|
||||
type: string
|
||||
example: host-abcde
|
||||
createdAt:
|
||||
description: The creation date
|
||||
type: string
|
||||
|
@ -1072,6 +1086,10 @@ components:
|
|||
$ref: '#/components/schemas/objective'
|
||||
settings:
|
||||
$ref: '#/components/schemas/settings'
|
||||
groupBy:
|
||||
description: optional group by field to use to generate an SLO per distinct value
|
||||
type: string
|
||||
example: some.field
|
||||
create_slo_response:
|
||||
title: Create SLO response
|
||||
type: object
|
||||
|
|
|
@ -35,3 +35,7 @@ properties:
|
|||
$ref: "objective.yaml"
|
||||
settings:
|
||||
$ref: "settings.yaml"
|
||||
groupBy:
|
||||
description: optional group by field to use to generate an SLO per distinct value
|
||||
type: string
|
||||
example: "some.field"
|
||||
|
|
|
@ -46,6 +46,14 @@ properties:
|
|||
description: Indicate if the SLO is enabled
|
||||
type: boolean
|
||||
example: true
|
||||
groupBy:
|
||||
description: optional group by field to use to generate an SLO per distinct value
|
||||
type: string
|
||||
example: "some.field"
|
||||
instanceId:
|
||||
description: the value derived from the groupBy field, if present, otherwise '*'
|
||||
type: string
|
||||
example: 'host-abcde'
|
||||
createdAt:
|
||||
description: The creation date
|
||||
type: string
|
||||
|
|
|
@ -10,6 +10,12 @@ get:
|
|||
- $ref: ../components/headers/kbn_xsrf.yaml
|
||||
- $ref: ../components/parameters/space_id.yaml
|
||||
- $ref: ../components/parameters/slo_id.yaml
|
||||
- name: instanceId
|
||||
in: query
|
||||
description: the specific instanceId used by the summary calculation
|
||||
schema:
|
||||
type: string
|
||||
example: 'host-abcde'
|
||||
responses:
|
||||
'200':
|
||||
description: Successful request
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { EuiBadge, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { euiLightVars } from '@kbn/ui-theme';
|
||||
import React from 'react';
|
||||
|
||||
export interface Props {
|
||||
slo: SLOWithSummaryResponse;
|
||||
}
|
||||
|
||||
export function SloGroupByBadge({ slo }: Props) {
|
||||
if (!slo.groupBy || slo.groupBy === ALL_VALUE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color={euiLightVars.euiColorDisabled}>
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={i18n.translate('xpack.observability.slo.groupByBadge', {
|
||||
defaultMessage: 'Group by {groupBy}',
|
||||
values: {
|
||||
groupBy: slo.groupBy,
|
||||
},
|
||||
})}
|
||||
display="block"
|
||||
>
|
||||
<span>
|
||||
{slo.groupBy}: {slo.instanceId}
|
||||
</span>
|
||||
</EuiToolTip>
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { v1 as uuidv1 } from 'uuid';
|
||||
import { FindSLOResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { ALL_VALUE, FindSLOResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import {
|
||||
buildDegradingSummary,
|
||||
buildHealthySummary,
|
||||
|
@ -62,6 +62,8 @@ const baseSlo: Omit<SLOWithSummaryResponse, 'id'> = {
|
|||
isEstimated: false,
|
||||
},
|
||||
},
|
||||
groupBy: ALL_VALUE,
|
||||
instanceId: ALL_VALUE,
|
||||
tags: ['k8s', 'production', 'critical'],
|
||||
enabled: true,
|
||||
createdAt: now,
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
RefetchQueryFilters,
|
||||
useQuery,
|
||||
} from '@tanstack/react-query';
|
||||
import { GetSLOResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { ALL_VALUE, GetSLOResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { sloKeys } from './query_key_factory';
|
||||
|
||||
|
@ -31,9 +31,11 @@ const LONG_REFETCH_INTERVAL = 1000 * 60; // 1 minute
|
|||
|
||||
export function useFetchSloDetails({
|
||||
sloId,
|
||||
instanceId,
|
||||
shouldRefetch,
|
||||
}: {
|
||||
sloId?: string;
|
||||
instanceId?: string;
|
||||
shouldRefetch?: boolean;
|
||||
}): UseFetchSloDetailsResponse {
|
||||
const { http } = useKibana().services;
|
||||
|
@ -44,7 +46,9 @@ export function useFetchSloDetails({
|
|||
queryFn: async ({ signal }) => {
|
||||
try {
|
||||
const response = await http.get<GetSLOResponse>(`/api/observability/slos/${sloId}`, {
|
||||
query: {},
|
||||
query: {
|
||||
...(!!instanceId && instanceId !== ALL_VALUE && { instanceId }),
|
||||
},
|
||||
signal,
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { GetSLOInstancesResponse } from '@kbn/slo-schema';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { sloKeys } from './query_key_factory';
|
||||
|
||||
export interface UseFetchSloInstancesResponse {
|
||||
isInitialLoading: boolean;
|
||||
isLoading: boolean;
|
||||
isRefetching: boolean;
|
||||
isSuccess: boolean;
|
||||
isError: boolean;
|
||||
data: GetSLOInstancesResponse | undefined;
|
||||
}
|
||||
|
||||
export function useFetchSloInstances({ sloId }: { sloId?: string }): UseFetchSloInstancesResponse {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({
|
||||
queryKey: sloKeys.detail(sloId),
|
||||
queryFn: async ({ signal }) => {
|
||||
try {
|
||||
const response = await http.get<GetSLOInstancesResponse>(
|
||||
`/internal/observability/slos/${sloId}/_instances`,
|
||||
{
|
||||
query: {},
|
||||
signal,
|
||||
}
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
// ignore error for retrieving slos
|
||||
}
|
||||
},
|
||||
keepPreviousData: true,
|
||||
enabled: Boolean(sloId),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
isLoading,
|
||||
isInitialLoading,
|
||||
isRefetching,
|
||||
isSuccess,
|
||||
isError,
|
||||
};
|
||||
}
|
|
@ -20,7 +20,7 @@ describe('SloEditLocator', () => {
|
|||
it('should return correct url when slo is provided', async () => {
|
||||
const location = await locator.getLocation(buildSlo({ id: 'foo' }));
|
||||
expect(location.path).toEqual(
|
||||
"/slos/edit/foo?_a=(budgetingMethod:occurrences,createdAt:'2022-12-29T10:11:12.000Z',description:'some%20description%20useful',enabled:!t,id:foo,indicator:(params:(filter:'baz:%20foo%20and%20bar%20%3E%202',good:'http_status:%202xx',index:some-index,timestampField:custom_timestamp,total:'a%20query'),type:sli.kql.custom),name:'super%20important%20level%20service',objective:(target:0.98),revision:1,settings:(frequency:'1m',syncDelay:'1m'),summary:(errorBudget:(consumed:0.064,initial:0.02,isEstimated:!f,remaining:0.936),sliValue:0.99872,status:HEALTHY),tags:!(k8s,production,critical),timeWindow:(duration:'30d',type:rolling),updatedAt:'2022-12-29T10:11:12.000Z')"
|
||||
"/slos/edit/foo?_a=(budgetingMethod:occurrences,createdAt:'2022-12-29T10:11:12.000Z',description:'some%20description%20useful',enabled:!t,groupBy:'*',id:foo,indicator:(params:(filter:'baz:%20foo%20and%20bar%20%3E%202',good:'http_status:%202xx',index:some-index,timestampField:custom_timestamp,total:'a%20query'),type:sli.kql.custom),instanceId:'*',name:'super%20important%20level%20service',objective:(target:0.98),revision:1,settings:(frequency:'1m',syncDelay:'1m'),summary:(errorBudget:(consumed:0.064,initial:0.02,isEstimated:!f,remaining:0.936),sliValue:0.99872,status:HEALTHY),tags:!(k8s,production,critical),timeWindow:(duration:'30d',type:rolling),updatedAt:'2022-12-29T10:11:12.000Z')"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { SloGroupByBadge } from '../../../components/slo/slo_status_badge/slo_group_by_badge';
|
||||
import { SloStatusBadge } from '../../../components/slo/slo_status_badge';
|
||||
|
||||
export interface Props {
|
||||
|
@ -33,8 +34,15 @@ export function HeaderTitle(props: Props) {
|
|||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>{slo.name}</EuiFlexItem>
|
||||
<EuiFlexGroup direction="row" gutterSize="s" alignItems="center" responsive={false}>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
justifyContent="flexStart"
|
||||
responsive={false}
|
||||
>
|
||||
<SloStatusBadge slo={slo} />
|
||||
<SloGroupByBadge slo={slo} />
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -7,7 +7,11 @@
|
|||
|
||||
import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { APMTransactionDurationIndicator, APMTransactionErrorRateIndicator } from '@kbn/slo-schema';
|
||||
import {
|
||||
ALL_VALUE,
|
||||
APMTransactionDurationIndicator,
|
||||
APMTransactionErrorRateIndicator,
|
||||
} from '@kbn/slo-schema';
|
||||
import React from 'react';
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
import { convertSliApmParamsToApmAppDeeplinkUrl } from '../../../../utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url';
|
||||
|
@ -48,7 +52,7 @@ export function ApmIndicatorOverview({ indicator }: Props) {
|
|||
)}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
{environment !== '*' && (
|
||||
{environment !== ALL_VALUE && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color="hollow" href={link}>
|
||||
{i18n.translate(
|
||||
|
@ -58,7 +62,7 @@ export function ApmIndicatorOverview({ indicator }: Props) {
|
|||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{transactionType !== '*' && (
|
||||
{transactionType !== ALL_VALUE && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color="hollow" href={link}>
|
||||
{i18n.translate(
|
||||
|
@ -68,7 +72,7 @@ export function ApmIndicatorOverview({ indicator }: Props) {
|
|||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{transactionName !== '*' && (
|
||||
{transactionName !== ALL_VALUE && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color="hollow" href={link}>
|
||||
{i18n.translate(
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
export const INSTANCE_SEARCH_PARAM = 'instanceId';
|
||||
|
||||
export function useGetInstanceIdQueryParam(): string | undefined {
|
||||
const { search } = useLocation();
|
||||
const searchParams = new URLSearchParams(search);
|
||||
|
||||
const instanceId = searchParams.get(INSTANCE_SEARCH_PARAM);
|
||||
|
||||
return !!instanceId && instanceId !== ALL_VALUE ? instanceId : undefined;
|
||||
}
|
|
@ -237,7 +237,17 @@ describe('SLO Details Page', () => {
|
|||
|
||||
fireEvent.click(button!);
|
||||
|
||||
const { id, createdAt, enabled, revision, summary, settings, updatedAt, ...newSlo } = slo;
|
||||
const {
|
||||
id,
|
||||
createdAt,
|
||||
enabled,
|
||||
revision,
|
||||
summary,
|
||||
settings,
|
||||
updatedAt,
|
||||
instanceId,
|
||||
...newSlo
|
||||
} = slo;
|
||||
|
||||
expect(mockClone).toBeCalledWith({
|
||||
originalSloId: slo.id,
|
||||
|
|
|
@ -27,6 +27,7 @@ import { paths } from '../../routes/paths';
|
|||
import type { SloDetailsPathParams } from './types';
|
||||
import { AutoRefreshButton } from '../slos/components/auto_refresh_button';
|
||||
import { FeedbackButton } from '../../components/slo/feedback_button/feedback_button';
|
||||
import { useGetInstanceIdQueryParam } from './hooks/use_get_instance_id_query_param';
|
||||
|
||||
export function SloDetailsPage() {
|
||||
const {
|
||||
|
@ -39,8 +40,13 @@ export function SloDetailsPage() {
|
|||
const hasRightLicense = hasAtLeast('platinum');
|
||||
|
||||
const { sloId } = useParams<SloDetailsPathParams>();
|
||||
const sloInstanceId = useGetInstanceIdQueryParam();
|
||||
const [isAutoRefreshing, setIsAutoRefreshing] = useState(true);
|
||||
const { isLoading, slo } = useFetchSloDetails({ sloId, shouldRefetch: isAutoRefreshing });
|
||||
const { isLoading, slo } = useFetchSloDetails({
|
||||
sloId,
|
||||
instanceId: sloInstanceId,
|
||||
shouldRefetch: isAutoRefreshing,
|
||||
});
|
||||
const isCloningOrDeleting = Boolean(useIsMutating());
|
||||
|
||||
useBreadcrumbs(getBreadcrumbs(basePath, slo));
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { EuiComboBox, EuiComboBoxOptionOption, EuiFlexItem, EuiFormRow } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
import { debounce } from 'lodash';
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import { Controller, FieldPath, useFormContext } from 'react-hook-form';
|
||||
|
@ -55,7 +56,7 @@ export function FieldSelector({
|
|||
allowAllOption
|
||||
? [
|
||||
{
|
||||
value: '*',
|
||||
value: ALL_VALUE,
|
||||
label: i18n.translate('xpack.observability.slo.sloEdit.fieldSelector.all', {
|
||||
defaultMessage: 'All',
|
||||
}),
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
ALL_VALUE,
|
||||
APMTransactionDurationIndicator,
|
||||
APMTransactionErrorRateIndicator,
|
||||
BudgetingMethod,
|
||||
|
@ -179,6 +180,7 @@ export const SLO_EDIT_FORM_DEFAULT_VALUES: CreateSLOForm = {
|
|||
objective: {
|
||||
target: 99,
|
||||
},
|
||||
groupBy: ALL_VALUE,
|
||||
};
|
||||
|
||||
export const SLO_EDIT_FORM_DEFAULT_VALUES_CUSTOM_METRIC: CreateSLOForm = {
|
||||
|
@ -194,4 +196,5 @@ export const SLO_EDIT_FORM_DEFAULT_VALUES_CUSTOM_METRIC: CreateSLOForm = {
|
|||
objective: {
|
||||
target: 99,
|
||||
},
|
||||
groupBy: ALL_VALUE,
|
||||
};
|
||||
|
|
|
@ -34,6 +34,7 @@ export function transformSloResponseToCreateSloForm(
|
|||
timesliceWindow: String(toDuration(values.objective.timesliceWindow).value),
|
||||
}),
|
||||
},
|
||||
groupBy: values.groupBy,
|
||||
tags: values.tags,
|
||||
};
|
||||
}
|
||||
|
@ -60,6 +61,7 @@ export function transformCreateSLOFormToCreateSLOInput(values: CreateSLOForm): C
|
|||
}),
|
||||
},
|
||||
tags: values.tags,
|
||||
groupBy: values.groupBy,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -85,6 +87,7 @@ export function transformValuesToUpdateSLOInput(values: CreateSLOForm): UpdateSL
|
|||
}),
|
||||
},
|
||||
tags: values.tags,
|
||||
groupBy: values.groupBy,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -22,4 +22,5 @@ export interface CreateSLOForm {
|
|||
timesliceTarget?: number;
|
||||
timesliceWindow?: string;
|
||||
};
|
||||
groupBy: string;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import { SloTimeWindowBadge } from './slo_time_window_badge';
|
|||
import { SloRulesBadge } from './slo_rules_badge';
|
||||
import type { ActiveAlerts } from '../../../../hooks/slo/use_fetch_active_alerts';
|
||||
import type { SloRule } from '../../../../hooks/slo/use_fetch_rules_for_slo';
|
||||
import { SloGroupByBadge } from '../../../../components/slo/slo_status_badge/slo_group_by_badge';
|
||||
|
||||
export interface Props {
|
||||
activeAlerts?: ActiveAlerts;
|
||||
|
@ -56,6 +57,7 @@ export function SloBadges({ activeAlerts, isLoading, rules, slo, onClickRuleBadg
|
|||
) : (
|
||||
<>
|
||||
<SloStatusBadge slo={slo} />
|
||||
<SloGroupByBadge slo={slo} />
|
||||
<SloIndicatorTypeBadge slo={slo} />
|
||||
<SloTimeWindowBadge slo={slo} />
|
||||
<SloActiveAlertsBadge slo={slo} activeAlerts={activeAlerts} />
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { HistoricalSummaryResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { ALL_VALUE, 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';
|
||||
|
@ -81,7 +81,14 @@ export function SloListItem({
|
|||
};
|
||||
|
||||
const handleViewDetails = () => {
|
||||
navigateToUrl(basePath.prepend(paths.observability.sloDetails(slo.id)));
|
||||
navigateToUrl(
|
||||
basePath.prepend(
|
||||
paths.observability.sloDetails(
|
||||
slo.id,
|
||||
slo.groupBy !== ALL_VALUE && slo.instanceId ? slo.instanceId : undefined
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import type { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import React from 'react';
|
||||
import { useFetchActiveAlerts } from '../../../hooks/slo/use_fetch_active_alerts';
|
||||
import { useFetchHistoricalSummary } from '../../../hooks/slo/use_fetch_historical_summary';
|
||||
|
@ -38,7 +38,7 @@ export function SloListItems({ sloList, loading, error }: Props) {
|
|||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
{sloList.map((slo) => (
|
||||
<EuiFlexItem key={slo.id}>
|
||||
<EuiFlexItem key={`${slo.id}-${slo.instanceId ?? ALL_VALUE}`}>
|
||||
<SloListItem
|
||||
activeAlerts={activeAlertsBySlo[slo.id]}
|
||||
rules={rulesBySlo?.[slo.id]}
|
||||
|
|
|
@ -34,6 +34,11 @@ export const paths = {
|
|||
sloCreateWithEncodedForm: (encodedParams: string) =>
|
||||
`${OBSERVABILITY_BASE_PATH}${SLO_CREATE_PATH}?_a=${encodedParams}`,
|
||||
sloEdit: (sloId: string) => `${OBSERVABILITY_BASE_PATH}${SLOS_PATH}/edit/${encodeURI(sloId)}`,
|
||||
sloDetails: (sloId: string) => `${OBSERVABILITY_BASE_PATH}${SLOS_PATH}/${encodeURI(sloId)}`,
|
||||
sloDetails: (sloId: string, instanceId?: string) =>
|
||||
!!instanceId
|
||||
? `${OBSERVABILITY_BASE_PATH}${SLOS_PATH}/${encodeURI(sloId)}?instanceId=${encodeURI(
|
||||
instanceId
|
||||
)}`
|
||||
: `${OBSERVABILITY_BASE_PATH}${SLOS_PATH}/${encodeURI(sloId)}`,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
|
||||
interface Props {
|
||||
duration?: string;
|
||||
environment: string;
|
||||
|
@ -29,11 +31,11 @@ export function convertSliApmParamsToApmAppDeeplinkUrl({
|
|||
const qs = new URLSearchParams('comparisonEnabled=true');
|
||||
|
||||
if (environment) {
|
||||
qs.append('environment', environment === '*' ? 'ENVIRONMENT_ALL' : environment);
|
||||
qs.append('environment', environment === ALL_VALUE ? 'ENVIRONMENT_ALL' : environment);
|
||||
}
|
||||
|
||||
if (transactionType) {
|
||||
qs.append('transactionType', transactionType === '*' ? '' : transactionType);
|
||||
qs.append('transactionType', transactionType === ALL_VALUE ? '' : transactionType);
|
||||
}
|
||||
|
||||
if (duration) {
|
||||
|
@ -42,7 +44,7 @@ export function convertSliApmParamsToApmAppDeeplinkUrl({
|
|||
}
|
||||
|
||||
const kueryParams = [];
|
||||
if (transactionName && transactionName !== '*') {
|
||||
if (transactionName && transactionName !== ALL_VALUE) {
|
||||
kueryParams.push(`transaction.name : "${transactionName}"`);
|
||||
}
|
||||
if (filter && filter.length > 0) {
|
||||
|
|
|
@ -50,6 +50,10 @@ export const getSLOMappingsTemplate = (name: string) => ({
|
|||
revision: {
|
||||
type: 'long',
|
||||
},
|
||||
groupBy: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
},
|
||||
instanceId: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
|
|
|
@ -46,6 +46,10 @@ export const getSLOSummaryMappingsTemplate = (name: string) => ({
|
|||
revision: {
|
||||
type: 'long',
|
||||
},
|
||||
groupBy: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
},
|
||||
instanceId: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
getPreviewDataParamsSchema,
|
||||
getSLOBurnRatesParamsSchema,
|
||||
getSLODiagnosisParamsSchema,
|
||||
getSLOInstancesParamsSchema,
|
||||
getSLOParamsSchema,
|
||||
manageSLOParamsSchema,
|
||||
updateSLOParamsSchema,
|
||||
|
@ -33,6 +34,7 @@ import { FetchHistoricalSummary } from '../../services/slo/fetch_historical_summ
|
|||
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 { GetSLOInstances } from '../../services/slo/get_slo_instances';
|
||||
import { DefaultHistoricalSummaryClient } from '../../services/slo/historical_summary_client';
|
||||
import { ManageSLO } from '../../services/slo/manage_slo';
|
||||
import { DefaultSummarySearchClient } from '../../services/slo/summary_search_client';
|
||||
|
@ -162,7 +164,7 @@ const getSLORoute = createObservabilityServerRoute({
|
|||
const summaryClient = new DefaultSummaryClient(esClient);
|
||||
const getSLO = new GetSLO(repository, summaryClient);
|
||||
|
||||
const response = await getSLO.execute(params.path.id);
|
||||
const response = await getSLO.execute(params.path.id, params.query);
|
||||
|
||||
return response;
|
||||
},
|
||||
|
@ -271,6 +273,31 @@ const fetchHistoricalSummary = createObservabilityServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const getSLOInstancesRoute = createObservabilityServerRoute({
|
||||
endpoint: 'GET /internal/observability/slos/{id}/_instances',
|
||||
options: {
|
||||
tags: ['access:slo_read'],
|
||||
},
|
||||
params: getSLOInstancesParamsSchema,
|
||||
handler: async ({ context, params }) => {
|
||||
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context);
|
||||
|
||||
if (!hasCorrectLicense) {
|
||||
throw forbidden('Platinum license or higher is needed to make use of this feature.');
|
||||
}
|
||||
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
const repository = new KibanaSavedObjectsSLORepository(soClient);
|
||||
|
||||
const getSLOInstances = new GetSLOInstances(repository, esClient);
|
||||
|
||||
const response = await getSLOInstances.execute(params.path.id);
|
||||
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
const getDiagnosisRoute = createObservabilityServerRoute({
|
||||
endpoint: 'GET /internal/observability/slos/_diagnosis',
|
||||
options: {
|
||||
|
@ -363,4 +390,5 @@ export const sloRouteRepository = {
|
|||
...getSloDiagnosisRoute,
|
||||
...getSloBurnRates,
|
||||
...getPreviewData,
|
||||
...getSLOInstancesRoute,
|
||||
};
|
||||
|
|
|
@ -18,6 +18,7 @@ Array [
|
|||
"slo": Object {
|
||||
"budgetingMethod": "occurrences",
|
||||
"description": "irrelevant",
|
||||
"groupBy": "*",
|
||||
"id": "unique-id",
|
||||
"indicator": Object {
|
||||
"type": "sli.apm.transactionErrorRate",
|
||||
|
|
41
x-pack/plugins/observability/server/services/slo/__snapshots__/get_slo_instances.test.ts.snap
generated
Normal file
41
x-pack/plugins/observability/server/services/slo/__snapshots__/get_slo_instances.test.ts.snap
generated
Normal file
|
@ -0,0 +1,41 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Get SLO Instances returns all instances of a SLO defined with a 'groupBy' 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"aggs": Object {
|
||||
"instances": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.instanceId",
|
||||
"size": 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
"index": ".slo-observability.sli-v2*",
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
"gte": "now-7d",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"slo.id": "slo-id",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"slo.revision": 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"size": 0,
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -3,38 +3,38 @@
|
|||
exports[`SummaryClient fetchSummary with calendar aligned and timeslices SLO returns the summary 1`] = `
|
||||
Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0.198413,
|
||||
"consumed": 0,
|
||||
"initial": 0.05,
|
||||
"isEstimated": false,
|
||||
"remaining": 0.801587,
|
||||
"remaining": 1,
|
||||
},
|
||||
"sliValue": 0.9,
|
||||
"status": "DEGRADING",
|
||||
"sliValue": -1,
|
||||
"status": "NO_DATA",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`SummaryClient fetchSummary with rolling and occurrences SLO returns the summary 1`] = `
|
||||
Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 100,
|
||||
"consumed": 0,
|
||||
"initial": 0.001,
|
||||
"isEstimated": false,
|
||||
"remaining": -99,
|
||||
"remaining": 1,
|
||||
},
|
||||
"sliValue": 0.9,
|
||||
"status": "VIOLATED",
|
||||
"sliValue": -1,
|
||||
"status": "NO_DATA",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`SummaryClient fetchSummary with rolling and timeslices SLO returns the summary 1`] = `
|
||||
Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 2,
|
||||
"consumed": 0,
|
||||
"initial": 0.05,
|
||||
"isEstimated": false,
|
||||
"remaining": -1,
|
||||
"remaining": 1,
|
||||
},
|
||||
"sliValue": 0.9,
|
||||
"status": "VIOLATED",
|
||||
"sliValue": -1,
|
||||
"status": "NO_DATA",
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -37,6 +37,7 @@ Object {
|
|||
"results": Array [
|
||||
Object {
|
||||
"id": "slo-one",
|
||||
"instanceId": "*",
|
||||
"summary": Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0.4,
|
||||
|
@ -50,6 +51,7 @@ Object {
|
|||
},
|
||||
Object {
|
||||
"id": "slo_two",
|
||||
"instanceId": "*",
|
||||
"summary": Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0.4,
|
||||
|
@ -63,6 +65,7 @@ Object {
|
|||
},
|
||||
Object {
|
||||
"id": "slo-three",
|
||||
"instanceId": "*",
|
||||
"summary": Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0.4,
|
||||
|
@ -76,6 +79,7 @@ Object {
|
|||
},
|
||||
Object {
|
||||
"id": "slo-five",
|
||||
"instanceId": "*",
|
||||
"summary": Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0.4,
|
||||
|
@ -89,6 +93,7 @@ Object {
|
|||
},
|
||||
Object {
|
||||
"id": "slo-four",
|
||||
"instanceId": "*",
|
||||
"summary": Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0.4,
|
||||
|
|
|
@ -18,6 +18,7 @@ Array [
|
|||
"slo": Object {
|
||||
"budgetingMethod": "occurrences",
|
||||
"description": "irrelevant",
|
||||
"groupBy": "*",
|
||||
"id": "unique-id",
|
||||
"indicator": Object {
|
||||
"type": "sli.apm.transactionErrorRate",
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { CreateSLOParams, CreateSLOResponse } from '@kbn/slo-schema';
|
||||
import { ALL_VALUE, 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';
|
||||
|
@ -69,6 +69,7 @@ export class CreateSLO {
|
|||
tags: params.tags ?? [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
groupBy: !!params.groupBy ? params.groupBy : ALL_VALUE,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
import { SLO } from '../../domain/models';
|
||||
import { FindSLO } from './find_slo';
|
||||
import { createSLO } from './fixtures/slo';
|
||||
|
@ -92,6 +93,8 @@ describe('FindSLO', () => {
|
|||
updatedAt: slo.updatedAt.toISOString(),
|
||||
enabled: slo.enabled,
|
||||
revision: slo.revision,
|
||||
groupBy: slo.groupBy,
|
||||
instanceId: ALL_VALUE,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -145,6 +148,7 @@ function summarySearchResult(slo: SLO): Paginated<SLOSummary> {
|
|||
results: [
|
||||
{
|
||||
id: slo.id,
|
||||
instanceId: slo.groupBy === ALL_VALUE ? ALL_VALUE : 'host-abcde',
|
||||
summary: {
|
||||
status: 'HEALTHY',
|
||||
sliValue: 0.9999,
|
||||
|
|
|
@ -43,6 +43,7 @@ function mergeSloWithSummary(sloList: SLO[], sloSummaryList: SLOSummary[]): SLOW
|
|||
.filter((sloSummary) => sloList.some((s) => s.id === sloSummary.id))
|
||||
.map((sloSummary) => ({
|
||||
...sloList.find((s) => s.id === sloSummary.id)!,
|
||||
instanceId: sloSummary.instanceId,
|
||||
summary: sloSummary.summary,
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { SavedObject } from '@kbn/core-saved-objects-server';
|
||||
import { CreateSLOParams, HistogramIndicator, sloSchema } from '@kbn/slo-schema';
|
||||
import { ALL_VALUE, CreateSLOParams, HistogramIndicator, sloSchema } from '@kbn/slo-schema';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { v1 as uuidv1 } from 'uuid';
|
||||
import {
|
||||
|
@ -129,6 +129,7 @@ const defaultSLO: Omit<SLO, 'id' | 'revision' | 'createdAt' | 'updatedAt'> = {
|
|||
},
|
||||
tags: ['critical', 'k8s'],
|
||||
enabled: true,
|
||||
groupBy: ALL_VALUE,
|
||||
};
|
||||
|
||||
const defaultCreateSloParams: CreateSLOParams = {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
import { v1 as uuidv1 } from 'uuid';
|
||||
|
||||
export const aSummaryDocument = ({
|
||||
|
@ -33,7 +34,7 @@ export const aSummaryDocument = ({
|
|||
duration: '30d',
|
||||
type: 'rolling',
|
||||
},
|
||||
instanceId: '*',
|
||||
instanceId: ALL_VALUE,
|
||||
name: 'irrelevant',
|
||||
description: '',
|
||||
id,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
import { createAPMTransactionErrorRateIndicator, createSLO } from './fixtures/slo';
|
||||
import { GetSLO } from './get_slo';
|
||||
import { createSummaryClientMock, createSLORepositoryMock } from './mocks';
|
||||
|
@ -26,16 +27,14 @@ describe('GetSLO', () => {
|
|||
it('retrieves the SLO from the repository', async () => {
|
||||
const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator() });
|
||||
mockRepository.findById.mockResolvedValueOnce(slo);
|
||||
mockSummaryClient.fetchSummary.mockResolvedValueOnce({
|
||||
[slo.id]: {
|
||||
status: 'HEALTHY',
|
||||
sliValue: 0.9999,
|
||||
errorBudget: {
|
||||
initial: 0.001,
|
||||
consumed: 0.1,
|
||||
remaining: 0.9,
|
||||
isEstimated: false,
|
||||
},
|
||||
mockSummaryClient.computeSummary.mockResolvedValueOnce({
|
||||
status: 'HEALTHY',
|
||||
sliValue: 0.9999,
|
||||
errorBudget: {
|
||||
initial: 0.001,
|
||||
consumed: 0.1,
|
||||
remaining: 0.9,
|
||||
isEstimated: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -83,6 +82,8 @@ describe('GetSLO', () => {
|
|||
updatedAt: slo.updatedAt.toISOString(),
|
||||
enabled: slo.enabled,
|
||||
revision: slo.revision,
|
||||
groupBy: slo.groupBy,
|
||||
instanceId: ALL_VALUE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,24 +5,23 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { GetSLOResponse, getSLOResponseSchema } from '@kbn/slo-schema';
|
||||
import { SLO, SLOId, SLOWithSummary, Summary } from '../../domain/models';
|
||||
import { ALL_VALUE, GetSLOParams, GetSLOResponse, getSLOResponseSchema } from '@kbn/slo-schema';
|
||||
import { SLO, Summary } from '../../domain/models';
|
||||
import { SLORepository } from './slo_repository';
|
||||
import { SummaryClient } from './summary_client';
|
||||
|
||||
export class GetSLO {
|
||||
constructor(private repository: SLORepository, private summaryClient: SummaryClient) {}
|
||||
|
||||
public async execute(sloId: string): Promise<GetSLOResponse> {
|
||||
public async execute(sloId: string, params: GetSLOParams = {}): Promise<GetSLOResponse> {
|
||||
const slo = await this.repository.findById(sloId);
|
||||
const summaryBySlo = await this.summaryClient.fetchSummary([slo]);
|
||||
const instanceId = params.instanceId ?? ALL_VALUE;
|
||||
const summary = await this.summaryClient.computeSummary(slo, instanceId);
|
||||
|
||||
const sloWithSummary = mergeSloWithSummary(slo, summaryBySlo);
|
||||
|
||||
return getSLOResponseSchema.encode(sloWithSummary);
|
||||
return getSLOResponseSchema.encode(mergeSloWithSummary(slo, summary, instanceId));
|
||||
}
|
||||
}
|
||||
|
||||
function mergeSloWithSummary(slo: SLO, summaryBySlo: Record<SLOId, Summary>): SLOWithSummary {
|
||||
return { ...slo, summary: summaryBySlo[slo.id] };
|
||||
function mergeSloWithSummary(slo: SLO, summary: Summary, instanceId: string) {
|
||||
return { ...slo, instanceId, summary };
|
||||
}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ElasticsearchClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { createSLO } from './fixtures/slo';
|
||||
import { GetSLOInstances, SLORepository } from '.';
|
||||
import { createSLORepositoryMock } from './mocks';
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
|
||||
describe('Get SLO Instances', () => {
|
||||
let repositoryMock: jest.Mocked<SLORepository>;
|
||||
let esClientMock: ElasticsearchClientMock;
|
||||
|
||||
beforeEach(() => {
|
||||
repositoryMock = createSLORepositoryMock();
|
||||
esClientMock = elasticsearchServiceMock.createElasticsearchClient();
|
||||
});
|
||||
|
||||
it("returns an empty response when the SLO has no 'groupBy' defined", async () => {
|
||||
const slo = createSLO({ groupBy: ALL_VALUE });
|
||||
repositoryMock.findById.mockResolvedValue(slo);
|
||||
|
||||
const service = new GetSLOInstances(repositoryMock, esClientMock);
|
||||
|
||||
const result = await service.execute(slo.id);
|
||||
|
||||
expect(result).toEqual({ groupBy: ALL_VALUE, instances: [] });
|
||||
});
|
||||
|
||||
it("returns all instances of a SLO defined with a 'groupBy'", async () => {
|
||||
const slo = createSLO({ id: 'slo-id', revision: 2, groupBy: 'field.to.host' });
|
||||
repositoryMock.findById.mockResolvedValue(slo);
|
||||
esClientMock.search.mockResolvedValue({
|
||||
took: 100,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 0,
|
||||
successful: 0,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
},
|
||||
hits: {
|
||||
hits: [],
|
||||
},
|
||||
aggregations: {
|
||||
instances: {
|
||||
buckets: [
|
||||
{ key: 'host-aaa', doc_value: 100 },
|
||||
{ key: 'host-bbb', doc_value: 200 },
|
||||
{ key: 'host-ccc', doc_value: 500 },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const service = new GetSLOInstances(repositoryMock, esClientMock);
|
||||
|
||||
const result = await service.execute(slo.id);
|
||||
|
||||
expect(result).toEqual({
|
||||
groupBy: 'field.to.host',
|
||||
instances: ['host-aaa', 'host-bbb', 'host-ccc'],
|
||||
});
|
||||
expect(esClientMock.search.mock.calls[0]).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import { ALL_VALUE, GetSLOInstancesResponse } from '@kbn/slo-schema';
|
||||
import { SLO_DESTINATION_INDEX_PATTERN } from '../../assets/constants';
|
||||
import { SLORepository } from './slo_repository';
|
||||
|
||||
export class GetSLOInstances {
|
||||
constructor(private repository: SLORepository, private esClient: ElasticsearchClient) {}
|
||||
|
||||
public async execute(sloId: string): Promise<GetSLOInstancesResponse> {
|
||||
const slo = await this.repository.findById(sloId);
|
||||
|
||||
if (slo.groupBy === ALL_VALUE) {
|
||||
return { groupBy: ALL_VALUE, instances: [] };
|
||||
}
|
||||
|
||||
const result = await this.esClient.search({
|
||||
index: SLO_DESTINATION_INDEX_PATTERN,
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ range: { '@timestamp': { gte: 'now-7d' } } },
|
||||
{ term: { 'slo.id': slo.id } },
|
||||
{ term: { 'slo.revision': slo.revision } },
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
instances: {
|
||||
terms: {
|
||||
size: 1000,
|
||||
field: 'slo.instanceId',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
const buckets = result?.aggregations?.instances.buckets ?? [];
|
||||
const instances = buckets.map((bucket: { key: string }) => bucket.key);
|
||||
return { groupBy: slo.groupBy, instances };
|
||||
}
|
||||
}
|
|
@ -19,3 +19,4 @@ export * from './slo_repository';
|
|||
export * from './transform_manager';
|
||||
export * from './update_slo';
|
||||
export * from './summary_client';
|
||||
export * from './get_slo_instances';
|
||||
|
|
|
@ -45,7 +45,7 @@ const createSLORepositoryMock = (): jest.Mocked<SLORepository> => {
|
|||
|
||||
const createSummaryClientMock = (): jest.Mocked<SummaryClient> => {
|
||||
return {
|
||||
fetchSummary: jest.fn(),
|
||||
computeSummary: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -54,33 +54,30 @@ describe('SummaryClient', () => {
|
|||
esClientMock.msearch.mockResolvedValueOnce(createEsResponse());
|
||||
const summaryClient = new DefaultSummaryClient(esClientMock);
|
||||
|
||||
const result = await summaryClient.fetchSummary([slo]);
|
||||
const result = await summaryClient.computeSummary(slo);
|
||||
|
||||
expect(result[slo.id]).toMatchSnapshot();
|
||||
// @ts-ignore
|
||||
expect(esClientMock.msearch.mock.calls[0][0].searches).toEqual([
|
||||
{ index: SLO_DESTINATION_INDEX_PATTERN },
|
||||
{
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { 'slo.id': slo.id } },
|
||||
{ term: { 'slo.revision': slo.revision } },
|
||||
{
|
||||
range: {
|
||||
'@timestamp': { gte: expect.anything(), lt: expect.anything() },
|
||||
},
|
||||
expect(result).toMatchSnapshot();
|
||||
expect(esClientMock.search.mock.calls[0][0]).toEqual({
|
||||
index: SLO_DESTINATION_INDEX_PATTERN,
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { 'slo.id': slo.id } },
|
||||
{ term: { 'slo.revision': slo.revision } },
|
||||
{
|
||||
range: {
|
||||
'@timestamp': { gte: expect.anything(), lt: expect.anything() },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
good: { sum: { field: 'slo.numerator' } },
|
||||
total: { sum: { field: 'slo.denominator' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
aggs: {
|
||||
good: { sum: { field: 'slo.numerator' } },
|
||||
total: { sum: { field: 'slo.denominator' } },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -92,35 +89,32 @@ describe('SummaryClient', () => {
|
|||
esClientMock.msearch.mockResolvedValueOnce(createEsResponse());
|
||||
const summaryClient = new DefaultSummaryClient(esClientMock);
|
||||
|
||||
await summaryClient.fetchSummary([slo]);
|
||||
await summaryClient.computeSummary(slo);
|
||||
|
||||
// @ts-ignore
|
||||
expect(esClientMock.msearch.mock.calls[0][0].searches).toEqual([
|
||||
{ index: SLO_DESTINATION_INDEX_PATTERN },
|
||||
{
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { 'slo.id': slo.id } },
|
||||
{ term: { 'slo.revision': slo.revision } },
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: expect.anything(),
|
||||
lt: expect.anything(),
|
||||
},
|
||||
expect(esClientMock.search.mock.calls[0][0]).toEqual({
|
||||
index: SLO_DESTINATION_INDEX_PATTERN,
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { 'slo.id': slo.id } },
|
||||
{ term: { 'slo.revision': slo.revision } },
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: expect.anything(),
|
||||
lt: expect.anything(),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
good: { sum: { field: 'slo.numerator' } },
|
||||
total: { sum: { field: 'slo.denominator' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
aggs: {
|
||||
good: { sum: { field: 'slo.numerator' } },
|
||||
total: { sum: { field: 'slo.denominator' } },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -138,41 +132,38 @@ describe('SummaryClient', () => {
|
|||
esClientMock.msearch.mockResolvedValueOnce(createEsResponse());
|
||||
const summaryClient = new DefaultSummaryClient(esClientMock);
|
||||
|
||||
const result = await summaryClient.fetchSummary([slo]);
|
||||
const result = await summaryClient.computeSummary(slo);
|
||||
|
||||
expect(result[slo.id]).toMatchSnapshot();
|
||||
// @ts-ignore searches not typed properly
|
||||
expect(esClientMock.msearch.mock.calls[0][0].searches).toEqual([
|
||||
{ index: SLO_DESTINATION_INDEX_PATTERN },
|
||||
{
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { 'slo.id': slo.id } },
|
||||
{ term: { 'slo.revision': slo.revision } },
|
||||
{
|
||||
range: {
|
||||
'@timestamp': { gte: expect.anything(), lt: expect.anything() },
|
||||
},
|
||||
expect(result).toMatchSnapshot();
|
||||
expect(esClientMock.search.mock.calls[0][0]).toEqual({
|
||||
index: SLO_DESTINATION_INDEX_PATTERN,
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { 'slo.id': slo.id } },
|
||||
{ term: { 'slo.revision': slo.revision } },
|
||||
{
|
||||
range: {
|
||||
'@timestamp': { gte: expect.anything(), lt: expect.anything() },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
good: {
|
||||
sum: {
|
||||
field: 'slo.isGoodSlice',
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
good: {
|
||||
sum: {
|
||||
field: 'slo.isGoodSlice',
|
||||
},
|
||||
},
|
||||
total: {
|
||||
value_count: {
|
||||
field: 'slo.isGoodSlice',
|
||||
},
|
||||
total: {
|
||||
value_count: {
|
||||
field: 'slo.isGoodSlice',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -190,44 +181,42 @@ describe('SummaryClient', () => {
|
|||
esClientMock.msearch.mockResolvedValueOnce(createEsResponse());
|
||||
const summaryClient = new DefaultSummaryClient(esClientMock);
|
||||
|
||||
const result = await summaryClient.fetchSummary([slo]);
|
||||
const result = await summaryClient.computeSummary(slo);
|
||||
|
||||
expect(result[slo.id]).toMatchSnapshot();
|
||||
// @ts-ignore searches not typed properly
|
||||
expect(esClientMock.msearch.mock.calls[0][0].searches).toEqual([
|
||||
{ index: SLO_DESTINATION_INDEX_PATTERN },
|
||||
{
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { 'slo.id': slo.id } },
|
||||
{ term: { 'slo.revision': slo.revision } },
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: expect.anything(),
|
||||
lt: expect.anything(),
|
||||
},
|
||||
expect(result).toMatchSnapshot();
|
||||
|
||||
expect(esClientMock.search.mock.calls[0][0]).toEqual({
|
||||
index: SLO_DESTINATION_INDEX_PATTERN,
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { 'slo.id': slo.id } },
|
||||
{ term: { 'slo.revision': slo.revision } },
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: expect.anything(),
|
||||
lt: expect.anything(),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
good: {
|
||||
sum: {
|
||||
field: 'slo.isGoodSlice',
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
good: {
|
||||
sum: {
|
||||
field: 'slo.isGoodSlice',
|
||||
},
|
||||
},
|
||||
total: {
|
||||
value_count: {
|
||||
field: 'slo.isGoodSlice',
|
||||
},
|
||||
total: {
|
||||
value_count: {
|
||||
field: 'slo.isGoodSlice',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { MsearchMultisearchBody } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import {
|
||||
ALL_VALUE,
|
||||
calendarAlignedTimeWindowSchema,
|
||||
Duration,
|
||||
occurrencesBudgetingMethodSchema,
|
||||
|
@ -16,75 +16,96 @@ import {
|
|||
} from '@kbn/slo-schema';
|
||||
import moment from 'moment';
|
||||
import { SLO_DESTINATION_INDEX_PATTERN } from '../../assets/constants';
|
||||
import { DateRange, SLO, SLOId, Summary } from '../../domain/models';
|
||||
import { DateRange, SLO, Summary } from '../../domain/models';
|
||||
import { computeSLI, computeSummaryStatus, toErrorBudget } from '../../domain/services';
|
||||
import { toDateRange } from '../../domain/services/date_range';
|
||||
|
||||
// TODO: Change name of this service...
|
||||
// It does compute a summary but from the rollup data.
|
||||
export interface SummaryClient {
|
||||
fetchSummary(sloList: SLO[]): Promise<Record<SLOId, Summary>>;
|
||||
computeSummary(slo: SLO, instanceId?: string): Promise<Summary>;
|
||||
}
|
||||
|
||||
export class DefaultSummaryClient implements SummaryClient {
|
||||
constructor(private esClient: ElasticsearchClient) {}
|
||||
|
||||
async fetchSummary(sloList: SLO[]): Promise<Record<SLOId, Summary>> {
|
||||
const dateRangeBySlo = sloList.reduce<Record<SLOId, DateRange>>((acc, slo) => {
|
||||
acc[slo.id] = toDateRange(slo.timeWindow);
|
||||
return acc;
|
||||
}, {});
|
||||
const searches = sloList.flatMap((slo) => [
|
||||
{ index: SLO_DESTINATION_INDEX_PATTERN },
|
||||
generateSearchQuery(slo, dateRangeBySlo[slo.id]),
|
||||
]);
|
||||
async computeSummary(slo: SLO, instanceId: string = ALL_VALUE): Promise<Summary> {
|
||||
const dateRange = toDateRange(slo.timeWindow);
|
||||
const isDefinedWithGroupBy = slo.groupBy !== ALL_VALUE;
|
||||
const hasInstanceId = instanceId !== ALL_VALUE;
|
||||
const extraInstanceIdFilter =
|
||||
isDefinedWithGroupBy && hasInstanceId ? [{ term: { 'slo.instanceId': instanceId } }] : [];
|
||||
|
||||
const summaryBySlo: Record<SLOId, Summary> = {};
|
||||
if (searches.length === 0) {
|
||||
return summaryBySlo;
|
||||
const result = await this.esClient.search({
|
||||
index: SLO_DESTINATION_INDEX_PATTERN,
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { 'slo.id': slo.id } },
|
||||
{ term: { 'slo.revision': slo.revision } },
|
||||
{
|
||||
range: {
|
||||
'@timestamp': { gte: dateRange.from.toISOString(), lt: dateRange.to.toISOString() },
|
||||
},
|
||||
},
|
||||
...extraInstanceIdFilter,
|
||||
],
|
||||
},
|
||||
},
|
||||
...(occurrencesBudgetingMethodSchema.is(slo.budgetingMethod) && {
|
||||
aggs: {
|
||||
good: { sum: { field: 'slo.numerator' } },
|
||||
total: { sum: { field: 'slo.denominator' } },
|
||||
},
|
||||
}),
|
||||
...(timeslicesBudgetingMethodSchema.is(slo.budgetingMethod) && {
|
||||
aggs: {
|
||||
good: {
|
||||
sum: { field: 'slo.isGoodSlice' },
|
||||
},
|
||||
total: {
|
||||
value_count: { field: 'slo.isGoodSlice' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// @ts-ignore value is not type correctly
|
||||
const good = result.aggregations?.good?.value ?? 0;
|
||||
// @ts-ignore value is not type correctly
|
||||
const total = result.aggregations?.total?.value ?? 0;
|
||||
|
||||
const sliValue = computeSLI(good, total);
|
||||
const initialErrorBudget = 1 - slo.objective.target;
|
||||
let errorBudget;
|
||||
|
||||
if (
|
||||
calendarAlignedTimeWindowSchema.is(slo.timeWindow) &&
|
||||
timeslicesBudgetingMethodSchema.is(slo.budgetingMethod)
|
||||
) {
|
||||
const totalSlices = computeTotalSlicesFromDateRange(
|
||||
dateRange,
|
||||
slo.objective.timesliceWindow!
|
||||
);
|
||||
const consumedErrorBudget =
|
||||
sliValue < 0 ? 0 : (total - good) / (totalSlices * initialErrorBudget);
|
||||
|
||||
errorBudget = toErrorBudget(initialErrorBudget, consumedErrorBudget);
|
||||
} else {
|
||||
const consumedErrorBudget = sliValue < 0 ? 0 : (1 - sliValue) / initialErrorBudget;
|
||||
errorBudget = toErrorBudget(
|
||||
initialErrorBudget,
|
||||
consumedErrorBudget,
|
||||
calendarAlignedTimeWindowSchema.is(slo.timeWindow)
|
||||
);
|
||||
}
|
||||
|
||||
const result = await this.esClient.msearch({ searches });
|
||||
|
||||
for (let i = 0; i < result.responses.length; i++) {
|
||||
const slo = sloList[i];
|
||||
|
||||
// @ts-ignore
|
||||
const { aggregations = {} } = result.responses[i];
|
||||
const good = aggregations?.good?.value ?? 0;
|
||||
const total = aggregations?.total?.value ?? 0;
|
||||
|
||||
const sliValue = computeSLI(good, total);
|
||||
const initialErrorBudget = 1 - slo.objective.target;
|
||||
let errorBudget;
|
||||
|
||||
if (
|
||||
calendarAlignedTimeWindowSchema.is(slo.timeWindow) &&
|
||||
timeslicesBudgetingMethodSchema.is(slo.budgetingMethod)
|
||||
) {
|
||||
const totalSlices = computeTotalSlicesFromDateRange(
|
||||
dateRangeBySlo[slo.id],
|
||||
slo.objective.timesliceWindow!
|
||||
);
|
||||
const consumedErrorBudget =
|
||||
sliValue < 0 ? 0 : (total - good) / (totalSlices * initialErrorBudget);
|
||||
|
||||
errorBudget = toErrorBudget(initialErrorBudget, consumedErrorBudget);
|
||||
} else {
|
||||
const consumedErrorBudget = sliValue < 0 ? 0 : (1 - sliValue) / initialErrorBudget;
|
||||
errorBudget = toErrorBudget(
|
||||
initialErrorBudget,
|
||||
consumedErrorBudget,
|
||||
calendarAlignedTimeWindowSchema.is(slo.timeWindow)
|
||||
);
|
||||
}
|
||||
|
||||
summaryBySlo[slo.id] = {
|
||||
sliValue,
|
||||
errorBudget,
|
||||
status: computeSummaryStatus(slo, sliValue, errorBudget),
|
||||
};
|
||||
}
|
||||
|
||||
return summaryBySlo;
|
||||
return {
|
||||
sliValue,
|
||||
errorBudget,
|
||||
status: computeSummaryStatus(slo, sliValue, errorBudget),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,38 +116,3 @@ function computeTotalSlicesFromDateRange(dateRange: DateRange, timesliceWindow:
|
|||
);
|
||||
return Math.ceil(dateRangeDurationInUnit / timesliceWindow!.value);
|
||||
}
|
||||
|
||||
function generateSearchQuery(slo: SLO, dateRange: DateRange): MsearchMultisearchBody {
|
||||
return {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { 'slo.id': slo.id } },
|
||||
{ term: { 'slo.revision': slo.revision } },
|
||||
{
|
||||
range: {
|
||||
'@timestamp': { gte: dateRange.from.toISOString(), lt: dateRange.to.toISOString() },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
...(occurrencesBudgetingMethodSchema.is(slo.budgetingMethod) && {
|
||||
aggs: {
|
||||
good: { sum: { field: 'slo.numerator' } },
|
||||
total: { sum: { field: 'slo.denominator' } },
|
||||
},
|
||||
}),
|
||||
...(timeslicesBudgetingMethodSchema.is(slo.budgetingMethod) && {
|
||||
aggs: {
|
||||
good: {
|
||||
sum: { field: 'slo.isGoodSlice' },
|
||||
},
|
||||
total: {
|
||||
value_count: { field: 'slo.isGoodSlice' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { ElasticsearchClient, Logger } from '@kbn/core/server';
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
import { assertNever } from '@kbn/std';
|
||||
import _ from 'lodash';
|
||||
import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../assets/constants';
|
||||
|
@ -17,6 +18,7 @@ interface EsSummaryDocument {
|
|||
slo: {
|
||||
id: string;
|
||||
revision: number;
|
||||
instanceId: string;
|
||||
};
|
||||
sliValue: number;
|
||||
errorBudgetConsumed: number;
|
||||
|
@ -37,6 +39,7 @@ export interface Paginated<T> {
|
|||
|
||||
export interface SLOSummary {
|
||||
id: SLOId;
|
||||
instanceId: string;
|
||||
summary: Summary;
|
||||
}
|
||||
|
||||
|
@ -122,6 +125,7 @@ export class DefaultSummarySearchClient implements SummarySearchClient {
|
|||
page: pagination.page,
|
||||
results: finalResults.map((doc) => ({
|
||||
id: doc._source!.slo.id,
|
||||
instanceId: doc._source?.slo.instanceId ?? ALL_VALUE,
|
||||
summary: {
|
||||
errorBudget: {
|
||||
initial: toHighPrecision(doc._source!.errorBudgetInitial),
|
||||
|
|
|
@ -112,6 +112,11 @@ Array [
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
@ -335,6 +340,11 @@ Array [
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
@ -558,6 +568,11 @@ Array [
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
@ -794,6 +809,11 @@ Array [
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
@ -1045,6 +1065,11 @@ Array [
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
import { SLO } from '../../../../domain/models';
|
||||
|
||||
export function createTempSummaryDocument(slo: SLO) {
|
||||
|
@ -25,7 +26,8 @@ export function createTempSummaryDocument(slo: SLO) {
|
|||
duration: slo.timeWindow.duration.format(),
|
||||
type: slo.timeWindow.type,
|
||||
},
|
||||
instanceId: '*',
|
||||
groupBy: !!slo.groupBy ? slo.groupBy : ALL_VALUE,
|
||||
instanceId: ALL_VALUE,
|
||||
name: slo.name,
|
||||
description: slo.description,
|
||||
id: slo.id,
|
||||
|
|
|
@ -16,6 +16,11 @@ export const groupBy = {
|
|||
field: 'slo.revision',
|
||||
},
|
||||
},
|
||||
'slo.groupBy': {
|
||||
terms: {
|
||||
field: 'slo.groupBy',
|
||||
},
|
||||
},
|
||||
'slo.instanceId': {
|
||||
terms: {
|
||||
field: 'slo.instanceId',
|
||||
|
|
|
@ -200,6 +200,11 @@ Object {
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
@ -309,6 +314,11 @@ Object {
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
@ -413,6 +423,11 @@ Object {
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
@ -522,6 +537,11 @@ Object {
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
@ -650,6 +670,11 @@ Object {
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
@ -780,6 +805,12 @@ Object {
|
|||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"script": Object {
|
||||
"source": "emit('*')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.id": Object {
|
||||
"script": Object {
|
||||
"source": Any<String>,
|
||||
|
@ -918,6 +949,11 @@ Object {
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
@ -1043,6 +1079,12 @@ Object {
|
|||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"script": Object {
|
||||
"source": "emit('*')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.id": Object {
|
||||
"script": Object {
|
||||
"source": Any<String>,
|
||||
|
|
|
@ -188,6 +188,11 @@ Object {
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
@ -293,6 +298,11 @@ Object {
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
@ -393,6 +403,11 @@ Object {
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
@ -498,6 +513,11 @@ Object {
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
@ -619,6 +639,11 @@ Object {
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
@ -745,6 +770,12 @@ Object {
|
|||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"script": Object {
|
||||
"source": "emit('*')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.id": Object {
|
||||
"script": Object {
|
||||
"source": Any<String>,
|
||||
|
@ -876,6 +907,11 @@ Object {
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
@ -997,6 +1033,12 @@ Object {
|
|||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"script": Object {
|
||||
"source": "emit('*')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.id": Object {
|
||||
"script": Object {
|
||||
"source": Any<String>,
|
||||
|
|
|
@ -148,6 +148,11 @@ Object {
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
@ -230,6 +235,12 @@ Object {
|
|||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"script": Object {
|
||||
"source": "emit('*')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.id": Object {
|
||||
"script": Object {
|
||||
"source": Any<String>,
|
||||
|
@ -382,6 +393,11 @@ Object {
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
@ -459,6 +475,12 @@ Object {
|
|||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"script": Object {
|
||||
"source": "emit('*')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.id": Object {
|
||||
"script": Object {
|
||||
"source": Any<String>,
|
||||
|
|
|
@ -163,6 +163,11 @@ Object {
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
@ -245,6 +250,12 @@ Object {
|
|||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"script": Object {
|
||||
"source": "emit('*')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.id": Object {
|
||||
"script": Object {
|
||||
"source": Any<String>,
|
||||
|
@ -371,6 +382,11 @@ Object {
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
@ -448,6 +464,12 @@ Object {
|
|||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"script": Object {
|
||||
"source": "emit('*')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.id": Object {
|
||||
"script": Object {
|
||||
"source": Any<String>,
|
||||
|
|
|
@ -172,6 +172,11 @@ Object {
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
@ -254,6 +259,12 @@ Object {
|
|||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"script": Object {
|
||||
"source": "emit('*')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.id": Object {
|
||||
"script": Object {
|
||||
"source": Any<String>,
|
||||
|
@ -418,6 +429,11 @@ Object {
|
|||
"field": "slo.description",
|
||||
},
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.groupBy",
|
||||
},
|
||||
},
|
||||
"slo.id": Object {
|
||||
"terms": Object {
|
||||
"field": "slo.id",
|
||||
|
@ -495,6 +511,12 @@ Object {
|
|||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.groupBy": Object {
|
||||
"script": Object {
|
||||
"source": "emit('*')",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"slo.id": Object {
|
||||
"script": Object {
|
||||
"source": Any<String>,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
import {
|
||||
createAPMTransactionDurationIndicator,
|
||||
createSLO,
|
||||
|
@ -47,10 +48,10 @@ describe('APM Transaction Duration Transform Generator', () => {
|
|||
it("does not include the query filter when params are '*'", () => {
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionDurationIndicator({
|
||||
environment: '*',
|
||||
service: '*',
|
||||
transactionName: '*',
|
||||
transactionType: '*',
|
||||
environment: ALL_VALUE,
|
||||
service: ALL_VALUE,
|
||||
transactionName: ALL_VALUE,
|
||||
transactionType: ALL_VALUE,
|
||||
}),
|
||||
});
|
||||
const transform = generator.getTransformParams(slo);
|
||||
|
@ -86,9 +87,9 @@ describe('APM Transaction Duration Transform Generator', () => {
|
|||
const slo = createSLO({
|
||||
indicator: createAPMTransactionDurationIndicator({
|
||||
service: 'my-service',
|
||||
environment: '*',
|
||||
transactionName: '*',
|
||||
transactionType: '*',
|
||||
environment: ALL_VALUE,
|
||||
transactionName: ALL_VALUE,
|
||||
transactionType: ALL_VALUE,
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -101,10 +102,10 @@ describe('APM Transaction Duration Transform Generator', () => {
|
|||
it("groups by the 'service.environment'", () => {
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionDurationIndicator({
|
||||
service: '*',
|
||||
service: ALL_VALUE,
|
||||
environment: 'production',
|
||||
transactionName: '*',
|
||||
transactionType: '*',
|
||||
transactionName: ALL_VALUE,
|
||||
transactionType: ALL_VALUE,
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -117,10 +118,10 @@ describe('APM Transaction Duration Transform Generator', () => {
|
|||
it("groups by the 'transaction.name'", () => {
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionDurationIndicator({
|
||||
service: '*',
|
||||
environment: '*',
|
||||
service: ALL_VALUE,
|
||||
environment: ALL_VALUE,
|
||||
transactionName: 'GET /foo',
|
||||
transactionType: '*',
|
||||
transactionType: ALL_VALUE,
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -133,9 +134,9 @@ describe('APM Transaction Duration Transform Generator', () => {
|
|||
it("groups by the 'transaction.type'", () => {
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionDurationIndicator({
|
||||
service: '*',
|
||||
environment: '*',
|
||||
transactionName: '*',
|
||||
service: ALL_VALUE,
|
||||
environment: ALL_VALUE,
|
||||
transactionName: ALL_VALUE,
|
||||
transactionType: 'request',
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
import {
|
||||
createAPMTransactionErrorRateIndicator,
|
||||
createSLO,
|
||||
|
@ -47,10 +48,10 @@ describe('APM Transaction Error Rate Transform Generator', () => {
|
|||
it("does not include the query filter when params are '*'", async () => {
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionErrorRateIndicator({
|
||||
environment: '*',
|
||||
service: '*',
|
||||
transactionName: '*',
|
||||
transactionType: '*',
|
||||
environment: ALL_VALUE,
|
||||
service: ALL_VALUE,
|
||||
transactionName: ALL_VALUE,
|
||||
transactionType: ALL_VALUE,
|
||||
}),
|
||||
});
|
||||
const transform = generator.getTransformParams(slo);
|
||||
|
@ -86,9 +87,9 @@ describe('APM Transaction Error Rate Transform Generator', () => {
|
|||
const slo = createSLO({
|
||||
indicator: createAPMTransactionErrorRateIndicator({
|
||||
service: 'my-service',
|
||||
environment: '*',
|
||||
transactionName: '*',
|
||||
transactionType: '*',
|
||||
environment: ALL_VALUE,
|
||||
transactionName: ALL_VALUE,
|
||||
transactionType: ALL_VALUE,
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -101,10 +102,10 @@ describe('APM Transaction Error Rate Transform Generator', () => {
|
|||
it("groups by the 'service.environment'", () => {
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionErrorRateIndicator({
|
||||
service: '*',
|
||||
service: ALL_VALUE,
|
||||
environment: 'production',
|
||||
transactionName: '*',
|
||||
transactionType: '*',
|
||||
transactionName: ALL_VALUE,
|
||||
transactionType: ALL_VALUE,
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -117,10 +118,10 @@ describe('APM Transaction Error Rate Transform Generator', () => {
|
|||
it("groups by the 'transaction.name'", () => {
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionErrorRateIndicator({
|
||||
service: '*',
|
||||
environment: '*',
|
||||
service: ALL_VALUE,
|
||||
environment: ALL_VALUE,
|
||||
transactionName: 'GET /foo',
|
||||
transactionType: '*',
|
||||
transactionType: ALL_VALUE,
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -133,9 +134,9 @@ describe('APM Transaction Error Rate Transform Generator', () => {
|
|||
it("groups by the 'transaction.type'", () => {
|
||||
const slo = createSLO({
|
||||
indicator: createAPMTransactionErrorRateIndicator({
|
||||
service: '*',
|
||||
environment: '*',
|
||||
transactionName: '*',
|
||||
service: ALL_VALUE,
|
||||
environment: ALL_VALUE,
|
||||
transactionName: ALL_VALUE,
|
||||
transactionType: 'request',
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -17,6 +17,8 @@ export abstract class TransformGenerator {
|
|||
public abstract getTransformParams(slo: SLO): TransformPutTransformRequest;
|
||||
|
||||
public buildCommonRuntimeMappings(slo: SLO): MappingRuntimeFields {
|
||||
const mustIncludeAllInstanceId = slo.groupBy === ALL_VALUE || slo.groupBy === '';
|
||||
|
||||
return {
|
||||
'slo.id': {
|
||||
type: 'keyword',
|
||||
|
@ -30,12 +32,20 @@ export abstract class TransformGenerator {
|
|||
source: `emit(${slo.revision})`,
|
||||
},
|
||||
},
|
||||
'slo.instanceId': {
|
||||
'slo.groupBy': {
|
||||
type: 'keyword',
|
||||
script: {
|
||||
source: `emit('${ALL_VALUE}')`,
|
||||
source: `emit('${!!slo.groupBy ? slo.groupBy : ALL_VALUE}')`,
|
||||
},
|
||||
},
|
||||
...(mustIncludeAllInstanceId && {
|
||||
'slo.instanceId': {
|
||||
type: 'keyword',
|
||||
script: {
|
||||
source: `emit('${ALL_VALUE}')`,
|
||||
},
|
||||
},
|
||||
}),
|
||||
'slo.name': {
|
||||
type: 'keyword',
|
||||
script: {
|
||||
|
@ -109,10 +119,14 @@ export abstract class TransformGenerator {
|
|||
fixedInterval = slo.objective.timesliceWindow!.format();
|
||||
}
|
||||
|
||||
const instanceIdField =
|
||||
slo.groupBy !== '' && slo.groupBy !== ALL_VALUE ? slo.groupBy : 'slo.instanceId';
|
||||
|
||||
return {
|
||||
'slo.id': { terms: { field: 'slo.id' } },
|
||||
'slo.revision': { terms: { field: 'slo.revision' } },
|
||||
'slo.instanceId': { terms: { field: 'slo.instanceId' } },
|
||||
'slo.groupBy': { terms: { field: 'slo.groupBy' } },
|
||||
'slo.instanceId': { terms: { field: instanceIdField } },
|
||||
'slo.name': { terms: { field: 'slo.name' } },
|
||||
'slo.description': { terms: { field: 'slo.description' } },
|
||||
'slo.tags': { terms: { field: 'slo.tags' } },
|
||||
|
|
|
@ -31,6 +31,7 @@ export class UpdateSLO {
|
|||
const updatedSlo: SLO = Object.assign({}, originalSlo, params, {
|
||||
updatedAt: new Date(),
|
||||
revision: originalSlo.revision + 1,
|
||||
groupBy: !!params.groupBy ? params.groupBy : originalSlo.groupBy,
|
||||
});
|
||||
|
||||
validateSLO(updatedSlo);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue