chore(slo): improve calendar slo (#159069)

This commit is contained in:
Kevin Delemme 2023-06-06 14:32:29 -04:00 committed by GitHub
parent 6cf0c8c564
commit 1a8e0d1207
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 194 additions and 83 deletions

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Duration } from '../..';
import { DurationUnit } from '../models/duration';
import { rollingTimeWindowSchema } from './time_window';
describe('time window schema', () => {
it('type guards correctly', () => {
expect(
rollingTimeWindowSchema.is({ duration: new Duration(1, DurationUnit.Month), type: 'rolling' })
).toBe(true);
expect(
rollingTimeWindowSchema.is({
duration: new Duration(1, DurationUnit.Month),
type: 'calendarAligned',
})
).toBe(false);
});
});

View file

@ -9,16 +9,24 @@
import * as t from 'io-ts';
import { durationType } from './duration';
const rollingTimeWindowTypeSchema = t.literal('rolling');
const rollingTimeWindowSchema = t.type({
duration: durationType,
isRolling: t.literal<boolean>(true),
type: rollingTimeWindowTypeSchema,
});
const calendarAlignedTimeWindowTypeSchema = t.literal('calendarAligned');
const calendarAlignedTimeWindowSchema = t.type({
duration: durationType,
isCalendar: t.literal<boolean>(true),
type: calendarAlignedTimeWindowTypeSchema,
});
const timeWindowSchema = t.union([rollingTimeWindowSchema, calendarAlignedTimeWindowSchema]);
export { rollingTimeWindowSchema, calendarAlignedTimeWindowSchema, timeWindowSchema };
export {
rollingTimeWindowSchema,
rollingTimeWindowTypeSchema,
calendarAlignedTimeWindowSchema,
calendarAlignedTimeWindowTypeSchema,
timeWindowSchema,
};

View file

@ -138,7 +138,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"siem-ui-timeline": "670a02b3c2a399bca781ff1e4781793b208b471a",
"siem-ui-timeline-note": "0a32fb776907f596bedca292b8c646496ae9c57b",
"siem-ui-timeline-pinned-event": "082daa3ce647b33873f6abccf340bdfa32057c8d",
"slo": "4415e0ae7af10b79a207843acee454a931a01386",
"slo": "2048ab6791df2e1ae0936f29c20765cb8d2fcfaa",
"space": "8de4ec513e9bbc6b2f1d635161d850be7747d38e",
"spaces-usage-stats": "3abca98713c52af8b30300e386c7779b3025a20e",
"synthetics-monitor": "ca7c0710c0607e44b2c52e5a41086b8b4a214f63",

View file

@ -43,7 +43,7 @@ const baseSlo: Omit<SLOWithSummaryResponse, 'id'> = {
},
timeWindow: {
duration: '30d',
isRolling: true,
type: 'rolling',
},
objective: { target: 0.98 },
budgetingMethod: 'occurrences',

View file

@ -12,7 +12,7 @@ export const buildRollingTimeWindow = (
): SLOWithSummaryResponse['timeWindow'] => {
return {
duration: '30d',
isRolling: true,
type: 'rolling',
...params,
};
};
@ -22,7 +22,7 @@ export const buildCalendarAlignedTimeWindow = (
): SLOWithSummaryResponse['timeWindow'] => {
return {
duration: '1M',
isCalendar: true,
type: 'calendarAligned',
...params,
};
};

View file

