Skip to content

Commit

Permalink
[8.8] feat(slo): simplify calendar aligned time window (#157759) (#15…
Browse files Browse the repository at this point in the history
…7789)

# Backport

This will backport the following commits from `main` to `8.8`:
- [feat(slo): simplify calendar aligned time window
(#157759)](#157759)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Kevin
Delemme","email":"kevin.delemme@elastic.co"},"sourceCommit":{"committedDate":"2023-05-15T18:59:36Z","message":"feat(slo):
simplify calendar aligned time window
(#157759)","sha":"e5f4dca5557a9d25acfd2ec2632aa3f35cd3732e","branchLabelMapping":{"^v8.9.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:
Actionable
Observability","backport:prev-minor","v8.9.0"],"number":157759,"url":"https://github.com/elastic/kibana/pull/157759","mergeCommit":{"message":"feat(slo):
simplify calendar aligned time window
(#157759)","sha":"e5f4dca5557a9d25acfd2ec2632aa3f35cd3732e"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.9.0","labelRegex":"^v8.9.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/157759","number":157759,"mergeCommit":{"message":"feat(slo):
simplify calendar aligned time window
(#157759)","sha":"e5f4dca5557a9d25acfd2ec2632aa3f35cd3732e"}}]}]
BACKPORT-->

Co-authored-by: Kevin Delemme <kevin.delemme@elastic.co>
  • Loading branch information
kibanamachine and kdelemme authored May 15, 2023
1 parent a3b5c2a commit 4c2b4a0
Show file tree
Hide file tree
Showing 19 changed files with 405 additions and 462 deletions.
3 changes: 1 addition & 2 deletions packages/kbn-slo-schema/src/schema/time_window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
*/

import * as t from 'io-ts';
import { dateType } from './common';
import { durationType } from './duration';

const rollingTimeWindowSchema = t.type({
Expand All @@ -17,7 +16,7 @@ const rollingTimeWindowSchema = t.type({

const calendarAlignedTimeWindowSchema = t.type({
duration: durationType,
calendar: t.type({ startTime: dateType }),
isCalendar: t.literal<boolean>(true),
});

const timeWindowSchema = t.union([rollingTimeWindowSchema, calendarAlignedTimeWindowSchema]);
Expand Down
16 changes: 6 additions & 10 deletions x-pack/plugins/observability/docs/openapi/slo/bundled.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -503,22 +503,18 @@ components:
title: Calendar aligned time window definition
required:
- duration
- calendar
- isCalendar
description: Defines properties for calendar aligned time window
type: object
properties:
duration:
description: the duration formatted as {duration}{unit}
description: the duration formatted as {duration}{unit}, accept '1w' (weekly calendar) or '1M' (monthly calendar) only
type: string
example: 1M
calendar:
description: Defines the calendar start date
type: object
properties:
startTime:
description: The start date to use.
type: string
example: '2022-01-01T08:00:00.000Z'
isCalendar:
description: Indicates a calendar aligned time window
type: boolean
example: true
budgeting_method:
title: Budgeting method
type: string
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
title: Calendar aligned time window definition
required:
- duration
- calendar
- isCalendar
description: Defines properties for calendar aligned time window
type: object
properties:
duration:
description: the duration formatted as {duration}{unit}
description: the duration formatted as {duration}{unit}, accept '1w' (weekly calendar) or '1M' (monthly calendar) only
type: string
example: 1M
calendar:
description: Defines the calendar start date
type: object
properties:
startTime:
description: The start date to use.
type: string
example: "2022-01-01T08:00:00.000Z"
isCalendar:
description: Indicates a calendar aligned time window
type: boolean
example: true
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,14 @@ export function SloStatusBadge({ slo }: SloStatusProps) {
</EuiBadge>
)}
</EuiFlexItem>

{slo.summary.errorBudget.isEstimated && (
<EuiFlexItem grow={false}>
<div>
<EuiBadge color="default">
{i18n.translate('xpack.observability.slo.sloStatusBadge.forecasted', {
defaultMessage: 'Forecasted',
})}
</EuiBadge>
</div>
<EuiBadge color="default">
{i18n.translate('xpack.observability.slo.sloStatusBadge.forecasted', {
defaultMessage: 'Forecasted',
})}
</EuiBadge>
</EuiFlexItem>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const buildCalendarAlignedTimeWindow = (
): SLOWithSummaryResponse['timeWindow'] => {
return {
duration: '1M',
calendar: { startTime: '2023-01-01T00:00:00.000Z' },
isCalendar: true,
...params,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -31,30 +31,16 @@ With7DaysRolling.args = { slo: buildSlo({ timeWindow: { duration: '7d', isRollin
export const With30DaysRolling = Template.bind({});
With30DaysRolling.args = { slo: buildSlo({ timeWindow: { duration: '30d', isRolling: true } }) };

export const WithMonthlyCalendarStartingToday = Template.bind({});
WithMonthlyCalendarStartingToday.args = {
export const WithWeeklyCalendar = Template.bind({});
WithWeeklyCalendar.args = {
slo: buildSlo({
timeWindow: { duration: '1M', calendar: { startTime: new Date().toISOString() } },
timeWindow: { duration: '1w', isCalendar: true },
}),
};

export const WithMonthlyCalendar = Template.bind({});
WithMonthlyCalendar.args = {
slo: buildSlo({
timeWindow: { duration: '1M', calendar: { startTime: '2022-01-01T00:00:00.000Z' } },
}),
};

export const WithBiWeeklyCalendar = Template.bind({});
WithBiWeeklyCalendar.args = {
slo: buildSlo({
timeWindow: { duration: '2w', calendar: { startTime: '2023-01-01T00:00:00.000Z' } },
}),
};

export const WithQuarterlyCalendar = Template.bind({});
WithQuarterlyCalendar.args = {
slo: buildSlo({
timeWindow: { duration: '1Q', calendar: { startTime: '2022-01-01T00:00:00.000Z' } },
timeWindow: { duration: '1M', isCalendar: true },
}),
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export interface Props {
}

export function SloTimeWindowBadge({ slo }: Props) {
const duration = Number(slo.timeWindow.duration.slice(0, -1));
const unit = slo.timeWindow.duration.slice(-1);
if ('isRolling' in slo.timeWindow) {
return (
Expand All @@ -38,15 +37,11 @@ export function SloTimeWindowBadge({ slo }: Props) {

const unitMoment = toMomentUnitOfTime(unit);
const now = moment.utc();
const startTime = moment.utc(slo.timeWindow.calendar.startTime);
const differenceInUnit = now.diff(startTime, unitMoment);

const periodStart = startTime
.clone()
.add(Math.floor(differenceInUnit / duration) * duration, unitMoment);
const periodEnd = periodStart.clone().add(duration, unitMoment);
const periodStart = now.clone().startOf(unitMoment!);
const periodEnd = now.clone().endOf(unitMoment!);

const totalDurationInDays = periodEnd.diff(periodStart, 'days');
const totalDurationInDays = periodEnd.diff(periodStart, 'days') + 1;
const elapsedDurationInDays = now.diff(periodStart, 'days') + 1;

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
* 2.0.
*/

import { twoDaysAgo } from '../../services/slo/fixtures/date';
import { oneMinute } from '../../services/slo/fixtures/duration';
import { createSLO } from '../../services/slo/fixtures/slo';
import { sevenDaysRolling, weeklyCalendarAligned } from '../../services/slo/fixtures/time_window';
Expand Down Expand Up @@ -64,10 +63,14 @@ describe('computeErrorBudget', () => {

describe('for calendar aligned time window', () => {
describe('for occurrences budgeting method', () => {
beforeEach(() => {
jest.useFakeTimers({ now: new Date('2023-05-09') });
});

it('computes the error budget with an estimation of total events', () => {
const slo = createSLO({
budgetingMethod: 'occurrences',
timeWindow: weeklyCalendarAligned(twoDaysAgo()),
timeWindow: weeklyCalendarAligned(),
objective: { target: 0.95 },
});
const dateRange = toDateRange(slo.timeWindow);
Expand All @@ -90,7 +93,7 @@ describe('computeErrorBudget', () => {
it('computes the error budget', () => {
const slo = createSLO({
budgetingMethod: 'timeslices',
timeWindow: weeklyCalendarAligned(twoDaysAgo()),
timeWindow: weeklyCalendarAligned(),
objective: { target: 0.95, timesliceTarget: 0.95, timesliceWindow: oneMinute() },
});
const dateRange = toDateRange(slo.timeWindow);
Expand All @@ -106,8 +109,8 @@ describe('computeErrorBudget', () => {
// consumed = error rate / error budget = 0.00565476 / 0.05 = 0.1130952
expect(errorBudget).toEqual({
initial: 0.05,
consumed: 0.113095,
remaining: 0.886905,
consumed: 0.113106,
remaining: 0.886894,
isEstimated: false,
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,97 +8,25 @@
import { TimeWindow } from '../models/time_window';
import { Duration } from '../models';
import { toDateRange } from './date_range';
import {
oneMonth,
oneQuarter,
oneWeek,
thirtyDays,
twoWeeks,
} from '../../services/slo/fixtures/duration';
import { oneMonth, oneQuarter, oneWeek, thirtyDays } from '../../services/slo/fixtures/duration';

const NOW = new Date('2022-08-11T08:31:00.000Z');

describe('toDateRange', () => {
describe('for calendar aligned time window', () => {
it('throws when start_time is in the future', () => {
const futureDate = new Date();
futureDate.setFullYear(futureDate.getFullYear() + 1);

const timeWindow = aCalendarTimeWindow(oneWeek(), futureDate);
expect(() => toDateRange(timeWindow, NOW)).toThrow(
'Cannot compute date range with future starting time'
);
});

describe("with 'weekly' duration", () => {
it('computes the date range when starting the same day', () => {
const timeWindow = aCalendarTimeWindow(oneWeek(), new Date('2022-08-11T08:30:00.000Z'));
expect(toDateRange(timeWindow, NOW)).toEqual({
from: new Date('2022-08-11T08:30:00.000Z'),
to: new Date('2022-08-18T08:30:00.000Z'),
});
});

it('computes the date range when starting a month ago', () => {
const timeWindow = aCalendarTimeWindow(oneWeek(), new Date('2022-07-05T08:00:00.000Z'));
expect(toDateRange(timeWindow, NOW)).toEqual({
from: new Date('2022-08-09T08:00:00.000Z'),
to: new Date('2022-08-16T08:00:00.000Z'),
});
it('computes the date range for weekly calendar', () => {
const timeWindow = aCalendarTimeWindow(oneWeek());
expect(toDateRange(timeWindow, NOW)).toEqual({
from: new Date('2022-08-07T00:00:00.000Z'),
to: new Date('2022-08-13T23:59:59.999Z'),
});
});

describe("with 'bi-weekly' duration", () => {
it('computes the date range when starting the same day', () => {
const timeWindow = aCalendarTimeWindow(twoWeeks(), new Date('2022-08-11T08:00:00.000Z'));
expect(toDateRange(timeWindow, NOW)).toEqual({
from: new Date('2022-08-11T08:00:00.000Z'),
to: new Date('2022-08-25T08:00:00.000Z'),
});
});

it('computes the date range when starting a month ago', () => {
const timeWindow = aCalendarTimeWindow(twoWeeks(), new Date('2022-07-05T08:00:00.000Z'));
expect(toDateRange(timeWindow, NOW)).toEqual({
from: new Date('2022-08-02T08:00:00.000Z'),
to: new Date('2022-08-16T08:00:00.000Z'),
});
});
});

describe("with 'monthly' duration", () => {
it('computes the date range when starting the same month', () => {
const timeWindow = aCalendarTimeWindow(oneMonth(), new Date('2022-08-01T08:00:00.000Z'));
expect(toDateRange(timeWindow, NOW)).toEqual({
from: new Date('2022-08-01T08:00:00.000Z'),
to: new Date('2022-09-01T08:00:00.000Z'),
});
});

it('computes the date range when starting a month ago', () => {
const timeWindow = aCalendarTimeWindow(oneMonth(), new Date('2022-07-01T08:00:00.000Z'));
expect(toDateRange(timeWindow, NOW)).toEqual({
from: new Date('2022-08-01T08:00:00.000Z'),
to: new Date('2022-09-01T08:00:00.000Z'),
});
});
});

describe("with 'quarterly' duration", () => {
it('computes the date range when starting the same quarter', () => {
const timeWindow = aCalendarTimeWindow(oneQuarter(), new Date('2022-07-01T08:00:00.000Z'));
expect(toDateRange(timeWindow, NOW)).toEqual({
from: new Date('2022-07-01T08:00:00.000Z'),
to: new Date('2022-10-01T08:00:00.000Z'),
});
});

it('computes the date range when starting a quarter ago', () => {
const timeWindow = aCalendarTimeWindow(oneQuarter(), new Date('2022-03-01T08:00:00.000Z'));
expect(toDateRange(timeWindow, NOW)).toEqual({
from: new Date('2022-06-01T08:00:00.000Z'),
to: new Date('2022-09-01T08:00:00.000Z'),
});
it('computes the date range for monthly calendar', () => {
const timeWindow = aCalendarTimeWindow(oneMonth());
expect(toDateRange(timeWindow, NOW)).toEqual({
from: new Date('2022-08-01T00:00:00.000Z'),
to: new Date('2022-08-31T23:59:59.999Z'),
});
});
});
Expand Down Expand Up @@ -134,10 +62,10 @@ describe('toDateRange', () => {
});
});

function aCalendarTimeWindow(duration: Duration, startTime: Date): TimeWindow {
function aCalendarTimeWindow(duration: Duration): TimeWindow {
return {
duration,
calendar: { startTime },
isCalendar: true,
};
}

Expand Down
21 changes: 4 additions & 17 deletions x-pack/plugins/observability/server/domain/services/date_range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,22 @@
import { assertNever } from '@kbn/std';
import moment from 'moment';
import { calendarAlignedTimeWindowSchema, rollingTimeWindowSchema } from '@kbn/slo-schema';
import { DateRange, toMomentUnitOfTime } from '../models';

import { DateRange, toMomentUnitOfTime } from '../models';
import type { TimeWindow } from '../models/time_window';

export const toDateRange = (timeWindow: TimeWindow, currentDate: Date = new Date()): DateRange => {
if (calendarAlignedTimeWindowSchema.is(timeWindow)) {
const unit = toMomentUnitOfTime(timeWindow.duration.unit);
const now = moment.utc(currentDate).startOf('minute');
const startTime = moment.utc(timeWindow.calendar.startTime);

const differenceInUnit = now.diff(startTime, unit);
if (differenceInUnit < 0) {
throw new Error('Cannot compute date range with future starting time');
}

const from = startTime
.clone()
.add(
Math.floor(differenceInUnit / timeWindow.duration.value) * timeWindow.duration.value,
unit
);
const to = from.clone().add(timeWindow.duration.value, unit);
const from = moment.utc(currentDate).startOf(unit);
const to = moment.utc(currentDate).endOf(unit);

return { from: from.toDate(), to: to.toDate() };
}

if (rollingTimeWindowSchema.is(timeWindow)) {
const unit = toMomentUnitOfTime(timeWindow.duration.unit);
const now = moment(currentDate).startOf('minute');
const now = moment.utc(currentDate).startOf('minute');

return {
from: now.clone().subtract(timeWindow.duration.value, unit).toDate(),
Expand Down
Loading

0 comments on commit 4c2b4a0

Please sign in to comment.