feat(slo): Add internal slo data (#143888)

* Add duration format method

* Add internal slo fields

* Update test
This commit is contained in:
Kevin Delemme 2022-10-26 08:46:01 -04:00 committed by GitHub
parent 3150520ac4
commit 2d7f5c904a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 359 additions and 132 deletions

View file

@ -20,6 +20,9 @@ export const getSLOMappingsTemplate = (name: string) => ({
type: 'keyword',
ignore_above: 256,
},
revision: {
type: 'long',
},
numerator: {
type: 'long',
},
@ -29,6 +32,25 @@ export const getSLOMappingsTemplate = (name: string) => ({
context: {
type: 'flattened',
},
_internal: {
properties: {
name: { type: 'keyword', ignore_above: 256 },
budgeting_method: { type: 'keyword' },
objective: {
properties: {
target: { type: 'double' },
timeslice_target: { type: 'double' },
timeslice_window: { type: 'keyword' },
},
},
time_window: {
properties: {
duration: { type: 'keyword' },
is_rolling: { type: 'boolean' },
},
},
},
},
},
},
},

View file

@ -57,6 +57,31 @@ Object {
"field": "@timestamp",
},
},
"slo._internal.budgeting_method": Object {
"terms": Object {
"field": "slo._internal.budgeting_method",
},
},
"slo._internal.name": Object {
"terms": Object {
"field": "slo._internal.name",
},
},
"slo._internal.objective.target": Object {
"terms": Object {
"field": "slo._internal.objective.target",
},
},
"slo._internal.time_window.duration": Object {
"terms": Object {
"field": "slo._internal.time_window.duration",
},
},
"slo._internal.time_window.is_rolling": Object {
"terms": Object {
"field": "slo._internal.time_window.is_rolling",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
@ -106,6 +131,36 @@ Object {
},
},
"runtime_mappings": Object {
"slo._internal.budgeting_method": Object {
"script": Object {
"source": "emit('occurrences')",
},
"type": "keyword",
},
"slo._internal.name": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo._internal.objective.target": Object {
"script": Object {
"source": "emit(0.999)",
},
"type": "double",
},
"slo._internal.time_window.duration": Object {
"script": Object {
"source": "emit('7d')",
},
"type": "keyword",
},
"slo._internal.time_window.is_rolling": Object {
"script": Object {
"source": "emit(true)",
},
"type": "boolean",
},
"slo.id": Object {
"script": Object {
"source": Any<String>,

View file

@ -62,6 +62,31 @@ Object {
"field": "@timestamp",
},
},
"slo._internal.budgeting_method": Object {
"terms": Object {
"field": "slo._internal.budgeting_method",
},
},
"slo._internal.name": Object {
"terms": Object {
"field": "slo._internal.name",
},
},
"slo._internal.objective.target": Object {
"terms": Object {
"field": "slo._internal.objective.target",
},
},
"slo._internal.time_window.duration": Object {
"terms": Object {
"field": "slo._internal.time_window.duration",
},
},
"slo._internal.time_window.is_rolling": Object {
"terms": Object {
"field": "slo._internal.time_window.is_rolling",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
@ -111,6 +136,36 @@ Object {
},
},
"runtime_mappings": Object {
"slo._internal.budgeting_method": Object {
"script": Object {
"source": "emit('occurrences')",
},
"type": "keyword",
},
"slo._internal.name": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo._internal.objective.target": Object {
"script": Object {
"source": "emit(0.999)",
},
"type": "double",
},
"slo._internal.time_window.duration": Object {
"script": Object {
"source": "emit('7d')",
},
"type": "keyword",
},
"slo._internal.time_window.is_rolling": Object {
"script": Object {
"source": "emit(true)",
},
"type": "boolean",
},
"slo.id": Object {
"script": Object {
"source": Any<String>,

View file

@ -141,6 +141,31 @@ Object {
"field": "@timestamp",
},
},
"slo._internal.budgeting_method": Object {
"terms": Object {
"field": "slo._internal.budgeting_method",
},
},
"slo._internal.name": Object {
"terms": Object {
"field": "slo._internal.name",
},
},
"slo._internal.objective.target": Object {
"terms": Object {
"field": "slo._internal.objective.target",
},
},
"slo._internal.time_window.duration": Object {
"terms": Object {
"field": "slo._internal.time_window.duration",
},
},
"slo._internal.time_window.is_rolling": Object {
"terms": Object {
"field": "slo._internal.time_window.is_rolling",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
@ -171,6 +196,36 @@ Object {
},
},
"runtime_mappings": Object {
"slo._internal.budgeting_method": Object {
"script": Object {
"source": "emit('occurrences')",
},
"type": "keyword",
},
"slo._internal.name": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo._internal.objective.target": Object {
"script": Object {
"source": "emit(0.999)",
},
"type": "double",
},
"slo._internal.time_window.duration": Object {
"script": Object {
"source": "emit('7d')",
},
"type": "keyword",
},
"slo._internal.time_window.is_rolling": Object {
"script": Object {
"source": "emit(true)",
},
"type": "boolean",
},
"slo.id": Object {
"script": Object {
"source": Any<String>,

View file

@ -5,11 +5,7 @@
* 2.0.
*/
import {
AggregationsCalendarInterval,
MappingRuntimeFieldType,
TransformPutTransformRequest,
} from '@elastic/elasticsearch/lib/api/types';
import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
import { InvalidTransformError } from '../../../errors';
import { ALL_VALUE, apmTransactionDurationIndicatorSchema } from '../../../types/schema';
import {
@ -23,7 +19,7 @@ import { TransformGenerator } from '.';
const APM_SOURCE_INDEX = 'metrics-apm*';
export class ApmTransactionDurationTransformGenerator implements TransformGenerator {
export class ApmTransactionDurationTransformGenerator extends TransformGenerator {
public getTransformParams(slo: SLO): TransformPutTransformRequest {
if (!apmTransactionDurationIndicatorSchema.is(slo.indicator)) {
throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`);
@ -33,7 +29,7 @@ export class ApmTransactionDurationTransformGenerator implements TransformGenera
this.buildTransformId(slo),
this.buildSource(slo, slo.indicator),
this.buildDestination(),
this.buildGroupBy(),
this.buildCommonGroupBy(slo),
this.buildAggregations(slo.indicator)
);
}
@ -78,20 +74,7 @@ export class ApmTransactionDurationTransformGenerator implements TransformGenera
return {
index: APM_SOURCE_INDEX,
runtime_mappings: {
'slo.id': {
type: 'keyword' as MappingRuntimeFieldType,
script: {
source: `emit('${slo.id}')`,
},
},
'slo.revision': {
type: 'long' as MappingRuntimeFieldType,
script: {
source: `emit(${slo.revision})`,
},
},
},
runtime_mappings: this.buildCommonRuntimeMappings(slo),
query: {
bool: {
filter: [
@ -114,27 +97,6 @@ export class ApmTransactionDurationTransformGenerator implements TransformGenera
};
}
private buildGroupBy() {
return {
'slo.id': {
terms: {
field: 'slo.id',
},
},
'slo.revision': {
terms: {
field: 'slo.revision',
},
},
'@timestamp': {
date_histogram: {
field: '@timestamp',
calendar_interval: '1m' as AggregationsCalendarInterval,
},
},
};
}
private buildAggregations(indicator: APMTransactionDurationIndicator) {
const truncatedThreshold = Math.trunc(indicator.params['threshold.us']);

View file

@ -5,11 +5,7 @@
* 2.0.
*/
import {
AggregationsCalendarInterval,
MappingRuntimeFieldType,
TransformPutTransformRequest,
} from '@elastic/elasticsearch/lib/api/types';
import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
import { InvalidTransformError } from '../../../errors';
import { ALL_VALUE, apmTransactionErrorRateIndicatorSchema } from '../../../types/schema';
import { getSLOTransformTemplate } from '../../../assets/transform_templates/slo_transform_template';
@ -25,7 +21,7 @@ const APM_SOURCE_INDEX = 'metrics-apm*';
const ALLOWED_STATUS_CODES = ['2xx', '3xx', '4xx', '5xx'];
const DEFAULT_GOOD_STATUS_CODES = ['2xx', '3xx', '4xx'];
export class ApmTransactionErrorRateTransformGenerator implements TransformGenerator {
export class ApmTransactionErrorRateTransformGenerator extends TransformGenerator {
public getTransformParams(slo: SLO): TransformPutTransformRequest {
if (!apmTransactionErrorRateIndicatorSchema.is(slo.indicator)) {
throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`);
@ -35,7 +31,7 @@ export class ApmTransactionErrorRateTransformGenerator implements TransformGener
this.buildTransformId(slo),
this.buildSource(slo, slo.indicator),
this.buildDestination(),
this.buildGroupBy(),
this.buildCommonGroupBy(slo),
this.buildAggregations(slo, slo.indicator)
);
}
@ -80,20 +76,7 @@ export class ApmTransactionErrorRateTransformGenerator implements TransformGener
return {
index: APM_SOURCE_INDEX,
runtime_mappings: {
'slo.id': {
type: 'keyword' as MappingRuntimeFieldType,
script: {
source: `emit('${slo.id}')`,
},
},
'slo.revision': {
type: 'long' as MappingRuntimeFieldType,
script: {
source: `emit(${slo.revision})`,
},
},
},
runtime_mappings: this.buildCommonRuntimeMappings(slo),
query: {
bool: {
filter: [
@ -116,27 +99,6 @@ export class ApmTransactionErrorRateTransformGenerator implements TransformGener
};
}
private buildGroupBy() {
return {
'slo.id': {
terms: {
field: 'slo.id',
},
},
'slo.revision': {
terms: {
field: 'slo.revision',
},
},
'@timestamp': {
date_histogram: {
field: '@timestamp',
calendar_interval: '1m' as AggregationsCalendarInterval,
},
},
};
}
private buildAggregations(slo: SLO, indicator: APMTransactionErrorRateIndicator) {
const goodStatusCodesFilter = this.getGoodStatusCodesFilter(indicator.params.good_status_codes);

View file

@ -5,11 +5,7 @@
* 2.0.
*/
import {
AggregationsCalendarInterval,
MappingRuntimeFieldType,
TransformPutTransformRequest,
} from '@elastic/elasticsearch/lib/api/types';
import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
import { InvalidTransformError } from '../../../errors';
@ -23,7 +19,7 @@ import {
} from '../../../assets/constants';
import { KQLCustomIndicator, SLO } from '../../../types/models';
export class KQLCustomTransformGenerator implements TransformGenerator {
export class KQLCustomTransformGenerator extends TransformGenerator {
public getTransformParams(slo: SLO): TransformPutTransformRequest {
if (!kqlCustomIndicatorSchema.is(slo.indicator)) {
throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`);
@ -33,7 +29,7 @@ export class KQLCustomTransformGenerator implements TransformGenerator {
this.buildTransformId(slo),
this.buildSource(slo, slo.indicator),
this.buildDestination(),
this.buildGroupBy(),
this.buildCommonGroupBy(slo),
this.buildAggregations(slo, slo.indicator)
);
}
@ -46,20 +42,7 @@ export class KQLCustomTransformGenerator implements TransformGenerator {
const filter = getElastichsearchQueryOrThrow(indicator.params.query_filter);
return {
index: indicator.params.index,
runtime_mappings: {
'slo.id': {
type: 'keyword' as MappingRuntimeFieldType,
script: {
source: `emit('${slo.id}')`,
},
},
'slo.revision': {
type: 'long' as MappingRuntimeFieldType,
script: {
source: `emit(${slo.revision})`,
},
},
},
runtime_mappings: this.buildCommonRuntimeMappings(slo),
query: filter,
};
}
@ -71,27 +54,6 @@ export class KQLCustomTransformGenerator implements TransformGenerator {
};
}
private buildGroupBy() {
return {
'slo.id': {
terms: {
field: 'slo.id',
},
},
'slo.revision': {
terms: {
field: 'slo.revision',
},
},
'@timestamp': {
date_histogram: {
field: '@timestamp',
calendar_interval: '1m' as AggregationsCalendarInterval,
},
},
};
}
private buildAggregations(slo: SLO, indicator: KQLCustomIndicator) {
const numerator = getElastichsearchQueryOrThrow(indicator.params.numerator);
const denominator = getElastichsearchQueryOrThrow(indicator.params.denominator);

View file

@ -5,9 +5,147 @@
* 2.0.
*/
import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { MappingRuntimeFieldType } from '@elastic/elasticsearch/lib/api/types';
import {
AggregationsCalendarInterval,
TransformPutTransformRequest,
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
calendarAlignedTimeWindowSchema,
rollingTimeWindowSchema,
timeslicesBudgetingMethodSchema,
} from '../../../types/schema';
import { SLO } from '../../../types/models';
export interface TransformGenerator {
getTransformParams(slo: SLO): TransformPutTransformRequest;
export abstract class TransformGenerator {
public abstract getTransformParams(slo: SLO): TransformPutTransformRequest;
public buildCommonRuntimeMappings(slo: SLO) {
return {
'slo.id': {
type: 'keyword' as MappingRuntimeFieldType,
script: {
source: `emit('${slo.id}')`,
},
},
'slo.revision': {
type: 'long' as MappingRuntimeFieldType,
script: {
source: `emit(${slo.revision})`,
},
},
'slo._internal.name': {
type: 'keyword' as MappingRuntimeFieldType,
script: {
source: `emit('${slo.name}')`,
},
},
'slo._internal.budgeting_method': {
type: 'keyword' as MappingRuntimeFieldType,
script: {
source: `emit('${slo.budgeting_method}')`,
},
},
'slo._internal.objective.target': {
type: 'double' as MappingRuntimeFieldType,
script: {
source: `emit(${slo.objective.target})`,
},
},
...(timeslicesBudgetingMethodSchema.is(slo.budgeting_method) && {
'slo._internal.objective.timeslice_target': {
type: 'double' as MappingRuntimeFieldType,
script: {
source: `emit(${slo.objective.timeslice_target})`,
},
},
'slo._internal.objective.timeslice_window': {
type: 'keyword' as MappingRuntimeFieldType,
script: {
source: `emit('${slo.objective.timeslice_window?.format()}')`,
},
},
}),
'slo._internal.time_window.duration': {
type: 'keyword' as MappingRuntimeFieldType,
script: {
source: `emit('${slo.time_window.duration.format()}')`,
},
},
...(calendarAlignedTimeWindowSchema.is(slo.time_window) && {
'slo._internal.time_window.is_rolling': {
type: 'boolean' as MappingRuntimeFieldType,
script: {
source: `emit(false)`,
},
},
}),
...(rollingTimeWindowSchema.is(slo.time_window) && {
'slo._internal.time_window.is_rolling': {
type: 'boolean' as MappingRuntimeFieldType,
script: {
source: `emit(true)`,
},
},
}),
};
}
public buildCommonGroupBy(slo: SLO) {
return {
'slo.id': {
terms: {
field: 'slo.id',
},
},
'slo.revision': {
terms: {
field: 'slo.revision',
},
},
'slo._internal.name': {
terms: {
field: 'slo._internal.name',
},
},
'slo._internal.budgeting_method': {
terms: {
field: 'slo._internal.budgeting_method',
},
},
'slo._internal.objective.target': {
terms: {
field: 'slo._internal.objective.target',
},
},
'slo._internal.time_window.duration': {
terms: {
field: 'slo._internal.time_window.duration',
},
},
'slo._internal.time_window.is_rolling': {
terms: {
field: 'slo._internal.time_window.is_rolling',
},
},
...(timeslicesBudgetingMethodSchema.is(slo.budgeting_method) && {
'slo._internal.objective.timeslice_target': {
terms: {
field: 'slo._internal.objective.timeslice_target',
},
},
'slo._internal.objective.timeslice_window': {
terms: {
field: 'slo._internal.objective.timeslice_window',
},
},
}),
'@timestamp': {
date_histogram: {
field: '@timestamp',
calendar_interval: '1m' as AggregationsCalendarInterval,
},
},
};
}
}

View file

@ -138,13 +138,13 @@ describe('TransformManager', () => {
});
});
class DummyTransformGenerator implements TransformGenerator {
class DummyTransformGenerator extends TransformGenerator {
getTransformParams(slo: SLO): TransformPutTransformRequest {
return {} as TransformPutTransformRequest;
}
}
class FailTransformGenerator implements TransformGenerator {
class FailTransformGenerator extends TransformGenerator {
getTransformParams(slo: SLO): TransformPutTransformRequest {
throw new Error('Some error');
}

View file

@ -20,6 +20,18 @@ describe('Duration', () => {
expect(() => new Duration(1, 'z' as DurationUnit)).toThrow('invalid duration unit');
});
describe('format', () => {
it('formats the duration correctly', () => {
expect(new Duration(1, DurationUnit.m).format()).toBe('1m');
expect(new Duration(1, DurationUnit.h).format()).toBe('1h');
expect(new Duration(1, DurationUnit.d).format()).toBe('1d');
expect(new Duration(1, DurationUnit.w).format()).toBe('1w');
expect(new Duration(1, DurationUnit.M).format()).toBe('1M');
expect(new Duration(1, DurationUnit.Q).format()).toBe('1Q');
expect(new Duration(1, DurationUnit.Y).format()).toBe('1Y');
});
});
describe('isShorterThan', () => {
it('returns true when the current duration is shorter than the other duration', () => {
const short = new Duration(1, DurationUnit.m);

View file

@ -33,6 +33,10 @@ class Duration {
const currentDurationMoment = moment.duration(this.value, toMomentUnitOfTime(this.unit));
return currentDurationMoment.asSeconds() < otherDurationMoment.asSeconds();
}
format(): string {
return `${this.value}${this.unit}`;
}
}
const toMomentUnitOfTime = (unit: DurationUnit): moment.unitOfTime.Diff => {

View file

@ -24,7 +24,7 @@ const durationType = new t.Type<Duration, string, unknown>(
return t.failure(input, context);
}
}),
(duration: Duration): string => `${duration.value}${duration.unit}`
(duration: Duration): string => duration.format()
);
export { durationType };