mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
feat(slo): Consider empty slice as good slice for sli calculation purposes on timeslice budgeting method (#181888)
This commit is contained in:
parent
d4c6e0710d
commit
2d63f721a9
20 changed files with 1170 additions and 1135 deletions
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { allOrAnyString, dateRangeSchema } from './common';
|
||||
import { allOrAnyString } from './common';
|
||||
|
||||
const kqlQuerySchema = t.string;
|
||||
|
||||
|
@ -271,12 +271,6 @@ const syntheticsAvailabilityIndicatorSchema = t.type({
|
|||
]),
|
||||
});
|
||||
|
||||
const indicatorDataSchema = t.type({
|
||||
dateRange: dateRangeSchema,
|
||||
good: t.number,
|
||||
total: t.number,
|
||||
});
|
||||
|
||||
const indicatorTypesSchema = t.union([
|
||||
apmTransactionDurationIndicatorTypeSchema,
|
||||
apmTransactionErrorRateIndicatorTypeSchema,
|
||||
|
@ -344,5 +338,4 @@ export {
|
|||
indicatorSchema,
|
||||
indicatorTypesArraySchema,
|
||||
indicatorTypesSchema,
|
||||
indicatorDataSchema,
|
||||
};
|
||||
|
|
|
@ -5,16 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import {
|
||||
apmTransactionDurationIndicatorSchema,
|
||||
apmTransactionErrorRateIndicatorSchema,
|
||||
indicatorDataSchema,
|
||||
indicatorSchema,
|
||||
indicatorTypesSchema,
|
||||
kqlCustomIndicatorSchema,
|
||||
metricCustomIndicatorSchema,
|
||||
} from '@kbn/slo-schema';
|
||||
import * as t from 'io-ts';
|
||||
|
||||
type APMTransactionErrorRateIndicator = t.TypeOf<typeof apmTransactionErrorRateIndicatorSchema>;
|
||||
type APMTransactionDurationIndicator = t.TypeOf<typeof apmTransactionDurationIndicatorSchema>;
|
||||
|
@ -22,7 +21,6 @@ type KQLCustomIndicator = t.TypeOf<typeof kqlCustomIndicatorSchema>;
|
|||
type MetricCustomIndicator = t.TypeOf<typeof metricCustomIndicatorSchema>;
|
||||
type Indicator = t.TypeOf<typeof indicatorSchema>;
|
||||
type IndicatorTypes = t.TypeOf<typeof indicatorTypesSchema>;
|
||||
type IndicatorData = t.TypeOf<typeof indicatorDataSchema>;
|
||||
|
||||
export type {
|
||||
Indicator,
|
||||
|
@ -31,5 +29,4 @@ export type {
|
|||
APMTransactionDurationIndicator,
|
||||
KQLCustomIndicator,
|
||||
MetricCustomIndicator,
|
||||
IndicatorData,
|
||||
};
|
||||
|
|
|
@ -5,59 +5,32 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { computeBurnRate } from './compute_burn_rate';
|
||||
import { toDateRange } from './date_range';
|
||||
import { createSLO } from '../../services/fixtures/slo';
|
||||
import { ninetyDaysRolling } from '../../services/fixtures/time_window';
|
||||
import { computeBurnRate } from './compute_burn_rate';
|
||||
|
||||
describe('computeBurnRate', () => {
|
||||
it('computes 0 when total is 0', () => {
|
||||
expect(
|
||||
computeBurnRate(createSLO(), {
|
||||
good: 10,
|
||||
total: 0,
|
||||
dateRange: toDateRange(ninetyDaysRolling()),
|
||||
})
|
||||
).toEqual(0);
|
||||
it('computes 0 when sliValue is 1', () => {
|
||||
const sliValue = 1;
|
||||
expect(computeBurnRate(createSLO(), sliValue)).toEqual(0);
|
||||
});
|
||||
|
||||
it('computes 0 when good is greater than total', () => {
|
||||
expect(
|
||||
computeBurnRate(createSLO(), {
|
||||
good: 9999,
|
||||
total: 1,
|
||||
dateRange: toDateRange(ninetyDaysRolling()),
|
||||
})
|
||||
).toEqual(0);
|
||||
it('computes 0 when sliValue is greater than 1', () => {
|
||||
const sliValue = 1.21;
|
||||
expect(computeBurnRate(createSLO(), sliValue)).toEqual(0);
|
||||
});
|
||||
|
||||
it('computes the burn rate as 1x the error budget', () => {
|
||||
expect(
|
||||
computeBurnRate(createSLO({ objective: { target: 0.9 } }), {
|
||||
good: 90,
|
||||
total: 100,
|
||||
dateRange: toDateRange(ninetyDaysRolling()),
|
||||
})
|
||||
).toEqual(1);
|
||||
const sliValue = 0.9;
|
||||
expect(computeBurnRate(createSLO({ objective: { target: 0.9 } }), sliValue)).toEqual(1);
|
||||
});
|
||||
|
||||
it('computes the burn rate as 10x the error budget', () => {
|
||||
expect(
|
||||
computeBurnRate(createSLO({ objective: { target: 0.99 } }), {
|
||||
good: 90,
|
||||
total: 100,
|
||||
dateRange: toDateRange(ninetyDaysRolling()),
|
||||
})
|
||||
).toEqual(10);
|
||||
const sliValue = 0.9;
|
||||
expect(computeBurnRate(createSLO({ objective: { target: 0.99 } }), sliValue)).toEqual(10);
|
||||
});
|
||||
|
||||
it('computes the burn rate as 0.5x the error budget', () => {
|
||||
expect(
|
||||
computeBurnRate(createSLO({ objective: { target: 0.8 } }), {
|
||||
good: 90,
|
||||
total: 100,
|
||||
dateRange: toDateRange(ninetyDaysRolling()),
|
||||
})
|
||||
).toEqual(0.5);
|
||||
const sliValue = 0.9;
|
||||
expect(computeBurnRate(createSLO({ objective: { target: 0.8 } }), sliValue)).toEqual(0.5);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,19 +6,18 @@
|
|||
*/
|
||||
|
||||
import { toHighPrecision } from '../../utils/number';
|
||||
import { IndicatorData, SLODefinition } from '../models';
|
||||
import { SLODefinition } from '../models';
|
||||
|
||||
/**
|
||||
* A Burn Rate is computed with the Indicator Data retrieved from a specific lookback period
|
||||
* A Burn Rate is computed with the sliValue retrieved from a specific lookback period
|
||||
* It tells how fast we are consumming our error budget during a specific period
|
||||
*/
|
||||
export function computeBurnRate(slo: SLODefinition, sliData: IndicatorData): number {
|
||||
const { good, total } = sliData;
|
||||
if (total === 0 || good >= total) {
|
||||
export function computeBurnRate(slo: SLODefinition, sliValue: number): number {
|
||||
if (sliValue >= 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const errorBudget = 1 - slo.objective.target;
|
||||
const errorRate = 1 - good / total;
|
||||
const errorRate = 1 - sliValue;
|
||||
return toHighPrecision(errorRate / errorBudget);
|
||||
}
|
||||
|
|
|
@ -23,4 +23,8 @@ describe('computeSLI', () => {
|
|||
it('returns rounds the value to 6 digits', () => {
|
||||
expect(computeSLI(33, 90)).toEqual(0.366667);
|
||||
});
|
||||
|
||||
it('returns the sli value using totalSlicesInRange when provided', () => {
|
||||
expect(computeSLI(90, 100, 10_080)).toEqual(0.999008);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,7 +9,14 @@ import { toHighPrecision } from '../../utils/number';
|
|||
|
||||
const NO_DATA = -1;
|
||||
|
||||
export function computeSLI(good: number, total: number): number {
|
||||
export function computeSLI(good: number, total: number, totalSlicesInRange?: number): number {
|
||||
// We calculate the sli based on the totalSlices in the dateRange, as
|
||||
// 1 - error rate observed = 1 - (1 - SLI Observed) = SLI
|
||||
// a slice without data will be considered as a good slice
|
||||
if (totalSlicesInRange !== undefined && totalSlicesInRange > 0) {
|
||||
return toHighPrecision(1 - (total - good) / totalSlicesInRange);
|
||||
}
|
||||
|
||||
if (total === 0) {
|
||||
return NO_DATA;
|
||||
}
|
||||
|
|
|
@ -596,7 +596,8 @@ const getSloBurnRates = createSloServerRoute({
|
|||
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
const { instanceId, windows, remoteName } = params.body;
|
||||
const burnRates = await getBurnRates({
|
||||
|
||||
return await getBurnRates({
|
||||
instanceId,
|
||||
spaceId,
|
||||
windows,
|
||||
|
@ -608,7 +609,6 @@ const getSloBurnRates = createSloServerRoute({
|
|||
logger,
|
||||
},
|
||||
});
|
||||
return { burnRates };
|
||||
},
|
||||
});
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -6,13 +6,13 @@ Object {
|
|||
"meta": Object {},
|
||||
"summary": Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0,
|
||||
"consumed": 0.19842,
|
||||
"initial": 0.05,
|
||||
"isEstimated": false,
|
||||
"remaining": 1,
|
||||
"remaining": 0.80158,
|
||||
},
|
||||
"sliValue": -1,
|
||||
"status": "NO_DATA",
|
||||
"sliValue": 0.990079,
|
||||
"status": "HEALTHY",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
@ -23,13 +23,13 @@ Object {
|
|||
"meta": Object {},
|
||||
"summary": Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0,
|
||||
"consumed": 100,
|
||||
"initial": 0.001,
|
||||
"isEstimated": false,
|
||||
"remaining": 1,
|
||||
"remaining": -99,
|
||||
},
|
||||
"sliValue": -1,
|
||||
"status": "NO_DATA",
|
||||
"sliValue": 0.9,
|
||||
"status": "VIOLATED",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
@ -40,13 +40,13 @@ Object {
|
|||
"meta": Object {},
|
||||
"summary": Object {
|
||||
"errorBudget": Object {
|
||||
"consumed": 0,
|
||||
"consumed": 0.19842,
|
||||
"initial": 0.05,
|
||||
"isEstimated": false,
|
||||
"remaining": 1,
|
||||
"remaining": 0.80158,
|
||||
},
|
||||
"sliValue": -1,
|
||||
"status": "NO_DATA",
|
||||
"sliValue": 0.990079,
|
||||
"status": "HEALTHY",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -6,12 +6,11 @@
|
|||
*/
|
||||
|
||||
import { ElasticsearchClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import moment from 'moment';
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
|
||||
import moment from 'moment';
|
||||
import { Duration, DurationUnit } from '../domain/models';
|
||||
import { DefaultBurnRatesClient } from './burn_rates_client';
|
||||
import { createSLO } from './fixtures/slo';
|
||||
import { DefaultSLIClient } from './sli_client';
|
||||
|
||||
const commonEsResponse = {
|
||||
took: 100,
|
||||
|
@ -41,7 +40,7 @@ describe('SummaryClient', () => {
|
|||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('fetchSLIDataFrom', () => {
|
||||
describe('burnRatesClient', () => {
|
||||
const LONG_WINDOW = 'long_window';
|
||||
const SHORT_WINDOW = 'short_window';
|
||||
|
||||
|
@ -65,10 +64,10 @@ describe('SummaryClient', () => {
|
|||
to_as_string: '2022-12-31T23:54:00.000Z',
|
||||
doc_count: 60,
|
||||
total: {
|
||||
value: 32169,
|
||||
value: 5000,
|
||||
},
|
||||
good: {
|
||||
value: 15748,
|
||||
value: 4500,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -83,19 +82,19 @@ describe('SummaryClient', () => {
|
|||
to_as_string: '2022-12-31T23:54:00.000Z',
|
||||
doc_count: 5,
|
||||
total: {
|
||||
value: 2211,
|
||||
value: 300,
|
||||
},
|
||||
good: {
|
||||
value: 772,
|
||||
value: 290,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
const summaryClient = new DefaultSLIClient(esClientMock);
|
||||
const client = new DefaultBurnRatesClient(esClientMock);
|
||||
|
||||
const result = await summaryClient.fetchSLIDataFrom(slo, ALL_VALUE, lookbackWindows);
|
||||
const results = await client.calculate(slo, ALL_VALUE, lookbackWindows);
|
||||
|
||||
expect(esClientMock?.search?.mock?.lastCall?.[0]).toMatchObject({
|
||||
aggs: {
|
||||
|
@ -132,8 +131,16 @@ describe('SummaryClient', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(result[LONG_WINDOW]).toMatchObject({ good: 15748, total: 32169 });
|
||||
expect(result[SHORT_WINDOW]).toMatchObject({ good: 772, total: 2211 });
|
||||
expect(results.find((result) => result.name === LONG_WINDOW)).toMatchObject({
|
||||
name: LONG_WINDOW,
|
||||
sli: 0.9,
|
||||
burnRate: 100,
|
||||
});
|
||||
expect(results.find((result) => result.name === SHORT_WINDOW)).toMatchObject({
|
||||
name: SHORT_WINDOW,
|
||||
sli: 0.966667,
|
||||
burnRate: 33.333,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -144,7 +151,7 @@ describe('SummaryClient', () => {
|
|||
objective: {
|
||||
target: 0.95,
|
||||
timesliceTarget: 0.9,
|
||||
timesliceWindow: new Duration(10, DurationUnit.Minute),
|
||||
timesliceWindow: new Duration(5, DurationUnit.Minute),
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -158,17 +165,17 @@ describe('SummaryClient', () => {
|
|||
[LONG_WINDOW]: {
|
||||
buckets: [
|
||||
{
|
||||
key: '2022-12-31T22:36:00.000Z-2022-12-31T23:36:00.000Z',
|
||||
key: '2022-12-31T22:46:00.000Z-2022-12-31T23:46:00.000Z',
|
||||
from: 1672526160000,
|
||||
from_as_string: '2022-12-31T22:36:00.000Z',
|
||||
from_as_string: '2022-12-31T22:46:00.000Z',
|
||||
to: 1672529760000,
|
||||
to_as_string: '2022-12-31T23:36:00.000Z',
|
||||
doc_count: 60,
|
||||
to_as_string: '2022-12-31T23:46:00.000Z',
|
||||
doc_count: 12,
|
||||
total: {
|
||||
value: 32169,
|
||||
value: 12,
|
||||
},
|
||||
good: {
|
||||
value: 15748,
|
||||
value: 10,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -176,26 +183,26 @@ describe('SummaryClient', () => {
|
|||
[SHORT_WINDOW]: {
|
||||
buckets: [
|
||||
{
|
||||
key: '2022-12-31T23:31:00.000Z-2022-12-31T23:36:00.000Z',
|
||||
key: '2022-12-31T23:41:00.000Z-2022-12-31T23:46:00.000Z',
|
||||
from: 1672529460000,
|
||||
from_as_string: '2022-12-31T23:31:00.000Z',
|
||||
from_as_string: '2022-12-31T23:41:00.000Z',
|
||||
to: 1672529760000,
|
||||
to_as_string: '2022-12-31T23:36:00.000Z',
|
||||
doc_count: 5,
|
||||
to_as_string: '2022-12-31T23:46:00.000Z',
|
||||
doc_count: 1,
|
||||
total: {
|
||||
value: 2211,
|
||||
value: 1,
|
||||
},
|
||||
good: {
|
||||
value: 772,
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
const summaryClient = new DefaultSLIClient(esClientMock);
|
||||
const client = new DefaultBurnRatesClient(esClientMock);
|
||||
|
||||
const result = await summaryClient.fetchSLIDataFrom(slo, ALL_VALUE, lookbackWindows);
|
||||
const results = await client.calculate(slo, ALL_VALUE, lookbackWindows);
|
||||
|
||||
expect(esClientMock?.search?.mock?.lastCall?.[0]).toMatchObject({
|
||||
aggs: {
|
||||
|
@ -204,8 +211,8 @@ describe('SummaryClient', () => {
|
|||
field: '@timestamp',
|
||||
ranges: [
|
||||
{
|
||||
from: '2022-12-31T22:36:00.000Z',
|
||||
to: '2022-12-31T23:36:00.000Z',
|
||||
from: '2022-12-31T22:46:00.000Z',
|
||||
to: '2022-12-31T23:46:00.000Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -227,8 +234,8 @@ describe('SummaryClient', () => {
|
|||
field: '@timestamp',
|
||||
ranges: [
|
||||
{
|
||||
from: '2022-12-31T23:31:00.000Z',
|
||||
to: '2022-12-31T23:36:00.000Z',
|
||||
from: '2022-12-31T23:41:00.000Z',
|
||||
to: '2022-12-31T23:46:00.000Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -248,8 +255,16 @@ describe('SummaryClient', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(result[LONG_WINDOW]).toMatchObject({ good: 15748, total: 32169 });
|
||||
expect(result[SHORT_WINDOW]).toMatchObject({ good: 772, total: 2211 });
|
||||
expect(results.find((result) => result.name === LONG_WINDOW)).toMatchObject({
|
||||
name: LONG_WINDOW,
|
||||
sli: 0.833333,
|
||||
burnRate: 3.33334,
|
||||
});
|
||||
expect(results.find((result) => result.name === SHORT_WINDOW)).toMatchObject({
|
||||
name: SHORT_WINDOW,
|
||||
sli: 1,
|
||||
burnRate: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -19,22 +19,23 @@ import {
|
|||
occurrencesBudgetingMethodSchema,
|
||||
timeslicesBudgetingMethodSchema,
|
||||
} from '@kbn/slo-schema';
|
||||
import { assertNever } from '@kbn/std';
|
||||
import { SLO_DESTINATION_INDEX_PATTERN } from '../../common/constants';
|
||||
import { DateRange, Duration, IndicatorData, SLODefinition } from '../domain/models';
|
||||
import { InternalQueryError } from '../errors';
|
||||
import { DateRange, Duration, SLODefinition } from '../domain/models';
|
||||
import { computeBurnRate, computeSLI } from '../domain/services';
|
||||
import { getDelayInSecondsFromSLO } from '../domain/services/get_delay_in_seconds_from_slo';
|
||||
import { getLookbackDateRange } from '../domain/services/get_lookback_date_range';
|
||||
|
||||
export interface SLIClient {
|
||||
fetchSLIDataFrom(
|
||||
slo: SLODefinition,
|
||||
instanceId: string,
|
||||
lookbackWindows: LookbackWindow[]
|
||||
): Promise<Record<WindowName, IndicatorData>>;
|
||||
}
|
||||
import { InternalQueryError } from '../errors';
|
||||
import { computeTotalSlicesFromDateRange } from './utils/compute_total_slices_from_date_range';
|
||||
|
||||
type WindowName = string;
|
||||
export interface BurnRatesClient {
|
||||
calculate(
|
||||
slo: SLODefinition,
|
||||
instanceId: string,
|
||||
lookbackWindows: LookbackWindow[],
|
||||
remoteName?: string
|
||||
): Promise<Array<{ burnRate: number; sli: number; name: WindowName }>>;
|
||||
}
|
||||
|
||||
interface LookbackWindow {
|
||||
name: WindowName;
|
||||
|
@ -43,15 +44,15 @@ interface LookbackWindow {
|
|||
|
||||
type EsAggregations = Record<WindowName, AggregationsDateRangeAggregate>;
|
||||
|
||||
export class DefaultSLIClient implements SLIClient {
|
||||
export class DefaultBurnRatesClient implements BurnRatesClient {
|
||||
constructor(private esClient: ElasticsearchClient) {}
|
||||
|
||||
async fetchSLIDataFrom(
|
||||
async calculate(
|
||||
slo: SLODefinition,
|
||||
instanceId: string,
|
||||
lookbackWindows: LookbackWindow[],
|
||||
remoteName?: string
|
||||
): Promise<Record<WindowName, IndicatorData>> {
|
||||
): Promise<Array<{ burnRate: number; sli: number; name: WindowName }>> {
|
||||
const sortedLookbackWindows = [...lookbackWindows].sort((a, b) =>
|
||||
a.duration.isShorterThan(b.duration) ? 1 : -1
|
||||
);
|
||||
|
@ -67,35 +68,23 @@ export class DefaultSLIClient implements SLIClient {
|
|||
? `${remoteName}:${SLO_DESTINATION_INDEX_PATTERN}`
|
||||
: SLO_DESTINATION_INDEX_PATTERN;
|
||||
|
||||
if (occurrencesBudgetingMethodSchema.is(slo.budgetingMethod)) {
|
||||
const result = await this.esClient.search<unknown, EsAggregations>({
|
||||
...commonQuery(slo, instanceId, longestDateRange),
|
||||
index,
|
||||
aggs: toLookbackWindowsAggregationsQuery(
|
||||
longestDateRange.to,
|
||||
sortedLookbackWindows,
|
||||
delayInSeconds
|
||||
),
|
||||
});
|
||||
const result = await this.esClient.search<unknown, EsAggregations>({
|
||||
...commonQuery(slo, instanceId, longestDateRange),
|
||||
index,
|
||||
aggs: occurrencesBudgetingMethodSchema.is(slo.budgetingMethod)
|
||||
? toLookbackWindowsAggregationsQuery(
|
||||
longestDateRange.to,
|
||||
sortedLookbackWindows,
|
||||
delayInSeconds
|
||||
)
|
||||
: toLookbackWindowsSlicedAggregationsQuery(
|
||||
longestDateRange.to,
|
||||
sortedLookbackWindows,
|
||||
delayInSeconds
|
||||
),
|
||||
});
|
||||
|
||||
return handleWindowedResult(result.aggregations, lookbackWindows);
|
||||
}
|
||||
|
||||
if (timeslicesBudgetingMethodSchema.is(slo.budgetingMethod)) {
|
||||
const result = await this.esClient.search<unknown, EsAggregations>({
|
||||
...commonQuery(slo, instanceId, longestDateRange),
|
||||
index,
|
||||
aggs: toLookbackWindowsSlicedAggregationsQuery(
|
||||
longestDateRange.to,
|
||||
sortedLookbackWindows,
|
||||
delayInSeconds
|
||||
),
|
||||
});
|
||||
|
||||
return handleWindowedResult(result.aggregations, lookbackWindows);
|
||||
}
|
||||
|
||||
assertNever(slo.budgetingMethod);
|
||||
return handleWindowedResult(result.aggregations, lookbackWindows, slo);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -210,14 +199,14 @@ function toLookbackWindowsSlicedAggregationsQuery(
|
|||
|
||||
function handleWindowedResult(
|
||||
aggregations: Record<WindowName, AggregationsDateRangeAggregate> | undefined,
|
||||
lookbackWindows: LookbackWindow[]
|
||||
): Record<WindowName, IndicatorData> {
|
||||
lookbackWindows: LookbackWindow[],
|
||||
slo: SLODefinition
|
||||
): Array<{ burnRate: number; sli: number; name: WindowName }> {
|
||||
if (aggregations === undefined) {
|
||||
throw new InternalQueryError('Invalid aggregation response');
|
||||
}
|
||||
|
||||
const indicatorDataPerLookbackWindow: Record<WindowName, IndicatorData> = {};
|
||||
for (const lookbackWindow of lookbackWindows) {
|
||||
return lookbackWindows.map((lookbackWindow) => {
|
||||
const windowAggBuckets = aggregations[lookbackWindow.name]?.buckets ?? [];
|
||||
if (!Array.isArray(windowAggBuckets) || windowAggBuckets.length === 0) {
|
||||
throw new InternalQueryError('Invalid aggregation bucket response');
|
||||
|
@ -229,12 +218,26 @@ function handleWindowedResult(
|
|||
throw new InternalQueryError('Invalid aggregation sum bucket response');
|
||||
}
|
||||
|
||||
indicatorDataPerLookbackWindow[lookbackWindow.name] = {
|
||||
good,
|
||||
total,
|
||||
dateRange: { from: new Date(bucket.from_as_string!), to: new Date(bucket.to_as_string!) },
|
||||
};
|
||||
}
|
||||
let sliValue;
|
||||
if (timeslicesBudgetingMethodSchema.is(slo.budgetingMethod)) {
|
||||
const dateRange = {
|
||||
from: new Date(bucket.from_as_string!),
|
||||
to: new Date(bucket.to_as_string!),
|
||||
};
|
||||
const totalSlices = computeTotalSlicesFromDateRange(
|
||||
dateRange,
|
||||
slo.objective.timesliceWindow!
|
||||
);
|
||||
|
||||
return indicatorDataPerLookbackWindow;
|
||||
sliValue = computeSLI(good, total, totalSlices);
|
||||
} else {
|
||||
sliValue = computeSLI(good, total);
|
||||
}
|
||||
|
||||
return {
|
||||
name: lookbackWindow.name,
|
||||
burnRate: computeBurnRate(slo, sliValue),
|
||||
sli: sliValue,
|
||||
};
|
||||
});
|
||||
}
|
|
@ -8,9 +8,9 @@
|
|||
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { GetSLOBurnRatesResponse } from '@kbn/slo-schema';
|
||||
import { Duration } from '../domain/models';
|
||||
import { computeBurnRate, computeSLI } from '../domain/services';
|
||||
import { DefaultSLIClient } from './sli_client';
|
||||
import { DefaultBurnRatesClient } from './burn_rates_client';
|
||||
import { SloDefinitionClient } from './slo_definition_client';
|
||||
import { KibanaSavedObjectsSLORepository } from './slo_repository';
|
||||
|
||||
|
@ -25,6 +25,15 @@ interface LookbackWindow {
|
|||
duration: Duration;
|
||||
}
|
||||
|
||||
interface Params {
|
||||
sloId: string;
|
||||
spaceId: string;
|
||||
instanceId: string;
|
||||
remoteName?: string;
|
||||
windows: LookbackWindow[];
|
||||
services: Services;
|
||||
}
|
||||
|
||||
export async function getBurnRates({
|
||||
sloId,
|
||||
spaceId,
|
||||
|
@ -32,28 +41,15 @@ export async function getBurnRates({
|
|||
instanceId,
|
||||
remoteName,
|
||||
services,
|
||||
}: {
|
||||
sloId: string;
|
||||
spaceId: string;
|
||||
instanceId: string;
|
||||
remoteName?: string;
|
||||
windows: LookbackWindow[];
|
||||
services: Services;
|
||||
}) {
|
||||
}: Params): Promise<GetSLOBurnRatesResponse> {
|
||||
const { soClient, esClient, logger } = services;
|
||||
|
||||
const repository = new KibanaSavedObjectsSLORepository(soClient, logger);
|
||||
const sliClient = new DefaultSLIClient(esClient);
|
||||
const burnRatesClient = new DefaultBurnRatesClient(esClient);
|
||||
const definitionClient = new SloDefinitionClient(repository, esClient, logger);
|
||||
|
||||
const { slo } = await definitionClient.execute(sloId, spaceId, remoteName);
|
||||
const burnRates = await burnRatesClient.calculate(slo, instanceId, windows, remoteName);
|
||||
|
||||
const sliData = await sliClient.fetchSLIDataFrom(slo, instanceId, windows, remoteName);
|
||||
return Object.keys(sliData).map((key) => {
|
||||
return {
|
||||
name: key,
|
||||
burnRate: computeBurnRate(slo, sliData[key]),
|
||||
sli: computeSLI(sliData[key].good, sliData[key].total),
|
||||
};
|
||||
});
|
||||
return { burnRates };
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
ALL_VALUE,
|
||||
BudgetingMethod,
|
||||
calendarAlignedTimeWindowSchema,
|
||||
Duration,
|
||||
DurationUnit,
|
||||
FetchHistoricalSummaryParams,
|
||||
fetchHistoricalSummaryResponseSchema,
|
||||
|
@ -31,8 +30,10 @@ import {
|
|||
Objective,
|
||||
SLOId,
|
||||
TimeWindow,
|
||||
toCalendarAlignedTimeWindowMomentUnit,
|
||||
} from '../domain/models';
|
||||
import { computeSLI, computeSummaryStatus, toDateRange, toErrorBudget } from '../domain/services';
|
||||
import { computeSLI, computeSummaryStatus, toErrorBudget } from '../domain/services';
|
||||
import { computeTotalSlicesFromDateRange } from './utils/compute_total_slices_from_date_range';
|
||||
|
||||
interface DailyAggBucket {
|
||||
key_as_string: string;
|
||||
|
@ -108,12 +109,26 @@ export class DefaultHistoricalSummaryClient implements HistoricalSummaryClient {
|
|||
const buckets = (result.responses[i].aggregations?.daily?.buckets as DailyAggBucket[]) || [];
|
||||
|
||||
if (rollingTimeWindowSchema.is(timeWindow)) {
|
||||
historicalSummary.push({
|
||||
sloId,
|
||||
instanceId,
|
||||
data: handleResultForRolling(objective, timeWindow, buckets),
|
||||
});
|
||||
continue;
|
||||
if (timeslicesBudgetingMethodSchema.is(budgetingMethod)) {
|
||||
historicalSummary.push({
|
||||
sloId,
|
||||
instanceId,
|
||||
data: handleResultForRollingAndTimeslices(objective, timeWindow, buckets),
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (occurrencesBudgetingMethodSchema.is(budgetingMethod)) {
|
||||
historicalSummary.push({
|
||||
sloId,
|
||||
instanceId,
|
||||
data: handleResultForRollingAndOccurrences(objective, timeWindow, buckets),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
assertNever(budgetingMethod);
|
||||
}
|
||||
|
||||
if (calendarAlignedTimeWindowSchema.is(timeWindow)) {
|
||||
|
@ -175,13 +190,13 @@ function handleResultForCalendarAlignedAndTimeslices(
|
|||
dateRange: DateRange
|
||||
): HistoricalSummary[] {
|
||||
const initialErrorBudget = 1 - objective.target;
|
||||
const totalSlices = computeTotalSlicesFromDateRange(dateRange, objective.timesliceWindow!);
|
||||
|
||||
return buckets.map((bucket: DailyAggBucket): HistoricalSummary => {
|
||||
const good = bucket.cumulative_good?.value ?? 0;
|
||||
const total = bucket.cumulative_total?.value ?? 0;
|
||||
const sliValue = computeSLI(good, total);
|
||||
const totalSlices = computeTotalSlicesFromDateRange(dateRange, objective.timesliceWindow!);
|
||||
const consumedErrorBudget = (total - good) / (totalSlices * initialErrorBudget);
|
||||
const sliValue = computeSLI(good, total, totalSlices);
|
||||
const consumedErrorBudget = sliValue < 0 ? 0 : (1 - sliValue) / initialErrorBudget;
|
||||
const errorBudget = toErrorBudget(initialErrorBudget, consumedErrorBudget);
|
||||
|
||||
return {
|
||||
|
@ -193,7 +208,7 @@ function handleResultForCalendarAlignedAndTimeslices(
|
|||
});
|
||||
}
|
||||
|
||||
function handleResultForRolling(
|
||||
function handleResultForRollingAndOccurrences(
|
||||
objective: Objective,
|
||||
timeWindow: TimeWindow,
|
||||
buckets: DailyAggBucket[]
|
||||
|
@ -210,6 +225,7 @@ function handleResultForRolling(
|
|||
.map((bucket: DailyAggBucket): HistoricalSummary => {
|
||||
const good = bucket.cumulative_good?.value ?? 0;
|
||||
const total = bucket.cumulative_total?.value ?? 0;
|
||||
|
||||
const sliValue = computeSLI(good, total);
|
||||
const consumedErrorBudget = sliValue < 0 ? 0 : (1 - sliValue) / initialErrorBudget;
|
||||
const errorBudget = toErrorBudget(initialErrorBudget, consumedErrorBudget);
|
||||
|
@ -223,6 +239,39 @@ function handleResultForRolling(
|
|||
});
|
||||
}
|
||||
|
||||
function handleResultForRollingAndTimeslices(
|
||||
objective: Objective,
|
||||
timeWindow: TimeWindow,
|
||||
buckets: DailyAggBucket[]
|
||||
): HistoricalSummary[] {
|
||||
const initialErrorBudget = 1 - objective.target;
|
||||
const rollingWindowDurationInDays = moment
|
||||
.duration(timeWindow.duration.value, toMomentUnitOfTime(timeWindow.duration.unit))
|
||||
.asDays();
|
||||
|
||||
const { bucketsPerDay } = getFixedIntervalAndBucketsPerDay(rollingWindowDurationInDays);
|
||||
const totalSlices = Math.ceil(
|
||||
timeWindow.duration.asSeconds() / objective.timesliceWindow!.asSeconds()
|
||||
);
|
||||
|
||||
return buckets
|
||||
.slice(-bucketsPerDay * rollingWindowDurationInDays)
|
||||
.map((bucket: DailyAggBucket): HistoricalSummary => {
|
||||
const good = bucket.cumulative_good?.value ?? 0;
|
||||
const total = bucket.cumulative_total?.value ?? 0;
|
||||
const sliValue = computeSLI(good, total, totalSlices);
|
||||
const consumedErrorBudget = sliValue < 0 ? 0 : (1 - sliValue) / initialErrorBudget;
|
||||
const errorBudget = toErrorBudget(initialErrorBudget, consumedErrorBudget);
|
||||
|
||||
return {
|
||||
date: new Date(bucket.key_as_string),
|
||||
errorBudget,
|
||||
sliValue,
|
||||
status: computeSummaryStatus(objective, sliValue, errorBudget),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function generateSearchQuery({
|
||||
sloId,
|
||||
groupBy,
|
||||
|
@ -343,20 +392,17 @@ function getDateRange(timeWindow: TimeWindow) {
|
|||
};
|
||||
}
|
||||
if (calendarAlignedTimeWindowSchema.is(timeWindow)) {
|
||||
return toDateRange(timeWindow);
|
||||
const now = moment();
|
||||
const unit = toCalendarAlignedTimeWindowMomentUnit(timeWindow);
|
||||
const from = moment.utc(now).startOf(unit);
|
||||
const to = moment.utc(now).endOf(unit);
|
||||
|
||||
return { from: from.toDate(), to: to.toDate() };
|
||||
}
|
||||
|
||||
assertNever(timeWindow);
|
||||
}
|
||||
|
||||
function computeTotalSlicesFromDateRange(dateRange: DateRange, timesliceWindow: Duration) {
|
||||
const dateRangeDurationInUnit = moment(dateRange.to).diff(
|
||||
dateRange.from,
|
||||
toMomentUnitOfTime(timesliceWindow.unit)
|
||||
);
|
||||
return Math.ceil(dateRangeDurationInUnit / timesliceWindow!.value);
|
||||
}
|
||||
|
||||
export function getFixedIntervalAndBucketsPerDay(durationInDays: number): {
|
||||
fixedInterval: string;
|
||||
bucketsPerDay: number;
|
||||
|
|
|
@ -14,7 +14,7 @@ export * from './get_slo';
|
|||
export * from './historical_summary_client';
|
||||
export * from './resource_installer';
|
||||
export * from './slo_installer';
|
||||
export * from './sli_client';
|
||||
export * from './burn_rates_client';
|
||||
export * from './slo_repository';
|
||||
export * from './transform_manager';
|
||||
export * from './summay_transform_manager';
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { ResourceInstaller } from '../resource_installer';
|
||||
import { SLIClient } from '../sli_client';
|
||||
import { BurnRatesClient } from '../burn_rates_client';
|
||||
import { SLORepository } from '../slo_repository';
|
||||
import { SummaryClient } from '../summary_client';
|
||||
import { SummarySearchClient } from '../summary_search_client';
|
||||
|
@ -62,9 +62,9 @@ const createSummarySearchClientMock = (): jest.Mocked<SummarySearchClient> => {
|
|||
};
|
||||
};
|
||||
|
||||
const createSLIClientMock = (): jest.Mocked<SLIClient> => {
|
||||
const createBurnRatesClientMock = (): jest.Mocked<BurnRatesClient> => {
|
||||
return {
|
||||
fetchSLIDataFrom: jest.fn(),
|
||||
calculate: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -75,5 +75,5 @@ export {
|
|||
createSLORepositoryMock,
|
||||
createSummaryClientMock,
|
||||
createSummarySearchClientMock,
|
||||
createSLIClientMock,
|
||||
createBurnRatesClientMock,
|
||||
};
|
||||
|
|
|
@ -13,7 +13,7 @@ import { createSLO } from './fixtures/slo';
|
|||
import { sevenDaysRolling, weeklyCalendarAligned } from './fixtures/time_window';
|
||||
import { DefaultSummaryClient } from './summary_client';
|
||||
|
||||
const commonEsResponse = {
|
||||
const createEsResponse = (good: number = 90, total: number = 100) => ({
|
||||
took: 100,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
|
@ -25,19 +25,10 @@ const commonEsResponse = {
|
|||
hits: {
|
||||
hits: [],
|
||||
},
|
||||
};
|
||||
|
||||
const createEsResponse = (good: number = 90, total: number = 100) => ({
|
||||
...commonEsResponse,
|
||||
responses: [
|
||||
{
|
||||
...commonEsResponse,
|
||||
aggregations: {
|
||||
good: { value: good },
|
||||
total: { value: total },
|
||||
},
|
||||
},
|
||||
],
|
||||
aggregations: {
|
||||
good: { value: good },
|
||||
total: { value: total },
|
||||
},
|
||||
});
|
||||
|
||||
describe('SummaryClient', () => {
|
||||
|
@ -51,7 +42,7 @@ describe('SummaryClient', () => {
|
|||
describe('with rolling and occurrences SLO', () => {
|
||||
it('returns the summary', async () => {
|
||||
const slo = createSLO({ timeWindow: sevenDaysRolling() });
|
||||
esClientMock.msearch.mockResolvedValueOnce(createEsResponse());
|
||||
esClientMock.search.mockResolvedValueOnce(createEsResponse());
|
||||
const summaryClient = new DefaultSummaryClient(esClientMock);
|
||||
|
||||
const result = await summaryClient.computeSummary({ slo });
|
||||
|
@ -86,7 +77,7 @@ describe('SummaryClient', () => {
|
|||
const slo = createSLO({
|
||||
timeWindow: weeklyCalendarAligned(),
|
||||
});
|
||||
esClientMock.msearch.mockResolvedValueOnce(createEsResponse());
|
||||
esClientMock.search.mockResolvedValueOnce(createEsResponse());
|
||||
const summaryClient = new DefaultSummaryClient(esClientMock);
|
||||
|
||||
await summaryClient.computeSummary({ slo });
|
||||
|
@ -129,7 +120,7 @@ describe('SummaryClient', () => {
|
|||
},
|
||||
timeWindow: sevenDaysRolling(),
|
||||
});
|
||||
esClientMock.msearch.mockResolvedValueOnce(createEsResponse());
|
||||
esClientMock.search.mockResolvedValueOnce(createEsResponse());
|
||||
const summaryClient = new DefaultSummaryClient(esClientMock);
|
||||
|
||||
const result = await summaryClient.computeSummary({ slo });
|
||||
|
@ -178,7 +169,7 @@ describe('SummaryClient', () => {
|
|||
},
|
||||
timeWindow: weeklyCalendarAligned(),
|
||||
});
|
||||
esClientMock.msearch.mockResolvedValueOnce(createEsResponse());
|
||||
esClientMock.search.mockResolvedValueOnce(createEsResponse());
|
||||
const summaryClient = new DefaultSummaryClient(esClientMock);
|
||||
|
||||
const result = await summaryClient.computeSummary({ slo });
|
||||
|
|
|
@ -5,21 +5,23 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
AggregationsSumAggregate,
|
||||
AggregationsTopHitsAggregate,
|
||||
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import {
|
||||
ALL_VALUE,
|
||||
calendarAlignedTimeWindowSchema,
|
||||
Duration,
|
||||
occurrencesBudgetingMethodSchema,
|
||||
timeslicesBudgetingMethodSchema,
|
||||
toMomentUnitOfTime,
|
||||
} from '@kbn/slo-schema';
|
||||
import moment from 'moment';
|
||||
import { SLO_DESTINATION_INDEX_PATTERN } from '../../common/constants';
|
||||
import { DateRange, Groupings, Meta, SLODefinition, Summary } from '../domain/models';
|
||||
import { Groupings, Meta, SLODefinition, Summary } from '../domain/models';
|
||||
import { computeSLI, computeSummaryStatus, toErrorBudget } from '../domain/services';
|
||||
import { toDateRange } from '../domain/services/date_range';
|
||||
import { getFlattenedGroupings } from './utils';
|
||||
import { computeTotalSlicesFromDateRange } from './utils/compute_total_slices_from_date_range';
|
||||
|
||||
interface Params {
|
||||
slo: SLODefinition;
|
||||
|
@ -53,25 +55,15 @@ export class DefaultSummaryClient implements SummaryClient {
|
|||
const instanceIdFilter = shouldIncludeInstanceIdFilter
|
||||
? [{ term: { 'slo.instanceId': instanceId } }]
|
||||
: [];
|
||||
const extraGroupingsAgg = {
|
||||
last_doc: {
|
||||
top_hits: {
|
||||
sort: [
|
||||
{
|
||||
'@timestamp': {
|
||||
order: 'desc',
|
||||
},
|
||||
},
|
||||
],
|
||||
_source: {
|
||||
includes: ['slo.groupings', 'monitor', 'observer', 'config_id'],
|
||||
},
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await this.esClient.search({
|
||||
const result = await this.esClient.search<
|
||||
any,
|
||||
{
|
||||
good: AggregationsSumAggregate;
|
||||
total: AggregationsSumAggregate;
|
||||
last_doc: AggregationsTopHitsAggregate;
|
||||
}
|
||||
>({
|
||||
index: remoteName
|
||||
? `${remoteName}:${SLO_DESTINATION_INDEX_PATTERN}`
|
||||
: SLO_DESTINATION_INDEX_PATTERN,
|
||||
|
@ -90,9 +82,24 @@ export class DefaultSummaryClient implements SummaryClient {
|
|||
],
|
||||
},
|
||||
},
|
||||
// @ts-expect-error AggregationsAggregationContainer needs to be updated with top_hits
|
||||
aggs: {
|
||||
...(shouldIncludeInstanceIdFilter && extraGroupingsAgg),
|
||||
...(shouldIncludeInstanceIdFilter && {
|
||||
last_doc: {
|
||||
top_hits: {
|
||||
sort: [
|
||||
{
|
||||
'@timestamp': {
|
||||
order: 'desc',
|
||||
},
|
||||
},
|
||||
],
|
||||
_source: {
|
||||
includes: ['slo.groupings', 'monitor', 'observer', 'config_id'],
|
||||
},
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
}),
|
||||
...(timeslicesBudgetingMethodSchema.is(slo.budgetingMethod) && {
|
||||
good: {
|
||||
sum: { field: 'slo.isGoodSlice' },
|
||||
|
@ -108,39 +115,32 @@ export class DefaultSummaryClient implements SummaryClient {
|
|||
},
|
||||
});
|
||||
|
||||
// @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 source = result.aggregations?.last_doc?.hits?.hits?.[0]?._source;
|
||||
const groupings = source?.slo?.groupings;
|
||||
|
||||
const sliValue = computeSLI(good, total);
|
||||
const initialErrorBudget = 1 - slo.objective.target;
|
||||
let errorBudget;
|
||||
|
||||
if (
|
||||
calendarAlignedTimeWindowSchema.is(slo.timeWindow) &&
|
||||
timeslicesBudgetingMethodSchema.is(slo.budgetingMethod)
|
||||
) {
|
||||
let sliValue;
|
||||
if (timeslicesBudgetingMethodSchema.is(slo.budgetingMethod)) {
|
||||
const totalSlices = computeTotalSlicesFromDateRange(
|
||||
dateRange,
|
||||
slo.objective.timesliceWindow!
|
||||
);
|
||||
const consumedErrorBudget =
|
||||
sliValue < 0 ? 0 : (total - good) / (totalSlices * initialErrorBudget);
|
||||
|
||||
errorBudget = toErrorBudget(initialErrorBudget, consumedErrorBudget);
|
||||
sliValue = computeSLI(good, total, totalSlices);
|
||||
} else {
|
||||
const consumedErrorBudget = sliValue < 0 ? 0 : (1 - sliValue) / initialErrorBudget;
|
||||
errorBudget = toErrorBudget(
|
||||
initialErrorBudget,
|
||||
consumedErrorBudget,
|
||||
calendarAlignedTimeWindowSchema.is(slo.timeWindow)
|
||||
);
|
||||
sliValue = computeSLI(good, total);
|
||||
}
|
||||
|
||||
const initialErrorBudget = 1 - slo.objective.target;
|
||||
const consumedErrorBudget = sliValue < 0 ? 0 : (1 - sliValue) / initialErrorBudget;
|
||||
const errorBudget = toErrorBudget(
|
||||
initialErrorBudget,
|
||||
consumedErrorBudget,
|
||||
calendarAlignedTimeWindowSchema.is(slo.timeWindow) &&
|
||||
occurrencesBudgetingMethodSchema.is(slo.budgetingMethod)
|
||||
);
|
||||
|
||||
return {
|
||||
summary: {
|
||||
sliValue,
|
||||
|
@ -153,14 +153,6 @@ export class DefaultSummaryClient implements SummaryClient {
|
|||
}
|
||||
}
|
||||
|
||||
function computeTotalSlicesFromDateRange(dateRange: DateRange, timesliceWindow: Duration) {
|
||||
const dateRangeDurationInUnit = moment(dateRange.to).diff(
|
||||
dateRange.from,
|
||||
toMomentUnitOfTime(timesliceWindow.unit)
|
||||
);
|
||||
return Math.ceil(dateRangeDurationInUnit / timesliceWindow!.value);
|
||||
}
|
||||
|
||||
function getMetaFields(
|
||||
slo: SLODefinition,
|
||||
source: { monitor?: { id?: string }; config_id?: string; observer?: { name?: string } }
|
||||
|
|
|
@ -98,9 +98,10 @@ export function generateSummaryTransformForTimeslicesAndCalendarAligned(
|
|||
buckets_path: {
|
||||
goodEvents: 'goodEvents',
|
||||
totalEvents: 'totalEvents',
|
||||
totalSlicesInPeriod: '_totalSlicesInPeriod',
|
||||
},
|
||||
script:
|
||||
'if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }',
|
||||
'if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return 1 - (params.totalEvents - params.goodEvents) / params.totalSlicesInPeriod }',
|
||||
},
|
||||
},
|
||||
errorBudgetInitial: {
|
||||
|
@ -112,13 +113,11 @@ export function generateSummaryTransformForTimeslicesAndCalendarAligned(
|
|||
errorBudgetConsumed: {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
goodEvents: 'goodEvents',
|
||||
totalEvents: 'totalEvents',
|
||||
totalSlicesInPeriod: '_totalSlicesInPeriod',
|
||||
sliValue: 'sliValue',
|
||||
errorBudgetInitial: 'errorBudgetInitial',
|
||||
},
|
||||
script:
|
||||
'if (params.totalEvents == 0) { return 0 } else { return (params.totalEvents - params.goodEvents) / (params.totalSlicesInPeriod * params.errorBudgetInitial) }',
|
||||
'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }',
|
||||
},
|
||||
},
|
||||
errorBudgetRemaining: {
|
||||
|
|
|
@ -19,6 +19,10 @@ import { getGroupBy } from './common';
|
|||
export function generateSummaryTransformForTimeslicesAndRolling(
|
||||
slo: SLODefinition
|
||||
): TransformPutTransformRequest {
|
||||
const sliceDurationInSeconds = slo.objective.timesliceWindow!.asSeconds();
|
||||
const timeWindowInSeconds = slo.timeWindow.duration.asSeconds();
|
||||
const totalSlicesInWindow = Math.ceil(timeWindowInSeconds / sliceDurationInSeconds);
|
||||
|
||||
return {
|
||||
transform_id: getSLOSummaryTransformId(slo.id, slo.revision),
|
||||
dest: {
|
||||
|
@ -71,8 +75,7 @@ export function generateSummaryTransformForTimeslicesAndRolling(
|
|||
goodEvents: 'goodEvents',
|
||||
totalEvents: 'totalEvents',
|
||||
},
|
||||
script:
|
||||
'if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }',
|
||||
script: `if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return 1 - (params.totalEvents - params.goodEvents) / ${totalSlicesInWindow} }`,
|
||||
},
|
||||
},
|
||||
errorBudgetInitial: {
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 moment from 'moment';
|
||||
import { DateRange, Duration, toMomentUnitOfTime } from '../../domain/models';
|
||||
|
||||
export function computeTotalSlicesFromDateRange(dateRange: DateRange, timesliceWindow: Duration) {
|
||||
const dateRangeDurationInUnit = moment(dateRange.to).diff(
|
||||
dateRange.from,
|
||||
toMomentUnitOfTime(timesliceWindow.unit)
|
||||
);
|
||||
return Math.ceil(dateRangeDurationInUnit / timesliceWindow!.value);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue