[SLO]: Limit initial data backfill for SLO for serverless (#208790)

Resolves #188428 

## Summary

- Add callout in serverless to indicate that initial data backfill is
limited to 7 days.
- If prevent data backfill is checked, transform will still use the SLO
delay for the filter range.
- If serverless and prevent data backfill is not checked, data backfill
is hard coded to 7 days, the lowest rolling time window option. Time
window will not be rounded down.

<img width="980" alt="Screenshot 2025-01-29 at 11 18 01 AM"
src="https://github.com/user-attachments/assets/b6481ef9-cc0e-4403-8309-50b6d8f37e70"
/>
This commit is contained in:
Bailey Cash 2025-01-29 16:37:34 -05:00 committed by GitHub
parent b53d3990a2
commit ee14e50fe1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 129 additions and 54 deletions

View file

@ -32,6 +32,7 @@ import { CreateSLOForm } from '../types';
import { MAX_WIDTH } from '../constants';
import { AdvancedSettings } from './indicator_section/advanced_settings/advanced_settings';
import { SloEditFormObjectiveSectionTimeslices } from './slo_edit_form_objective_section_timeslices';
import { usePluginContext } from '../../../hooks/use_plugin_context';
export function SloEditFormObjectiveSection() {
const {
@ -41,6 +42,8 @@ export function SloEditFormObjectiveSection() {
setValue,
formState: { defaultValues },
} = useFormContext<CreateSLOForm>();
const { isServerless } = usePluginContext();
const budgetingSelect = useGeneratedHtmlId({ prefix: 'budgetingSelect' });
const timeWindowTypeSelect = useGeneratedHtmlId({ prefix: 'timeWindowTypeSelect' });
const timeWindowSelect = useGeneratedHtmlId({ prefix: 'timeWindowSelect' });
@ -94,6 +97,13 @@ export function SloEditFormObjectiveSection() {
data-test-subj="sloEditFormObjectiveSection"
>
<EuiFlexGroup direction="column" gutterSize="m">
{isServerless && (
<EuiCallOut>
{i18n.translate('xpack.slo.sloEdit.timeWindow.serverlessWarning', {
defaultMessage: 'Initial data backfill is limited to the past 7 days',
})}
</EuiCallOut>
)}
<EuiFlexGrid columns={3} gutterSize="m">
<EuiFlexItem>
<EuiFormRow

View file

@ -16,7 +16,7 @@ import {
import { ApmTransactionDurationTransformGenerator } from './apm_transaction_duration';
const SPACE_ID = 'custom-space';
const generator = new ApmTransactionDurationTransformGenerator(SPACE_ID, dataViewsService);
const generator = new ApmTransactionDurationTransformGenerator(SPACE_ID, dataViewsService, false);
describe('APM Transaction Duration Transform Generator', () => {
it('returns the expected transform params with every specified indicator params', async () => {

View file

@ -26,8 +26,8 @@ import { InvalidTransformError } from '../../errors';
import { getFilterRange, getTimesliceTargetComparator, parseIndex } from './common';
export class ApmTransactionDurationTransformGenerator extends TransformGenerator {
constructor(spaceId: string, dataViewService: DataViewsService) {
super(spaceId, dataViewService);
constructor(spaceId: string, dataViewService: DataViewsService, isServerless: boolean) {
super(spaceId, dataViewService, isServerless);
}
public async getTransformParams(slo: SLODefinition): Promise<TransformPutTransformRequest> {
@ -75,7 +75,9 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator
}
private async buildSource(slo: SLODefinition, indicator: APMTransactionDurationIndicator) {
const queryFilter: estypes.QueryDslQueryContainer[] = [getFilterRange(slo, '@timestamp')];
const queryFilter: estypes.QueryDslQueryContainer[] = [
getFilterRange(slo, '@timestamp', this.isServerless),
];
if (indicator.params.service !== ALL_VALUE) {
queryFilter.push({

View file

@ -16,7 +16,7 @@ import {
import { ApmTransactionErrorRateTransformGenerator } from './apm_transaction_error_rate';
const SPACE_ID = 'custom-space';
const generator = new ApmTransactionErrorRateTransformGenerator(SPACE_ID, dataViewsService);
const generator = new ApmTransactionErrorRateTransformGenerator(SPACE_ID, dataViewsService, false);
describe('APM Transaction Error Rate Transform Generator', () => {
it('returns the expected transform params with every specified indicator params', async () => {

View file

@ -25,8 +25,8 @@ import { InvalidTransformError } from '../../errors';
import { getFilterRange, getTimesliceTargetComparator, parseIndex } from './common';
export class ApmTransactionErrorRateTransformGenerator extends TransformGenerator {
constructor(spaceId: string, dataViewService: DataViewsService) {
super(spaceId, dataViewService);
constructor(spaceId: string, dataViewService: DataViewsService, isServerless: boolean) {
super(spaceId, dataViewService, isServerless);
}
public async getTransformParams(slo: SLODefinition): Promise<TransformPutTransformRequest> {
@ -74,7 +74,9 @@ export class ApmTransactionErrorRateTransformGenerator extends TransformGenerato
}
private async buildSource(slo: SLODefinition, indicator: APMTransactionErrorRateIndicator) {
const queryFilter: estypes.QueryDslQueryContainer[] = [getFilterRange(slo, '@timestamp')];
const queryFilter: estypes.QueryDslQueryContainer[] = [
getFilterRange(slo, '@timestamp', this.isServerless),
];
if (indicator.params.service !== ALL_VALUE) {
queryFilter.push({

View file

@ -45,7 +45,8 @@ describe('common', () => {
preventInitialBackfill: true,
},
}),
'@timestamp'
'@timestamp',
false
)
).toEqual({
range: {
@ -67,7 +68,8 @@ describe('common', () => {
preventInitialBackfill: false,
},
}),
'@timestamp'
'@timestamp',
false
)
).toEqual({
range: {
@ -77,5 +79,28 @@ describe('common', () => {
},
});
});
it('starts at now minus 7 days when preventInitialBackfill is false and serverless is true', () => {
expect(
getFilterRange(
createSLO({
timeWindow: thirtyDaysRolling(),
settings: {
frequency: twoMinute(),
syncDelay: fiveMinute(),
preventInitialBackfill: false,
},
}),
'@timestamp',
true
)
).toEqual({
range: {
'@timestamp': {
gte: 'now-7d',
},
},
});
});
});
});

View file

@ -61,20 +61,32 @@ export function getTimesliceTargetComparator(timesliceTarget: number) {
* preventInitialBackfill == true: we use the current time minus some buffer to account for the ingestion delay
* preventInitialBackfill === false: we use the time window duration to get the data for the last N days
*/
export function getFilterRange(slo: SLODefinition, timestampField: string) {
return slo.settings.preventInitialBackfill === true
? {
range: {
[timestampField]: {
gte: `now-${getDelayInSecondsFromSLO(slo)}s/m`,
},
export function getFilterRange(slo: SLODefinition, timestampField: string, isServerless: boolean) {
if (slo.settings.preventInitialBackfill) {
return {
range: {
[timestampField]: {
gte: `now-${getDelayInSecondsFromSLO(slo)}s/m`,
},
}
: {
range: {
[timestampField]: {
gte: `now-${slo.timeWindow.duration.format()}/d`,
},
},
};
}
if (isServerless) {
return {
range: {
[timestampField]: {
gte: `now-7d`,
},
};
},
};
}
return {
range: {
[timestampField]: {
gte: `now-${slo.timeWindow.duration.format()}/d`,
},
},
};
}

View file

@ -15,7 +15,7 @@ import { HistogramTransformGenerator } from './histogram';
import { dataViewsService } from '@kbn/data-views-plugin/server/mocks';
const SPACE_ID = 'custom-space';
const generator = new HistogramTransformGenerator(SPACE_ID, dataViewsService);
const generator = new HistogramTransformGenerator(SPACE_ID, dataViewsService, false);
describe('Histogram Transform Generator', () => {
describe('validation', () => {

View file

@ -25,8 +25,8 @@ import { GetHistogramIndicatorAggregation } from '../aggregations';
import { getFilterRange, getTimesliceTargetComparator } from './common';
export class HistogramTransformGenerator extends TransformGenerator {
constructor(spaceId: string, dataViewService: DataViewsService) {
super(spaceId, dataViewService);
constructor(spaceId: string, dataViewService: DataViewsService, isServerless: boolean) {
super(spaceId, dataViewService, isServerless);
}
public async getTransformParams(slo: SLODefinition): Promise<TransformPutTransformRequest> {
@ -59,7 +59,7 @@ export class HistogramTransformGenerator extends TransformGenerator {
query: {
bool: {
filter: [
getFilterRange(slo, indicator.params.timestampField),
getFilterRange(slo, indicator.params.timestampField, this.isServerless),
getElasticsearchQueryOrThrow(indicator.params.filter, dataView),
],
},

View file

@ -15,7 +15,7 @@ import { KQLCustomTransformGenerator } from './kql_custom';
import { dataViewsService } from '@kbn/data-views-plugin/server/mocks';
const SPACE_ID = 'custom-space';
const generator = new KQLCustomTransformGenerator(SPACE_ID, dataViewsService);
const generator = new KQLCustomTransformGenerator(SPACE_ID, dataViewsService, false);
describe('KQL Custom Transform Generator', () => {
describe('validation', () => {

View file

@ -20,8 +20,8 @@ import { InvalidTransformError } from '../../errors';
import { getFilterRange, getTimesliceTargetComparator } from './common';
export class KQLCustomTransformGenerator extends TransformGenerator {
constructor(spaceId: string, dataViewService: DataViewsService) {
super(spaceId, dataViewService);
constructor(spaceId: string, dataViewService: DataViewsService, isServerless: boolean) {
super(spaceId, dataViewService, isServerless);
}
public async getTransformParams(slo: SLODefinition): Promise<TransformPutTransformRequest> {
@ -53,7 +53,7 @@ export class KQLCustomTransformGenerator extends TransformGenerator {
query: {
bool: {
filter: [
getFilterRange(slo, indicator.params.timestampField),
getFilterRange(slo, indicator.params.timestampField, this.isServerless),
getElasticsearchQueryOrThrow(indicator.params.filter),
],
},

View file

@ -15,7 +15,7 @@ import { MetricCustomTransformGenerator } from './metric_custom';
import { dataViewsService } from '@kbn/data-views-plugin/server/mocks';
const SPACE_ID = 'custom-space';
const generator = new MetricCustomTransformGenerator(SPACE_ID, dataViewsService);
const generator = new MetricCustomTransformGenerator(SPACE_ID, dataViewsService, false);
describe('Metric Custom Transform Generator', () => {
describe('validation', () => {

View file

@ -24,8 +24,8 @@ import { getFilterRange, getTimesliceTargetComparator } from './common';
export const INVALID_EQUATION_REGEX = /[^A-Z|+|\-|\s|\d+|\.|\(|\)|\/|\*|>|<|=|\?|\:|&|\!|\|]+/g;
export class MetricCustomTransformGenerator extends TransformGenerator {
constructor(spaceId: string, dataViewService: DataViewsService) {
super(spaceId, dataViewService);
constructor(spaceId: string, dataViewService: DataViewsService, isServerless: boolean) {
super(spaceId, dataViewService, isServerless);
}
public async getTransformParams(slo: SLODefinition): Promise<TransformPutTransformRequest> {
@ -57,7 +57,7 @@ export class MetricCustomTransformGenerator extends TransformGenerator {
query: {
bool: {
filter: [
getFilterRange(slo, indicator.params.timestampField),
getFilterRange(slo, indicator.params.timestampField, this.isServerless),
getElasticsearchQueryOrThrow(indicator.params.filter, dataView),
],
},

View file

@ -111,7 +111,7 @@ export class SyntheticsAvailabilityTransformGenerator extends TransformGenerator
const queryFilter: estypes.QueryDslQueryContainer[] = [
{ term: { 'summary.final_attempt': true } },
{ term: { 'meta.space_id': this.spaceId } },
getFilterRange(slo, '@timestamp'),
getFilterRange(slo, '@timestamp', this.isServerless),
];
const { monitorIds, tags, projects } = buildParamValues({
monitorIds: indicator.params.monitorIds || [],

View file

@ -15,7 +15,7 @@ import {
import { TimesliceMetricTransformGenerator } from './timeslice_metric';
const SPACE_ID = 'custom-space';
const generator = new TimesliceMetricTransformGenerator(SPACE_ID, dataViewsService);
const generator = new TimesliceMetricTransformGenerator(SPACE_ID, dataViewsService, false);
const everythingIndicator = createTimesliceMetricIndicator(
[
{ name: 'A', aggregation: 'avg', field: 'test.field', filter: 'test.category: "test"' },

View file

@ -28,8 +28,8 @@ import { getFilterRange } from './common';
const INVALID_EQUATION_REGEX = /[^A-Z|+|\-|\s|\d+|\.|\(|\)|\/|\*|>|<|=|\?|\:|&|\!|\|]+/g;
export class TimesliceMetricTransformGenerator extends TransformGenerator {
constructor(spaceId: string, dataViewService: DataViewsService) {
super(spaceId, dataViewService);
constructor(spaceId: string, dataViewService: DataViewsService, isServerless: boolean) {
super(spaceId, dataViewService, isServerless);
}
public async getTransformParams(slo: SLODefinition): Promise<TransformPutTransformRequest> {
@ -62,7 +62,7 @@ export class TimesliceMetricTransformGenerator extends TransformGenerator {
query: {
bool: {
filter: [
getFilterRange(slo, indicator.params.timestampField),
getFilterRange(slo, indicator.params.timestampField, this.isServerless),
getElasticsearchQueryOrThrow(indicator.params.filter, dataView),
],
},

View file

@ -10,7 +10,11 @@ import { createAPMTransactionErrorRateIndicator, createSLO } from '../fixtures/s
import { ApmTransactionErrorRateTransformGenerator } from './apm_transaction_error_rate';
import { dataViewsService } from '@kbn/data-views-plugin/server/mocks';
const generator = new ApmTransactionErrorRateTransformGenerator('my-space-id', dataViewsService);
const generator = new ApmTransactionErrorRateTransformGenerator(
'my-space-id',
dataViewsService,
false
);
describe('Transform Generator', () => {
describe('buildCommonGroupBy', () => {

View file

@ -26,20 +26,34 @@ export function createTransformGenerators(
return {
'sli.apm.transactionDuration': new ApmTransactionDurationTransformGenerator(
spaceId,
dataViewsService
dataViewsService,
isServerless
),
'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator(
spaceId,
dataViewsService
dataViewsService,
isServerless
),
'sli.synthetics.availability': new SyntheticsAvailabilityTransformGenerator(
spaceId,
dataViewsService,
isServerless
),
'sli.kql.custom': new KQLCustomTransformGenerator(spaceId, dataViewsService),
'sli.metric.custom': new MetricCustomTransformGenerator(spaceId, dataViewsService),
'sli.histogram.custom': new HistogramTransformGenerator(spaceId, dataViewsService),
'sli.metric.timeslice': new TimesliceMetricTransformGenerator(spaceId, dataViewsService),
'sli.kql.custom': new KQLCustomTransformGenerator(spaceId, dataViewsService, isServerless),
'sli.metric.custom': new MetricCustomTransformGenerator(
spaceId,
dataViewsService,
isServerless
),
'sli.histogram.custom': new HistogramTransformGenerator(
spaceId,
dataViewsService,
isServerless
),
'sli.metric.timeslice': new TimesliceMetricTransformGenerator(
spaceId,
dataViewsService,
isServerless
),
};
}

View file

@ -81,7 +81,8 @@ describe('TransformManager', () => {
const generators: Record<IndicatorTypes, TransformGenerator> = {
'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator(
spaceId,
dataViewsService
dataViewsService,
false
),
};
const transformManager = new DefaultTransformManager(
@ -106,7 +107,8 @@ describe('TransformManager', () => {
const generators: Record<IndicatorTypes, TransformGenerator> = {
'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator(
spaceId,
dataViewsService
dataViewsService,
false
),
};
const transformManager = new DefaultTransformManager(
@ -129,7 +131,8 @@ describe('TransformManager', () => {
const generators: Record<IndicatorTypes, TransformGenerator> = {
'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator(
spaceId,
dataViewsService
dataViewsService,
false
),
};
const transformManager = new DefaultTransformManager(
@ -152,7 +155,8 @@ describe('TransformManager', () => {
const generators: Record<IndicatorTypes, TransformGenerator> = {
'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator(
spaceId,
dataViewsService
dataViewsService,
false
),
};
const transformManager = new DefaultTransformManager(
@ -175,7 +179,8 @@ describe('TransformManager', () => {
const generators: Record<IndicatorTypes, TransformGenerator> = {
'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator(
spaceId,
dataViewsService
dataViewsService,
false
),
};
const transformManager = new DefaultTransformManager(
@ -199,7 +204,8 @@ describe('TransformManager', () => {
const generators: Record<IndicatorTypes, TransformGenerator> = {
'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator(
spaceId,
dataViewsService
dataViewsService,
false
),
};
const transformManager = new DefaultTransformManager(