@ -8,12 +8,11 @@
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStat, EuiText, EuiTitle } from '@elastic/eui';
import numeral from '@elastic/numeral';
import { i18n } from '@kbn/i18n';
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import { rollingTimeWindowTypeSchema, SLOWithSummaryResponse } from '@kbn/slo-schema';
import React from 'react';
import { useKibana } from '../../../utils/kibana_react';
import { toDurationLabel } from '../../../utils/slo/labels';
import { ChartData } from '../../../typings/slo';
import { useKibana } from '../../../utils/kibana_react';
import { toDurationAdverbLabel, toDurationLabel } from '../../../utils/slo/labels';
import { WideChart } from './wide_chart';
export interface Props {
@ -43,10 +42,15 @@ export function ErrorBudgetChartPanel({ data, isLoading, slo }: Props) {
</EuiFlexItem>
<EuiFlexItem>
<EuiText color="subdued" size="s">
{i18n.translate('xpack.observability.slo.sloDetails.errorBudgetChartPanel.duration', {
defaultMessage: 'Last {duration}',
values: { duration: toDurationLabel(slo.timeWindow.duration) },
})}
{rollingTimeWindowTypeSchema.is(slo.timeWindow.type)
? i18n.translate(
'xpack.observability.slo.sloDetails.errorBudgetChartPanel.duration',
{
defaultMessage: 'Last {duration}',
values: { duration: toDurationLabel(slo.timeWindow.duration) },
}
)
: toDurationAdverbLabel(slo.timeWindow.duration)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -8,13 +8,21 @@
import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui';
import numeral from '@elastic/numeral';
import { i18n } from '@kbn/i18n';
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import {
occurrencesBudgetingMethodSchema,
rollingTimeWindowTypeSchema,
SLOWithSummaryResponse,
} from '@kbn/slo-schema';
import moment from 'moment';
import React from 'react';
import { toBudgetingMethodLabel, toIndicatorTypeLabel } from '../../../utils/slo/labels';
import { toDurationLabel } from '../../../utils/slo/labels';
import { useKibana } from '../../../utils/kibana_react';
import {
BUDGETING_METHOD_OCCURRENCES,
BUDGETING_METHOD_TIMESLICES,
toDurationAdverbLabel,
toDurationLabel,
toIndicatorTypeLabel,
} from '../../../utils/slo/labels';
import { OverviewItem } from './overview_item';
export interface Props {
@ -71,11 +79,28 @@ export function Overview({ slo }: Props) {
<OverviewItem
title={i18n.translate(
'xpack.observability.slo.sloDetails.overview.budgetingMethodTitle',
{
defaultMessage: 'Budgeting method',
}
{ defaultMessage: 'Budgeting method' }
)}
subtitle={<EuiText size="s">{toBudgetingMethodLabel(slo.budgetingMethod)}</EuiText>}
subtitle={
occurrencesBudgetingMethodSchema.is(slo.budgetingMethod) ? (
<EuiText size="s">{BUDGETING_METHOD_OCCURRENCES}</EuiText>
) : (
<EuiText size="s">
{BUDGETING_METHOD_TIMESLICES} (
{i18n.translate(
'xpack.observability.slo.sloDetails.overview.timeslicesBudgetingMethodDetails',
{
defaultMessage: '{duration} slices, {target} target',
values: {
duration: toDurationLabel(slo.objective.timesliceWindow!),
target: numeral(slo.objective.timesliceTarget!).format(percentFormat),
},
}
)}
)
</EuiText>
)
}
/>
</EuiFlexGroup>
@ -128,7 +153,7 @@ export function Overview({ slo }: Props) {
}
function toTimeWindowLabel(timeWindow: SLOWithSummaryResponse['timeWindow']): string {
if ('isRolling' in timeWindow) {
if (rollingTimeWindowTypeSchema.is(timeWindow.type)) {
return i18n.translate('xpack.observability.slo.sloDetails.overview.rollingTimeWindow', {
defaultMessage: '{duration} rolling',
values: {
@ -138,9 +163,9 @@ function toTimeWindowLabel(timeWindow: SLOWithSummaryResponse['timeWindow']): st
}
return i18n.translate('xpack.observability.slo.sloDetails.overview.calendarAlignedTimeWindow', {
defaultMessage: '{duration}',
defaultMessage: '{duration} calendar aligned',
values: {
duration: timeWindow.duration,
duration: toDurationAdverbLabel(timeWindow.duration),
},
});
}

View file

@ -8,12 +8,11 @@
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStat, EuiText, EuiTitle } from '@elastic/eui';
import numeral from '@elastic/numeral';
import { i18n } from '@kbn/i18n';
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import { rollingTimeWindowTypeSchema, SLOWithSummaryResponse } from '@kbn/slo-schema';
import React from 'react';
import { useKibana } from '../../../utils/kibana_react';
import { toDurationLabel } from '../../../utils/slo/labels';
import { ChartData } from '../../../typings/slo';
import { useKibana } from '../../../utils/kibana_react';
import { toDurationAdverbLabel, toDurationLabel } from '../../../utils/slo/labels';
import { WideChart } from './wide_chart';
export interface Props {
@ -44,10 +43,15 @@ export function SliChartPanel({ data, isLoading, slo }: Props) {
</EuiFlexItem>
<EuiFlexItem>
<EuiText color="subdued" size="s">
{i18n.translate('xpack.observability.slo.sloDetails.sliHistoryChartPanel.duration', {
defaultMessage: 'Last {duration}',
values: { duration: toDurationLabel(slo.timeWindow.duration) },
})}
{rollingTimeWindowTypeSchema.is(slo.timeWindow.type)
? i18n.translate(
'xpack.observability.slo.sloDetails.sliHistoryChartPanel.duration',
{
defaultMessage: 'Last {duration}',
values: { duration: toDurationLabel(slo.timeWindow.duration) },
}
)
: toDurationAdverbLabel(slo.timeWindow.duration)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -73,7 +73,7 @@ export const SLO_EDIT_FORM_DEFAULT_VALUES: CreateSLOInput = {
timeWindow: {
duration:
TIMEWINDOW_OPTIONS[TIMEWINDOW_OPTIONS.findIndex((option) => option.value === '30d')].value,
isRolling: true,
type: 'rolling',
},
tags: [],
budgetingMethod: BUDGETING_METHOD_OPTIONS[0].value,
@ -98,7 +98,7 @@ export const SLO_EDIT_FORM_DEFAULT_VALUES_CUSTOM_METRIC: CreateSLOInput = {
timeWindow: {
duration:
TIMEWINDOW_OPTIONS[TIMEWINDOW_OPTIONS.findIndex((option) => option.value === '30d')].value,
isRolling: true,
type: 'rolling',
},
tags: [],
budgetingMethod: BUDGETING_METHOD_OPTIONS[0].value,

View file

@ -338,7 +338,7 @@ describe('SLO Edit Page', () => {
},
"timeWindow": Object {
"duration": "7d",
"isRolling": true,
"type": "rolling",
},
},
],

View file

@ -26,21 +26,21 @@ const Template: ComponentStory<typeof Component> = (props: Props) => (
);
export const With7DaysRolling = Template.bind({});
With7DaysRolling.args = { slo: buildSlo({ timeWindow: { duration: '7d', isRolling: true } }) };
With7DaysRolling.args = { slo: buildSlo({ timeWindow: { duration: '7d', type: 'rolling' } }) };
export const With30DaysRolling = Template.bind({});
With30DaysRolling.args = { slo: buildSlo({ timeWindow: { duration: '30d', isRolling: true } }) };
With30DaysRolling.args = { slo: buildSlo({ timeWindow: { duration: '30d', type: 'rolling' } }) };
export const WithWeeklyCalendar = Template.bind({});
WithWeeklyCalendar.args = {
slo: buildSlo({
timeWindow: { duration: '1w', isCalendar: true },
timeWindow: { duration: '1w', type: 'calendarAligned' },
}),
};
export const WithMonthlyCalendar = Template.bind({});
WithMonthlyCalendar.args = {
slo: buildSlo({
timeWindow: { duration: '1M', isCalendar: true },
timeWindow: { duration: '1M', type: 'calendarAligned' },
}),
};

View file

@ -5,13 +5,12 @@
* 2.0.
*/
import moment from 'moment';
import React from 'react';
import { EuiBadge, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { rollingTimeWindowTypeSchema, SLOWithSummaryResponse } from '@kbn/slo-schema';
import { euiLightVars } from '@kbn/ui-theme';
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import moment from 'moment';
import React from 'react';
import { toMomentUnitOfTime } from '../../../../utils/slo/duration';
import { toDurationLabel } from '../../../../utils/slo/labels';
@ -21,7 +20,7 @@ export interface Props {
export function SloTimeWindowBadge({ slo }: Props) {
const unit = slo.timeWindow.duration.slice(-1);
if ('isRolling' in slo.timeWindow) {
if (rollingTimeWindowTypeSchema.is(slo.timeWindow.type)) {
return (
<EuiFlexItem grow={false}>
<EuiBadge

View file

@ -66,16 +66,6 @@ export const BUDGETING_METHOD_TIMESLICES = i18n.translate(
}
);
export function toBudgetingMethodLabel(
budgetingMethod: SLOWithSummaryResponse['budgetingMethod']
): string {
if (budgetingMethod === 'occurrences') {
return BUDGETING_METHOD_OCCURRENCES;
}
return BUDGETING_METHOD_TIMESLICES;
}
export function toDurationLabel(durationStr: string): string {
const duration = toDuration(durationStr);
@ -124,3 +114,34 @@ export function toDurationLabel(durationStr: string): string {
});
}
}
export function toDurationAdverbLabel(durationStr: string): string {
const duration = toDuration(durationStr);
switch (duration.unit) {
case 'm':
return i18n.translate('xpack.observability.slo.duration.minutely', {
defaultMessage: 'Minutely',
});
case 'h':
return i18n.translate('xpack.observability.slo.duration.hourly', {
defaultMessage: 'Hourly',
});
case 'd':
return i18n.translate('xpack.observability.slo.duration.daily', {
defaultMessage: 'Daily',
});
case 'w':
return i18n.translate('xpack.observability.slo.duration.weekly', {
defaultMessage: 'Weekly',
});
case 'M':
return i18n.translate('xpack.observability.slo.duration.monthly', {
defaultMessage: 'Monthly',
});
case 'Y':
return i18n.translate('xpack.observability.slo.duration.yearly', {
defaultMessage: 'Yearly',
});
}
}

View file

@ -65,10 +65,10 @@ describe('toDateRange', () => {
function aCalendarTimeWindow(duration: Duration): TimeWindow {
return {
duration,
isCalendar: true,
type: 'calendarAligned',
};
}
function aRollingTimeWindow(duration: Duration): TimeWindow {
return { duration, isRolling: true };
return { duration, type: 'rolling' };
}

View file

@ -24,14 +24,14 @@ describe('validateSLO', () => {
it("throws when time window duration unit is 'm'", () => {
const slo = createSLO({
timeWindow: { duration: new Duration(1, DurationUnit.Minute), isRolling: true },
timeWindow: { duration: new Duration(1, DurationUnit.Minute), type: 'rolling' },
});
expect(() => validateSLO(slo)).toThrowError('Invalid time_window.duration');
});
it("throws when time window duration unit is 'h'", () => {
const slo = createSLO({
timeWindow: { duration: new Duration(1, DurationUnit.Hour), isRolling: true },
timeWindow: { duration: new Duration(1, DurationUnit.Hour), type: 'rolling' },
});
expect(() => validateSLO(slo)).toThrowError('Invalid time_window.duration');
});
@ -56,7 +56,7 @@ describe('validateSLO', () => {
expect(() =>
validateSLO(
createSLO({
timeWindow: { duration, isCalendar: true },
timeWindow: { duration, type: 'calendarAligned' },
})
)
).toThrowError('Invalid time_window.duration');
@ -64,7 +64,7 @@ describe('validateSLO', () => {
expect(() =>
validateSLO(
createSLO({
timeWindow: { duration, isCalendar: true },
timeWindow: { duration, type: 'calendarAligned' },
})
)
).not.toThrowError();
@ -191,7 +191,7 @@ describe('validateSLO', () => {
it("throws when 'objective.timeslice_window' is longer than 'slo.time_window'", () => {
const slo = createSLO({
timeWindow: { duration: new Duration(1, DurationUnit.Week), isRolling: true },
timeWindow: { duration: new Duration(1, DurationUnit.Week), type: 'rolling' },
budgetingMethod: 'timeslices',
objective: {
target: 0.95,

View file

@ -47,14 +47,14 @@ export const fetcher = async (context: CollectorFetchContext) => {
},
by_rolling_duration: {
...acc.by_rolling_duration,
...('isRolling' in so.attributes.timeWindow && {
...(so.attributes.timeWindow.type === 'rolling' && {
[so.attributes.timeWindow.duration]:
(acc.by_rolling_duration[so.attributes.timeWindow.duration] ?? 0) + 1,
}),
},
by_calendar_aligned_duration: {
...acc.by_calendar_aligned_duration,
...('isCalendar' in so.attributes.timeWindow && {
...(so.attributes.timeWindow.type === 'calendarAligned' && {
[so.attributes.timeWindow.duration]:
(acc.by_calendar_aligned_duration[so.attributes.timeWindow.duration] ?? 0) + 1,
}),

View file

@ -5,11 +5,33 @@
* 2.0.
*/
import { SavedObjectsType } from '@kbn/core-saved-objects-server';
import { SavedObjectMigrationFn, SavedObjectsType } from '@kbn/core-saved-objects-server';
import { SavedObject } from '@kbn/core/server';
import { StoredSLO } from '../domain/models';
type StoredSLOBefore890 = StoredSLO & {
timeWindow: {
duration: string;
isRolling?: boolean;
isCalendar?: boolean;
};
};
const migrateSlo890: SavedObjectMigrationFn<StoredSLOBefore890, StoredSLO> = (doc) => {
const { timeWindow, ...other } = doc.attributes;
return {
...doc,
attributes: {
...other,
timeWindow: {
duration: timeWindow.duration,
type: timeWindow.isCalendar ? 'calendarAligned' : 'rolling',
},
},
};
};
export const SO_SLO_TYPE = 'slo';
export const slo: SavedObjectsType = {
@ -40,4 +62,7 @@ export const slo: SavedObjectsType = {
return `SLO: [${sloSavedObject.attributes.name}]`;
},
},
migrations: {
'8.9.0': migrateSlo890,
},
};

View file

@ -63,7 +63,7 @@ describe('FindSLO', () => {
},
timeWindow: {
duration: '7d',
isRolling: true,
type: 'rolling',
},
settings: {
syncDelay: '1m',

View file

@ -162,7 +162,7 @@ export const createSLOWithCalendarTimeWindow = (params: Partial<SLO> = {}): SLO
return createSLO({
timeWindow: {
duration: oneWeek(),
isCalendar: true,
type: 'calendarAligned',
},
...params,
});

View file

@ -11,26 +11,26 @@ import { oneWeek, sevenDays, sixHours, thirtyDays } from './duration';
export function sixHoursRolling(): TimeWindow {
return {
duration: sixHours(),
isRolling: true,
type: 'rolling',
};
}
export function sevenDaysRolling(): RollingTimeWindow {
return {
duration: sevenDays(),
isRolling: true,
type: 'rolling',
};
}
export function thirtyDaysRolling(): RollingTimeWindow {
return {
duration: thirtyDays(),
isRolling: true,
type: 'rolling',
};
}
export function weeklyCalendarAligned(): TimeWindow {
return {
duration: oneWeek(),
isCalendar: true,
type: 'calendarAligned',
};
}

View file

@ -62,7 +62,7 @@ describe('GetSLO', () => {
},
timeWindow: {
duration: '7d',
isRolling: true,
type: 'rolling',
},
settings: {
syncDelay: '1m',

View file

@ -138,7 +138,7 @@ describe('FetchHistoricalSummary', () => {
describe('Rolling and Occurrences SLOs', () => {
it('returns the summary', async () => {
const slo = createSLO({
timeWindow: { isRolling: true, duration: thirtyDays() },
timeWindow: { type: 'rolling', duration: thirtyDays() },
objective: { target: 0.95 },
});
esClientMock.msearch.mockResolvedValueOnce(generateEsResponseForRollingSLO(30));
@ -156,7 +156,7 @@ describe('FetchHistoricalSummary', () => {
describe('Rolling and Timeslices SLOs', () => {
it('returns the summary', async () => {
const slo = createSLO({
timeWindow: { isRolling: true, duration: thirtyDays() },
timeWindow: { type: 'rolling', duration: thirtyDays() },
budgetingMethod: 'timeslices',
objective: { target: 0.95, timesliceTarget: 0.9, timesliceWindow: oneMinute() },
});
@ -177,7 +177,7 @@ describe('FetchHistoricalSummary', () => {
const slo = createSLO({
timeWindow: {
duration: oneMonth(),
isCalendar: true,
type: 'calendarAligned',
},
budgetingMethod: 'timeslices',
objective: { target: 0.95, timesliceTarget: 0.9, timesliceWindow: oneMinute() },
@ -200,7 +200,7 @@ describe('FetchHistoricalSummary', () => {
const slo = createSLO({
timeWindow: {
duration: oneMonth(),
isCalendar: true,
type: 'calendarAligned',
},
budgetingMethod: 'occurrences',
objective: { target: 0.95 },

View file

@ -49,7 +49,7 @@ export class DefaultSLIClient implements SLIClient {
const longestLookbackWindow = sortedLookbackWindows[0];
const longestDateRange = toDateRange({
duration: longestLookbackWindow.duration,
isRolling: true,
type: 'rolling',
});
if (occurrencesBudgetingMethodSchema.is(slo.budgetingMethod)) {

View file

@ -89,7 +89,7 @@ export default function ({ getService }: FtrProviderContext) {
createCompositeSLOInput({
timeWindow: {
duration: '30d',
isRolling: true,
type: 'rolling',
},
sources: [
{ id: 'f9072790-f97c-11ed-895c-170d13e61076', revision: 2, weight: 1 },

View file

@ -13,7 +13,7 @@ const defaultCompositeSLOInput: CreateCompositeSLOInput = {
name: 'some composite slo',
timeWindow: {
duration: '7d',
isRolling: true,
type: 'rolling',
},
budgetingMethod: 'occurrences',
objective: {

View file

@ -11,7 +11,7 @@
{ "id": "f9072790-f97c-11ed-895c-170d13e61076", "revision": 2, "weight": 1 }
],
"tags": [],
"timeWindow": { "duration": "7d", "isRolling": true },
"timeWindow": { "duration": "7d", "type": "rolling" },
"updatedAt": "2023-05-24T21:12:37.831Z"
},
"coreMigrationVersion": "8.8.0",

View file

@ -27,7 +27,7 @@
"tags": [],
"timeWindow": {
"duration": "7d",
"isRolling": true
"type": "rolling"
},
"updatedAt": "2023-05-23T15:18:31.650Z"
},
@ -70,7 +70,7 @@
"tags": [],
"timeWindow": {
"duration": "7d",
"isRolling": true
"type": "rolling"
},
"updatedAt": "2023-05-23T15:18:39.734Z"
},