From d4bf252caf11bd42b83a54642c54e78b72b32c15 Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Fri, 14 Jul 2023 15:40:50 -0400 Subject: [PATCH] Improve time window handling and validation (#161978) --- .../src/models/duration.test.ts | 14 +---- .../kbn-slo-schema/src/models/duration.ts | 10 ---- .../kbn-slo-schema/src/schema/common.ts | 4 -- .../badges/slo_time_window_badge.tsx | 8 +-- .../observability/public/typings/slo/index.ts | 2 +- .../public/utils/slo/duration.ts | 17 ++---- .../observability/public/utils/slo/labels.ts | 11 ---- .../server/domain/models/time_window.ts | 36 +++++++++++- .../domain/services/compute_burn_rate.test.ts | 12 ++-- .../server/domain/services/date_range.test.ts | 44 +++++---------- .../server/domain/services/date_range.ts | 29 +++++----- .../domain/services/validate_slo.test.ts | 55 +++++++++++-------- .../server/domain/services/validate_slo.ts | 9 +-- .../server/services/slo/fixtures/duration.ts | 8 +-- .../server/services/slo/fixtures/slo.ts | 16 ++---- .../services/slo/fixtures/time_window.ts | 31 +++++++---- .../server/services/slo/sli_client.ts | 25 +++++++-- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 20 files changed, 165 insertions(+), 169 deletions(-) diff --git a/x-pack/packages/kbn-slo-schema/src/models/duration.test.ts b/x-pack/packages/kbn-slo-schema/src/models/duration.test.ts index 7a7e6ebfec99f..5e1f0e14a5dc3 100644 --- a/x-pack/packages/kbn-slo-schema/src/models/duration.test.ts +++ b/x-pack/packages/kbn-slo-schema/src/models/duration.test.ts @@ -27,8 +27,6 @@ describe('Duration', () => { expect(new Duration(1, DurationUnit.Day).format()).toBe('1d'); expect(new Duration(1, DurationUnit.Week).format()).toBe('1w'); expect(new Duration(1, DurationUnit.Month).format()).toBe('1M'); - expect(new Duration(1, DurationUnit.Quarter).format()).toBe('1Q'); - expect(new Duration(1, DurationUnit.Year).format()).toBe('1Y'); }); }); @@ -39,31 +37,25 @@ describe('Duration', () => { expect(short.isShorterThan(new Duration(1, DurationUnit.Day))).toBe(true); expect(short.isShorterThan(new Duration(1, DurationUnit.Week))).toBe(true); expect(short.isShorterThan(new Duration(1, DurationUnit.Month))).toBe(true); - expect(short.isShorterThan(new Duration(1, DurationUnit.Quarter))).toBe(true); - expect(short.isShorterThan(new Duration(1, DurationUnit.Year))).toBe(true); }); it('returns false when the current duration is longer (or equal) than the other duration', () => { - const long = new Duration(1, DurationUnit.Year); + const long = new Duration(1, DurationUnit.Month); expect(long.isShorterThan(new Duration(1, DurationUnit.Minute))).toBe(false); expect(long.isShorterThan(new Duration(1, DurationUnit.Hour))).toBe(false); expect(long.isShorterThan(new Duration(1, DurationUnit.Day))).toBe(false); expect(long.isShorterThan(new Duration(1, DurationUnit.Week))).toBe(false); expect(long.isShorterThan(new Duration(1, DurationUnit.Month))).toBe(false); - expect(long.isShorterThan(new Duration(1, DurationUnit.Quarter))).toBe(false); - expect(long.isShorterThan(new Duration(1, DurationUnit.Year))).toBe(false); }); }); describe('isLongerOrEqualThan', () => { it('returns true when the current duration is longer or equal than the other duration', () => { - const long = new Duration(2, DurationUnit.Year); + const long = new Duration(2, DurationUnit.Month); expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Hour))).toBe(true); expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Day))).toBe(true); expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Week))).toBe(true); expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Month))).toBe(true); - expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Quarter))).toBe(true); - expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Year))).toBe(true); }); it('returns false when the current duration is shorter than the other duration', () => { @@ -73,8 +65,6 @@ describe('Duration', () => { expect(short.isLongerOrEqualThan(new Duration(1, DurationUnit.Day))).toBe(false); expect(short.isLongerOrEqualThan(new Duration(1, DurationUnit.Week))).toBe(false); expect(short.isLongerOrEqualThan(new Duration(1, DurationUnit.Month))).toBe(false); - expect(short.isLongerOrEqualThan(new Duration(1, DurationUnit.Quarter))).toBe(false); - expect(short.isLongerOrEqualThan(new Duration(1, DurationUnit.Year))).toBe(false); }); }); diff --git a/x-pack/packages/kbn-slo-schema/src/models/duration.ts b/x-pack/packages/kbn-slo-schema/src/models/duration.ts index b6286fad735fb..33ff6cbd25ac8 100644 --- a/x-pack/packages/kbn-slo-schema/src/models/duration.ts +++ b/x-pack/packages/kbn-slo-schema/src/models/duration.ts @@ -14,8 +14,6 @@ enum DurationUnit { 'Day' = 'd', 'Week' = 'w', 'Month' = 'M', - 'Quarter' = 'Q', - 'Year' = 'Y', } class Duration { @@ -73,10 +71,6 @@ const toDurationUnit = (unit: string): DurationUnit => { return DurationUnit.Week; case 'M': return DurationUnit.Month; - case 'Q': - return DurationUnit.Quarter; - case 'y': - return DurationUnit.Year; default: throw new Error('invalid duration unit'); } @@ -94,10 +88,6 @@ const toMomentUnitOfTime = (unit: DurationUnit): moment.unitOfTime.Diff => { return 'weeks'; case DurationUnit.Month: return 'months'; - case DurationUnit.Quarter: - return 'quarters'; - case DurationUnit.Year: - return 'years'; default: assertNever(unit); } diff --git a/x-pack/packages/kbn-slo-schema/src/schema/common.ts b/x-pack/packages/kbn-slo-schema/src/schema/common.ts index 250525ce2192c..166f3eab34a92 100644 --- a/x-pack/packages/kbn-slo-schema/src/schema/common.ts +++ b/x-pack/packages/kbn-slo-schema/src/schema/common.ts @@ -43,8 +43,6 @@ const summarySchema = t.type({ errorBudget: errorBudgetSchema, }); -type SummarySchema = t.TypeOf; - const historicalSummarySchema = t.intersection([ t.type({ date: dateType, @@ -59,8 +57,6 @@ const previewDataSchema = t.type({ const dateRangeSchema = t.type({ from: dateType, to: dateType }); -export type { SummarySchema }; - export { ALL_VALUE, allOrAnyString, diff --git a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_time_window_badge.tsx b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_time_window_badge.tsx index 6cd3168995d25..d218eeda7f0ed 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_time_window_badge.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_time_window_badge.tsx @@ -11,7 +11,7 @@ import { rollingTimeWindowTypeSchema, SLOWithSummaryResponse } from '@kbn/slo-sc import { euiLightVars } from '@kbn/ui-theme'; import moment from 'moment'; import React from 'react'; -import { toMomentUnitOfTime } from '../../../../utils/slo/duration'; +import { toCalendarAlignedMomentUnitOfTime } from '../../../../utils/slo/duration'; import { toDurationLabel } from '../../../../utils/slo/labels'; export interface Props { @@ -34,11 +34,11 @@ export function SloTimeWindowBadge({ slo }: Props) { ); } - const unitMoment = toMomentUnitOfTime(unit); + const unitMoment = toCalendarAlignedMomentUnitOfTime(unit); const now = moment.utc(); - const periodStart = now.clone().startOf(unitMoment!).add(1, 'day'); - const periodEnd = now.clone().endOf(unitMoment!).add(1, 'day'); + const periodStart = now.clone().startOf(unitMoment); + const periodEnd = now.clone().endOf(unitMoment); const totalDurationInDays = periodEnd.diff(periodStart, 'days') + 1; const elapsedDurationInDays = now.diff(periodStart, 'days') + 1; diff --git a/x-pack/plugins/observability/public/typings/slo/index.ts b/x-pack/plugins/observability/public/typings/slo/index.ts index 3b02b76ecacc2..1bc3d7f8de69d 100644 --- a/x-pack/plugins/observability/public/typings/slo/index.ts +++ b/x-pack/plugins/observability/public/typings/slo/index.ts @@ -7,7 +7,7 @@ import { RuleTypeParams } from '@kbn/alerting-plugin/common'; -type DurationUnit = 'm' | 'h' | 'd' | 'w' | 'M' | 'Y'; +type DurationUnit = 'm' | 'h' | 'd' | 'w' | 'M'; interface Duration { value: number; diff --git a/x-pack/plugins/observability/public/utils/slo/duration.ts b/x-pack/plugins/observability/public/utils/slo/duration.ts index 2be1a51a69eab..4e64326116979 100644 --- a/x-pack/plugins/observability/public/utils/slo/duration.ts +++ b/x-pack/plugins/observability/public/utils/slo/duration.ts @@ -28,24 +28,17 @@ export function toMinutes(duration: Duration) { return duration.value * 7 * 24 * 60; case 'M': return duration.value * 30 * 24 * 60; - case 'Y': - return duration.value * 365 * 24 * 60; + default: + assertNever(duration.unit); } - - assertNever(duration.unit); } -export function toMomentUnitOfTime(unit: string): moment.unitOfTime.Diff | undefined { +export function toCalendarAlignedMomentUnitOfTime(unit: string): moment.unitOfTime.StartOf { switch (unit) { - case 'd': - return 'days'; + default: case 'w': - return 'weeks'; + return 'isoWeek'; case 'M': return 'months'; - case 'Q': - return 'quarters'; - case 'Y': - return 'years'; } } diff --git a/x-pack/plugins/observability/public/utils/slo/labels.ts b/x-pack/plugins/observability/public/utils/slo/labels.ts index 43384539f2a84..40c58e624bb2b 100644 --- a/x-pack/plugins/observability/public/utils/slo/labels.ts +++ b/x-pack/plugins/observability/public/utils/slo/labels.ts @@ -112,13 +112,6 @@ export function toDurationLabel(durationStr: string): string { duration: duration.value, }, }); - case 'Y': - return i18n.translate('xpack.observability.slo.duration.year', { - defaultMessage: '{duration, plural, one {1 year} other {# years}}', - values: { - duration: duration.value, - }, - }); } } @@ -146,9 +139,5 @@ export function toDurationAdverbLabel(durationStr: string): string { return i18n.translate('xpack.observability.slo.duration.monthly', { defaultMessage: 'Monthly', }); - case 'Y': - return i18n.translate('xpack.observability.slo.duration.yearly', { - defaultMessage: 'Yearly', - }); } } diff --git a/x-pack/plugins/observability/server/domain/models/time_window.ts b/x-pack/plugins/observability/server/domain/models/time_window.ts index 30aa35cbd9f72..aa12aa70a8ae8 100644 --- a/x-pack/plugins/observability/server/domain/models/time_window.ts +++ b/x-pack/plugins/observability/server/domain/models/time_window.ts @@ -5,10 +5,42 @@ * 2.0. */ +import { + calendarAlignedTimeWindowSchema, + rollingTimeWindowSchema, + timeWindowSchema, +} from '@kbn/slo-schema'; +import moment from 'moment'; import * as t from 'io-ts'; -import { rollingTimeWindowSchema, timeWindowSchema } from '@kbn/slo-schema'; type TimeWindow = t.TypeOf; type RollingTimeWindow = t.TypeOf; +type CalendarAlignedTimeWindow = t.TypeOf; -export type { RollingTimeWindow, TimeWindow }; +export type { RollingTimeWindow, TimeWindow, CalendarAlignedTimeWindow }; + +export function toCalendarAlignedTimeWindowMomentUnit( + timeWindow: CalendarAlignedTimeWindow +): moment.unitOfTime.StartOf { + const unit = timeWindow.duration.unit; + switch (unit) { + case 'w': + return 'isoWeeks'; + case 'M': + return 'months'; + default: + throw new Error(`Invalid calendar aligned time window duration unit: ${unit}`); + } +} + +export function toRollingTimeWindowMomentUnit( + timeWindow: RollingTimeWindow +): moment.unitOfTime.Diff { + const unit = timeWindow.duration.unit; + switch (unit) { + case 'd': + return 'days'; + default: + throw new Error(`Invalid rolling time window duration unit: ${unit}`); + } +} diff --git a/x-pack/plugins/observability/server/domain/services/compute_burn_rate.test.ts b/x-pack/plugins/observability/server/domain/services/compute_burn_rate.test.ts index 542766b6c6544..84fef850e096c 100644 --- a/x-pack/plugins/observability/server/domain/services/compute_burn_rate.test.ts +++ b/x-pack/plugins/observability/server/domain/services/compute_burn_rate.test.ts @@ -8,7 +8,7 @@ import { computeBurnRate } from './compute_burn_rate'; import { toDateRange } from './date_range'; import { createSLO } from '../../services/slo/fixtures/slo'; -import { sixHoursRolling } from '../../services/slo/fixtures/time_window'; +import { ninetyDaysRolling } from '../../services/slo/fixtures/time_window'; describe('computeBurnRate', () => { it('computes 0 when total is 0', () => { @@ -16,7 +16,7 @@ describe('computeBurnRate', () => { computeBurnRate(createSLO(), { good: 10, total: 0, - dateRange: toDateRange(sixHoursRolling()), + dateRange: toDateRange(ninetyDaysRolling()), }) ).toEqual(0); }); @@ -26,7 +26,7 @@ describe('computeBurnRate', () => { computeBurnRate(createSLO(), { good: 9999, total: 1, - dateRange: toDateRange(sixHoursRolling()), + dateRange: toDateRange(ninetyDaysRolling()), }) ).toEqual(0); }); @@ -36,7 +36,7 @@ describe('computeBurnRate', () => { computeBurnRate(createSLO({ objective: { target: 0.9 } }), { good: 90, total: 100, - dateRange: toDateRange(sixHoursRolling()), + dateRange: toDateRange(ninetyDaysRolling()), }) ).toEqual(1); }); @@ -46,7 +46,7 @@ describe('computeBurnRate', () => { computeBurnRate(createSLO({ objective: { target: 0.99 } }), { good: 90, total: 100, - dateRange: toDateRange(sixHoursRolling()), + dateRange: toDateRange(ninetyDaysRolling()), }) ).toEqual(10); }); @@ -56,7 +56,7 @@ describe('computeBurnRate', () => { computeBurnRate(createSLO({ objective: { target: 0.8 } }), { good: 90, total: 100, - dateRange: toDateRange(sixHoursRolling()), + dateRange: toDateRange(ninetyDaysRolling()), }) ).toEqual(0.5); }); diff --git a/x-pack/plugins/observability/server/domain/services/date_range.test.ts b/x-pack/plugins/observability/server/domain/services/date_range.test.ts index e96325fef9088..524aecbfab10f 100644 --- a/x-pack/plugins/observability/server/domain/services/date_range.test.ts +++ b/x-pack/plugins/observability/server/domain/services/date_range.test.ts @@ -5,17 +5,21 @@ * 2.0. */ -import { TimeWindow } from '../models/time_window'; -import { Duration } from '../models'; +import { + monthlyCalendarAligned, + ninetyDaysRolling, + sevenDaysRolling, + thirtyDaysRolling, + weeklyCalendarAligned, +} from '../../services/slo/fixtures/time_window'; import { toDateRange } from './date_range'; -import { oneMonth, oneQuarter, oneWeek, thirtyDays } from '../../services/slo/fixtures/duration'; const NOW = new Date('2022-08-11T08:31:00.000Z'); describe('toDateRange', () => { describe('for calendar aligned time window', () => { it('computes the date range for weekly calendar', () => { - const timeWindow = aCalendarTimeWindow(oneWeek()); + const timeWindow = weeklyCalendarAligned(); expect(toDateRange(timeWindow, NOW)).toEqual({ from: new Date('2022-08-08T00:00:00.000Z'), to: new Date('2022-08-14T23:59:59.999Z'), @@ -23,7 +27,7 @@ describe('toDateRange', () => { }); it('computes the date range for monthly calendar', () => { - const timeWindow = aCalendarTimeWindow(oneMonth()); + const timeWindow = monthlyCalendarAligned(); expect(toDateRange(timeWindow, NOW)).toEqual({ from: new Date('2022-08-01T00:00:00.000Z'), to: new Date('2022-08-31T23:59:59.999Z'), @@ -33,42 +37,24 @@ describe('toDateRange', () => { describe('for rolling time window', () => { it("computes the date range using a '30days' rolling window", () => { - expect(toDateRange(aRollingTimeWindow(thirtyDays()), NOW)).toEqual({ + expect(toDateRange(thirtyDaysRolling(), NOW)).toEqual({ from: new Date('2022-07-12T08:31:00.000Z'), to: new Date('2022-08-11T08:31:00.000Z'), }); }); - it("computes the date range using a 'weekly' rolling window", () => { - expect(toDateRange(aRollingTimeWindow(oneWeek()), NOW)).toEqual({ + it("computes the date range using a '7days' rolling window", () => { + expect(toDateRange(sevenDaysRolling(), NOW)).toEqual({ from: new Date('2022-08-04T08:31:00.000Z'), to: new Date('2022-08-11T08:31:00.000Z'), }); }); - it("computes the date range using a 'monthly' rolling window", () => { - expect(toDateRange(aRollingTimeWindow(oneMonth()), NOW)).toEqual({ - from: new Date('2022-07-11T08:31:00.000Z'), - to: new Date('2022-08-11T08:31:00.000Z'), - }); - }); - - it("computes the date range using a 'quarterly' rolling window", () => { - expect(toDateRange(aRollingTimeWindow(oneQuarter()), NOW)).toEqual({ - from: new Date('2022-05-11T08:31:00.000Z'), + it("computes the date range using a '90days' rolling window", () => { + expect(toDateRange(ninetyDaysRolling(), NOW)).toEqual({ + from: new Date('2022-05-13T08:31:00.000Z'), to: new Date('2022-08-11T08:31:00.000Z'), }); }); }); }); - -function aCalendarTimeWindow(duration: Duration): TimeWindow { - return { - duration, - type: 'calendarAligned', - }; -} - -function aRollingTimeWindow(duration: Duration): TimeWindow { - return { duration, type: 'rolling' }; -} diff --git a/x-pack/plugins/observability/server/domain/services/date_range.ts b/x-pack/plugins/observability/server/domain/services/date_range.ts index 9ca86583e1af5..9c54197aa39e3 100644 --- a/x-pack/plugins/observability/server/domain/services/date_range.ts +++ b/x-pack/plugins/observability/server/domain/services/date_range.ts @@ -5,24 +5,19 @@ * 2.0. */ +import { calendarAlignedTimeWindowSchema, rollingTimeWindowSchema } from '@kbn/slo-schema'; import { assertNever } from '@kbn/std'; import moment from 'moment'; -import { calendarAlignedTimeWindowSchema, rollingTimeWindowSchema } from '@kbn/slo-schema'; - -import { DateRange, toMomentUnitOfTime } from '../models'; -import type { TimeWindow } from '../models/time_window'; +import { DateRange } from '../models'; +import { + TimeWindow, + toCalendarAlignedTimeWindowMomentUnit, + toRollingTimeWindowMomentUnit, +} from '../models/time_window'; export const toDateRange = (timeWindow: TimeWindow, currentDate: Date = new Date()): DateRange => { if (calendarAlignedTimeWindowSchema.is(timeWindow)) { - const unit = toMomentUnitOfTime(timeWindow.duration.unit); - if (unit === 'weeks') { - // moment startOf(week) returns sunday, but we want to stay consistent with es "now/w" date math which returns monday. - const from = moment.utc(currentDate).startOf(unit).add(1, 'day'); - const to = moment.utc(currentDate).endOf(unit).add(1, 'day'); - - return { from: from.toDate(), to: to.toDate() }; - } - + const unit = toCalendarAlignedTimeWindowMomentUnit(timeWindow); const from = moment.utc(currentDate).startOf(unit); const to = moment.utc(currentDate).endOf(unit); @@ -30,12 +25,14 @@ export const toDateRange = (timeWindow: TimeWindow, currentDate: Date = new Date } if (rollingTimeWindowSchema.is(timeWindow)) { - const unit = toMomentUnitOfTime(timeWindow.duration.unit); + const unit = toRollingTimeWindowMomentUnit(timeWindow); const now = moment.utc(currentDate).startOf('minute'); + const from = now.clone().subtract(timeWindow.duration.value, unit); + const to = now.clone(); return { - from: now.clone().subtract(timeWindow.duration.value, unit).toDate(), - to: now.toDate(), + from: from.toDate(), + to: to.toDate(), }; } diff --git a/x-pack/plugins/observability/server/domain/services/validate_slo.test.ts b/x-pack/plugins/observability/server/domain/services/validate_slo.test.ts index 12d3ebe77b82b..70cc408ceb175 100644 --- a/x-pack/plugins/observability/server/domain/services/validate_slo.test.ts +++ b/x-pack/plugins/observability/server/domain/services/validate_slo.test.ts @@ -8,6 +8,7 @@ import { validateSLO } from '.'; import { oneMinute, sixHours } from '../../services/slo/fixtures/duration'; import { createSLO } from '../../services/slo/fixtures/slo'; +import { sevenDaysRolling } from '../../services/slo/fixtures/time_window'; import { Duration, DurationUnit } from '../models'; describe('validateSLO', () => { @@ -41,16 +42,12 @@ describe('validateSLO', () => { { duration: new Duration(2, DurationUnit.Hour), shouldThrow: true }, { duration: new Duration(1, DurationUnit.Day), shouldThrow: true }, { duration: new Duration(7, DurationUnit.Day), shouldThrow: true }, - { duration: new Duration(1, DurationUnit.Week), shouldThrow: false }, { duration: new Duration(2, DurationUnit.Week), shouldThrow: true }, - { duration: new Duration(1, DurationUnit.Month), shouldThrow: false }, { duration: new Duration(2, DurationUnit.Month), shouldThrow: true }, - { duration: new Duration(1, DurationUnit.Quarter), shouldThrow: true }, - { duration: new Duration(3, DurationUnit.Quarter), shouldThrow: true }, - { duration: new Duration(1, DurationUnit.Year), shouldThrow: true }, - { duration: new Duration(3, DurationUnit.Year), shouldThrow: true }, + { duration: new Duration(1, DurationUnit.Week), shouldThrow: false }, + { duration: new Duration(1, DurationUnit.Month), shouldThrow: false }, ])( - 'throws when time window calendar aligned is not 1 week or 1 month', + 'throws when calendar aligned time window is not 1 week or 1 month', ({ duration, shouldThrow }) => { if (shouldThrow) { expect(() => @@ -72,6 +69,34 @@ describe('validateSLO', () => { } ); + it.each([ + { duration: new Duration(7, DurationUnit.Day), shouldThrow: false }, + { duration: new Duration(30, DurationUnit.Day), shouldThrow: false }, + { duration: new Duration(90, DurationUnit.Day), shouldThrow: false }, + { duration: new Duration(1, DurationUnit.Hour), shouldThrow: true }, + { duration: new Duration(1, DurationUnit.Day), shouldThrow: true }, + { duration: new Duration(1, DurationUnit.Week), shouldThrow: true }, + { duration: new Duration(1, DurationUnit.Month), shouldThrow: true }, + ])('throws when rolling time window is not 7, 30 or 90days', ({ duration, shouldThrow }) => { + if (shouldThrow) { + expect(() => + validateSLO( + createSLO({ + timeWindow: { duration, type: 'rolling' }, + }) + ) + ).toThrowError('Invalid time_window.duration'); + } else { + expect(() => + validateSLO( + createSLO({ + timeWindow: { duration, type: 'rolling' }, + }) + ) + ).not.toThrowError(); + } + }); + describe('settings', () => { it("throws when frequency is longer or equal than '1h'", () => { const slo = createSLO({ @@ -173,25 +198,11 @@ describe('validateSLO', () => { objective: { ...slo.objective, timesliceWindow: new Duration(1, DurationUnit.Month) }, }) ).toThrowError('Invalid objective.timeslice_window'); - - expect(() => - validateSLO({ - ...slo, - objective: { ...slo.objective, timesliceWindow: new Duration(1, DurationUnit.Quarter) }, - }) - ).toThrowError('Invalid objective.timeslice_window'); - - expect(() => - validateSLO({ - ...slo, - objective: { ...slo.objective, timesliceWindow: new Duration(1, DurationUnit.Year) }, - }) - ).toThrowError('Invalid objective.timeslice_window'); }); it("throws when 'objective.timeslice_window' is longer than 'slo.time_window'", () => { const slo = createSLO({ - timeWindow: { duration: new Duration(1, DurationUnit.Week), type: 'rolling' }, + timeWindow: sevenDaysRolling(), budgetingMethod: 'timeslices', objective: { target: 0.95, diff --git a/x-pack/plugins/observability/server/domain/services/validate_slo.ts b/x-pack/plugins/observability/server/domain/services/validate_slo.ts index a1207b2c17f52..eb253f44cdf5a 100644 --- a/x-pack/plugins/observability/server/domain/services/validate_slo.ts +++ b/x-pack/plugins/observability/server/domain/services/validate_slo.ts @@ -85,13 +85,8 @@ function isValidTargetNumber(value: number): boolean { } function isValidRollingTimeWindowDuration(duration: Duration): boolean { - return [ - DurationUnit.Day, - DurationUnit.Week, - DurationUnit.Month, - DurationUnit.Quarter, - DurationUnit.Year, - ].includes(duration.unit); + // 7, 30 or 90days accepted + return duration.unit === DurationUnit.Day && [7, 30, 90].includes(duration.value); } function isValidCalendarAlignedTimeWindowDuration(duration: Duration): boolean { diff --git a/x-pack/plugins/observability/server/services/slo/fixtures/duration.ts b/x-pack/plugins/observability/server/services/slo/fixtures/duration.ts index a07eab2642e91..26690b6680f18 100644 --- a/x-pack/plugins/observability/server/services/slo/fixtures/duration.ts +++ b/x-pack/plugins/observability/server/services/slo/fixtures/duration.ts @@ -7,14 +7,14 @@ import { Duration, DurationUnit } from '../../../domain/models'; -export function oneQuarter(): Duration { - return new Duration(1, DurationUnit.Quarter); -} - export function thirtyDays(): Duration { return new Duration(30, DurationUnit.Day); } +export function ninetyDays(): Duration { + return new Duration(90, DurationUnit.Day); +} + export function oneMonth(): Duration { return new Duration(1, DurationUnit.Month); } diff --git a/x-pack/plugins/observability/server/services/slo/fixtures/slo.ts b/x-pack/plugins/observability/server/services/slo/fixtures/slo.ts index fc7cf292095c9..26bea08a3c238 100644 --- a/x-pack/plugins/observability/server/services/slo/fixtures/slo.ts +++ b/x-pack/plugins/observability/server/services/slo/fixtures/slo.ts @@ -5,12 +5,10 @@ * 2.0. */ +import { SavedObject } from '@kbn/core-saved-objects-server'; +import { CreateSLOParams, HistogramIndicator, sloSchema } from '@kbn/slo-schema'; import { cloneDeep } from 'lodash'; import { v1 as uuidv1 } from 'uuid'; -import { SavedObject } from '@kbn/core-saved-objects-server'; -import { sloSchema, CreateSLOParams, HistogramIndicator } from '@kbn/slo-schema'; - -import { SO_SLO_TYPE } from '../../../saved_objects'; import { APMTransactionDurationIndicator, APMTransactionErrorRateIndicator, @@ -22,9 +20,10 @@ import { SLO, StoredSLO, } from '../../../domain/models'; +import { SO_SLO_TYPE } from '../../../saved_objects'; import { Paginated } from '../slo_repository'; -import { oneWeek, twoMinute } from './duration'; -import { sevenDaysRolling } from './time_window'; +import { twoMinute } from './duration'; +import { sevenDaysRolling, weeklyCalendarAligned } from './time_window'; export const createAPMTransactionErrorRateIndicator = ( params: Partial = {} @@ -184,10 +183,7 @@ export const createSLOWithTimeslicesBudgetingMethod = (params: Partial = {} export const createSLOWithCalendarTimeWindow = (params: Partial = {}): SLO => { return createSLO({ - timeWindow: { - duration: oneWeek(), - type: 'calendarAligned', - }, + timeWindow: weeklyCalendarAligned(), ...params, }); }; diff --git a/x-pack/plugins/observability/server/services/slo/fixtures/time_window.ts b/x-pack/plugins/observability/server/services/slo/fixtures/time_window.ts index e2ffa8459720a..c8d1601a22e2b 100644 --- a/x-pack/plugins/observability/server/services/slo/fixtures/time_window.ts +++ b/x-pack/plugins/observability/server/services/slo/fixtures/time_window.ts @@ -5,15 +5,12 @@ * 2.0. */ -import { RollingTimeWindow, TimeWindow } from '../../../domain/models/time_window'; -import { oneWeek, sevenDays, sixHours, thirtyDays } from './duration'; - -export function sixHoursRolling(): TimeWindow { - return { - duration: sixHours(), - type: 'rolling', - }; -} +import { + CalendarAlignedTimeWindow, + RollingTimeWindow, + TimeWindow, +} from '../../../domain/models/time_window'; +import { ninetyDays, oneMonth, oneWeek, sevenDays, thirtyDays } from './duration'; export function sevenDaysRolling(): RollingTimeWindow { return { @@ -28,9 +25,23 @@ export function thirtyDaysRolling(): RollingTimeWindow { }; } -export function weeklyCalendarAligned(): TimeWindow { +export function ninetyDaysRolling(): TimeWindow { + return { + duration: ninetyDays(), + type: 'rolling', + }; +} + +export function weeklyCalendarAligned(): CalendarAlignedTimeWindow { return { duration: oneWeek(), type: 'calendarAligned', }; } + +export function monthlyCalendarAligned(): CalendarAlignedTimeWindow { + return { + duration: oneMonth(), + type: 'calendarAligned', + }; +} diff --git a/x-pack/plugins/observability/server/services/slo/sli_client.ts b/x-pack/plugins/observability/server/services/slo/sli_client.ts index c40ca6d216fac..1a9e6f46ec82c 100644 --- a/x-pack/plugins/observability/server/services/slo/sli_client.ts +++ b/x-pack/plugins/observability/server/services/slo/sli_client.ts @@ -13,11 +13,15 @@ import { MsearchMultisearchBody, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ElasticsearchClient } from '@kbn/core/server'; -import { occurrencesBudgetingMethodSchema, timeslicesBudgetingMethodSchema } from '@kbn/slo-schema'; +import { + occurrencesBudgetingMethodSchema, + timeslicesBudgetingMethodSchema, + toMomentUnitOfTime, +} from '@kbn/slo-schema'; import { assertNever } from '@kbn/std'; +import moment from 'moment'; import { SLO_DESTINATION_INDEX_PATTERN } from '../../assets/constants'; import { DateRange, Duration, IndicatorData, SLO } from '../../domain/models'; -import { toDateRange } from '../../domain/services/date_range'; import { InternalQueryError } from '../../errors'; export interface SLIClient { @@ -47,10 +51,7 @@ export class DefaultSLIClient implements SLIClient { a.duration.isShorterThan(b.duration) ? 1 : -1 ); const longestLookbackWindow = sortedLookbackWindows[0]; - const longestDateRange = toDateRange({ - duration: longestLookbackWindow.duration, - type: 'rolling', - }); + const longestDateRange = getLookbackDateRange(longestLookbackWindow.duration); if (occurrencesBudgetingMethodSchema.is(slo.budgetingMethod)) { const result = await this.esClient.search({ @@ -179,3 +180,15 @@ function handleWindowedResult( return indicatorDataPerLookbackWindow; } + +function getLookbackDateRange(duration: Duration): { from: Date; to: Date } { + const unit = toMomentUnitOfTime(duration.unit); + const now = moment.utc().startOf('minute'); + const from = now.clone().subtract(duration.value, unit); + const to = now.clone(); + + return { + from: from.toDate(), + to: to.toDate(), + }; +} diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index ba4b5e29ac9df..f789025755725 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -27325,7 +27325,6 @@ "xpack.observability.slo.duration.minute": "{duration, plural, one {1 minute} many {# minutes} other {# minutes}}", "xpack.observability.slo.duration.month": "{duration, plural, one {1 mois} many {# mois} other {# mois}}", "xpack.observability.slo.duration.week": "{duration, plural, one {1 semaine} many {# semaines} other {# semaines}}", - "xpack.observability.slo.duration.year": "{duration, plural, one {1 an} many {# ans} other {# prochaines années}}", "xpack.observability.slo.indicatorTypeBadge.exploreInApm": "Afficher les détails de {service}", "xpack.observability.slo.list.sortByType": "Trier par {type}", "xpack.observability.slo.rules.burnRate.errors.invalidThresholdValue": "Le seuil du taux d'avancement doit être compris entre 1 et {maxBurnRate}.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 30f3ee0c95ac9..649ca37ccdc50 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -27325,7 +27325,6 @@ "xpack.observability.slo.duration.minute": "{duration, plural, other {#分}}", "xpack.observability.slo.duration.month": "{duration, plural, other {#月}}", "xpack.observability.slo.duration.week": "{duration, plural, other {#週}}", - "xpack.observability.slo.duration.year": "{duration, plural, other {#年}}", "xpack.observability.slo.indicatorTypeBadge.exploreInApm": "{service}詳細を表示", "xpack.observability.slo.list.sortByType": "{type}で並べ替え", "xpack.observability.slo.rules.burnRate.errors.invalidThresholdValue": "バーンレートしきい値は1以上{maxBurnRate}以下でなければなりません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a25740e041d4a..09f5527166c8b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -27322,7 +27322,6 @@ "xpack.observability.slo.duration.minute": "{duration, plural, other {# 分钟}}", "xpack.observability.slo.duration.month": "{duration, plural, other {# 个月}}", "xpack.observability.slo.duration.week": "{duration, plural, other {# 周}}", - "xpack.observability.slo.duration.year": "{duration, plural, other {# 年}}", "xpack.observability.slo.indicatorTypeBadge.exploreInApm": "查看 {service} 详情", "xpack.observability.slo.list.sortByType": "按 {type} 排序", "xpack.observability.slo.rules.burnRate.errors.invalidThresholdValue": "消耗速度阈值必须介于 1 和 {maxBurnRate} 之间。",