mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[SLO] allow multi group by in creation form (#175063)
## Summary Enables grouping SLOs by multiple fields. Resolves https://github.com/elastic/kibana/issues/174228 Adjusts the `instanceId` [implementation](https://github.com/elastic/kibana/pull/175063/files#diff-c9c4989cf8d323448464b70825408b535b1fa6cc355df26a2acfba45d8c05232R35) to concat values from each field to build the instance id. Uses the `groupings` key from the [summary](https://github.com/elastic/kibana/pull/175063/files#diff-40b886fca239a397d0990f3db135f7b35822ee0aa93063be219bd23cafc9be6cR137) [documents](https://github.com/elastic/kibana/pull/175063/files#diff-d5cd07fb8fb91091a7f65d9f59c268600f03305167b375ec579773144428ee68R156) to display group by values in the SLO list and SLO detail pages. ### Testing 1. Before checking out this PR, create an SLO on main with a group by 2. Check out this PR. The instance information should continue to populate, now with the field label <img width="364" alt="Screenshot 2024-02-05 at 9 50 12 PM" src="d6004cc6
-58b8-4319-b28b-b09e7849deba"> <img width="1427" alt="Screenshot 2024-02-05 at 9 50 26 PM" src="10aacc47
-ae68-489d-b728-6f74816e7c69"> <img width="583" alt="Screenshot 2024-02-05 at 9 50 38 PM" src="50bcc168
-bdb7-421b-8a6f-9fd0b079b612"> 3. Navigate to the edit flow. The group by field should appear in the ComboBox. <img width="803" alt="Screenshot 2024-02-05 at 9 56 28 PM" src="c9601e61
-85b9-49ad-adc0-0ebfad67701e"> 4. Attempt to create a second group by <img width="815" alt="Screenshot 2024-02-05 at 10 57 38 PM" src="e461681c
-720f-4290-a01c-0cd00c9cbb72"> 5. Save and observe the created instances. <img width="716" alt="Screenshot 2024-02-06 at 10 39 56 AM" src="46599aaa
-ac93-4362-943a-4579b2e8c552"> <img width="590" alt="Screenshot 2024-02-06 at 11 24 52 AM" src="ef9d65fb
-301c-446d-a748-e82d8460423c"> <img width="666" alt="Screenshot 2024-02-06 at 11 25 08 AM" src="6713ba80
-6a2a-4087-9335-cfa935bf0ebf"> <img width="1449" alt="Screenshot 2024-02-06 at 11 25 20 AM" src="c7088ab3
-847f-436e-8918-2a5f15593ac4"> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
8122a3ef64
commit
882682b6bf
51 changed files with 909 additions and 310 deletions
|
@ -14,6 +14,7 @@ import {
|
|||
budgetingMethodSchema,
|
||||
dateType,
|
||||
durationType,
|
||||
groupingsSchema,
|
||||
histogramIndicatorSchema,
|
||||
historicalSummarySchema,
|
||||
indicatorSchema,
|
||||
|
@ -34,6 +35,7 @@ import {
|
|||
timesliceMetricBasicMetricWithField,
|
||||
timesliceMetricDocCountMetric,
|
||||
timesliceMetricPercentileMetric,
|
||||
allOrAnyStringOrArray,
|
||||
kqlWithFiltersSchema,
|
||||
querySchema,
|
||||
} from '../schema';
|
||||
|
@ -52,7 +54,7 @@ const createSLOParamsSchema = t.type({
|
|||
id: sloIdSchema,
|
||||
settings: optionalSettingsSchema,
|
||||
tags: tagsSchema,
|
||||
groupBy: allOrAnyString,
|
||||
groupBy: allOrAnyStringOrArray,
|
||||
revision: t.number,
|
||||
}),
|
||||
]),
|
||||
|
@ -75,6 +77,7 @@ const getPreviewDataParamsSchema = t.type({
|
|||
objective: objectiveSchema,
|
||||
instanceId: t.string,
|
||||
groupBy: t.string,
|
||||
groupings: t.record(t.string, t.unknown),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
@ -136,7 +139,7 @@ const sloResponseSchema = t.intersection([
|
|||
settings: settingsSchema,
|
||||
enabled: t.boolean,
|
||||
tags: tagsSchema,
|
||||
groupBy: allOrAnyString,
|
||||
groupBy: allOrAnyStringOrArray,
|
||||
createdAt: dateType,
|
||||
updatedAt: dateType,
|
||||
version: t.number,
|
||||
|
@ -148,7 +151,7 @@ const sloResponseSchema = t.intersection([
|
|||
|
||||
const sloWithSummaryResponseSchema = t.intersection([
|
||||
sloResponseSchema,
|
||||
t.type({ summary: summarySchema }),
|
||||
t.type({ summary: summarySchema, groupings: groupingsSchema }),
|
||||
]);
|
||||
|
||||
const sloGroupWithSummaryResponseSchema = t.type({
|
||||
|
@ -186,7 +189,7 @@ const updateSLOParamsSchema = t.type({
|
|||
objective: objectiveSchema,
|
||||
settings: optionalSettingsSchema,
|
||||
tags: tagsSchema,
|
||||
groupBy: allOrAnyString,
|
||||
groupBy: allOrAnyStringOrArray,
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -221,7 +224,9 @@ const deleteSLOInstancesParamsSchema = t.type({
|
|||
});
|
||||
|
||||
const fetchHistoricalSummaryParamsSchema = t.type({
|
||||
body: t.type({ list: t.array(t.type({ sloId: sloIdSchema, instanceId: allOrAnyString })) }),
|
||||
body: t.type({
|
||||
list: t.array(t.type({ sloId: sloIdSchema, instanceId: t.string })),
|
||||
}),
|
||||
});
|
||||
|
||||
const fetchHistoricalSummaryResponseSchema = t.array(
|
||||
|
@ -276,7 +281,7 @@ const getSLOInstancesParamsSchema = t.type({
|
|||
});
|
||||
|
||||
const getSLOInstancesResponseSchema = t.type({
|
||||
groupBy: t.string,
|
||||
groupBy: t.union([t.string, t.array(t.string)]),
|
||||
instances: t.array(t.string),
|
||||
});
|
||||
|
||||
|
|
|
@ -12,6 +12,12 @@ const ALL_VALUE = '*';
|
|||
|
||||
const allOrAnyString = t.union([t.literal(ALL_VALUE), t.string]);
|
||||
|
||||
const allOrAnyStringOrArray = t.union([
|
||||
t.literal(ALL_VALUE),
|
||||
t.string,
|
||||
t.array(t.union([t.literal(ALL_VALUE), t.string])),
|
||||
]);
|
||||
|
||||
const dateType = new t.Type<Date, string, unknown>(
|
||||
'DateType',
|
||||
(input: unknown): input is Date => input instanceof Date,
|
||||
|
@ -43,6 +49,8 @@ const summarySchema = t.type({
|
|||
errorBudget: errorBudgetSchema,
|
||||
});
|
||||
|
||||
const groupingsSchema = t.record(t.string, t.union([t.string, t.number]));
|
||||
|
||||
const groupSummarySchema = t.type({
|
||||
total: t.number,
|
||||
worst: t.type({
|
||||
|
@ -115,9 +123,11 @@ const querySchema = t.union([kqlQuerySchema, kqlWithFiltersSchema]);
|
|||
export {
|
||||
ALL_VALUE,
|
||||
allOrAnyString,
|
||||
allOrAnyStringOrArray,
|
||||
dateRangeSchema,
|
||||
dateType,
|
||||
errorBudgetSchema,
|
||||
groupingsSchema,
|
||||
historicalSummarySchema,
|
||||
previewDataSchema,
|
||||
statusSchema,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { allOrAnyString, dateType, summarySchema } from './common';
|
||||
import { allOrAnyStringOrArray, dateType, summarySchema, groupingsSchema } from './common';
|
||||
import { durationType } from './duration';
|
||||
import { indicatorSchema } from './indicators';
|
||||
import { timeWindowSchema } from './time_window';
|
||||
|
@ -49,11 +49,14 @@ const sloSchema = t.type({
|
|||
tags: tagsSchema,
|
||||
createdAt: dateType,
|
||||
updatedAt: dateType,
|
||||
groupBy: allOrAnyString,
|
||||
groupBy: allOrAnyStringOrArray,
|
||||
version: t.number,
|
||||
});
|
||||
|
||||
const sloWithSummarySchema = t.intersection([sloSchema, t.type({ summary: summarySchema })]);
|
||||
const sloWithSummarySchema = t.intersection([
|
||||
sloSchema,
|
||||
t.type({ summary: summarySchema, groupings: groupingsSchema }),
|
||||
]);
|
||||
|
||||
export {
|
||||
budgetingMethodSchema,
|
||||
|
|
|
@ -18,6 +18,7 @@ import { ValidationBurnRateRuleResult } from './validation';
|
|||
import { createNewWindow, Windows } from './windows';
|
||||
import { BURN_RATE_DEFAULTS } from './constants';
|
||||
import { AlertTimeTable } from './alert_time_table';
|
||||
import { getGroupKeysProse } from '../../utils/slo/groupings';
|
||||
|
||||
type Props = Pick<
|
||||
RuleTypeParamsExpressionProps<BurnRateRuleParams>,
|
||||
|
@ -67,7 +68,7 @@ export function BurnRateRuleEditor(props: Props) {
|
|||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<SloSelector initialSlo={selectedSlo} onSelected={onSelectedSlo} errors={errors.sloId} />
|
||||
{selectedSlo?.groupBy && selectedSlo.groupBy !== ALL_VALUE && (
|
||||
{selectedSlo?.groupBy && ![selectedSlo.groupBy].flat().includes(ALL_VALUE) && (
|
||||
<>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiCallOut
|
||||
|
@ -75,8 +76,8 @@ export function BurnRateRuleEditor(props: Props) {
|
|||
size="s"
|
||||
title={i18n.translate('xpack.observability.slo.rules.groupByMessage', {
|
||||
defaultMessage:
|
||||
'The SLO you selected has been created with a group-by on "{groupByField}". This rule will monitor and generate an alert for every instance found in the group-by field.',
|
||||
values: { groupByField: selectedSlo.groupBy },
|
||||
'The SLO you selected has been created with a group-by on {groupByField}. This rule will monitor and generate an alert for every instance found in the group-by field.',
|
||||
values: { groupByField: getGroupKeysProse(selectedSlo.groupBy) },
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -9,6 +9,7 @@ import { EuiConfirmModal } from '@elastic/eui';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { ALL_VALUE, SLOResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import React from 'react';
|
||||
import { getGroupKeysProse } from '../../../utils/slo/groupings';
|
||||
|
||||
export interface SloDeleteConfirmationModalProps {
|
||||
slo: SLOWithSummaryResponse | SLOResponse;
|
||||
|
@ -44,8 +45,8 @@ export function SloDeleteConfirmationModal({
|
|||
{groupBy !== ALL_VALUE
|
||||
? i18n.translate('xpack.observability.slo.deleteConfirmationModal.groupByDisclaimerText', {
|
||||
defaultMessage:
|
||||
'This SLO has been generated with a group key on "{groupKey}". Deleting this SLO definition will result in all instances being deleted.',
|
||||
values: { groupKey: groupBy },
|
||||
'This SLO has been generated with a group key on {groupKey}. Deleting this SLO definition will result in all instances being deleted.',
|
||||
values: { groupKey: getGroupKeysProse(slo.groupBy) },
|
||||
})
|
||||
: i18n.translate('xpack.observability.slo.deleteConfirmationModal.descriptionText', {
|
||||
defaultMessage: "You can't recover this SLO after deleting it.",
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiBadge, EuiBadgeProps, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import React from 'react';
|
||||
import { euiLightVars } from '@kbn/ui-theme';
|
||||
|
||||
export interface Props {
|
||||
color?: EuiBadgeProps['color'];
|
||||
slo: SLOWithSummaryResponse;
|
||||
}
|
||||
|
||||
export function SloGroupByBadge({ slo, color }: Props) {
|
||||
if (!slo.groupBy || slo.groupBy === ALL_VALUE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color={color ?? euiLightVars.euiColorDisabled}>
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={i18n.translate('xpack.observability.slo.groupByBadge', {
|
||||
defaultMessage: 'Group by {groupKey}',
|
||||
values: {
|
||||
groupKey: slo.groupBy,
|
||||
},
|
||||
})}
|
||||
display="block"
|
||||
>
|
||||
<span>
|
||||
{slo.groupBy}: {slo.instanceId}
|
||||
</span>
|
||||
</EuiToolTip>
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
|
@ -63,6 +63,7 @@ const baseSlo: Omit<SLOWithSummaryResponse, 'id'> = {
|
|||
},
|
||||
},
|
||||
groupBy: ALL_VALUE,
|
||||
groupings: {},
|
||||
instanceId: ALL_VALUE,
|
||||
tags: ['k8s', 'production', 'critical'],
|
||||
enabled: true,
|
||||
|
|
|
@ -27,8 +27,7 @@ const SLO_REQUIRED = i18n.translate('xpack.observability.sloEmbeddable.config.er
|
|||
export function SloSelector({ initialSlos, onSelected, hasError, singleSelection }: Props) {
|
||||
const mapSlosToOptions = (slos: SloItem[] | SLOWithSummaryResponse[] | undefined) =>
|
||||
slos?.map((slo) => ({
|
||||
label:
|
||||
slo.instanceId !== ALL_VALUE ? `${slo.name} (${slo.groupBy}: ${slo.instanceId})` : slo.name,
|
||||
label: slo.instanceId !== ALL_VALUE ? `${slo.name} (${slo.instanceId})` : slo.name,
|
||||
value: `${slo.id}-${slo.instanceId}`,
|
||||
})) ?? [];
|
||||
const [options, setOptions] = useState<Array<EuiComboBoxOptionOption<string>>>([]);
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
SloDetails,
|
||||
SloTabId,
|
||||
} from '../../../pages/slo_details/components/slo_details';
|
||||
import { SLOGroupings } from '../../../pages/slos/components/common/slo_groupings';
|
||||
|
||||
export function SloOverviewDetails({
|
||||
slo,
|
||||
|
@ -65,6 +66,7 @@ export function SloOverviewDetails({
|
|||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<SLOGroupings slo={slo} />
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<HeaderTitle slo={slo} isLoading={false} showTitle={false} />
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLoadingChart, EuiLink, EuiTitle } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLoadingChart, EuiLink } from '@elastic/eui';
|
||||
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
|
@ -19,6 +19,7 @@ import { ErrorBudgetChart } from '../../../pages/slo_details/components/error_bu
|
|||
import { EmbeddableSloProps } from './types';
|
||||
import { SloOverviewDetails } from '../common/slo_overview_details'; // TODO change to slo_details
|
||||
import { ErrorBudgetHeader } from '../../../pages/slo_details/components/error_budget_header';
|
||||
import { SLOGroupings } from '../../../pages/slos/components/common/slo_groupings';
|
||||
|
||||
export function SloErrorBudget({
|
||||
sloId,
|
||||
|
@ -106,33 +107,20 @@ export function SloErrorBudget({
|
|||
<div ref={containerRef} style={{ width: '100%', padding: 10 }}>
|
||||
<EuiFlexGroup direction="column" gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
{hasGroupBy ? (
|
||||
<EuiTitle size="xs">
|
||||
<h4>{slo.name}</h4>
|
||||
</EuiTitle>
|
||||
) : (
|
||||
<EuiLink
|
||||
css={{ fontSize: '16px' }}
|
||||
data-test-subj="o11ySloErrorBudgetLink"
|
||||
onClick={() => {
|
||||
setSelectedSlo(slo);
|
||||
}}
|
||||
>
|
||||
<h4>{slo.name}</h4>
|
||||
</EuiLink>
|
||||
)}
|
||||
<EuiLink
|
||||
css={{ fontSize: '16px' }}
|
||||
data-test-subj="o11ySloErrorBudgetLink"
|
||||
onClick={() => {
|
||||
setSelectedSlo(slo);
|
||||
}}
|
||||
>
|
||||
<h4>{slo.name}</h4>
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
|
||||
{hasGroupBy && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink
|
||||
data-test-subj="o11ySloErrorBudgetLink"
|
||||
onClick={() => {
|
||||
setSelectedSlo(slo);
|
||||
}}
|
||||
>
|
||||
{slo.groupBy}: {slo.instanceId}
|
||||
</EuiLink>
|
||||
<SLOGroupings slo={slo} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -47,8 +47,11 @@ export const sloKeys = {
|
|||
globalDiagnosis: () => [...sloKeys.all, 'globalDiagnosis'] as const,
|
||||
burnRates: (sloId: string, instanceId: string | undefined) =>
|
||||
[...sloKeys.all, 'burnRates', sloId, instanceId] as const,
|
||||
preview: (indicator: Indicator, range: { start: number; end: number }) =>
|
||||
[...sloKeys.all, 'preview', indicator, range] as const,
|
||||
preview: (
|
||||
indicator: Indicator,
|
||||
range: { start: number; end: number },
|
||||
groupings?: Record<string, unknown>
|
||||
) => [...sloKeys.all, 'preview', indicator, range, groupings] as const,
|
||||
};
|
||||
|
||||
export type SloKeys = typeof sloKeys;
|
||||
|
|
|
@ -19,10 +19,18 @@ export interface UseFetchIndexPatternFieldsResponse {
|
|||
|
||||
const HIGH_CARDINALITY_THRESHOLD = 1000;
|
||||
|
||||
const buildInstanceId = (groupBy: string | string[]): string => {
|
||||
const groups = [groupBy].flat().filter((value) => !!value);
|
||||
const groupings = groups.map((group) => `'${group}:'+doc['${group}'].value`).join(`+'|'+`);
|
||||
|
||||
const hasAllGroupings = groups.map((group) => `doc['${group}'].size() > 0`).join(' && ');
|
||||
return `if (${hasAllGroupings}) { emit(${groupings}) }`;
|
||||
};
|
||||
|
||||
export function useFetchGroupByCardinality(
|
||||
indexPattern: string,
|
||||
timestampField: string,
|
||||
groupBy: string
|
||||
groupBy: string | string[]
|
||||
): UseFetchIndexPatternFieldsResponse {
|
||||
const { data: dataService } = useKibana().services;
|
||||
|
||||
|
@ -40,10 +48,16 @@ export function useFetchGroupByCardinality(
|
|||
filter: [{ range: { [timestampField]: { gte: 'now-24h' } } }],
|
||||
},
|
||||
},
|
||||
runtime_mappings: {
|
||||
group_combinations: {
|
||||
type: 'keyword',
|
||||
script: buildInstanceId(groupBy),
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
groupByCardinality: {
|
||||
cardinality: {
|
||||
field: groupBy,
|
||||
field: 'group_combinations',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -24,11 +24,13 @@ export function useGetPreviewData({
|
|||
indicator,
|
||||
objective,
|
||||
groupBy,
|
||||
groupings,
|
||||
instanceId,
|
||||
}: {
|
||||
isValid: boolean;
|
||||
groupBy?: string;
|
||||
instanceId?: string;
|
||||
groupings?: Record<string, unknown>;
|
||||
objective?: Objective;
|
||||
indicator: Indicator;
|
||||
range: { start: number; end: number };
|
||||
|
@ -36,7 +38,7 @@ export function useGetPreviewData({
|
|||
const { http } = useKibana().services;
|
||||
|
||||
const { isInitialLoading, isLoading, isError, isSuccess, data } = useQuery({
|
||||
queryKey: sloKeys.preview(indicator, range),
|
||||
queryKey: sloKeys.preview(indicator, range, groupings),
|
||||
queryFn: async ({ signal }) => {
|
||||
const response = await http.post<GetPreviewDataResponse>(
|
||||
'/internal/observability/slos/_preview',
|
||||
|
@ -46,6 +48,7 @@ export function useGetPreviewData({
|
|||
range,
|
||||
groupBy,
|
||||
instanceId,
|
||||
groupings,
|
||||
...(objective ? { objective } : null),
|
||||
}),
|
||||
signal,
|
||||
|
|
|
@ -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,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',version:2)"
|
||||
"/slos/edit/foo?_a=(budgetingMethod:occurrences,createdAt:'2022-12-29T10:11:12.000Z',description:'some%20description%20useful',enabled:!t,groupBy:'*',groupings:(),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',version:2)"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
AnnotationDomainType,
|
||||
AreaSeries,
|
||||
|
@ -61,7 +60,7 @@ export function EventsChartPanel({ slo, range }: Props) {
|
|||
range,
|
||||
isValid: true,
|
||||
indicator: slo.indicator,
|
||||
groupBy: slo.groupBy,
|
||||
groupings: slo.groupings,
|
||||
instanceId: slo.instanceId,
|
||||
});
|
||||
|
||||
|
|
|
@ -10,7 +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 { SLOGroupings } from '../../slos/components/common/slo_groupings';
|
||||
import { SloStatusBadge } from '../../../components/slo/slo_status_badge';
|
||||
|
||||
export interface Props {
|
||||
|
@ -32,22 +32,25 @@ export function HeaderTitle(props: Props) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
{showTitle && <EuiFlexItem grow={false}>{slo.name}</EuiFlexItem>}
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
justifyContent="flexStart"
|
||||
responsive={false}
|
||||
>
|
||||
<SloStatusBadge slo={slo} />
|
||||
<SloGroupByBadge slo={slo} />
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup direction="column" gutterSize="xs">
|
||||
{showTitle && (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>{slo.name}</EuiFlexItem>
|
||||
<SLOGroupings slo={slo} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
justifyContent="flexStart"
|
||||
responsive={false}
|
||||
>
|
||||
<SloStatusBadge slo={slo} />
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color="subdued" size="xs">
|
||||
|
|
|
@ -15,6 +15,7 @@ import { OptionalText } from './optional_text';
|
|||
import { useFetchGroupByCardinality } from '../../../../hooks/slo/use_fetch_group_by_cardinality';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { IndexFieldSelector } from './index_field_selector';
|
||||
import { getGroupKeysProse } from '../../../../utils/slo/groupings';
|
||||
|
||||
export function GroupByField({ dataView, isLoading }: { dataView?: DataView; isLoading: boolean }) {
|
||||
const { watch } = useFormContext<CreateSLOForm>();
|
||||
|
@ -60,8 +61,11 @@ export function GroupByField({ dataView, isLoading }: { dataView?: DataView; isL
|
|||
color={groupByCardinality.isHighCardinality ? 'warning' : 'primary'}
|
||||
title={i18n.translate('xpack.observability.slo.sloEdit.groupBy.cardinalityInfo', {
|
||||
defaultMessage:
|
||||
"Selected group by field '{groupBy}' will generate at least {card} SLO instances based on the last 24h sample data.",
|
||||
values: { card: groupByCardinality.cardinality, groupBy: groupByField },
|
||||
'Selected group by field {groupBy} will generate at least {card} SLO instances based on the last 24h sample data.',
|
||||
values: {
|
||||
card: groupByCardinality.cardinality,
|
||||
groupBy: getGroupKeysProse(groupByField),
|
||||
},
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -41,47 +41,55 @@ export function IndexFieldSelector({
|
|||
setOptions(createOptionsFromFields(indexFields));
|
||||
}, [indexFields]);
|
||||
|
||||
const getSelectedItems = (value: string | string[], fields: FieldSpec[]) => {
|
||||
const values = [value].flat();
|
||||
const selectedItems: Array<EuiComboBoxOptionOption<string>> = [];
|
||||
fields.forEach((field) => {
|
||||
if (values.includes(field.name)) {
|
||||
selectedItems.push({ value: field.name, label: field.name });
|
||||
}
|
||||
});
|
||||
return selectedItems;
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow label={label} isInvalid={getFieldState(name).invalid} labelAppend={labelAppend}>
|
||||
<Controller
|
||||
defaultValue={defaultValue}
|
||||
defaultValue={[defaultValue].flat()}
|
||||
name={name}
|
||||
control={control}
|
||||
rules={{ required: isRequired && !isDisabled }}
|
||||
render={({ field, fieldState }) => (
|
||||
<EuiComboBox<string>
|
||||
{...field}
|
||||
async
|
||||
placeholder={placeholder}
|
||||
aria-label={placeholder}
|
||||
isClearable
|
||||
isDisabled={isLoading || isDisabled}
|
||||
isInvalid={fieldState.invalid}
|
||||
isLoading={isLoading}
|
||||
onChange={(selected: EuiComboBoxOptionOption[]) => {
|
||||
if (selected.length) {
|
||||
return field.onChange(selected[0].value);
|
||||
}
|
||||
render={({ field, fieldState }) => {
|
||||
return (
|
||||
<EuiComboBox<string>
|
||||
{...field}
|
||||
async
|
||||
placeholder={placeholder}
|
||||
aria-label={placeholder}
|
||||
isClearable
|
||||
isDisabled={isLoading || isDisabled}
|
||||
isInvalid={fieldState.invalid}
|
||||
isLoading={isLoading}
|
||||
onChange={(selected: EuiComboBoxOptionOption[]) => {
|
||||
if (selected.length) {
|
||||
return field.onChange(selected.map((selection) => selection.value));
|
||||
}
|
||||
|
||||
field.onChange(defaultValue);
|
||||
}}
|
||||
options={options}
|
||||
onSearchChange={(searchValue: string) => {
|
||||
setOptions(
|
||||
createOptionsFromFields(indexFields, ({ value }) => value.includes(searchValue))
|
||||
);
|
||||
}}
|
||||
selectedOptions={
|
||||
!!indexFields &&
|
||||
!!field.value &&
|
||||
indexFields.some((indexField) => indexField.name === field.value)
|
||||
? [{ value: field.value, label: field.value }]
|
||||
: []
|
||||
}
|
||||
singleSelection
|
||||
/>
|
||||
)}
|
||||
field.onChange(defaultValue);
|
||||
}}
|
||||
options={options}
|
||||
onSearchChange={(searchValue: string) => {
|
||||
setOptions(
|
||||
createOptionsFromFields(indexFields, ({ value }) => value.includes(searchValue))
|
||||
);
|
||||
}}
|
||||
selectedOptions={
|
||||
!!indexFields && !!field.value ? getSelectedItems(field.value, indexFields) : []
|
||||
}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -46,7 +46,7 @@ export function transformSloResponseToCreateSloForm(
|
|||
timesliceWindow: String(toDuration(values.objective.timesliceWindow).value),
|
||||
}),
|
||||
},
|
||||
groupBy: values.groupBy,
|
||||
groupBy: [values.groupBy].flat(),
|
||||
tags: values.tags,
|
||||
};
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ export function transformCreateSLOFormToCreateSLOInput(values: CreateSLOForm): C
|
|||
}),
|
||||
},
|
||||
tags: values.tags,
|
||||
groupBy: values.groupBy,
|
||||
groupBy: [values.groupBy].flat(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -99,7 +99,7 @@ export function transformValuesToUpdateSLOInput(values: CreateSLOForm): UpdateSL
|
|||
}),
|
||||
},
|
||||
tags: values.tags,
|
||||
groupBy: values.groupBy,
|
||||
groupBy: [values.groupBy].flat(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -185,7 +185,7 @@ export function transformPartialUrlStateToFormState(
|
|||
}
|
||||
|
||||
if (values.groupBy) {
|
||||
state.groupBy = values.groupBy;
|
||||
state.groupBy = [values.groupBy].flat().filter((group) => !!group) as string[];
|
||||
}
|
||||
|
||||
if (values.timeWindow?.duration && values.timeWindow?.type) {
|
||||
|
|
|
@ -22,5 +22,5 @@ export interface CreateSLOForm {
|
|||
timesliceTarget?: number;
|
||||
timesliceWindow?: string;
|
||||
};
|
||||
groupBy: string;
|
||||
groupBy: string[] | string;
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ import { SloActiveAlertsBadge } from '../../../../components/slo/slo_status_badg
|
|||
import { SloTimeWindowBadge } from './slo_time_window_badge';
|
||||
import { SloRulesBadge } from './slo_rules_badge';
|
||||
import type { SloRule } from '../../../../hooks/slo/use_fetch_rules_for_slo';
|
||||
import { SloGroupByBadge } from '../../../../components/slo/slo_status_badge/slo_group_by_badge';
|
||||
export type ViewMode = 'default' | 'compact';
|
||||
|
||||
export interface SloBadgesProps {
|
||||
|
@ -43,7 +42,6 @@ export function SloBadges({
|
|||
<>
|
||||
<SloStatusBadge slo={slo} />
|
||||
<SloActiveAlertsBadge slo={slo} activeAlerts={activeAlerts} />
|
||||
<SloGroupByBadge slo={slo} />
|
||||
<SloIndicatorTypeBadge slo={slo} />
|
||||
<SloTimeWindowBadge slo={slo} />
|
||||
<SloRulesBadge rules={rules} onClick={onClickRuleBadge} />
|
||||
|
|
|
@ -58,7 +58,13 @@ export const useSloCardColor = (status?: SLOWithSummaryResponse['summary']['stat
|
|||
};
|
||||
|
||||
export const getSubTitle = (slo: SLOWithSummaryResponse) => {
|
||||
return slo.groupBy && slo.groupBy !== ALL_VALUE ? `${slo.groupBy}: ${slo.instanceId}` : '';
|
||||
return getFirstGroupBy(slo);
|
||||
};
|
||||
|
||||
const getFirstGroupBy = (slo: SLOWithSummaryResponse) => {
|
||||
const firstGroupBy = Object.entries(slo.groupings).map(([key, value]) => `${key}: ${value}`)[0];
|
||||
|
||||
return slo.groupBy && ![slo.groupBy].flat().includes(ALL_VALUE) ? firstGroupBy : '';
|
||||
};
|
||||
|
||||
export function SloCardItem({ slo, rules, activeAlerts, historicalSummary, cardsPerRow }: Props) {
|
||||
|
|
|
@ -13,11 +13,11 @@ import { EuiFlexGroup } from '@elastic/eui';
|
|||
import { SloTagsList } from '../common/slo_tags_list';
|
||||
import { useUrlSearchState } from '../../hooks/use_url_search_state';
|
||||
import { LoadingBadges } from '../badges/slo_badges';
|
||||
import { SloIndicatorTypeBadge } from '../badges/slo_indicator_type_badge';
|
||||
import { SloTimeWindowBadge } from '../badges/slo_time_window_badge';
|
||||
import { SloActiveAlertsBadge } from '../../../../components/slo/slo_status_badge/slo_active_alerts_badge';
|
||||
import { SloRulesBadge } from '../badges/slo_rules_badge';
|
||||
import { SloRule } from '../../../../hooks/slo/use_fetch_rules_for_slo';
|
||||
import { SLOCardItemInstanceBadge } from './slo_card_item_instance_badge';
|
||||
|
||||
interface Props {
|
||||
hasGroupBy: boolean;
|
||||
|
@ -51,7 +51,7 @@ export function SloCardItemBadges({ slo, activeAlerts, rules, handleCreateRule }
|
|||
) : (
|
||||
<>
|
||||
<SloActiveAlertsBadge slo={slo} activeAlerts={activeAlerts} viewMode="compact" />
|
||||
<SloIndicatorTypeBadge slo={slo} color="default" />
|
||||
<SLOCardItemInstanceBadge slo={slo} />
|
||||
<SloTimeWindowBadge slo={slo} color="default" />
|
||||
<SloRulesBadge rules={rules} onClick={handleCreateRule} />
|
||||
<SloTagsList
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 React, { useState } from 'react';
|
||||
import { EuiBadge, EuiFlexItem, EuiPopover } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { SLOGroupings } from '../common/slo_groupings';
|
||||
|
||||
export interface Props {
|
||||
slo: SLOWithSummaryResponse;
|
||||
}
|
||||
|
||||
export function SLOCardItemInstanceBadge({ slo }: Props) {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const entries = Object.entries(slo.groupings);
|
||||
const show = entries.length > 1;
|
||||
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
isOpen={isPopoverOpen}
|
||||
button={
|
||||
<EuiBadge
|
||||
onClick={() => {
|
||||
setIsPopoverOpen(!isPopoverOpen);
|
||||
}}
|
||||
onClickAriaLabel={i18n.translate('xpack.observability.slo.instances.seeAllBadge', {
|
||||
defaultMessage: 'see all instance ids',
|
||||
})}
|
||||
data-test-subj="o11ySlosSeeAllInstanceIdsBadge"
|
||||
>
|
||||
{`${i18n.translate('xpack.observability.slos.extraInstanceIds.badge', {
|
||||
defaultMessage: '+{count, plural, one {# more instance} other {# more instances}}',
|
||||
values: {
|
||||
count: entries.length - 1,
|
||||
},
|
||||
})}`}
|
||||
</EuiBadge>
|
||||
}
|
||||
>
|
||||
<SLOGroupings slo={slo} direction="column" truncate={false} />
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import {
|
||||
EuiCopy,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiText,
|
||||
EuiAccordion,
|
||||
EuiButtonEmpty,
|
||||
EuiLink,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
|
||||
export interface Props {
|
||||
slo: SLOWithSummaryResponse | undefined;
|
||||
direction?: 'column' | 'row';
|
||||
truncate?: boolean;
|
||||
}
|
||||
|
||||
export function SLOGroupings({ slo, direction = 'row', truncate = true }: Props) {
|
||||
const groups = Object.entries(slo?.groupings || []);
|
||||
const shouldTruncate = truncate && groups.length > 3;
|
||||
const firstThree = shouldTruncate ? groups.slice(0, 3) : groups;
|
||||
const rest = shouldTruncate ? groups.slice(3, groups.length) : [];
|
||||
|
||||
const buttonCss = css`
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
|
||||
if (!groups.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{shouldTruncate ? (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiAccordion
|
||||
id="sloGroupingsAccordion"
|
||||
arrowDisplay="right"
|
||||
buttonElement="div"
|
||||
buttonProps={{ css: buttonCss }}
|
||||
buttonContent={
|
||||
<>
|
||||
<EuiFlexGroup
|
||||
alignItems={direction === 'column' ? 'flexStart' : 'center'}
|
||||
gutterSize="s"
|
||||
direction={direction}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<Entries entries={firstThree} direction={direction} />{' '}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{rest.length && (
|
||||
<span>
|
||||
<EuiButtonEmpty data-test-subj="accordion" flush="left">
|
||||
{`(${i18n.translate('xpack.observability.slos.andLabel', {
|
||||
defaultMessage:
|
||||
'and {count, plural, one {# more instance} other {# more instances}}',
|
||||
values: {
|
||||
count: rest.length,
|
||||
},
|
||||
})})`}
|
||||
</EuiButtonEmpty>
|
||||
</span>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Entries entries={rest} direction={direction} />
|
||||
</EuiAccordion>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<Entries entries={truncate ? firstThree : groups} direction={direction} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Entries({
|
||||
entries,
|
||||
direction,
|
||||
}: {
|
||||
entries: Array<[string, unknown]>;
|
||||
direction: 'row' | 'column';
|
||||
}) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" direction={direction}>
|
||||
{entries.map(([key, value]) => (
|
||||
<EuiFlexItem grow={false} key={key}>
|
||||
<EuiText size="s">
|
||||
<span>
|
||||
{`${key}: `}
|
||||
<EuiCopy textToCopy={`${value}`}>
|
||||
{(copy) => (
|
||||
<EuiLink
|
||||
data-test-subj="sloInstanceCopy"
|
||||
style={{
|
||||
fontWeight: euiTheme.font.weight.semiBold,
|
||||
}}
|
||||
color="text"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
copy();
|
||||
}}
|
||||
>
|
||||
{`${value}`}
|
||||
</EuiLink>
|
||||
)}
|
||||
</EuiCopy>
|
||||
</span>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -4,7 +4,6 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
DefaultItemAction,
|
||||
EuiBasicTable,
|
||||
|
@ -42,6 +41,7 @@ import { SloListEmpty } from '../slo_list_empty';
|
|||
import { SloListError } from '../slo_list_error';
|
||||
import { SloSparkline } from '../slo_sparkline';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
|
||||
import { SLOGroupings } from '../common/slo_groupings';
|
||||
|
||||
export interface Props {
|
||||
sloList: SLOWithSummaryResponse[];
|
||||
|
@ -114,7 +114,7 @@ export function SloListCompactView({ sloList, loading, error }: Props) {
|
|||
const sloDetailsUrl = basePath.prepend(
|
||||
paths.observability.sloDetails(
|
||||
slo.id,
|
||||
slo.groupBy !== ALL_VALUE && slo.instanceId ? slo.instanceId : undefined
|
||||
![slo.groupBy].flat().includes(ALL_VALUE) && slo.instanceId ? slo.instanceId : undefined
|
||||
)
|
||||
);
|
||||
navigateToUrl(sloDetailsUrl);
|
||||
|
@ -241,7 +241,7 @@ export function SloListCompactView({ sloList, loading, error }: Props) {
|
|||
const sloDetailsUrl = basePath.prepend(
|
||||
paths.observability.sloDetails(
|
||||
slo.id,
|
||||
slo.groupBy !== ALL_VALUE && slo.instanceId ? slo.instanceId : undefined
|
||||
![slo.groupBy].flat().includes(ALL_VALUE) && slo.instanceId ? slo.instanceId : undefined
|
||||
)
|
||||
);
|
||||
return (
|
||||
|
@ -267,23 +267,14 @@ export function SloListCompactView({ sloList, loading, error }: Props) {
|
|||
{
|
||||
field: 'instance',
|
||||
name: 'Instance',
|
||||
render: (_, slo: SLOWithSummaryResponse) =>
|
||||
slo.groupBy !== ALL_VALUE ? (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={i18n.translate('xpack.observability.slo.groupByBadge', {
|
||||
defaultMessage: 'Group by {groupKey}',
|
||||
values: {
|
||||
groupKey: slo.groupBy,
|
||||
},
|
||||
})}
|
||||
display="block"
|
||||
>
|
||||
<span>{slo.instanceId}</span>
|
||||
</EuiToolTip>
|
||||
render: (_, slo: SLOWithSummaryResponse) => {
|
||||
const groups = [slo.groupBy].flat();
|
||||
return !groups.includes(ALL_VALUE) ? (
|
||||
<SLOGroupings slo={slo} direction="column" />
|
||||
) : (
|
||||
<span>{NOT_AVAILABLE_LABEL}</span>
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'objective',
|
||||
|
|
|
@ -75,7 +75,7 @@ export function SloItemActions({
|
|||
const sloDetailsUrl = basePath.prepend(
|
||||
paths.observability.sloDetails(
|
||||
slo.id,
|
||||
slo.groupBy !== ALL_VALUE && slo.instanceId ? slo.instanceId : undefined
|
||||
![slo.groupBy].flat().includes(ALL_VALUE) && slo.instanceId ? slo.instanceId : undefined
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import { SloItemActions } from './slo_item_actions';
|
|||
import type { SloRule } from '../../../hooks/slo/use_fetch_rules_for_slo';
|
||||
import { SloBadges } from './badges/slo_badges';
|
||||
import { SloSummary } from './slo_summary';
|
||||
import { SLOGroupings } from './common/slo_groupings';
|
||||
|
||||
export interface SloListItemProps {
|
||||
slo: SLOWithSummaryResponse;
|
||||
|
@ -54,13 +55,16 @@ export function SloListItem({
|
|||
<EuiFlexItem grow>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow>
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
{slo.summary ? (
|
||||
<a data-test-subj="o11ySloListItemLink" href={sloDetailsUrl}>
|
||||
{slo.name}
|
||||
</a>
|
||||
<>
|
||||
<a data-test-subj="o11ySloListItemLink" href={sloDetailsUrl}>
|
||||
{slo.name}
|
||||
</a>
|
||||
<SLOGroupings slo={slo} />
|
||||
</>
|
||||
) : (
|
||||
<span>{slo.name}</span>
|
||||
)}
|
||||
|
|
|
@ -48,7 +48,7 @@ export const getSloFormattedSummary = (
|
|||
const sloDetailsUrl = basePath.prepend(
|
||||
paths.observability.sloDetails(
|
||||
slo.id,
|
||||
slo.groupBy !== ALL_VALUE && slo.instanceId ? slo.instanceId : undefined
|
||||
![slo.groupBy].flat().includes(ALL_VALUE) && slo.instanceId ? slo.instanceId : undefined
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 function getGroupKeysProse(groupBy: string | string[]) {
|
||||
const groups = [groupBy].flat().map((group) => `"${group}"`);
|
||||
const groupKeys =
|
||||
groups.length > 1
|
||||
? `${groups.slice(0, groups.length - 1).join(', ')} and ${groups.slice(-1)}`
|
||||
: groups[0];
|
||||
|
||||
return groupKeys;
|
||||
}
|
|
@ -11,6 +11,7 @@ import {
|
|||
historicalSummarySchema,
|
||||
statusSchema,
|
||||
summarySchema,
|
||||
groupingsSchema,
|
||||
groupSummarySchema,
|
||||
} from '@kbn/slo-schema';
|
||||
|
||||
|
@ -18,6 +19,7 @@ type Status = t.TypeOf<typeof statusSchema>;
|
|||
type DateRange = t.TypeOf<typeof dateRangeSchema>;
|
||||
type HistoricalSummary = t.TypeOf<typeof historicalSummarySchema>;
|
||||
type Summary = t.TypeOf<typeof summarySchema>;
|
||||
type Groupings = t.TypeOf<typeof groupingsSchema>;
|
||||
type GroupSummary = t.TypeOf<typeof groupSummarySchema>;
|
||||
|
||||
export type { DateRange, HistoricalSummary, Status, Summary, GroupSummary };
|
||||
export type { DateRange, Groupings, HistoricalSummary, Status, Summary, GroupSummary };
|
||||
|
|
|
@ -2,39 +2,48 @@
|
|||
|
||||
exports[`SummaryClient fetchSummary with calendar aligned and timeslices SLO returns the summary 1`] = `
|
||||
Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0,
|
||||
"initial": 0.05,
|
||||
"isEstimated": false,
|
||||
"remaining": 1,
|
||||
"groupings": Object {},
|
||||
"summary": Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0,
|
||||
"initial": 0.05,
|
||||
"isEstimated": false,
|
||||
"remaining": 1,
|
||||
},
|
||||
"sliValue": -1,
|
||||
"status": "NO_DATA",
|
||||
},
|
||||
"sliValue": -1,
|
||||
"status": "NO_DATA",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`SummaryClient fetchSummary with rolling and occurrences SLO returns the summary 1`] = `
|
||||
Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0,
|
||||
"initial": 0.001,
|
||||
"isEstimated": false,
|
||||
"remaining": 1,
|
||||
"groupings": Object {},
|
||||
"summary": Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0,
|
||||
"initial": 0.001,
|
||||
"isEstimated": false,
|
||||
"remaining": 1,
|
||||
},
|
||||
"sliValue": -1,
|
||||
"status": "NO_DATA",
|
||||
},
|
||||
"sliValue": -1,
|
||||
"status": "NO_DATA",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`SummaryClient fetchSummary with rolling and timeslices SLO returns the summary 1`] = `
|
||||
Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0,
|
||||
"initial": 0.05,
|
||||
"isEstimated": false,
|
||||
"remaining": 1,
|
||||
"groupings": Object {},
|
||||
"summary": Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0,
|
||||
"initial": 0.05,
|
||||
"isEstimated": false,
|
||||
"remaining": 1,
|
||||
},
|
||||
"sliValue": -1,
|
||||
"status": "NO_DATA",
|
||||
},
|
||||
"sliValue": -1,
|
||||
"status": "NO_DATA",
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -36,6 +36,7 @@ Object {
|
|||
"perPage": 20,
|
||||
"results": Array [
|
||||
Object {
|
||||
"groupings": Object {},
|
||||
"id": "slo-one",
|
||||
"instanceId": "*",
|
||||
"summary": Object {
|
||||
|
@ -50,6 +51,7 @@ Object {
|
|||
},
|
||||
},
|
||||
Object {
|
||||
"groupings": Object {},
|
||||
"id": "slo_two",
|
||||
"instanceId": "*",
|
||||
"summary": Object {
|
||||
|
@ -64,6 +66,7 @@ Object {
|
|||
},
|
||||
},
|
||||
Object {
|
||||
"groupings": Object {},
|
||||
"id": "slo-three",
|
||||
"instanceId": "*",
|
||||
"summary": Object {
|
||||
|
@ -78,6 +81,7 @@ Object {
|
|||
},
|
||||
},
|
||||
Object {
|
||||
"groupings": Object {},
|
||||
"id": "slo-five",
|
||||
"instanceId": "*",
|
||||
"summary": Object {
|
||||
|
@ -92,6 +96,7 @@ Object {
|
|||
},
|
||||
},
|
||||
Object {
|
||||
"groupings": Object {},
|
||||
"id": "slo-four",
|
||||
"instanceId": "*",
|
||||
"summary": Object {
|
||||
|
|
|
@ -96,6 +96,7 @@ describe('FindSLO', () => {
|
|||
enabled: slo.enabled,
|
||||
revision: slo.revision,
|
||||
groupBy: slo.groupBy,
|
||||
groupings: {},
|
||||
instanceId: ALL_VALUE,
|
||||
version: SLO_MODEL_VERSION,
|
||||
},
|
||||
|
@ -166,6 +167,7 @@ function summarySearchResult(slo: SLO): Paginated<SLOSummary> {
|
|||
{
|
||||
id: slo.id,
|
||||
instanceId: slo.groupBy === ALL_VALUE ? ALL_VALUE : 'host-abcde',
|
||||
groupings: {},
|
||||
summary: {
|
||||
status: 'HEALTHY',
|
||||
sliValue: 0.9999,
|
||||
|
|
|
@ -48,6 +48,7 @@ function mergeSloWithSummary(sloList: SLO[], sloSummaryList: SLOSummary[]): SLOW
|
|||
...sloList.find((s) => s.id === sloSummary.id)!,
|
||||
instanceId: sloSummary.instanceId,
|
||||
summary: sloSummary.summary,
|
||||
groupings: sloSummary.groupings,
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ interface Options {
|
|||
interval: string;
|
||||
instanceId?: string;
|
||||
groupBy?: string;
|
||||
groupings?: Record<string, unknown>;
|
||||
}
|
||||
export class GetPreviewData {
|
||||
constructor(private esClient: ElasticsearchClient) {}
|
||||
|
@ -47,11 +48,7 @@ export class GetPreviewData {
|
|||
options: Options
|
||||
): Promise<GetPreviewDataResponse> {
|
||||
const filter: estypes.QueryDslQueryContainer[] = [];
|
||||
if (options.instanceId !== ALL_VALUE && options.groupBy) {
|
||||
filter.push({
|
||||
term: { [options.groupBy]: options.instanceId },
|
||||
});
|
||||
}
|
||||
this.getGroupingsFilter(options, filter);
|
||||
if (indicator.params.service !== ALL_VALUE)
|
||||
filter.push({
|
||||
match: { 'service.name': indicator.params.service },
|
||||
|
@ -145,11 +142,7 @@ export class GetPreviewData {
|
|||
options: Options
|
||||
): Promise<GetPreviewDataResponse> {
|
||||
const filter: estypes.QueryDslQueryContainer[] = [];
|
||||
if (options.instanceId !== ALL_VALUE && options.groupBy) {
|
||||
filter.push({
|
||||
term: { [options.groupBy]: options.instanceId },
|
||||
});
|
||||
}
|
||||
this.getGroupingsFilter(options, filter);
|
||||
if (indicator.params.service !== ALL_VALUE)
|
||||
filter.push({
|
||||
match: { 'service.name': indicator.params.service },
|
||||
|
@ -242,11 +235,7 @@ export class GetPreviewData {
|
|||
filterQuery,
|
||||
];
|
||||
|
||||
if (options.instanceId !== ALL_VALUE && options.groupBy) {
|
||||
filter.push({
|
||||
term: { [options.groupBy]: options.instanceId },
|
||||
});
|
||||
}
|
||||
this.getGroupingsFilter(options, filter);
|
||||
|
||||
const result = await this.esClient.search({
|
||||
index: indicator.params.index,
|
||||
|
@ -305,11 +294,7 @@ export class GetPreviewData {
|
|||
{ range: { [timestampField]: { gte: options.range.start, lte: options.range.end } } },
|
||||
filterQuery,
|
||||
];
|
||||
if (options.instanceId !== ALL_VALUE && options.groupBy) {
|
||||
filter.push({
|
||||
term: { [options.groupBy]: options.instanceId },
|
||||
});
|
||||
}
|
||||
this.getGroupingsFilter(options, filter);
|
||||
|
||||
const result = await this.esClient.search({
|
||||
index: indicator.params.index,
|
||||
|
@ -371,11 +356,7 @@ export class GetPreviewData {
|
|||
filterQuery,
|
||||
];
|
||||
|
||||
if (options.instanceId !== ALL_VALUE && options.groupBy) {
|
||||
filter.push({
|
||||
term: { [options.groupBy]: options.instanceId },
|
||||
});
|
||||
}
|
||||
this.getGroupingsFilter(options, filter);
|
||||
|
||||
const result = await this.esClient.search({
|
||||
index: indicator.params.index,
|
||||
|
@ -422,11 +403,7 @@ export class GetPreviewData {
|
|||
filterQuery,
|
||||
];
|
||||
|
||||
if (options.instanceId !== ALL_VALUE && options.groupBy) {
|
||||
filter.push({
|
||||
term: { [options.groupBy]: options.instanceId },
|
||||
});
|
||||
}
|
||||
this.getGroupingsFilter(options, filter);
|
||||
|
||||
const result = await this.esClient.search({
|
||||
index: indicator.params.index,
|
||||
|
@ -469,6 +446,21 @@ export class GetPreviewData {
|
|||
}));
|
||||
}
|
||||
|
||||
private getGroupingsFilter(options: Options, filter: estypes.QueryDslQueryContainer[]) {
|
||||
const groupingsKeys = Object.keys(options.groupings || []);
|
||||
if (groupingsKeys.length) {
|
||||
groupingsKeys.forEach((key) => {
|
||||
filter.push({
|
||||
term: { [key]: options.groupings?.[key] },
|
||||
});
|
||||
});
|
||||
} else if (options.instanceId !== ALL_VALUE && options.groupBy) {
|
||||
filter.push({
|
||||
term: { [options.groupBy]: options.instanceId },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async execute(params: GetPreviewDataParams): Promise<GetPreviewDataResponse> {
|
||||
try {
|
||||
// If the time range is 24h or less, then we want to use a 1m bucket for the
|
||||
|
@ -490,6 +482,7 @@ export class GetPreviewData {
|
|||
instanceId: params.instanceId,
|
||||
range: params.range,
|
||||
groupBy: params.groupBy,
|
||||
groupings: params.groupings,
|
||||
interval: `${bucketSize}m`,
|
||||
};
|
||||
|
||||
|
|
|
@ -29,13 +29,16 @@ describe('GetSLO', () => {
|
|||
const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator() });
|
||||
mockRepository.findById.mockResolvedValueOnce(slo);
|
||||
mockSummaryClient.computeSummary.mockResolvedValueOnce({
|
||||
status: 'HEALTHY',
|
||||
sliValue: 0.9999,
|
||||
errorBudget: {
|
||||
initial: 0.001,
|
||||
consumed: 0.1,
|
||||
remaining: 0.9,
|
||||
isEstimated: false,
|
||||
groupings: {},
|
||||
summary: {
|
||||
status: 'HEALTHY',
|
||||
sliValue: 0.9999,
|
||||
errorBudget: {
|
||||
initial: 0.001,
|
||||
consumed: 0.1,
|
||||
remaining: 0.9,
|
||||
isEstimated: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -84,6 +87,7 @@ describe('GetSLO', () => {
|
|||
enabled: slo.enabled,
|
||||
revision: slo.revision,
|
||||
groupBy: slo.groupBy,
|
||||
groupings: {},
|
||||
instanceId: ALL_VALUE,
|
||||
version: SLO_MODEL_VERSION,
|
||||
});
|
||||
|
|
|
@ -4,9 +4,8 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ALL_VALUE, GetSLOParams, GetSLOResponse, getSLOResponseSchema } from '@kbn/slo-schema';
|
||||
import { SLO, Summary } from '../../domain/models';
|
||||
import { Groupings, SLO, Summary } from '../../domain/models';
|
||||
import { SLORepository } from './slo_repository';
|
||||
import { SummaryClient } from './summary_client';
|
||||
|
||||
|
@ -15,13 +14,14 @@ export class GetSLO {
|
|||
|
||||
public async execute(sloId: string, params: GetSLOParams = {}): Promise<GetSLOResponse> {
|
||||
const slo = await this.repository.findById(sloId);
|
||||
const instanceId = params.instanceId ?? ALL_VALUE;
|
||||
const summary = await this.summaryClient.computeSummary(slo, instanceId);
|
||||
|
||||
return getSLOResponseSchema.encode(mergeSloWithSummary(slo, summary, instanceId));
|
||||
const instanceId = params.instanceId ?? ALL_VALUE;
|
||||
const { summary, groupings } = await this.summaryClient.computeSummary(slo, instanceId);
|
||||
|
||||
return getSLOResponseSchema.encode(mergeSloWithSummary(slo, summary, instanceId, groupings));
|
||||
}
|
||||
}
|
||||
|
||||
function mergeSloWithSummary(slo: SLO, summary: Summary, instanceId: string) {
|
||||
return { ...slo, instanceId, summary };
|
||||
function mergeSloWithSummary(slo: SLO, summary: Summary, instanceId: string, groupings: Groupings) {
|
||||
return { ...slo, instanceId, summary, groupings };
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ export class GetSLOInstances {
|
|||
public async execute(sloId: string): Promise<GetSLOInstancesResponse> {
|
||||
const slo = await this.repository.findById(sloId);
|
||||
|
||||
if (slo.groupBy === ALL_VALUE) {
|
||||
if ([slo.groupBy].flat().includes(ALL_VALUE)) {
|
||||
return { groupBy: ALL_VALUE, instances: [] };
|
||||
}
|
||||
|
||||
|
|
|
@ -215,7 +215,7 @@ function generateSearchQuery(
|
|||
getFixedIntervalAndBucketsPerDay(timeWindowDurationInDays);
|
||||
|
||||
const extraFilterByInstanceId =
|
||||
!!slo.groupBy && slo.groupBy !== ALL_VALUE && instanceId !== ALL_VALUE
|
||||
!!slo.groupBy && ![slo.groupBy].flat().includes(ALL_VALUE) && instanceId !== ALL_VALUE
|
||||
? [{ term: { 'slo.instanceId': instanceId } }]
|
||||
: [];
|
||||
|
||||
|
|
|
@ -16,23 +16,50 @@ import {
|
|||
} from '@kbn/slo-schema';
|
||||
import moment from 'moment';
|
||||
import { SLO_DESTINATION_INDEX_PATTERN } from '../../../common/slo/constants';
|
||||
import { DateRange, SLO, Summary } from '../../domain/models';
|
||||
import { DateRange, SLO, Summary, Groupings } from '../../domain/models';
|
||||
import { computeSLI, computeSummaryStatus, toErrorBudget } from '../../domain/services';
|
||||
import { toDateRange } from '../../domain/services/date_range';
|
||||
import { getFlattenedGroupings } from './utils';
|
||||
|
||||
export interface SummaryClient {
|
||||
computeSummary(slo: SLO, instanceId?: string): Promise<Summary>;
|
||||
computeSummary(
|
||||
slo: SLO,
|
||||
groupings?: string,
|
||||
instanceId?: string
|
||||
): Promise<{ summary: Summary; groupings: Groupings }>;
|
||||
}
|
||||
|
||||
export class DefaultSummaryClient implements SummaryClient {
|
||||
constructor(private esClient: ElasticsearchClient) {}
|
||||
|
||||
async computeSummary(slo: SLO, instanceId: string = ALL_VALUE): Promise<Summary> {
|
||||
async computeSummary(
|
||||
slo: SLO,
|
||||
instanceId: string = ALL_VALUE
|
||||
): Promise<{ summary: Summary; groupings: Groupings }> {
|
||||
const dateRange = toDateRange(slo.timeWindow);
|
||||
const isDefinedWithGroupBy = slo.groupBy !== ALL_VALUE;
|
||||
const isDefinedWithGroupBy = ![slo.groupBy].flat().includes(ALL_VALUE);
|
||||
const hasInstanceId = instanceId !== ALL_VALUE;
|
||||
const extraInstanceIdFilter =
|
||||
isDefinedWithGroupBy && hasInstanceId ? [{ term: { 'slo.instanceId': instanceId } }] : [];
|
||||
const includeInstanceIdQueries = isDefinedWithGroupBy && hasInstanceId;
|
||||
const extraInstanceIdFilter = includeInstanceIdQueries
|
||||
? [{ term: { 'slo.instanceId': instanceId } }]
|
||||
: [];
|
||||
const extraGroupingsAgg = {
|
||||
last_doc: {
|
||||
top_hits: {
|
||||
sort: [
|
||||
{
|
||||
'@timestamp': {
|
||||
order: 'desc',
|
||||
},
|
||||
},
|
||||
],
|
||||
_source: {
|
||||
includes: ['slo.groupings'],
|
||||
},
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await this.esClient.search({
|
||||
index: SLO_DESTINATION_INDEX_PATTERN,
|
||||
|
@ -51,28 +78,30 @@ export class DefaultSummaryClient implements SummaryClient {
|
|||
],
|
||||
},
|
||||
},
|
||||
...(occurrencesBudgetingMethodSchema.is(slo.budgetingMethod) && {
|
||||
aggs: {
|
||||
good: { sum: { field: 'slo.numerator' } },
|
||||
total: { sum: { field: 'slo.denominator' } },
|
||||
},
|
||||
}),
|
||||
...(timeslicesBudgetingMethodSchema.is(slo.budgetingMethod) && {
|
||||
aggs: {
|
||||
// @ts-expect-error AggregationsAggregationContainer needs to be updated with top_hits
|
||||
aggs: {
|
||||
...(includeInstanceIdQueries && extraGroupingsAgg),
|
||||
...(timeslicesBudgetingMethodSchema.is(slo.budgetingMethod) && {
|
||||
good: {
|
||||
sum: { field: 'slo.isGoodSlice' },
|
||||
},
|
||||
total: {
|
||||
value_count: { field: 'slo.isGoodSlice' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
...(occurrencesBudgetingMethodSchema.is(slo.budgetingMethod) && {
|
||||
good: { sum: { field: 'slo.numerator' } },
|
||||
total: { sum: { field: 'slo.denominator' } },
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
// @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;
|
||||
// @ts-expect-error AggregationsAggregationContainer needs to be updated with top_hits
|
||||
const groupings = result.aggregations?.last_doc?.hits?.hits?.[0]?._source?.slo?.groupings;
|
||||
|
||||
const sliValue = computeSLI(good, total);
|
||||
const initialErrorBudget = 1 - slo.objective.target;
|
||||
|
@ -100,9 +129,12 @@ export class DefaultSummaryClient implements SummaryClient {
|
|||
}
|
||||
|
||||
return {
|
||||
sliValue,
|
||||
errorBudget,
|
||||
status: computeSummaryStatus(slo, sliValue, errorBudget),
|
||||
summary: {
|
||||
sliValue,
|
||||
errorBudget,
|
||||
status: computeSummaryStatus(slo, sliValue, errorBudget),
|
||||
},
|
||||
groupings: groupings ? getFlattenedGroupings({ groupBy: slo.groupBy, groupings }) : {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,11 +8,12 @@
|
|||
import { ElasticsearchClient, Logger } from '@kbn/core/server';
|
||||
import { ALL_VALUE, Paginated, Pagination } from '@kbn/slo-schema';
|
||||
import { assertNever } from '@kbn/std';
|
||||
import _ from 'lodash';
|
||||
import { partition } from 'lodash';
|
||||
import { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../../common/slo/constants';
|
||||
import { SLOId, Status, Summary } from '../../domain/models';
|
||||
import { SLOId, Status, Summary, Groupings } from '../../domain/models';
|
||||
import { toHighPrecision } from '../../utils/number';
|
||||
import { getFlattenedGroupings } from './utils';
|
||||
import { getElasticsearchQueryOrThrow } from './transform_generators';
|
||||
|
||||
interface EsSummaryDocument {
|
||||
|
@ -20,6 +21,8 @@ interface EsSummaryDocument {
|
|||
id: string;
|
||||
revision: number;
|
||||
instanceId: string;
|
||||
groupings: Groupings;
|
||||
groupBy: string[];
|
||||
};
|
||||
sliValue: number;
|
||||
errorBudgetConsumed: number;
|
||||
|
@ -35,6 +38,7 @@ export interface SLOSummary {
|
|||
id: SLOId;
|
||||
instanceId: string;
|
||||
summary: Summary;
|
||||
groupings: Groupings;
|
||||
}
|
||||
|
||||
export type SortField = 'error_budget_consumed' | 'error_budget_remaining' | 'sli_value' | 'status';
|
||||
|
@ -105,7 +109,7 @@ export class DefaultSummarySearchClient implements SummarySearchClient {
|
|||
return { total: 0, perPage: pagination.perPage, page: pagination.page, results: [] };
|
||||
}
|
||||
|
||||
const [tempSummaryDocuments, summaryDocuments] = _.partition(
|
||||
const [tempSummaryDocuments, summaryDocuments] = partition(
|
||||
summarySearch.hits.hits,
|
||||
(doc) => !!doc._source?.isTempDoc
|
||||
);
|
||||
|
@ -149,6 +153,10 @@ export class DefaultSummarySearchClient implements SummarySearchClient {
|
|||
sliValue: toHighPrecision(doc._source!.sliValue),
|
||||
status: doc._source!.status,
|
||||
},
|
||||
groupings: getFlattenedGroupings({
|
||||
groupings: doc._source!.slo.groupings,
|
||||
groupBy: doc._source!.slo.groupBy,
|
||||
}),
|
||||
})),
|
||||
};
|
||||
} catch (err) {
|
||||
|
|
|
@ -9,9 +9,11 @@ import { ALL_VALUE } from '@kbn/slo-schema';
|
|||
import { SLO } from '../../../../domain/models/slo';
|
||||
|
||||
export const getGroupBy = (slo: SLO) => {
|
||||
const groups = [slo.groupBy].flat().filter((group) => !!group);
|
||||
|
||||
const groupings =
|
||||
slo.groupBy !== '' && slo.groupBy !== ALL_VALUE
|
||||
? [slo.groupBy].flat().reduce((acc, field) => {
|
||||
!groups.includes(ALL_VALUE) && groups.length
|
||||
? groups.reduce((acc, field) => {
|
||||
return {
|
||||
...acc,
|
||||
[`slo.groupings.${field}`]: {
|
||||
|
|
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* 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 { createAPMTransactionErrorRateIndicator, createSLO } from '../fixtures/slo';
|
||||
import { ApmTransactionErrorRateTransformGenerator } from './apm_transaction_error_rate';
|
||||
|
||||
const generator = new ApmTransactionErrorRateTransformGenerator();
|
||||
|
||||
describe('Transform Generator', () => {
|
||||
it('builds common runtime mappings without group by', async () => {
|
||||
const slo = createSLO({
|
||||
id: 'irrelevant',
|
||||
indicator: createAPMTransactionErrorRateIndicator(),
|
||||
});
|
||||
const transform = generator.buildCommonRuntimeMappings(slo);
|
||||
|
||||
expect(transform).toEqual({
|
||||
'slo.id': {
|
||||
script: {
|
||||
source: "emit('irrelevant')",
|
||||
},
|
||||
type: 'keyword',
|
||||
},
|
||||
'slo.instanceId': {
|
||||
script: {
|
||||
source: "emit('*')",
|
||||
},
|
||||
type: 'keyword',
|
||||
},
|
||||
'slo.revision': {
|
||||
script: {
|
||||
source: 'emit(1)',
|
||||
},
|
||||
type: 'long',
|
||||
},
|
||||
});
|
||||
|
||||
const commonGroupBy = generator.buildCommonGroupBy(slo);
|
||||
|
||||
expect(commonGroupBy).toEqual({
|
||||
'@timestamp': {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: '1m',
|
||||
},
|
||||
},
|
||||
'slo.id': {
|
||||
terms: {
|
||||
field: 'slo.id',
|
||||
},
|
||||
},
|
||||
'slo.instanceId': {
|
||||
terms: {
|
||||
field: 'slo.instanceId',
|
||||
},
|
||||
},
|
||||
'slo.revision': {
|
||||
terms: {
|
||||
field: 'slo.revision',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.each(['example', ['example']])(
|
||||
'builds common runtime mappings and group by with single group by',
|
||||
async (groupBy) => {
|
||||
const indicator = createAPMTransactionErrorRateIndicator();
|
||||
const slo = createSLO({
|
||||
id: 'irrelevant',
|
||||
groupBy,
|
||||
indicator,
|
||||
});
|
||||
const commonRuntime = generator.buildCommonRuntimeMappings(slo);
|
||||
|
||||
expect(commonRuntime).toEqual({
|
||||
'slo.id': {
|
||||
script: {
|
||||
source: "emit('irrelevant')",
|
||||
},
|
||||
type: 'keyword',
|
||||
},
|
||||
'slo.instanceId': {
|
||||
script: {
|
||||
source: "emit('example:'+doc['example'].value)",
|
||||
},
|
||||
type: 'keyword',
|
||||
},
|
||||
'slo.revision': {
|
||||
script: {
|
||||
source: 'emit(1)',
|
||||
},
|
||||
type: 'long',
|
||||
},
|
||||
});
|
||||
|
||||
const commonGroupBy = generator.buildCommonGroupBy(slo);
|
||||
|
||||
expect(commonGroupBy).toEqual({
|
||||
'@timestamp': {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: '1m',
|
||||
},
|
||||
},
|
||||
'slo.groupings.example': {
|
||||
terms: {
|
||||
field: 'example',
|
||||
},
|
||||
},
|
||||
'slo.id': {
|
||||
terms: {
|
||||
field: 'slo.id',
|
||||
},
|
||||
},
|
||||
'slo.instanceId': {
|
||||
terms: {
|
||||
field: 'slo.instanceId',
|
||||
},
|
||||
},
|
||||
'slo.revision': {
|
||||
terms: {
|
||||
field: 'slo.revision',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it('builds common runtime mappings without multi group by', async () => {
|
||||
const indicator = createAPMTransactionErrorRateIndicator();
|
||||
const slo = createSLO({
|
||||
id: 'irrelevant',
|
||||
groupBy: ['example1', 'example2'],
|
||||
indicator,
|
||||
});
|
||||
const commonRuntime = generator.buildCommonRuntimeMappings(slo);
|
||||
|
||||
expect(commonRuntime).toEqual({
|
||||
'slo.id': {
|
||||
script: {
|
||||
source: "emit('irrelevant')",
|
||||
},
|
||||
type: 'keyword',
|
||||
},
|
||||
'slo.instanceId': {
|
||||
script: {
|
||||
source: "emit('example1:'+doc['example1'].value+'|'+'example2:'+doc['example2'].value)",
|
||||
},
|
||||
type: 'keyword',
|
||||
},
|
||||
'slo.revision': {
|
||||
script: {
|
||||
source: 'emit(1)',
|
||||
},
|
||||
type: 'long',
|
||||
},
|
||||
});
|
||||
|
||||
const commonGroupBy = generator.buildCommonGroupBy(slo);
|
||||
|
||||
expect(commonGroupBy).toEqual({
|
||||
'@timestamp': {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: '1m',
|
||||
},
|
||||
},
|
||||
'slo.groupings.example1': {
|
||||
terms: {
|
||||
field: 'example1',
|
||||
},
|
||||
},
|
||||
'slo.groupings.example2': {
|
||||
terms: {
|
||||
field: 'example2',
|
||||
},
|
||||
},
|
||||
'slo.id': {
|
||||
terms: {
|
||||
field: 'slo.id',
|
||||
},
|
||||
},
|
||||
'slo.instanceId': {
|
||||
terms: {
|
||||
field: 'slo.instanceId',
|
||||
},
|
||||
},
|
||||
'slo.revision': {
|
||||
terms: {
|
||||
field: 'slo.revision',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -17,8 +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 === '';
|
||||
|
||||
const groupings = [slo.groupBy].flat().filter((value) => !!value);
|
||||
const hasGroupings = !groupings.includes(ALL_VALUE) && groupings.length;
|
||||
return {
|
||||
'slo.id': {
|
||||
type: 'keyword',
|
||||
|
@ -32,17 +32,32 @@ export abstract class TransformGenerator {
|
|||
source: `emit(${slo.revision})`,
|
||||
},
|
||||
},
|
||||
...(mustIncludeAllInstanceId && {
|
||||
'slo.instanceId': {
|
||||
type: 'keyword',
|
||||
script: {
|
||||
source: `emit('${ALL_VALUE}')`,
|
||||
},
|
||||
},
|
||||
}),
|
||||
...(hasGroupings
|
||||
? {
|
||||
'slo.instanceId': {
|
||||
type: 'keyword',
|
||||
script: {
|
||||
source: this.buildInstanceId(slo),
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
'slo.instanceId': {
|
||||
type: 'keyword',
|
||||
script: {
|
||||
source: `emit('${ALL_VALUE}')`,
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
public buildInstanceId(slo: SLO): string {
|
||||
const groups = [slo.groupBy].flat().filter((value) => !!value);
|
||||
const groupings = groups.map((group) => `'${group}:'+doc['${group}'].value`).join(`+'|'+`);
|
||||
return `emit(${groupings})`;
|
||||
}
|
||||
|
||||
public buildDescription(slo: SLO): string {
|
||||
return `Rolled-up SLI data for SLO: ${slo.name} [id: ${slo.id}, revision: ${slo.revision}]`;
|
||||
}
|
||||
|
@ -57,9 +72,11 @@ export abstract class TransformGenerator {
|
|||
fixedInterval = slo.objective.timesliceWindow!.format();
|
||||
}
|
||||
|
||||
const groups = [slo.groupBy].flat().filter((group) => !!group);
|
||||
|
||||
const groupings =
|
||||
slo.groupBy !== '' && slo.groupBy !== ALL_VALUE
|
||||
? [slo.groupBy].flat().reduce(
|
||||
!groups.includes(ALL_VALUE) && groups.length
|
||||
? groups.reduce(
|
||||
(acc, field) => {
|
||||
return {
|
||||
...acc,
|
||||
|
@ -70,7 +87,11 @@ export abstract class TransformGenerator {
|
|||
},
|
||||
};
|
||||
},
|
||||
{ 'slo.instanceId': { terms: { field: slo.groupBy } } }
|
||||
{
|
||||
'slo.instanceId': {
|
||||
terms: { field: 'slo.instanceId' },
|
||||
},
|
||||
}
|
||||
)
|
||||
: { 'slo.instanceId': { terms: { field: 'slo.instanceId' } } };
|
||||
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { getFlattenedGroupings } from '.';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('getFlattenedGroupings', () => {
|
||||
it.each(['a.nested.key', ['a.nested.key']])(
|
||||
'handles single group by with nested keys',
|
||||
(groupBy) => {
|
||||
const groupings = {
|
||||
a: {
|
||||
nested: {
|
||||
key: 'value',
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(getFlattenedGroupings({ groupBy, groupings })).toEqual({ 'a.nested.key': 'value' });
|
||||
}
|
||||
);
|
||||
|
||||
it.each(['not_nested', ['not_nested']])(
|
||||
'handles single group by with regular keys',
|
||||
(groupBy) => {
|
||||
const groupings = {
|
||||
not_nested: 'value',
|
||||
};
|
||||
expect(getFlattenedGroupings({ groupBy, groupings })).toEqual({ not_nested: 'value' });
|
||||
}
|
||||
);
|
||||
|
||||
it('handles multi group by with nested and regular keys', () => {
|
||||
const groupBy = ['a.nested.key', 'not_nested'];
|
||||
const groupings = {
|
||||
not_nested: 'value2',
|
||||
a: {
|
||||
nested: {
|
||||
key: 'value',
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(getFlattenedGroupings({ groupBy, groupings })).toEqual({
|
||||
'a.nested.key': 'value',
|
||||
not_nested: 'value2',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 { get } from 'lodash';
|
||||
import { Groupings } from '../../../domain/models';
|
||||
|
||||
/**
|
||||
* Takes a list of groupBy fields and the nested groupings object provided from
|
||||
* ES search results and returns a flatted object with the `groupBy` fields as keys
|
||||
* @param groupBy an array of groupBy fields
|
||||
* @param groupings a nested object of groupings
|
||||
* @returns a flattened object of groupings
|
||||
*/
|
||||
|
||||
export const getFlattenedGroupings = ({
|
||||
groupBy,
|
||||
groupings,
|
||||
}: {
|
||||
groupBy: string[] | string;
|
||||
groupings: Record<string, unknown>;
|
||||
}): Groupings => {
|
||||
const groupByFields = groupBy ? [groupBy].flat() : [];
|
||||
const hasGroupings = Object.keys(groupings || []).length;
|
||||
const formattedGroupings = hasGroupings
|
||||
? groupByFields.reduce<Groupings>((acc, group) => {
|
||||
acc[group] = `${get(groupings, group)}`;
|
||||
return acc;
|
||||
}, {})
|
||||
: {};
|
||||
return formattedGroupings;
|
||||
};
|
|
@ -129,6 +129,12 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
script: { source: `emit('${id}')` },
|
||||
},
|
||||
'slo.revision': { type: 'long', script: { source: 'emit(1)' } },
|
||||
'slo.instanceId': {
|
||||
script: {
|
||||
source: "emit('tags:'+doc['tags'].value)",
|
||||
},
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
dest: {
|
||||
|
@ -141,7 +147,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
group_by: {
|
||||
'slo.id': { terms: { field: 'slo.id' } },
|
||||
'slo.revision': { terms: { field: 'slo.revision' } },
|
||||
'slo.instanceId': { terms: { field: 'tags' } },
|
||||
'slo.instanceId': { terms: { field: 'slo.instanceId' } },
|
||||
'slo.groupings.tags': { terms: { field: 'tags' } },
|
||||
'@timestamp': { date_histogram: { field: '@timestamp', fixed_interval: '1m' } },
|
||||
},
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { cleanup } from '@kbn/infra-forge';
|
||||
import expect from '@kbn/expect';
|
||||
import expect from 'expect';
|
||||
import type { CreateSLOInput } from '@kbn/slo-schema';
|
||||
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
@ -44,7 +44,9 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await slo.deleteAllSLOs();
|
||||
await retry.tryForTime(60 * 1000, async () => {
|
||||
await slo.deleteAllSLOs();
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
@ -64,7 +66,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(getResponse.body).eql({
|
||||
expect(getResponse.body).toEqual({
|
||||
name: 'Test SLO for api integration',
|
||||
description: 'Fixture for api integration tests',
|
||||
indicator: {
|
||||
|
@ -82,6 +84,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
objective: { target: 0.99 },
|
||||
tags: ['test'],
|
||||
groupBy: '*',
|
||||
groupings: {},
|
||||
id,
|
||||
settings: { syncDelay: '1m', frequency: '1m' },
|
||||
revision: 1,
|
||||
|
@ -121,7 +124,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.expect(200);
|
||||
|
||||
// expect summary transform to be created
|
||||
expect(getResponse.body).eql({
|
||||
expect(getResponse.body).toEqual({
|
||||
name: 'Test SLO for api integration',
|
||||
description: 'Fixture for api integration tests',
|
||||
indicator: {
|
||||
|
@ -139,6 +142,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
objective: { target: 0.99 },
|
||||
tags: ['test'],
|
||||
groupBy: '*',
|
||||
groupings: {},
|
||||
id,
|
||||
settings: { syncDelay: '1m', frequency: '1m' },
|
||||
revision: 1,
|
||||
|
@ -184,7 +188,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.expect(200);
|
||||
|
||||
// expect summary transform to be created
|
||||
expect(getResponse.body).eql({
|
||||
expect(getResponse.body).toEqual({
|
||||
name: 'Test SLO for api integration',
|
||||
description: 'Fixture for api integration tests',
|
||||
indicator: {
|
||||
|
@ -206,6 +210,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
},
|
||||
tags: ['test'],
|
||||
groupBy: '*',
|
||||
groupings: {},
|
||||
id,
|
||||
settings: { syncDelay: '1m', frequency: '1m' },
|
||||
revision: 1,
|
||||
|
@ -250,7 +255,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(getResponse.body).eql({
|
||||
expect(getResponse.body).toEqual({
|
||||
name: 'Test SLO for api integration',
|
||||
description: 'Fixture for api integration tests',
|
||||
indicator: {
|
||||
|
@ -272,6 +277,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
},
|
||||
tags: ['test'],
|
||||
groupBy: '*',
|
||||
groupings: {},
|
||||
id,
|
||||
settings: { syncDelay: '1m', frequency: '1m' },
|
||||
revision: 1,
|
||||
|
@ -295,7 +301,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('gets slos by query', async () => {
|
||||
const id = await createSLO();
|
||||
await createSLO();
|
||||
await createSLO({ name: 'test int' });
|
||||
|
||||
await retry.tryForTime(300 * 1000, async () => {
|
||||
|
@ -305,7 +311,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.results.length).eql(2);
|
||||
expect(response.body.results.length).toEqual(2);
|
||||
|
||||
const searchResponse = await supertestAPI
|
||||
.get(`/api/observability/slos?kqlQuery=slo.name%3Aapi*`)
|
||||
|
@ -313,7 +319,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(searchResponse.body.results.length).eql(1);
|
||||
expect(searchResponse.body.results.length).toEqual(1);
|
||||
|
||||
const searchResponse2 = await supertestAPI
|
||||
.get(`/api/observability/slos?kqlQuery=slo.name%3Aint`)
|
||||
|
@ -321,7 +327,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(searchResponse2.body.results.length).eql(1);
|
||||
expect(searchResponse2.body.results.length).toEqual(1);
|
||||
|
||||
const searchResponse3 = await supertestAPI
|
||||
.get(`/api/observability/slos?kqlQuery=slo.name%3Aint*`)
|
||||
|
@ -329,7 +335,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(searchResponse3.body.results.length).eql(2);
|
||||
expect(searchResponse3.body.results.length).toEqual(2);
|
||||
|
||||
const searchResponse4 = await supertestAPI
|
||||
.get(`/api/observability/slos?kqlQuery=int*`)
|
||||
|
@ -337,7 +343,25 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(searchResponse4.body.results.length).eql(2);
|
||||
expect(searchResponse4.body.results.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('gets slos instances', async () => {
|
||||
const id = await createSLO();
|
||||
|
||||
await retry.tryForTime(400 * 1000, async () => {
|
||||
const response = await supertestAPI
|
||||
.get(`/api/observability/slos`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.results.length).toEqual(3);
|
||||
|
||||
response.body.results.forEach((result: Record<string, unknown>, i: number) => {
|
||||
expect(result.groupings).toEqual(expect.objectContaining({ tags: `${i + 1}` }));
|
||||
});
|
||||
|
||||
const instanceResponse = await supertestAPI
|
||||
.get(`/internal/observability/slos/${id}/_instances`)
|
||||
|
@ -346,8 +370,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.expect(200);
|
||||
|
||||
// expect 3 instances to be created
|
||||
expect(instanceResponse.body.groupBy).eql('tags');
|
||||
expect(instanceResponse.body.instances.sort()).eql(['1', '2', '3']);
|
||||
expect(instanceResponse.body.groupBy).toEqual('tags');
|
||||
expect(instanceResponse.body.instances.sort()).toEqual(['tags:1', 'tags:2', 'tags:3']);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -360,7 +384,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).eql({
|
||||
expect(response.body).toEqual({
|
||||
page: 1,
|
||||
perPage: 100,
|
||||
results: [
|
||||
|
@ -443,7 +467,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(searchResponse.body.total).eql(1);
|
||||
expect(searchResponse.body.total).toEqual(1);
|
||||
|
||||
const searchResponse2 = await supertestAPI
|
||||
.get(`/api/observability/slos/_definitions?search=int`)
|
||||
|
@ -451,7 +475,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(searchResponse2.body.total).eql(1);
|
||||
expect(searchResponse2.body.total).toEqual(1);
|
||||
|
||||
const searchResponse3 = await supertestAPI
|
||||
.get(`/api/observability/slos/_definitions?search=int*`)
|
||||
|
@ -459,7 +483,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(searchResponse3.body.total).eql(2);
|
||||
expect(searchResponse3.body.total).toEqual(2);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -146,6 +146,12 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
script: { source: `emit('${id}')` },
|
||||
},
|
||||
'slo.revision': { type: 'long', script: { source: 'emit(2)' } },
|
||||
'slo.instanceId': {
|
||||
script: {
|
||||
source: "emit('hosts:'+doc['hosts'].value)",
|
||||
},
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
dest: {
|
||||
|
@ -158,7 +164,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
group_by: {
|
||||
'slo.id': { terms: { field: 'slo.id' } },
|
||||
'slo.revision': { terms: { field: 'slo.revision' } },
|
||||
'slo.instanceId': { terms: { field: 'hosts' } },
|
||||
'slo.instanceId': { terms: { field: 'slo.instanceId' } },
|
||||
'slo.groupings.hosts': { terms: { field: 'hosts' } },
|
||||
'@timestamp': { date_histogram: { field: '@timestamp', fixed_interval: '1m' } },
|
||||
},
|
||||
|
|
|
@ -37,15 +37,13 @@ export function SloApiProvider({ getService }: FtrProviderContext) {
|
|||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
await Promise.all(
|
||||
(response.body as FindSLODefinitionsResponse).results.map(({ id }) => {
|
||||
return supertest
|
||||
.delete(`/api/observability/slos/${id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(204);
|
||||
})
|
||||
);
|
||||
for (const { id } of (response.body as FindSLODefinitionsResponse).results) {
|
||||
await supertest
|
||||
.delete(`/api/observability/slos/${id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(204);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue