[8.12] feat(slo): new slo architecture (#172224) (#173157)

# Backport

This will backport the following commits from `main` to `8.12`:
- [feat(slo): new slo architecture
(#172224)](https://github.com/elastic/kibana/pull/172224)

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

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

<!--BACKPORT [{"author":{"name":"Kevin
Delemme","email":"kevin.delemme@elastic.co"},"sourceCommit":{"committedDate":"2023-12-12T13:45:12Z","message":"feat(slo):
new slo architecture
(#172224)","sha":"b51304f3f3c3e8510c44a235d0fc65c44fcce225","branchLabelMapping":{"^v8.13.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:breaking","backport:prev-minor","ci:build-serverless-image","Feature:SLO","v8.12.0","Team:obs-ux-management","v8.13.0"],"number":172224,"url":"https://github.com/elastic/kibana/pull/172224","mergeCommit":{"message":"feat(slo):
new slo architecture
(#172224)","sha":"b51304f3f3c3e8510c44a235d0fc65c44fcce225"}},"sourceBranch":"main","suggestedTargetBranches":["8.12"],"targetPullRequestStates":[{"branch":"8.12","label":"v8.12.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.13.0","labelRegex":"^v8.13.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/172224","number":172224,"mergeCommit":{"message":"feat(slo):
new slo architecture
(#172224)","sha":"b51304f3f3c3e8510c44a235d0fc65c44fcce225"}}]}]
BACKPORT-->

Co-authored-by: Kevin Delemme <kevin.delemme@elastic.co>
This commit is contained in:
Kibana Machine 2023-12-12 09:57:24 -05:00 committed by GitHub
parent 2d81cce6af
commit 8ef72a6256
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
104 changed files with 3702 additions and 5606 deletions

View file

@ -721,7 +721,8 @@
"indicator.params",
"indicator.type",
"name",
"tags"
"tags",
"version"
],
"threshold-explorer-view": [],
"observability-onboarding-state": [

View file

@ -2379,6 +2379,9 @@
},
"tags": {
"type": "keyword"
},
"version": {
"type": "long"
}
}
},

View file

@ -142,7 +142,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"siem-ui-timeline": "d3de8ff3617be8f2a799d66b1471b9be6124bf40",
"siem-ui-timeline-note": "0a32fb776907f596bedca292b8c646496ae9c57b",
"siem-ui-timeline-pinned-event": "082daa3ce647b33873f6abccf340bdfa32057c8d",
"slo": "2048ab6791df2e1ae0936f29c20765cb8d2fcfaa",
"slo": "9a9995e4572de1839651c43b5fc4dc8276bb5815",
"space": "8de4ec513e9bbc6b2f1d635161d850be7747d38e",
"spaces-usage-stats": "3abca98713c52af8b30300e386c7779b3025a20e",
"synthetics-monitor": "33ddc4b8979f378edf58bcc7ba13c5c5b572f42d",

View file

@ -8,3 +8,4 @@
export * from './src/schema';
export * from './src/rest_specs';
export * from './src/models/duration';
export * from './src/models/pagination';

View file

@ -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.
*/
export interface Paginated<T> {
total: number;
page: number;
perPage: number;
results: T[];
}
export interface Pagination {
page: number;
perPage: number;
}

View file

@ -6,6 +6,7 @@
*/
import * as t from 'io-ts';
import { toBooleanRt } from '@kbn/io-ts-utils';
import {
allOrAnyString,
apmTransactionDurationIndicatorSchema,
@ -109,6 +110,7 @@ const sloResponseSchema = t.intersection([
groupBy: allOrAnyString,
createdAt: dateType,
updatedAt: dateType,
version: t.number,
}),
t.partial({
instanceId: allOrAnyString,
@ -157,6 +159,12 @@ const manageSLOParamsSchema = t.type({
path: t.type({ id: sloIdSchema }),
});
const resetSLOParamsSchema = t.type({
path: t.type({ id: sloIdSchema }),
});
const resetSLOResponseSchema = sloResponseSchema;
const updateSLOResponseSchema = sloResponseSchema;
const findSLOResponseSchema = t.type({
@ -182,23 +190,21 @@ const fetchHistoricalSummaryResponseSchema = t.array(
})
);
/**
* The query params schema for /internal/observability/slo/_definitions
*
* @private
*/
const findSloDefinitionsParamsSchema = t.type({
query: t.type({
const findSloDefinitionsParamsSchema = t.partial({
query: t.partial({
search: t.string,
includeOutdatedOnly: toBooleanRt,
page: t.string,
perPage: t.string,
}),
});
/**
* The response schema for /internal/observability/slo/_definitions
*
* @private
*/
const findSloDefinitionsResponseSchema = t.array(sloResponseSchema);
const findSloDefinitionsResponseSchema = t.type({
page: t.number,
perPage: t.number,
total: t.number,
results: t.array(sloResponseSchema),
});
const getSLOBurnRatesResponseSchema = t.type({
burnRates: t.array(
@ -244,6 +250,9 @@ type GetSLOResponse = t.OutputOf<typeof getSLOResponseSchema>;
type ManageSLOParams = t.TypeOf<typeof manageSLOParamsSchema.props.path>;
type ResetSLOParams = t.TypeOf<typeof resetSLOParamsSchema.props.path>;
type ResetSLOResponse = t.OutputOf<typeof resetSLOResponseSchema>;
type UpdateSLOInput = t.OutputOf<typeof updateSLOParamsSchema.props.body>;
type UpdateSLOParams = t.TypeOf<typeof updateSLOParamsSchema.props.body>;
type UpdateSLOResponse = t.OutputOf<typeof updateSLOResponseSchema>;
@ -258,12 +267,8 @@ type FetchHistoricalSummaryParams = t.TypeOf<typeof fetchHistoricalSummaryParams
type FetchHistoricalSummaryResponse = t.OutputOf<typeof fetchHistoricalSummaryResponseSchema>;
type HistoricalSummaryResponse = t.OutputOf<typeof historicalSummarySchema>;
/**
* The response type for /internal/observability/slo/_definitions
*
* @private
*/
type FindSloDefinitionsResponse = t.OutputOf<typeof findSloDefinitionsResponseSchema>;
type FindSLODefinitionsParams = t.TypeOf<typeof findSloDefinitionsParamsSchema.props.query>;
type FindSLODefinitionsResponse = t.OutputOf<typeof findSloDefinitionsResponseSchema>;
type GetPreviewDataParams = t.TypeOf<typeof getPreviewDataParamsSchema.props.body>;
type GetPreviewDataResponse = t.OutputOf<typeof getPreviewDataResponseSchema>;
@ -300,6 +305,8 @@ export {
findSloDefinitionsParamsSchema,
findSloDefinitionsResponseSchema,
manageSLOParamsSchema,
resetSLOParamsSchema,
resetSLOResponseSchema,
sloResponseSchema,
sloWithSummaryResponseSchema,
updateSLOParamsSchema,
@ -325,8 +332,11 @@ export type {
FetchHistoricalSummaryParams,
FetchHistoricalSummaryResponse,
HistoricalSummaryResponse,
FindSloDefinitionsResponse,
FindSLODefinitionsParams,
FindSLODefinitionsResponse,
ManageSLOParams,
ResetSLOParams,
ResetSLOResponse,
SLOResponse,
SLOWithSummaryResponse,
UpdateSLOInput,

View file

@ -50,6 +50,7 @@ const sloSchema = t.type({
createdAt: dateType,
updatedAt: dateType,
groupBy: allOrAnyString,
version: t.number,
});
const sloWithSummarySchema = t.intersection([sloSchema, t.type({ summary: summarySchema })]);

View file

@ -11,7 +11,8 @@
"**/*.ts"
],
"kbn_references": [
"@kbn/std"
"@kbn/std",
"@kbn/io-ts-utils"
],
"exclude": [
"target/**/*",

View file

@ -15,11 +15,11 @@ export const journey = new Journey({
.step('Go to Transforms', async ({ page, kbnUrl, kibanaPage }) => {
await page.goto(kbnUrl.get(`app/management/data/transform`));
await kibanaPage.waitForHeader();
await page.waitForSelector(subj('transformButtonCreate'));
await page.waitForSelector(subj('transformCreateFirstButton'));
await page.waitForSelector(subj('globalLoadingIndicator-hidden'));
})
.step('Go to data view selection', async ({ page }) => {
const createButtons = page.locator(subj('transformButtonCreate'));
const createButtons = page.locator(subj('transformCreateFirstButton'));
await createButtons.first().click();
await page.waitForSelector(subj('savedObjectsFinderTable'));
})

View file

@ -5,8 +5,8 @@
* 2.0.
*/
export const SLO_RESOURCES_VERSION = 2;
export const SLO_SUMMARY_TRANSFORMS_VERSION = 3;
export const SLO_MODEL_VERSION = 2;
export const SLO_RESOURCES_VERSION = 3;
export const SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME = '.slo-observability.sli-mappings';
export const SLO_COMPONENT_TEMPLATE_SETTINGS_NAME = '.slo-observability.sli-settings';
@ -17,8 +17,7 @@ export const SLO_INDEX_TEMPLATE_PATTERN = `.slo-observability.sli-*`;
export const SLO_DESTINATION_INDEX_NAME = `.slo-observability.sli-v${SLO_RESOURCES_VERSION}`;
export const SLO_DESTINATION_INDEX_PATTERN = `.slo-observability.sli-v${SLO_RESOURCES_VERSION}*`;
export const SLO_INGEST_PIPELINE_NAME = `.slo-observability.sli.pipeline`;
// slo-observability.sli-v<version>.(YYYY-MM-DD)
export const SLO_INGEST_PIPELINE_NAME = `.slo-observability.sli.pipeline-v${SLO_RESOURCES_VERSION}`;
export const SLO_INGEST_PIPELINE_INDEX_NAME_PREFIX = `.slo-observability.sli-v${SLO_RESOURCES_VERSION}.`;
export const SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME = '.slo-observability.summary-mappings';
@ -26,12 +25,15 @@ export const SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME = '.slo-observability.
export const SLO_SUMMARY_INDEX_TEMPLATE_NAME = '.slo-observability.summary';
export const SLO_SUMMARY_INDEX_TEMPLATE_PATTERN = `.slo-observability.summary-*`;
export const SLO_SUMMARY_TRANSFORM_NAME_PREFIX = 'slo-summary-';
export const SLO_SUMMARY_DESTINATION_INDEX_NAME = `.slo-observability.summary-v${SLO_RESOURCES_VERSION}`; // store the temporary summary document generated by transform
export const SLO_SUMMARY_TEMP_INDEX_NAME = `.slo-observability.summary-v${SLO_RESOURCES_VERSION}.temp`; // store the temporary summary document
export const SLO_SUMMARY_DESTINATION_INDEX_PATTERN = `.slo-observability.summary-v${SLO_RESOURCES_VERSION}*`; // include temp and non-temp summary indices
export const SLO_SUMMARY_INGEST_PIPELINE_NAME = `.slo-observability.summary.pipeline`;
export const getSLOTransformId = (sloId: string, sloRevision: number) =>
`slo-${sloId}-${sloRevision}`;
export const getSLOSummaryTransformId = (sloId: string, sloRevision: number) =>
`slo-summary-${sloId}-${sloRevision}`;
export const getSLOSummaryPipelineId = (sloId: string, sloRevision: number) =>
`.slo-observability.summary.pipeline-${sloId}-${sloRevision}`;

View file

@ -143,7 +143,7 @@
{
"name": "page",
"in": "query",
"description": "The page number to return",
"description": "The page to use for pagination, must be greater or equal than 1",
"schema": {
"type": "integer",
"default": 1
@ -153,7 +153,7 @@
{
"name": "perPage",
"in": "query",
"description": "The number of SLOs to return per page",
"description": "Number of SLOs returned by page",
"schema": {
"type": "integer",
"default": 25,
@ -280,7 +280,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/slo_response"
"$ref": "#/components/schemas/slo_with_summary_response"
}
}
}
@ -361,7 +361,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/slo_response"
"$ref": "#/components/schemas/slo_definition_response"
}
}
}
@ -605,6 +605,79 @@
}
}
},
"/s/{spaceId}/api/observability/slos/{sloId}/_reset": {
"post": {
"summary": "Resets an SLO.",
"operationId": "resetSloOp",
"description": "You must have the `write` privileges for the **SLOs** feature in the **Observability** section of the Kibana feature privileges.\n",
"tags": [
"slo"
],
"parameters": [
{
"$ref": "#/components/parameters/kbn_xsrf"
},
{
"$ref": "#/components/parameters/space_id"
},
{
"$ref": "#/components/parameters/slo_id"
}
],
"responses": {
"204": {
"description": "Successful request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/slo_definition_response"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/400_response"
}
}
}
},
"401": {
"description": "Unauthorized response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/401_response"
}
}
}
},
"403": {
"description": "Unauthorized response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/403_response"
}
}
}
},
"404": {
"description": "Not found response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/404_response"
}
}
}
}
}
}
},
"/s/{spaceId}/internal/observability/slos/_historical_summary": {
"post": {
"summary": "Retrieves the historical summary for a list of SLOs",
@ -675,6 +748,104 @@
}
}
},
"/s/{spaceId}/internal/observability/slos/_definitions": {
"get": {
"summary": "Get the SLO definitions",
"operationId": "getDefinitionsOp",
"description": "You must have the `read` privileges for the **SLOs** feature in the **Observability** section of the Kibana feature privileges.\n",
"tags": [
"slo"
],
"parameters": [
{
"$ref": "#/components/parameters/kbn_xsrf"
},
{
"$ref": "#/components/parameters/space_id"
},
{
"name": "includeOutdatedOnly",
"in": "query",
"description": "Indicates if the API returns only outdated SLO or all SLO definitions",
"schema": {
"type": "boolean"
},
"example": true
},
{
"name": "search",
"in": "query",
"description": "Filters the SLOs by name",
"schema": {
"type": "string"
},
"example": "my service availability"
},
{
"name": "page",
"in": "query",
"description": "The page to use for pagination, must be greater or equal than 1",
"schema": {
"type": "number"
},
"example": 1
},
{
"name": "perPage",
"in": "query",
"description": "Number of SLOs returned by page",
"schema": {
"type": "integer",
"default": 100,
"maximum": 1000
},
"example": 100
}
],
"responses": {
"200": {
"description": "Successful request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/find_slo_definitions_response"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/400_response"
}
}
}
},
"401": {
"description": "Unauthorized response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/401_response"
}
}
}
},
"403": {
"description": "Unauthorized response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/403_response"
}
}
}
}
}
}
},
"/s/{spaceId}/api/observability/slos/_delete_instances": {
"post": {
"summary": "Batch delete rollup and summary data for the matching list of sloId and instanceId",
@ -1587,7 +1758,7 @@
}
}
},
"slo_response": {
"slo_with_summary_response": {
"title": "SLO response",
"type": "object",
"required": [
@ -1606,7 +1777,8 @@
"instanceId",
"tags",
"createdAt",
"updatedAt"
"updatedAt",
"version"
],
"properties": {
"id": {
@ -1708,6 +1880,11 @@
"description": "The last update date",
"type": "string",
"example": "2023-01-12T10:03:19.000Z"
},
"version": {
"description": "The internal SLO version",
"type": "number",
"example": 2
}
}
},
@ -1731,7 +1908,7 @@
"results": {
"type": "array",
"items": {
"$ref": "#/components/schemas/slo_response"
"$ref": "#/components/schemas/slo_with_summary_response"
}
}
}
@ -1994,6 +2171,126 @@
}
}
},
"slo_definition_response": {
"title": "SLO definition response",
"type": "object",
"required": [
"id",
"name",
"description",
"indicator",
"timeWindow",
"budgetingMethod",
"objective",
"settings",
"revision",
"enabled",
"groupBy",
"tags",
"createdAt",
"updatedAt",
"version"
],
"properties": {
"id": {
"description": "The identifier of the SLO.",
"type": "string",
"example": "8853df00-ae2e-11ed-90af-09bb6422b258"
},
"name": {
"description": "The name of the SLO.",
"type": "string",
"example": "My Service SLO"
},
"description": {
"description": "The description of the SLO.",
"type": "string",
"example": "My SLO description"
},
"indicator": {
"discriminator": {
"propertyName": "type",
"mapping": {
"sli.apm.transactionErrorRate": "#/components/schemas/indicator_properties_apm_availability",
"sli.kql.custom": "#/components/schemas/indicator_properties_custom_kql",
"sli.apm.transactionDuration": "#/components/schemas/indicator_properties_apm_latency",
"sli.metric.custom": "#/components/schemas/indicator_properties_custom_metric",
"sli.histogram.custom": "#/components/schemas/indicator_properties_histogram",
"sli.metric.timeslice": "#/components/schemas/indicator_properties_timeslice_metric"
}
},
"oneOf": [
{
"$ref": "#/components/schemas/indicator_properties_custom_kql"
},
{
"$ref": "#/components/schemas/indicator_properties_apm_availability"
},
{
"$ref": "#/components/schemas/indicator_properties_apm_latency"
},
{
"$ref": "#/components/schemas/indicator_properties_custom_metric"
},
{
"$ref": "#/components/schemas/indicator_properties_histogram"
},
{
"$ref": "#/components/schemas/indicator_properties_timeslice_metric"
}
]
},
"timeWindow": {
"$ref": "#/components/schemas/time_window"
},
"budgetingMethod": {
"$ref": "#/components/schemas/budgeting_method"
},
"objective": {
"$ref": "#/components/schemas/objective"
},
"settings": {
"$ref": "#/components/schemas/settings"
},
"revision": {
"description": "The SLO revision",
"type": "number",
"example": 2
},
"enabled": {
"description": "Indicate if the SLO is enabled",
"type": "boolean",
"example": true
},
"groupBy": {
"description": "optional group by field to use to generate an SLO per distinct value",
"type": "string",
"example": "some.field"
},
"tags": {
"description": "List of tags",
"type": "array",
"items": {
"type": "string"
}
},
"createdAt": {
"description": "The creation date",
"type": "string",
"example": "2023-01-12T10:03:19.000Z"
},
"updatedAt": {
"description": "The last update date",
"type": "string",
"example": "2023-01-12T10:03:19.000Z"
},
"version": {
"description": "The internal SLO version",
"type": "number",
"example": 2
}
}
},
"historical_summary_request": {
"title": "Historical summary request",
"type": "object",
@ -2037,6 +2334,31 @@
}
}
},
"find_slo_definitions_response": {
"title": "Find SLO definitions response",
"description": "A paginated response of SLO definitions matching the query.\n",
"type": "object",
"properties": {
"page": {
"type": "number",
"example": 2
},
"perPage": {
"type": "number",
"example": 100
},
"total": {
"type": "number",
"example": 123
},
"results": {
"type": "array",
"items": {
"$ref": "#/components/schemas/slo_definition_response"
}
}
}
},
"delete_slo_instances_request": {
"title": "Delete SLO instances request",
"description": "The delete SLO instances request takes a list of SLO id and instance id, then delete the rollup and summary data. This API can be used to remove the staled data of an instance SLO that no longer get updated.\n",

View file

@ -86,14 +86,14 @@ paths:
example: 'slo.name:latency* and slo.tags : "prod"'
- name: page
in: query
description: The page number to return
description: The page to use for pagination, must be greater or equal than 1
schema:
type: integer
default: 1
example: 1
- name: perPage
in: query
description: The number of SLOs to return per page
description: Number of SLOs returned by page
schema:
type: integer
default: 25
@ -176,7 +176,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/slo_response'
$ref: '#/components/schemas/slo_with_summary_response'
'400':
description: Bad request
content:
@ -224,7 +224,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/slo_response'
$ref: '#/components/schemas/slo_definition_response'
'400':
description: Bad request
content:
@ -365,6 +365,49 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/404_response'
/s/{spaceId}/api/observability/slos/{sloId}/_reset:
post:
summary: Resets an SLO.
operationId: resetSloOp
description: |
You must have the `write` privileges for the **SLOs** feature in the **Observability** section of the Kibana feature privileges.
tags:
- slo
parameters:
- $ref: '#/components/parameters/kbn_xsrf'
- $ref: '#/components/parameters/space_id'
- $ref: '#/components/parameters/slo_id'
responses:
'204':
description: Successful request
content:
application/json:
schema:
$ref: '#/components/schemas/slo_definition_response'
'400':
description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/400_response'
'401':
description: Unauthorized response
content:
application/json:
schema:
$ref: '#/components/schemas/401_response'
'403':
description: Unauthorized response
content:
application/json:
schema:
$ref: '#/components/schemas/403_response'
'404':
description: Not found response
content:
application/json:
schema:
$ref: '#/components/schemas/404_response'
/s/{spaceId}/internal/observability/slos/_historical_summary:
post:
summary: Retrieves the historical summary for a list of SLOs
@ -407,6 +450,68 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/403_response'
/s/{spaceId}/internal/observability/slos/_definitions:
get:
summary: Get the SLO definitions
operationId: getDefinitionsOp
description: |
You must have the `read` privileges for the **SLOs** feature in the **Observability** section of the Kibana feature privileges.
tags:
- slo
parameters:
- $ref: '#/components/parameters/kbn_xsrf'
- $ref: '#/components/parameters/space_id'
- name: includeOutdatedOnly
in: query
description: Indicates if the API returns only outdated SLO or all SLO definitions
schema:
type: boolean
example: true
- name: search
in: query
description: Filters the SLOs by name
schema:
type: string
example: my service availability
- name: page
in: query
description: The page to use for pagination, must be greater or equal than 1
schema:
type: number
example: 1
- name: perPage
in: query
description: Number of SLOs returned by page
schema:
type: integer
default: 100
maximum: 1000
example: 100
responses:
'200':
description: Successful request
content:
application/json:
schema:
$ref: '#/components/schemas/find_slo_definitions_response'
'400':
description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/400_response'
'401':
description: Unauthorized response
content:
application/json:
schema:
$ref: '#/components/schemas/401_response'
'403':
description: Unauthorized response
content:
application/json:
schema:
$ref: '#/components/schemas/403_response'
/s/{spaceId}/api/observability/slos/_delete_instances:
post:
summary: Batch delete rollup and summary data for the matching list of sloId and instanceId
@ -1103,7 +1208,7 @@ components:
example: 0.9836
errorBudget:
$ref: '#/components/schemas/error_budget'
slo_response:
slo_with_summary_response:
title: SLO response
type: object
required:
@ -1123,6 +1228,7 @@ components:
- tags
- createdAt
- updatedAt
- version
properties:
id:
description: The identifier of the SLO.
@ -1192,6 +1298,10 @@ components:
description: The last update date
type: string
example: '2023-01-12T10:03:19.000Z'
version:
description: The internal SLO version
type: number
example: 2
find_slo_response:
title: Find SLO response
description: |
@ -1210,7 +1320,7 @@ components:
results:
type: array
items:
$ref: '#/components/schemas/slo_response'
$ref: '#/components/schemas/slo_with_summary_response'
400_response:
title: Bad request
type: object
@ -1386,6 +1496,92 @@ components:
type: array
items:
type: string
slo_definition_response:
title: SLO definition response
type: object
required:
- id
- name
- description
- indicator
- timeWindow
- budgetingMethod
- objective
- settings
- revision
- enabled
- groupBy
- tags
- createdAt
- updatedAt
- version
properties:
id:
description: The identifier of the SLO.
type: string
example: 8853df00-ae2e-11ed-90af-09bb6422b258
name:
description: The name of the SLO.
type: string
example: My Service SLO
description:
description: The description of the SLO.
type: string
example: My SLO description
indicator:
discriminator:
propertyName: type
mapping:
sli.apm.transactionErrorRate: '#/components/schemas/indicator_properties_apm_availability'
sli.kql.custom: '#/components/schemas/indicator_properties_custom_kql'
sli.apm.transactionDuration: '#/components/schemas/indicator_properties_apm_latency'
sli.metric.custom: '#/components/schemas/indicator_properties_custom_metric'
sli.histogram.custom: '#/components/schemas/indicator_properties_histogram'
sli.metric.timeslice: '#/components/schemas/indicator_properties_timeslice_metric'
oneOf:
- $ref: '#/components/schemas/indicator_properties_custom_kql'
- $ref: '#/components/schemas/indicator_properties_apm_availability'
- $ref: '#/components/schemas/indicator_properties_apm_latency'
- $ref: '#/components/schemas/indicator_properties_custom_metric'
- $ref: '#/components/schemas/indicator_properties_histogram'
- $ref: '#/components/schemas/indicator_properties_timeslice_metric'
timeWindow:
$ref: '#/components/schemas/time_window'
budgetingMethod:
$ref: '#/components/schemas/budgeting_method'
objective:
$ref: '#/components/schemas/objective'
settings:
$ref: '#/components/schemas/settings'
revision:
description: The SLO revision
type: number
example: 2
enabled:
description: Indicate if the SLO is enabled
type: boolean
example: true
groupBy:
description: optional group by field to use to generate an SLO per distinct value
type: string
example: some.field
tags:
description: List of tags
type: array
items:
type: string
createdAt:
description: The creation date
type: string
example: '2023-01-12T10:03:19.000Z'
updatedAt:
description: The last update date
type: string
example: '2023-01-12T10:03:19.000Z'
version:
description: The internal SLO version
type: number
example: 2
historical_summary_request:
title: Historical summary request
type: object
@ -1416,6 +1612,25 @@ components:
example: 0.9836
errorBudget:
$ref: '#/components/schemas/error_budget'
find_slo_definitions_response:
title: Find SLO definitions response
description: |
A paginated response of SLO definitions matching the query.
type: object
properties:
page:
type: number
example: 2
perPage:
type: number
example: 100
total:
type: number
example: 123
results:
type: array
items:
$ref: '#/components/schemas/slo_definition_response'
delete_slo_instances_request:
title: Delete SLO instances request
description: |

View file

@ -0,0 +1,18 @@
title: Find SLO definitions response
description: >
A paginated response of SLO definitions matching the query.
type: object
properties:
page:
type: number
example: 2
perPage:
type: number
example: 100
total:
type: number
example: 123
results:
type: array
items:
$ref: 'slo_definition_response.yaml'

View file

@ -15,4 +15,4 @@ properties:
results:
type: array
items:
$ref: 'slo_response.yaml'
$ref: 'slo_with_summary_response.yaml'

View file

@ -0,0 +1,85 @@
title: SLO definition response
type: object
required:
- id
- name
- description
- indicator
- timeWindow
- budgetingMethod
- objective
- settings
- revision
- enabled
- groupBy
- tags
- createdAt
- updatedAt
- version
properties:
id:
description: The identifier of the SLO.
type: string
example: 8853df00-ae2e-11ed-90af-09bb6422b258
name:
description: The name of the SLO.
type: string
example: My Service SLO
description:
description: The description of the SLO.
type: string
example: My SLO description
indicator:
discriminator:
propertyName: type
mapping:
sli.apm.transactionErrorRate: './indicator_properties_apm_availability.yaml'
sli.kql.custom: './indicator_properties_custom_kql.yaml'
sli.apm.transactionDuration: './indicator_properties_apm_latency.yaml'
sli.metric.custom: './indicator_properties_custom_metric.yaml'
sli.histogram.custom: './indicator_properties_histogram.yaml'
sli.metric.timeslice: './indicator_properties_timeslice_metric.yaml'
oneOf:
- $ref: "indicator_properties_custom_kql.yaml"
- $ref: "indicator_properties_apm_availability.yaml"
- $ref: "indicator_properties_apm_latency.yaml"
- $ref: "indicator_properties_custom_metric.yaml"
- $ref: "indicator_properties_histogram.yaml"
- $ref: "indicator_properties_timeslice_metric.yaml"
timeWindow:
$ref: "time_window.yaml"
budgetingMethod:
$ref: "budgeting_method.yaml"
objective:
$ref: "objective.yaml"
settings:
$ref: "settings.yaml"
revision:
description: The SLO revision
type: number
example: 2
enabled:
description: Indicate if the SLO is enabled
type: boolean
example: true
groupBy:
description: optional group by field to use to generate an SLO per distinct value
type: string
example: "some.field"
tags:
description: List of tags
type: array
items:
type: string
createdAt:
description: The creation date
type: string
example: "2023-01-12T10:03:19.000Z"
updatedAt:
description: The last update date
type: string
example: "2023-01-12T10:03:19.000Z"
version:
description: The internal SLO version
type: number
example: 2

View file

@ -17,6 +17,7 @@ required:
- tags
- createdAt
- updatedAt
- version
properties:
id:
description: The identifier of the SLO.
@ -86,3 +87,7 @@ properties:
description: The last update date
type: string
example: "2023-01-12T10:03:19.000Z"
version:
description: The internal SLO version
type: number
example: 2

View file

@ -20,11 +20,15 @@ paths:
"/s/{spaceId}/api/observability/slos/{sloId}":
$ref: "paths/s@{spaceid}@api@slos@{sloid}.yaml"
"/s/{spaceId}/api/observability/slos/{sloId}/enable":
$ref: "paths/s@{spaceid}@api@slos@{sloid}@{enable}.yaml"
$ref: "paths/s@{spaceid}@api@slos@{sloid}@enable.yaml"
"/s/{spaceId}/api/observability/slos/{sloId}/disable":
$ref: "paths/s@{spaceid}@api@slos@{sloid}@{disable}.yaml"
$ref: "paths/s@{spaceid}@api@slos@{sloid}@disable.yaml"
"/s/{spaceId}/api/observability/slos/{sloId}/_reset":
$ref: "paths/s@{spaceid}@api@slos@{sloid}@_reset.yaml"
"/s/{spaceId}/internal/observability/slos/_historical_summary":
$ref: "paths/s@{spaceid}@api@slos@_historical_summary.yaml"
"/s/{spaceId}/internal/observability/slos/_definitions":
$ref: "paths/s@{spaceid}@api@slos@_definitions.yaml"
"/s/{spaceId}/api/observability/slos/_delete_instances":
$ref: "paths/s@{spaceid}@api@slos@_delete_instances.yaml"
components:

View file

@ -68,14 +68,14 @@ get:
example: 'slo.name:latency* and slo.tags : "prod"'
- name: page
in: query
description: The page number to return
description: The page to use for pagination, must be greater or equal than 1
schema:
type: integer
default: 1
example: 1
- name: perPage
in: query
description: The number of SLOs to return per page
description: Number of SLOs returned by page
schema:
type: integer
default: 25

View file

@ -0,0 +1,62 @@
get:
summary: Get the SLO definitions
operationId: getDefinitionsOp
description: >
You must have the `read` privileges for the **SLOs** feature in the
**Observability** section of the Kibana feature privileges.
tags:
- slo
parameters:
- $ref: ../components/headers/kbn_xsrf.yaml
- $ref: ../components/parameters/space_id.yaml
- name: includeOutdatedOnly
in: query
description: Indicates if the API returns only outdated SLO or all SLO definitions
schema:
type: boolean
example: true
- name: search
in: query
description: Filters the SLOs by name
schema:
type: string
example: 'my service availability'
- name: page
in: query
description: The page to use for pagination, must be greater or equal than 1
schema:
type: number
example: 1
- name: perPage
in: query
description: Number of SLOs returned by page
schema:
type: integer
default: 100
maximum: 1000
example: 100
responses:
'200':
description: Successful request
content:
application/json:
schema:
$ref: '../components/schemas/find_slo_definitions_response.yaml'
'400':
description: Bad request
content:
application/json:
schema:
$ref: '../components/schemas/400_response.yaml'
'401':
description: Unauthorized response
content:
application/json:
schema:
$ref: '../components/schemas/401_response.yaml'
'403':
description: Unauthorized response
content:
application/json:
schema:
$ref: '../components/schemas/403_response.yaml'

View file

@ -22,7 +22,7 @@ get:
content:
application/json:
schema:
$ref: '../components/schemas/slo_response.yaml'
$ref: '../components/schemas/slo_with_summary_response.yaml'
'400':
description: Bad request
content:
@ -72,7 +72,7 @@ put:
content:
application/json:
schema:
$ref: '../components/schemas/slo_response.yaml'
$ref: '../components/schemas/slo_definition_response.yaml'
'400':
description: Bad request
content:

View file

@ -0,0 +1,43 @@
post:
summary: Resets an SLO.
operationId: resetSloOp
description: >
You must have the `write` privileges for the **SLOs** feature in the
**Observability** section of the Kibana feature privileges.
tags:
- slo
parameters:
- $ref: ../components/headers/kbn_xsrf.yaml
- $ref: ../components/parameters/space_id.yaml
- $ref: ../components/parameters/slo_id.yaml
responses:
'204':
description: Successful request
content:
application/json:
schema:
$ref: '../components/schemas/slo_definition_response.yaml'
'400':
description: Bad request
content:
application/json:
schema:
$ref: '../components/schemas/400_response.yaml'
'401':
description: Unauthorized response
content:
application/json:
schema:
$ref: '../components/schemas/401_response.yaml'
'403':
description: Unauthorized response
content:
application/json:
schema:
$ref: '../components/schemas/403_response.yaml'
'404':
description: Not found response
content:
application/json:
schema:
$ref: '../components/schemas/404_response.yaml'

View file

@ -22,7 +22,7 @@ function SloSelector({ initialSlo, onSelected, errors }: Props) {
const [options, setOptions] = useState<Array<EuiComboBoxOptionOption<string>>>([]);
const [selectedOptions, setSelectedOptions] = useState<Array<EuiComboBoxOptionOption<string>>>();
const [searchValue, setSearchValue] = useState<string>('');
const { isLoading, data: sloList } = useFetchSloDefinitions({ name: searchValue });
const { isLoading, data } = useFetchSloDefinitions({ name: searchValue });
const hasError = errors !== undefined && errors.length > 0;
useEffect(() => {
@ -30,17 +30,17 @@ function SloSelector({ initialSlo, onSelected, errors }: Props) {
}, [initialSlo]);
useEffect(() => {
const isLoadedWithData = !isLoading && sloList !== undefined;
const isLoadedWithData = !isLoading && !!data?.results;
const opts: Array<EuiComboBoxOptionOption<string>> = isLoadedWithData
? sloList.map((slo) => ({ value: slo.id, label: slo.name }))
? data?.results?.map((slo) => ({ value: slo.id, label: slo.name }))
: [];
setOptions(opts);
}, [isLoading, sloList]);
}, [isLoading, data]);
const onChange = (opts: Array<EuiComboBoxOptionOption<string>>) => {
setSelectedOptions(opts);
const selectedSlo =
opts.length === 1 ? sloList?.find((slo) => slo.id === opts[0].value) : undefined;
opts.length === 1 ? data?.results?.find((slo) => slo.id === opts[0].value) : undefined;
onSelected(selectedSlo);
};

View file

@ -5,9 +5,9 @@
* 2.0.
*/
import { ALL_VALUE, FindSLOResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
import { cloneDeep } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { ALL_VALUE, FindSLOResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
import {
buildDegradingSummary,
buildHealthySummary,
@ -16,8 +16,8 @@ import {
buildTimeslicesObjective,
buildViolatedSummary,
} from './common';
import { buildCalendarAlignedTimeWindow, buildRollingTimeWindow } from './time_window';
import { buildApmAvailabilityIndicator, buildCustomKqlIndicator } from './indicator';
import { buildCalendarAlignedTimeWindow, buildRollingTimeWindow } from './time_window';
export const emptySloList: FindSLOResponse = {
results: [],
@ -68,6 +68,7 @@ const baseSlo: Omit<SLOWithSummaryResponse, 'id'> = {
enabled: true,
createdAt: now,
updatedAt: now,
version: 2,
};
export const sloList: FindSLOResponse = {

View file

@ -5,24 +5,16 @@
* 2.0.
*/
import { FindSloDefinitionsResponse, SLOResponse } from '@kbn/slo-schema';
import {
QueryObserverResult,
RefetchOptions,
RefetchQueryFilters,
useQuery,
} from '@tanstack/react-query';
import { FindSLODefinitionsResponse } from '@kbn/slo-schema';
import { useQuery } from '@tanstack/react-query';
import { useKibana } from '../../utils/kibana_react';
import { sloKeys } from './query_key_factory';
export interface UseFetchSloDefinitionsResponse {
data: FindSLODefinitionsResponse | undefined;
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
data: SLOResponse[] | undefined;
refetch: <TPageData>(
options?: (RefetchOptions & RefetchQueryFilters<TPageData>) | undefined
) => Promise<QueryObserverResult<SLOResponse[], unknown>>;
}
interface Params {
@ -33,18 +25,13 @@ export function useFetchSloDefinitions({ name = '' }: Params): UseFetchSloDefini
const { http } = useKibana().services;
const search = name.endsWith('*') ? name : `${name}*`;
const { isLoading, isError, isSuccess, data, refetch } = useQuery({
const { isLoading, isError, isSuccess, data } = useQuery({
queryKey: sloKeys.definitions(search),
queryFn: async ({ signal }) => {
try {
const response = await http.get<FindSloDefinitionsResponse>(
'/internal/observability/slos/_definitions',
{
query: {
search,
},
signal,
}
const response = await http.get<FindSLODefinitionsResponse>(
'/api/observability/slos/_definitions',
{ query: { search }, signal }
);
return response;
@ -56,5 +43,5 @@ export function useFetchSloDefinitions({ name = '' }: Params): UseFetchSloDefini
refetchOnWindowFocus: false,
});
return { isLoading, isError, isSuccess, data, refetch };
return { isLoading, isError, isSuccess, data };
}

View file

@ -20,7 +20,7 @@ describe('SloEditLocator', () => {
it('should return correct url when slo is provided', async () => {
const location = await locator.getLocation(buildSlo({ id: 'foo' }));
expect(location.path).toEqual(
"/slos/edit/foo?_a=(budgetingMethod:occurrences,createdAt:'2022-12-29T10:11:12.000Z',description:'some%20description%20useful',enabled:!t,groupBy:'*',id:foo,indicator:(params:(filter:'baz:%20foo%20and%20bar%20%3E%202',good:'http_status:%202xx',index:some-index,timestampField:custom_timestamp,total:'a%20query'),type:sli.kql.custom),instanceId:'*',name:'super%20important%20level%20service',objective:(target:0.98),revision:1,settings:(frequency:'1m',syncDelay:'1m'),summary:(errorBudget:(consumed:0.064,initial:0.02,isEstimated:!f,remaining:0.936),sliValue:0.99872,status:HEALTHY),tags:!(k8s,production,critical),timeWindow:(duration:'30d',type:rolling),updatedAt:'2022-12-29T10:11:12.000Z')"
"/slos/edit/foo?_a=(budgetingMethod:occurrences,createdAt:'2022-12-29T10:11:12.000Z',description:'some%20description%20useful',enabled:!t,groupBy:'*',id:foo,indicator:(params:(filter:'baz:%20foo%20and%20bar%20%3E%202',good:'http_status:%202xx',index:some-index,timestampField:custom_timestamp,total:'a%20query'),type:sli.kql.custom),instanceId:'*',name:'super%20important%20level%20service',objective:(target:0.98),revision:1,settings:(frequency:'1m',syncDelay:'1m'),summary:(errorBudget:(consumed:0.064,initial:0.02,isEstimated:!f,remaining:0.936),sliValue:0.99872,status:HEALTHY),tags:!(k8s,production,critical),timeWindow:(duration:'30d',type:rolling),updatedAt:'2022-12-29T10:11:12.000Z',version:2)"
);
});
});

View file

@ -257,6 +257,7 @@ describe('SLO Details Page', () => {
settings,
updatedAt,
instanceId,
version,
...newSlo
} = slo;

View file

@ -12,6 +12,14 @@ export const getSLOMappingsTemplate = (name: string) => ({
template: {
mappings: {
properties: {
event: {
properties: {
ingested: {
type: 'date',
format: 'strict_date_optional_time',
},
},
},
'@timestamp': {
type: 'date',
format: 'date_optional_time||epoch_millis',
@ -21,11 +29,9 @@ export const getSLOMappingsTemplate = (name: string) => ({
properties: {
name: {
type: 'keyword',
ignore_above: 256,
},
environment: {
type: 'keyword',
ignore_above: 256,
},
},
},
@ -33,11 +39,9 @@ export const getSLOMappingsTemplate = (name: string) => ({
properties: {
name: {
type: 'keyword',
ignore_above: 256,
},
type: {
type: 'keyword',
ignore_above: 256,
},
},
},
@ -50,56 +54,8 @@ export const getSLOMappingsTemplate = (name: string) => ({
revision: {
type: 'long',
},
groupBy: {
type: 'keyword',
ignore_above: 256,
},
instanceId: {
type: 'keyword',
ignore_above: 256,
},
name: {
type: 'keyword',
ignore_above: 256,
},
description: {
type: 'keyword',
ignore_above: 256,
},
tags: {
type: 'keyword',
ignore_above: 256,
},
indicator: {
properties: {
type: {
type: 'keyword',
ignore_above: 256,
},
},
},
objective: {
properties: {
target: {
type: 'double',
},
sliceDurationInSeconds: {
type: 'long',
},
},
},
budgetingMethod: {
type: 'keyword',
},
timeWindow: {
properties: {
duration: {
type: 'keyword',
},
type: {
type: 'keyword',
},
},
},
numerator: {
type: 'long',
@ -110,6 +66,9 @@ export const getSLOMappingsTemplate = (name: string) => ({
isGoodSlice: {
type: 'byte',
},
groupings: {
type: 'flattened',
},
},
},
},

View file

@ -17,11 +17,9 @@ export const getSLOSummaryMappingsTemplate = (name: string) => ({
properties: {
name: {
type: 'keyword',
ignore_above: 256,
},
environment: {
type: 'keyword',
ignore_above: 256,
},
},
},
@ -29,11 +27,9 @@ export const getSLOSummaryMappingsTemplate = (name: string) => ({
properties: {
name: {
type: 'keyword',
ignore_above: 256,
},
type: {
type: 'keyword',
ignore_above: 256,
},
},
},
@ -48,29 +44,49 @@ export const getSLOSummaryMappingsTemplate = (name: string) => ({
},
groupBy: {
type: 'keyword',
ignore_above: 256,
},
groupings: {
type: 'flattened',
},
instanceId: {
type: 'keyword',
ignore_above: 256,
fields: {
text: {
type: 'text',
},
},
},
name: {
type: 'keyword',
ignore_above: 256,
type: 'text',
fields: {
keyword: {
type: 'keyword',
},
},
},
description: {
type: 'keyword',
ignore_above: 256,
type: 'text',
},
tags: {
type: 'keyword',
ignore_above: 256,
},
indicator: {
properties: {
type: {
type: 'keyword',
ignore_above: 256,
},
},
},
objective: {
properties: {
target: {
type: 'double',
},
timesliceTarget: {
type: 'double',
},
timesliceWindow: {
type: 'keyword',
},
},
},
@ -115,11 +131,21 @@ export const getSLOSummaryMappingsTemplate = (name: string) => ({
},
status: {
type: 'keyword',
ignore_above: 32,
},
isTempDoc: {
type: 'boolean',
},
latestSliTimestamp: {
type: 'date',
format: 'date_optional_time||epoch_millis',
},
summaryUpdatedAt: {
type: 'date',
format: 'date_optional_time||epoch_millis',
},
spaceId: {
type: 'keyword',
},
},
},
},

View file

@ -9,18 +9,25 @@ import { SLO_RESOURCES_VERSION } from '../../../common/slo/constants';
export const getSLOPipelineTemplate = (id: string, indexNamePrefix: string) => ({
id,
description: 'Monthly date-time index naming for SLO data',
description: 'Ingest pipeline for SLO rollup data',
processors: [
{
set: {
field: 'event.ingested',
value: '{{{_ingest.timestamp}}}',
},
},
{
date_index_name: {
field: '@timestamp',
index_name_prefix: indexNamePrefix,
date_rounding: 'M',
date_formats: ['UNIX_MS', 'ISO8601', "yyyy-MM-dd'T'HH:mm:ss.SSSXX"],
},
},
],
_meta: {
description: 'SLO ingest pipeline',
description: 'Ingest pipeline for SLO rollup data',
version: SLO_RESOURCES_VERSION,
managed: true,
managed_by: 'observability',

View file

@ -5,56 +5,167 @@
* 2.0.
*/
import { SLO_RESOURCES_VERSION } from '../../../common/slo/constants';
import { timeslicesBudgetingMethodSchema } from '@kbn/slo-schema';
import { getSLOSummaryPipelineId, SLO_RESOURCES_VERSION } from '../../../common/slo/constants';
import { SLO } from '../../domain/models';
export const getSLOSummaryPipelineTemplate = (id: string) => ({
id,
description: 'SLO summary ingest pipeline',
processors: [
{
split: {
description: 'Split comma separated list of tags into an array',
field: 'slo.tags',
separator: ',',
export const getSLOSummaryPipelineTemplate = (slo: SLO, spaceId: string) => {
const errorBudgetEstimated =
slo.budgetingMethod === 'occurrences' && slo.timeWindow.type === 'calendarAligned';
const optionalObjectiveTimesliceProcessors = timeslicesBudgetingMethodSchema.is(
slo.budgetingMethod
)
? [
{
set: {
description: 'Set objective.timesliceTarget field',
field: 'slo.objective.timesliceTarget',
value: slo.objective.timesliceTarget,
},
},
{
set: {
description: 'Set objective.timesliceWindow field',
field: 'slo.objective.timesliceWindow',
value: slo.objective.timesliceWindow!.format(),
},
},
]
: [];
return {
id: getSLOSummaryPipelineId(slo.id, slo.revision),
description: `Ingest pipeline for SLO summary data [id: ${slo.id}, revision: ${slo.revision}]`,
processors: [
{
set: {
description: 'Set errorBudgetEstimated field',
field: 'errorBudgetEstimated',
value: errorBudgetEstimated,
},
},
},
{
set: {
description: "if 'statusCode == 0', set status to NO_DATA",
if: 'ctx.statusCode == 0',
field: 'status',
value: 'NO_DATA',
{
set: {
description: 'Set isTempDoc field',
field: 'isTempDoc',
value: false,
},
},
},
{
set: {
description: "if 'statusCode == 1', set statusLabel to VIOLATED",
if: 'ctx.statusCode == 1',
field: 'status',
value: 'VIOLATED',
{
set: {
description: 'Set groupBy field',
field: 'slo.groupBy',
value: slo.groupBy,
},
},
},
{
set: {
description: "if 'statusCode == 2', set status to DEGRADING",
if: 'ctx.statusCode == 2',
field: 'status',
value: 'DEGRADING',
{
set: {
description: 'Set name field',
field: 'slo.name',
value: slo.name,
},
},
},
{
set: {
description: "if 'statusCode == 4', set status to HEALTHY",
if: 'ctx.statusCode == 4',
field: 'status',
value: 'HEALTHY',
{
set: {
description: 'Set description field',
field: 'slo.description',
value: slo.description,
},
},
{
set: {
description: 'Set tags field',
field: 'slo.tags',
value: slo.tags,
},
},
{
set: {
description: 'Set indicator.type field',
field: 'slo.indicator.type',
value: slo.indicator.type,
},
},
{
set: {
description: 'Set budgetingMethod field',
field: 'slo.budgetingMethod',
value: slo.budgetingMethod,
},
},
{
set: {
description: 'Set timeWindow.duration field',
field: 'slo.timeWindow.duration',
value: slo.timeWindow.duration.format(),
},
},
{
set: {
description: 'Set timeWindow.type field',
field: 'slo.timeWindow.type',
value: slo.timeWindow.type,
},
},
{
set: {
description: 'Set objective.target field',
field: 'slo.objective.target',
value: slo.objective.target,
},
},
...optionalObjectiveTimesliceProcessors,
{
set: {
description: "if 'statusCode == 0', set status to NO_DATA",
if: 'ctx.statusCode == 0',
field: 'status',
value: 'NO_DATA',
},
},
{
set: {
description: "if 'statusCode == 1', set statusLabel to VIOLATED",
if: 'ctx.statusCode == 1',
field: 'status',
value: 'VIOLATED',
},
},
{
set: {
description: "if 'statusCode == 2', set status to DEGRADING",
if: 'ctx.statusCode == 2',
field: 'status',
value: 'DEGRADING',
},
},
{
set: {
description: "if 'statusCode == 4', set status to HEALTHY",
if: 'ctx.statusCode == 4',
field: 'status',
value: 'HEALTHY',
},
},
{
set: {
field: 'summaryUpdatedAt',
value: '{{{_ingest.timestamp}}}',
},
},
{
set: {
field: 'spaceId',
value: spaceId,
},
},
],
_meta: {
description: `Ingest pipeline for SLO summary data [id: ${slo.id}, revision: ${slo.revision}]`,
version: SLO_RESOURCES_VERSION,
managed: true,
managed_by: 'observability',
},
],
_meta: {
description: 'SLO summary ingest pipeline',
version: SLO_RESOURCES_VERSION,
managed: true,
managed_by: 'observability',
},
});
};
};

View file

@ -18,21 +18,21 @@ import {
Plugin,
PluginInitializerContext,
} from '@kbn/core/server';
import { LOG_EXPLORER_LOCATOR_ID, LogExplorerLocatorParams } from '@kbn/deeplinks-observability';
import { LogExplorerLocatorParams, LOG_EXPLORER_LOCATOR_ID } from '@kbn/deeplinks-observability';
import { PluginSetupContract as FeaturesSetup } from '@kbn/features-plugin/server';
import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects';
import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server';
import { i18n } from '@kbn/i18n';
import { RuleRegistryPluginSetupContract } from '@kbn/rule-registry-plugin/server';
import { SharePluginSetup } from '@kbn/share-plugin/server';
import { SpacesPluginSetup } from '@kbn/spaces-plugin/server';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import {
ApmRuleType,
ES_QUERY_ID,
METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
} from '@kbn/rule-data-utils';
import { RuleRegistryPluginSetupContract } from '@kbn/rule-registry-plugin/server';
import { SharePluginSetup } from '@kbn/share-plugin/server';
import { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import { ObservabilityConfig } from '.';
import { casesFeatureId, observabilityFeatureId, sloFeatureId } from '../common';
import { SLO_BURN_RATE_RULE_TYPE_ID } from '../common/constants';
@ -52,11 +52,7 @@ import { getObservabilityServerRouteRepository } from './routes/get_global_obser
import { registerRoutes } from './routes/register_routes';
import { slo, SO_SLO_TYPE } from './saved_objects';
import { threshold } from './saved_objects/threshold';
import {
DefaultResourceInstaller,
DefaultSLOInstaller,
DefaultSummaryTransformInstaller,
} from './services/slo';
import { DefaultResourceInstaller, DefaultSLOInstaller } from './services/slo';
import { uiSettings } from './ui_settings';
@ -75,6 +71,7 @@ interface PluginSetup {
interface PluginStart {
alerting: PluginStartContract;
spaces?: SpacesPluginStart;
}
const sloRuleTypes = [SLO_BURN_RATE_RULE_TYPE_ID];
@ -350,6 +347,7 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
...plugins,
core,
},
spaces: pluginStart.spaces,
ruleDataService,
getRulesClientWithRequest: pluginStart.alerting.getRulesClientWithRequest,
},
@ -360,15 +358,7 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
const esInternalClient = coreStart.elasticsearch.client.asInternalUser;
const sloResourceInstaller = new DefaultResourceInstaller(esInternalClient, this.logger);
const sloSummaryInstaller = new DefaultSummaryTransformInstaller(
esInternalClient,
this.logger
);
const sloInstaller = new DefaultSLOInstaller(
sloResourceInstaller,
sloSummaryInstaller,
this.logger
);
const sloInstaller = new DefaultSLOInstaller(sloResourceInstaller, this.logger);
sloInstaller.install();
});

View file

@ -14,6 +14,7 @@ import {
parseEndpoint,
routeValidationObject,
} from '@kbn/server-route-repository';
import { SpacesPluginStart } from '@kbn/spaces-plugin/server';
import axios from 'axios';
import * as t from 'io-ts';
import { ObservabilityConfig } from '..';
@ -33,6 +34,7 @@ export interface RegisterRoutesDependencies {
pluginsSetup: {
core: CoreSetup;
};
spaces?: SpacesPluginStart;
ruleDataService: RuleDataPluginService;
getRulesClientWithRequest: (request: KibanaRequest) => RulesClientApi;
}

View file

@ -19,12 +19,14 @@ import {
getSLOInstancesParamsSchema,
getSLOParamsSchema,
manageSLOParamsSchema,
resetSLOParamsSchema,
updateSLOParamsSchema,
} from '@kbn/slo-schema';
import type { IndicatorTypes } from '../../domain/models';
import {
CreateSLO,
DefaultSummaryClient,
DefaultSummaryTransformManager,
DefaultTransformManager,
DeleteSLO,
DeleteSLOInstances,
@ -41,15 +43,17 @@ import { GetPreviewData } from '../../services/slo/get_preview_data';
import { GetSLOInstances } from '../../services/slo/get_slo_instances';
import { DefaultHistoricalSummaryClient } from '../../services/slo/historical_summary_client';
import { ManageSLO } from '../../services/slo/manage_slo';
import { ResetSLO } from '../../services/slo/reset_slo';
import { DefaultSummarySearchClient } from '../../services/slo/summary_search_client';
import { DefaultSummaryTransformGenerator } from '../../services/slo/summary_transform_generator/summary_transform_generator';
import {
ApmTransactionDurationTransformGenerator,
ApmTransactionErrorRateTransformGenerator,
HistogramTransformGenerator,
KQLCustomTransformGenerator,
MetricCustomTransformGenerator,
TransformGenerator,
TimesliceMetricTransformGenerator,
TransformGenerator,
} from '../../services/slo/transform_generators';
import type { ObservabilityRequestHandlerContext } from '../../types';
import { createObservabilityServerRoute } from '../create_observability_server_route';
@ -79,14 +83,30 @@ const createSLORoute = createObservabilityServerRoute({
access: 'public',
},
params: createSLOParamsSchema,
handler: async ({ context, params, logger }) => {
handler: async ({ context, params, logger, dependencies, request }) => {
await assertPlatinumLicense(context);
const spaceId =
(await dependencies.spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default';
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
const soClient = (await context.core).savedObjects.client;
const repository = new KibanaSavedObjectsSLORepository(soClient);
const transformManager = new DefaultTransformManager(transformGenerators, esClient, logger);
const createSLO = new CreateSLO(esClient, repository, transformManager);
const summaryTransformManager = new DefaultSummaryTransformManager(
new DefaultSummaryTransformGenerator(),
esClient,
logger
);
const createSLO = new CreateSLO(
esClient,
repository,
transformManager,
summaryTransformManager,
logger,
spaceId
);
const response = await createSLO.execute(params.body);
@ -101,15 +121,30 @@ const updateSLORoute = createObservabilityServerRoute({
access: 'public',
},
params: updateSLOParamsSchema,
handler: async ({ context, params, logger }) => {
handler: async ({ context, request, params, logger, dependencies }) => {
await assertPlatinumLicense(context);
const spaceId =
(await dependencies.spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default';
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
const soClient = (await context.core).savedObjects.client;
const repository = new KibanaSavedObjectsSLORepository(soClient);
const transformManager = new DefaultTransformManager(transformGenerators, esClient, logger);
const updateSLO = new UpdateSLO(repository, transformManager, esClient);
const summaryTransformManager = new DefaultSummaryTransformManager(
new DefaultSummaryTransformGenerator(),
esClient,
logger
);
const updateSLO = new UpdateSLO(
repository,
transformManager,
summaryTransformManager,
esClient,
logger,
spaceId
);
const response = await updateSLO.execute(params.path.id, params.body);
@ -140,7 +175,19 @@ const deleteSLORoute = createObservabilityServerRoute({
const repository = new KibanaSavedObjectsSLORepository(soClient);
const transformManager = new DefaultTransformManager(transformGenerators, esClient, logger);
const deleteSLO = new DeleteSLO(repository, transformManager, esClient, rulesClient);
const summaryTransformManager = new DefaultSummaryTransformManager(
new DefaultSummaryTransformGenerator(),
esClient,
logger
);
const deleteSLO = new DeleteSLO(
repository,
transformManager,
summaryTransformManager,
esClient,
rulesClient
);
await deleteSLO.execute(params.path.id);
},
@ -183,7 +230,13 @@ const enableSLORoute = createObservabilityServerRoute({
const repository = new KibanaSavedObjectsSLORepository(soClient);
const transformManager = new DefaultTransformManager(transformGenerators, esClient, logger);
const manageSLO = new ManageSLO(repository, transformManager);
const summaryTransformManager = new DefaultSummaryTransformManager(
new DefaultSummaryTransformGenerator(),
esClient,
logger
);
const manageSLO = new ManageSLO(repository, transformManager, summaryTransformManager);
const response = await manageSLO.enable(params.path.id);
@ -206,7 +259,13 @@ const disableSLORoute = createObservabilityServerRoute({
const repository = new KibanaSavedObjectsSLORepository(soClient);
const transformManager = new DefaultTransformManager(transformGenerators, esClient, logger);
const manageSLO = new ManageSLO(repository, transformManager);
const summaryTransformManager = new DefaultSummaryTransformManager(
new DefaultSummaryTransformGenerator(),
esClient,
logger
);
const manageSLO = new ManageSLO(repository, transformManager, summaryTransformManager);
const response = await manageSLO.disable(params.path.id);
@ -214,6 +273,44 @@ const disableSLORoute = createObservabilityServerRoute({
},
});
const resetSLORoute = createObservabilityServerRoute({
endpoint: 'POST /api/observability/slos/{id}/_reset 2023-10-31',
options: {
tags: ['access:slo_write'],
access: 'public',
},
params: resetSLOParamsSchema,
handler: async ({ context, request, params, logger, dependencies }) => {
await assertPlatinumLicense(context);
const spaceId =
(await dependencies.spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default';
const soClient = (await context.core).savedObjects.client;
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
const repository = new KibanaSavedObjectsSLORepository(soClient);
const transformManager = new DefaultTransformManager(transformGenerators, esClient, logger);
const summaryTransformManager = new DefaultSummaryTransformManager(
new DefaultSummaryTransformGenerator(),
esClient,
logger
);
const resetSLO = new ResetSLO(
esClient,
repository,
transformManager,
summaryTransformManager,
logger,
spaceId
);
const response = await resetSLO.execute(params.path.id);
return response;
},
});
const findSLORoute = createObservabilityServerRoute({
endpoint: 'GET /api/observability/slos 2023-10-31',
options: {
@ -221,13 +318,16 @@ const findSLORoute = createObservabilityServerRoute({
access: 'public',
},
params: findSLOParamsSchema,
handler: async ({ context, params, logger }) => {
handler: async ({ context, request, params, logger, dependencies }) => {
await assertPlatinumLicense(context);
const spaceId =
(await dependencies.spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default';
const soClient = (await context.core).savedObjects.client;
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
const repository = new KibanaSavedObjectsSLORepository(soClient);
const summarySearchClient = new DefaultSummarySearchClient(esClient, logger);
const summarySearchClient = new DefaultSummarySearchClient(esClient, logger, spaceId);
const findSLO = new FindSLO(repository, summarySearchClient);
const response = await findSLO.execute(params?.query ?? {});
@ -253,10 +353,9 @@ const deleteSloInstancesRoute = createObservabilityServerRoute({
});
const findSloDefinitionsRoute = createObservabilityServerRoute({
endpoint: 'GET /internal/observability/slos/_definitions',
endpoint: 'GET /api/observability/slos/_definitions 2023-10-31',
options: {
tags: ['access:slo_read'],
access: 'internal',
},
params: findSloDefinitionsParamsSchema,
handler: async ({ context, params }) => {
@ -266,7 +365,7 @@ const findSloDefinitionsRoute = createObservabilityServerRoute({
const repository = new KibanaSavedObjectsSLORepository(soClient);
const findSloDefinitions = new FindSLODefinitions(repository);
const response = await findSloDefinitions.execute(params.query.search);
const response = await findSloDefinitions.execute(params?.query ?? {});
return response;
},
@ -395,4 +494,5 @@ export const sloRouteRepository = {
...getSloBurnRates,
...getPreviewData,
...getSLOInstancesRoute,
...resetSLORoute,
};

View file

@ -17,7 +17,6 @@ type StoredSLOBefore890 = StoredSLO & {
isCalendar?: boolean;
};
};
const migrateSlo890: SavedObjectMigrationFn<StoredSLOBefore890, StoredSLO> = (doc) => {
const { timeWindow, ...other } = doc.attributes;
return {
@ -38,6 +37,21 @@ export const slo: SavedObjectsType = {
name: SO_SLO_TYPE,
hidden: false,
namespaceType: 'multiple-isolated',
switchToModelVersionAt: '8.10.0',
modelVersions: {
1: {
changes: [
{ type: 'mappings_addition', addedMappings: { version: { type: 'long' } } },
{
type: 'data_backfill',
backfillFn: (doc) => {
// we explicitely set the version to 1, so we know which SLOs requires a migration to the following version.
return { attributes: { version: doc.attributes.version ?? 1 } };
},
},
],
},
},
mappings: {
dynamic: false,
properties: {
@ -53,6 +67,7 @@ export const slo: SavedObjectsType = {
budgetingMethod: { type: 'keyword' },
enabled: { type: 'boolean' },
tags: { type: 'keyword' },
version: { type: 'long' },
},
},
management: {

View file

@ -1,6 +1,144 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CreateSLO happy path calls the expected services 1`] = `
Array [
Object {
"_meta": Object {
"description": "Ingest pipeline for SLO summary data [id: unique-id, revision: 1]",
"managed": true,
"managed_by": "observability",
"version": 3,
},
"description": "Ingest pipeline for SLO summary data [id: unique-id, revision: 1]",
"id": ".slo-observability.summary.pipeline-unique-id-1",
"processors": Array [
Object {
"set": Object {
"description": "Set errorBudgetEstimated field",
"field": "errorBudgetEstimated",
"value": false,
},
},
Object {
"set": Object {
"description": "Set isTempDoc field",
"field": "isTempDoc",
"value": false,
},
},
Object {
"set": Object {
"description": "Set groupBy field",
"field": "slo.groupBy",
"value": "*",
},
},
Object {
"set": Object {
"description": "Set name field",
"field": "slo.name",
"value": "irrelevant",
},
},
Object {
"set": Object {
"description": "Set description field",
"field": "slo.description",
"value": "irrelevant",
},
},
Object {
"set": Object {
"description": "Set tags field",
"field": "slo.tags",
"value": Array [],
},
},
Object {
"set": Object {
"description": "Set indicator.type field",
"field": "slo.indicator.type",
"value": "sli.apm.transactionErrorRate",
},
},
Object {
"set": Object {
"description": "Set budgetingMethod field",
"field": "slo.budgetingMethod",
"value": "occurrences",
},
},
Object {
"set": Object {
"description": "Set timeWindow.duration field",
"field": "slo.timeWindow.duration",
"value": "7d",
},
},
Object {
"set": Object {
"description": "Set timeWindow.type field",
"field": "slo.timeWindow.type",
"value": "rolling",
},
},
Object {
"set": Object {
"description": "Set objective.target field",
"field": "slo.objective.target",
"value": 0.99,
},
},
Object {
"set": Object {
"description": "if 'statusCode == 0', set status to NO_DATA",
"field": "status",
"if": "ctx.statusCode == 0",
"value": "NO_DATA",
},
},
Object {
"set": Object {
"description": "if 'statusCode == 1', set statusLabel to VIOLATED",
"field": "status",
"if": "ctx.statusCode == 1",
"value": "VIOLATED",
},
},
Object {
"set": Object {
"description": "if 'statusCode == 2', set status to DEGRADING",
"field": "status",
"if": "ctx.statusCode == 2",
"value": "DEGRADING",
},
},
Object {
"set": Object {
"description": "if 'statusCode == 4', set status to HEALTHY",
"field": "status",
"if": "ctx.statusCode == 4",
"value": "HEALTHY",
},
},
Object {
"set": Object {
"field": "summaryUpdatedAt",
"value": "{{{_ingest.timestamp}}}",
},
},
Object {
"set": Object {
"field": "spaceId",
"value": "some-space",
},
},
],
},
]
`;
exports[`CreateSLO happy path calls the expected services 2`] = `
Array [
Object {
"document": Object {
@ -25,6 +163,11 @@ Array [
},
"instanceId": "*",
"name": "irrelevant",
"objective": Object {
"target": 0.99,
"timesliceTarget": null,
"timesliceWindow": null,
},
"revision": 1,
"tags": Array [],
"timeWindow": Object {
@ -32,6 +175,7 @@ Array [
"type": "rolling",
},
},
"spaceId": "some-space",
"status": "NO_DATA",
"statusCode": 0,
"totalEvents": 0,
@ -41,7 +185,7 @@ Array [
},
},
"id": "slo-unique-id",
"index": ".slo-observability.summary-v2.temp",
"index": ".slo-observability.summary-v3.temp",
"refresh": true,
},
]

View file

@ -0,0 +1,177 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DeleteSLO happy path removes all resources associatde to the slo 1`] = `
[MockFunction] {
"calls": Array [
Array [
"irrelevant",
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`;
exports[`DeleteSLO happy path removes all resources associatde to the slo 2`] = `
[MockFunction] {
"calls": Array [
Array [
"slo-summary-irrelevant-1",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;
exports[`DeleteSLO happy path removes all resources associatde to the slo 3`] = `
[MockFunction] {
"calls": Array [
Array [
"slo-summary-irrelevant-1",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;
exports[`DeleteSLO happy path removes all resources associatde to the slo 4`] = `
[MockFunction] {
"calls": Array [
Array [
"slo-irrelevant-1",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;
exports[`DeleteSLO happy path removes all resources associatde to the slo 5`] = `
[MockFunction] {
"calls": Array [
Array [
"slo-irrelevant-1",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;
exports[`DeleteSLO happy path removes all resources associatde to the slo 6`] = `
[MockFunction] {
"calls": Array [
Array [
Object {
"id": ".slo-observability.summary.pipeline-irrelevant-1",
},
Object {
"ignore": Array [
404,
],
},
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`;
exports[`DeleteSLO happy path removes all resources associatde to the slo 7`] = `
[MockFunction] {
"calls": Array [
Array [
Object {
"index": ".slo-observability.sli-v3*",
"query": Object {
"match": Object {
"slo.id": "irrelevant",
},
},
"wait_for_completion": false,
},
],
Array [
Object {
"index": ".slo-observability.summary-v3*",
"query": Object {
"match": Object {
"slo.id": "irrelevant",
},
},
"refresh": true,
},
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
Object {
"type": "return",
"value": Promise {},
},
],
}
`;
exports[`DeleteSLO happy path removes all resources associatde to the slo 8`] = `
[MockFunction] {
"calls": Array [
Array [
Object {
"filter": "alert.attributes.params.sloId:irrelevant",
},
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;
exports[`DeleteSLO happy path removes all resources associatde to the slo 9`] = `
[MockFunction] {
"calls": Array [
Array [
"irrelevant",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;

View file

@ -11,7 +11,7 @@ Array [
},
},
},
"index": ".slo-observability.sli-v2*",
"index": ".slo-observability.sli-v3*",
"query": Object {
"bool": Object {
"filter": Array [

View file

@ -0,0 +1,65 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ManageSLO Disable disables the slo when enabled 1`] = `
[MockFunction] {
"calls": Array [
Array [
"slo-irrelevant-1",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;
exports[`ManageSLO Disable disables the slo when enabled 2`] = `
[MockFunction] {
"calls": Array [
Array [
"slo-summary-irrelevant-1",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;
exports[`ManageSLO Enable enables the slo when disabled 1`] = `
[MockFunction] {
"calls": Array [
Array [
"slo-irrelevant-1",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;
exports[`ManageSLO Enable enables the slo when disabled 2`] = `
[MockFunction] {
"calls": Array [
Array [
"slo-summary-irrelevant-1",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;

View file

@ -0,0 +1,489 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ResetSLO resets all associated resources 1`] = `
[MockFunction] {
"calls": Array [
Array [
"slo-summary-irrelevant-1",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;
exports[`ResetSLO resets all associated resources 2`] = `
[MockFunction] {
"calls": Array [
Array [
"slo-summary-irrelevant-1",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;
exports[`ResetSLO resets all associated resources 3`] = `
[MockFunction] {
"calls": Array [
Array [
"slo-irrelevant-1",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;
exports[`ResetSLO resets all associated resources 4`] = `
[MockFunction] {
"calls": Array [
Array [
"slo-irrelevant-1",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;
exports[`ResetSLO resets all associated resources 5`] = `
[MockFunction] {
"calls": Array [
Array [
Object {
"index": ".slo-observability.sli-v3*",
"query": Object {
"bool": Object {
"filter": Array [
Object {
"term": Object {
"slo.id": "irrelevant",
},
},
],
},
},
"refresh": true,
},
],
Array [
Object {
"index": ".slo-observability.summary-v3*",
"query": Object {
"bool": Object {
"filter": Array [
Object {
"term": Object {
"slo.id": "irrelevant",
},
},
],
},
},
"refresh": true,
},
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
Object {
"type": "return",
"value": Promise {},
},
],
}
`;
exports[`ResetSLO resets all associated resources 6`] = `
[MockFunction] {
"calls": Array [
Array [
Object {
"budgetingMethod": "occurrences",
"createdAt": 2023-01-01T00:00:00.000Z,
"description": "irrelevant",
"enabled": true,
"groupBy": "*",
"id": "irrelevant",
"indicator": Object {
"params": Object {
"environment": "irrelevant",
"index": "metrics-apm*",
"service": "irrelevant",
"threshold": 500,
"transactionName": "irrelevant",
"transactionType": "irrelevant",
},
"type": "sli.apm.transactionDuration",
},
"name": "irrelevant",
"objective": Object {
"target": 0.999,
},
"revision": 1,
"settings": Object {
"frequency": Duration {
"unit": "m",
"value": 1,
},
"syncDelay": Duration {
"unit": "m",
"value": 1,
},
},
"tags": Array [
"critical",
"k8s",
],
"timeWindow": Object {
"duration": Duration {
"unit": "d",
"value": 7,
},
"type": "rolling",
},
"updatedAt": 2023-01-01T00:00:00.000Z,
"version": 1,
},
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;
exports[`ResetSLO resets all associated resources 7`] = `
[MockFunction] {
"calls": Array [
Array [
"slo-summary-irrelevant-1",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;
exports[`ResetSLO resets all associated resources 8`] = `
[MockFunction] {
"calls": Array [
Array [
Object {
"_meta": Object {
"description": "Ingest pipeline for SLO summary data [id: irrelevant, revision: 1]",
"managed": true,
"managed_by": "observability",
"version": 3,
},
"description": "Ingest pipeline for SLO summary data [id: irrelevant, revision: 1]",
"id": ".slo-observability.summary.pipeline-irrelevant-1",
"processors": Array [
Object {
"set": Object {
"description": "Set errorBudgetEstimated field",
"field": "errorBudgetEstimated",
"value": false,
},
},
Object {
"set": Object {
"description": "Set isTempDoc field",
"field": "isTempDoc",
"value": false,
},
},
Object {
"set": Object {
"description": "Set groupBy field",
"field": "slo.groupBy",
"value": "*",
},
},
Object {
"set": Object {
"description": "Set name field",
"field": "slo.name",
"value": "irrelevant",
},
},
Object {
"set": Object {
"description": "Set description field",
"field": "slo.description",
"value": "irrelevant",
},
},
Object {
"set": Object {
"description": "Set tags field",
"field": "slo.tags",
"value": Array [
"critical",
"k8s",
],
},
},
Object {
"set": Object {
"description": "Set indicator.type field",
"field": "slo.indicator.type",
"value": "sli.apm.transactionDuration",
},
},
Object {
"set": Object {
"description": "Set budgetingMethod field",
"field": "slo.budgetingMethod",
"value": "occurrences",
},
},
Object {
"set": Object {
"description": "Set timeWindow.duration field",
"field": "slo.timeWindow.duration",
"value": "7d",
},
},
Object {
"set": Object {
"description": "Set timeWindow.type field",
"field": "slo.timeWindow.type",
"value": "rolling",
},
},
Object {
"set": Object {
"description": "Set objective.target field",
"field": "slo.objective.target",
"value": 0.999,
},
},
Object {
"set": Object {
"description": "if 'statusCode == 0', set status to NO_DATA",
"field": "status",
"if": "ctx.statusCode == 0",
"value": "NO_DATA",
},
},
Object {
"set": Object {
"description": "if 'statusCode == 1', set statusLabel to VIOLATED",
"field": "status",
"if": "ctx.statusCode == 1",
"value": "VIOLATED",
},
},
Object {
"set": Object {
"description": "if 'statusCode == 2', set status to DEGRADING",
"field": "status",
"if": "ctx.statusCode == 2",
"value": "DEGRADING",
},
},
Object {
"set": Object {
"description": "if 'statusCode == 4', set status to HEALTHY",
"field": "status",
"if": "ctx.statusCode == 4",
"value": "HEALTHY",
},
},
Object {
"set": Object {
"field": "summaryUpdatedAt",
"value": "{{{_ingest.timestamp}}}",
},
},
Object {
"set": Object {
"field": "spaceId",
"value": "some-space",
},
},
],
},
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`;
exports[`ResetSLO resets all associated resources 9`] = `
[MockFunction] {
"calls": Array [
Array [
Object {
"budgetingMethod": "occurrences",
"createdAt": 2023-01-01T00:00:00.000Z,
"description": "irrelevant",
"enabled": true,
"groupBy": "*",
"id": "irrelevant",
"indicator": Object {
"params": Object {
"environment": "irrelevant",
"index": "metrics-apm*",
"service": "irrelevant",
"threshold": 500,
"transactionName": "irrelevant",
"transactionType": "irrelevant",
},
"type": "sli.apm.transactionDuration",
},
"name": "irrelevant",
"objective": Object {
"target": 0.999,
},
"revision": 1,
"settings": Object {
"frequency": Duration {
"unit": "m",
"value": 1,
},
"syncDelay": Duration {
"unit": "m",
"value": 1,
},
},
"tags": Array [
"critical",
"k8s",
],
"timeWindow": Object {
"duration": Duration {
"unit": "d",
"value": 7,
},
"type": "rolling",
},
"updatedAt": 2023-01-01T00:00:00.000Z,
"version": 1,
},
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;
exports[`ResetSLO resets all associated resources 10`] = `
[MockFunction] {
"calls": Array [
Array [
"slo-irrelevant-1",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;
exports[`ResetSLO resets all associated resources 11`] = `
[MockFunction] {
"calls": Array [
Array [
Object {
"document": Object {
"errorBudgetConsumed": 0,
"errorBudgetEstimated": false,
"errorBudgetInitial": 0.0010000000000000009,
"errorBudgetRemaining": 1,
"goodEvents": 0,
"isTempDoc": true,
"service": Object {
"environment": null,
"name": null,
},
"sliValue": -1,
"slo": Object {
"budgetingMethod": "occurrences",
"description": "irrelevant",
"groupBy": "*",
"id": "irrelevant",
"indicator": Object {
"type": "sli.apm.transactionDuration",
},
"instanceId": "*",
"name": "irrelevant",
"objective": Object {
"target": 0.999,
"timesliceTarget": null,
"timesliceWindow": null,
},
"revision": 1,
"tags": Array [
"critical",
"k8s",
],
"timeWindow": Object {
"duration": "7d",
"type": "rolling",
},
},
"spaceId": "some-space",
"status": "NO_DATA",
"statusCode": 0,
"totalEvents": 0,
"transaction": Object {
"name": null,
"type": null,
},
},
"id": "slo-irrelevant",
"index": ".slo-observability.summary-v3.temp",
"refresh": true,
},
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`;

View file

@ -3,7 +3,7 @@
exports[`Summary Search Client returns the summary documents without duplicate temporary summary documents 1`] = `
Array [
Object {
"index": ".slo-observability.summary-v2*",
"index": ".slo-observability.summary-v3*",
"query": Object {
"bool": Object {
"filter": Array [

View file

@ -1,51 +1,88 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UpdateSLO index a temporary summary document 1`] = `
Array [
Object {
"document": Object {
"errorBudgetConsumed": 0,
"errorBudgetEstimated": false,
"errorBudgetInitial": 0.0010000000000000009,
"errorBudgetRemaining": 1,
"goodEvents": 0,
"isTempDoc": true,
"service": Object {
"environment": null,
"name": null,
},
"sliValue": -1,
"slo": Object {
"budgetingMethod": "occurrences",
"description": "irrelevant",
"groupBy": "*",
"id": "unique-id",
"indicator": Object {
"type": "sli.apm.transactionErrorRate",
},
"instanceId": "*",
"name": "irrelevant",
"revision": 2,
"tags": Array [
"critical",
"k8s",
],
"timeWindow": Object {
"duration": "7d",
"type": "rolling",
},
},
"status": "NO_DATA",
"statusCode": 0,
"totalEvents": 0,
"transaction": Object {
"name": null,
"type": null,
},
exports[`UpdateSLO when error happens during the update restores the previous SLO definition in the repository 1`] = `
[MockFunction] {
"calls": Array [
Array [
"slo-summary-original-id-2",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
"id": "slo-unique-id",
"index": ".slo-observability.summary-v2.temp",
"refresh": true,
},
]
],
}
`;
exports[`UpdateSLO when error happens during the update restores the previous SLO definition in the repository 2`] = `
[MockFunction] {
"calls": Array [
Array [
"slo-summary-original-id-2",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;
exports[`UpdateSLO when error happens during the update restores the previous SLO definition in the repository 3`] = `
[MockFunction] {
"calls": Array [
Array [
"slo-original-id-2",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;
exports[`UpdateSLO when error happens during the update restores the previous SLO definition in the repository 4`] = `
[MockFunction] {
"calls": Array [
Array [
"slo-original-id-2",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;
exports[`UpdateSLO when error happens during the update restores the previous SLO definition in the repository 5`] = `
[MockFunction] {
"calls": Array [
Array [
Object {
"id": ".slo-observability.summary.pipeline-original-id-2",
},
Object {
"ignore": Array [
404,
],
},
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`;

View file

@ -5,25 +5,45 @@
* 2.0.
*/
import { ElasticsearchClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
import {
ElasticsearchClientMock,
elasticsearchServiceMock,
loggingSystemMock,
} from '@kbn/core/server/mocks';
import { MockedLogger } from '@kbn/logging-mocks';
import { CreateSLO } from './create_slo';
import { fiveMinute, oneMinute } from './fixtures/duration';
import { createAPMTransactionErrorRateIndicator, createSLOParams } from './fixtures/slo';
import { createSLORepositoryMock, createTransformManagerMock } from './mocks';
import {
createSLORepositoryMock,
createSummaryTransformManagerMock,
createTransformManagerMock,
} from './mocks';
import { SLORepository } from './slo_repository';
import { TransformManager } from './transform_manager';
describe('CreateSLO', () => {
let esClientMock: ElasticsearchClientMock;
let loggerMock: jest.Mocked<MockedLogger>;
let mockRepository: jest.Mocked<SLORepository>;
let mockTransformManager: jest.Mocked<TransformManager>;
let mockSummaryTransformManager: jest.Mocked<TransformManager>;
let createSLO: CreateSLO;
beforeEach(() => {
esClientMock = elasticsearchServiceMock.createElasticsearchClient();
loggerMock = loggingSystemMock.createLogger();
mockRepository = createSLORepositoryMock();
mockTransformManager = createTransformManagerMock();
createSLO = new CreateSLO(esClientMock, mockRepository, mockTransformManager);
mockSummaryTransformManager = createSummaryTransformManagerMock();
createSLO = new CreateSLO(
esClientMock,
mockRepository,
mockTransformManager,
mockSummaryTransformManager,
loggerMock,
'some-space'
);
});
describe('happy path', () => {
@ -32,7 +52,8 @@ describe('CreateSLO', () => {
id: 'unique-id',
indicator: createAPMTransactionErrorRateIndicator(),
});
mockTransformManager.install.mockResolvedValue('slo-transform-id');
mockTransformManager.install.mockResolvedValue('slo-id-revision');
mockSummaryTransformManager.install.mockResolvedValue('slo-summary-id-revision');
const response = await createSLO.execute(sloParams);
@ -47,18 +68,21 @@ describe('CreateSLO', () => {
revision: 1,
tags: [],
enabled: true,
version: 2,
createdAt: expect.any(Date),
updatedAt: expect.any(Date),
}),
{ throwOnConflict: true }
);
expect(mockTransformManager.install).toHaveBeenCalledWith(
expect.objectContaining({ ...sloParams, id: 'unique-id' })
);
expect(mockTransformManager.preview).toHaveBeenCalledWith('slo-transform-id');
expect(mockTransformManager.start).toHaveBeenCalledWith('slo-transform-id');
expect(response).toEqual(expect.objectContaining({ id: 'unique-id' }));
expect(mockTransformManager.install).toHaveBeenCalled();
expect(mockTransformManager.start).toHaveBeenCalled();
expect(esClientMock.ingest.putPipeline.mock.calls[0]).toMatchSnapshot();
expect(mockSummaryTransformManager.install).toHaveBeenCalled();
expect(mockSummaryTransformManager.start).toHaveBeenCalled();
expect(esClientMock.index.mock.calls[0]).toMatchSnapshot();
expect(response).toEqual(expect.objectContaining({ id: 'unique-id' }));
});
it('overrides the default values when provided', async () => {
@ -93,32 +117,20 @@ describe('CreateSLO', () => {
});
describe('unhappy path', () => {
it('deletes the SLO when transform installation fails', async () => {
mockTransformManager.install.mockRejectedValue(new Error('Transform install error'));
it('rollbacks new resources on failure', async () => {
mockTransformManager.install.mockRejectedValue(new Error('Rollup transform install error'));
const sloParams = createSLOParams({ indicator: createAPMTransactionErrorRateIndicator() });
await expect(createSLO.execute(sloParams)).rejects.toThrowError('Transform install error');
expect(mockRepository.deleteById).toBeCalled();
});
await expect(createSLO.execute(sloParams)).rejects.toThrowError(
'Rollup transform install error'
);
it('removes the transform and deletes the SLO when transform preview fails', async () => {
mockTransformManager.install.mockResolvedValue('slo-transform-id');
mockTransformManager.preview.mockRejectedValue(new Error('Transform preview error'));
const sloParams = createSLOParams({ indicator: createAPMTransactionErrorRateIndicator() });
await expect(createSLO.execute(sloParams)).rejects.toThrowError('Transform preview error');
expect(mockTransformManager.uninstall).toBeCalledWith('slo-transform-id');
expect(mockRepository.deleteById).toBeCalled();
});
it('removes the transform and deletes the SLO when transform start fails', async () => {
mockTransformManager.install.mockResolvedValue('slo-transform-id');
mockTransformManager.start.mockRejectedValue(new Error('Transform start error'));
const sloParams = createSLOParams({ indicator: createAPMTransactionErrorRateIndicator() });
await expect(createSLO.execute(sloParams)).rejects.toThrowError('Transform start error');
expect(mockTransformManager.uninstall).toBeCalledWith('slo-transform-id');
expect(mockRepository.deleteById).toBeCalled();
expect(mockSummaryTransformManager.stop).toHaveBeenCalled();
expect(mockSummaryTransformManager.uninstall).toHaveBeenCalled();
expect(mockTransformManager.stop).toHaveBeenCalled();
expect(mockTransformManager.uninstall).toHaveBeenCalled();
expect(esClientMock.ingest.deletePipeline).toHaveBeenCalled();
expect(mockRepository.deleteById).toHaveBeenCalled();
});
});
});

View file

@ -5,21 +5,32 @@
* 2.0.
*/
import { ElasticsearchClient } from '@kbn/core/server';
import { ElasticsearchClient, Logger } from '@kbn/core/server';
import { ALL_VALUE, CreateSLOParams, CreateSLOResponse } from '@kbn/slo-schema';
import { v4 as uuidv4 } from 'uuid';
import { SLO_SUMMARY_TEMP_INDEX_NAME } from '../../../common/slo/constants';
import {
getSLOSummaryPipelineId,
getSLOSummaryTransformId,
getSLOTransformId,
SLO_MODEL_VERSION,
SLO_SUMMARY_TEMP_INDEX_NAME,
} from '../../../common/slo/constants';
import { getSLOSummaryPipelineTemplate } from '../../assets/ingest_templates/slo_summary_pipeline_template';
import { Duration, DurationUnit, SLO } from '../../domain/models';
import { validateSLO } from '../../domain/services';
import { retryTransientEsErrors } from '../../utils/retry';
import { SLORepository } from './slo_repository';
import { createTempSummaryDocument } from './summary_transform/helpers/create_temp_summary';
import { createTempSummaryDocument } from './summary_transform_generator/helpers/create_temp_summary';
import { TransformManager } from './transform_manager';
export class CreateSLO {
constructor(
private esClient: ElasticsearchClient,
private repository: SLORepository,
private transformManager: TransformManager
private transformManager: TransformManager,
private summaryTransformManager: TransformManager,
private logger: Logger,
private spaceId: string
) {}
public async execute(params: CreateSLOParams): Promise<CreateSLOResponse> {
@ -27,33 +38,48 @@ export class CreateSLO {
validateSLO(slo);
await this.repository.save(slo, { throwOnConflict: true });
let sloTransformId;
const rollupTransformId = getSLOTransformId(slo.id, slo.revision);
const summaryTransformId = getSLOSummaryTransformId(slo.id, slo.revision);
try {
sloTransformId = await this.transformManager.install(slo);
await this.transformManager.install(slo);
await this.transformManager.start(rollupTransformId);
await retryTransientEsErrors(
() => this.esClient.ingest.putPipeline(getSLOSummaryPipelineTemplate(slo, this.spaceId)),
{ logger: this.logger }
);
await this.summaryTransformManager.install(slo);
await this.summaryTransformManager.start(summaryTransformId);
await retryTransientEsErrors(
() =>
this.esClient.index({
index: SLO_SUMMARY_TEMP_INDEX_NAME,
id: `slo-${slo.id}`,
document: createTempSummaryDocument(slo, this.spaceId),
refresh: true,
}),
{ logger: this.logger }
);
} catch (err) {
this.logger.error(
`Cannot install the SLO [id: ${slo.id}, revision: ${slo.revision}]. Rolling back.`
);
await this.summaryTransformManager.stop(summaryTransformId);
await this.summaryTransformManager.uninstall(summaryTransformId);
await this.transformManager.stop(rollupTransformId);
await this.transformManager.uninstall(rollupTransformId);
await this.esClient.ingest.deletePipeline(
{ id: getSLOSummaryPipelineId(slo.id, slo.revision) },
{ ignore: [404] }
);
await this.repository.deleteById(slo.id);
throw err;
}
try {
await this.transformManager.preview(sloTransformId);
await this.transformManager.start(sloTransformId);
} catch (err) {
await Promise.all([
this.transformManager.uninstall(sloTransformId),
this.repository.deleteById(slo.id),
]);
throw err;
}
await this.esClient.index({
index: SLO_SUMMARY_TEMP_INDEX_NAME,
id: `slo-${slo.id}`,
document: createTempSummaryDocument(slo),
refresh: true,
});
return this.toResponse(slo);
}
@ -72,6 +98,7 @@ export class CreateSLO {
createdAt: now,
updatedAt: now,
groupBy: !!params.groupBy ? params.groupBy : ALL_VALUE,
version: SLO_MODEL_VERSION,
};
}

View file

@ -9,20 +9,20 @@ import { rulesClientMock } from '@kbn/alerting-plugin/server/rules_client.mock';
import { RulesClientApi } from '@kbn/alerting-plugin/server/types';
import { ElasticsearchClient } from '@kbn/core/server';
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
import {
getSLOTransformId,
SLO_DESTINATION_INDEX_PATTERN,
SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
} from '../../../common/slo/constants';
import { DeleteSLO } from './delete_slo';
import { createAPMTransactionErrorRateIndicator, createSLO } from './fixtures/slo';
import { createSLORepositoryMock, createTransformManagerMock } from './mocks';
import {
createSLORepositoryMock,
createSummaryTransformManagerMock,
createTransformManagerMock,
} from './mocks';
import { SLORepository } from './slo_repository';
import { TransformManager } from './transform_manager';
describe('DeleteSLO', () => {
let mockRepository: jest.Mocked<SLORepository>;
let mockTransformManager: jest.Mocked<TransformManager>;
let mockSummaryTransformManager: jest.Mocked<TransformManager>;
let mockEsClient: jest.Mocked<ElasticsearchClient>;
let mockRulesClient: jest.Mocked<RulesClientApi>;
let deleteSLO: DeleteSLO;
@ -30,52 +30,37 @@ describe('DeleteSLO', () => {
beforeEach(() => {
mockRepository = createSLORepositoryMock();
mockTransformManager = createTransformManagerMock();
mockSummaryTransformManager = createSummaryTransformManagerMock();
mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
mockRulesClient = rulesClientMock.create();
deleteSLO = new DeleteSLO(mockRepository, mockTransformManager, mockEsClient, mockRulesClient);
deleteSLO = new DeleteSLO(
mockRepository,
mockTransformManager,
mockSummaryTransformManager,
mockEsClient,
mockRulesClient
);
});
describe('happy path', () => {
it('removes the transform, the roll up data, the associated rules and the SLO from the repository', async () => {
const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator() });
it('removes all resources associatde to the slo', async () => {
const slo = createSLO({
id: 'irrelevant',
indicator: createAPMTransactionErrorRateIndicator(),
});
mockRepository.findById.mockResolvedValueOnce(slo);
await deleteSLO.execute(slo.id);
expect(mockRepository.findById).toHaveBeenCalledWith(slo.id);
expect(mockTransformManager.stop).toHaveBeenCalledWith(
getSLOTransformId(slo.id, slo.revision)
);
expect(mockTransformManager.uninstall).toHaveBeenCalledWith(
getSLOTransformId(slo.id, slo.revision)
);
expect(mockEsClient.deleteByQuery).toHaveBeenCalledTimes(2);
expect(mockEsClient.deleteByQuery).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
index: SLO_DESTINATION_INDEX_PATTERN,
query: {
match: {
'slo.id': slo.id,
},
},
})
);
expect(mockEsClient.deleteByQuery).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
query: {
match: {
'slo.id': slo.id,
},
},
})
);
expect(mockRulesClient.bulkDeleteRules).toHaveBeenCalledWith({
filter: `alert.attributes.params.sloId:${slo.id}`,
});
expect(mockRepository.deleteById).toHaveBeenCalledWith(slo.id);
expect(mockRepository.findById).toMatchSnapshot();
expect(mockSummaryTransformManager.stop).toMatchSnapshot();
expect(mockSummaryTransformManager.uninstall).toMatchSnapshot();
expect(mockTransformManager.stop).toMatchSnapshot();
expect(mockTransformManager.uninstall).toMatchSnapshot();
expect(mockEsClient.ingest.deletePipeline).toMatchSnapshot();
expect(mockEsClient.deleteByQuery).toMatchSnapshot();
expect(mockRulesClient.bulkDeleteRules).toMatchSnapshot();
expect(mockRepository.deleteById).toMatchSnapshot();
});
});
});

View file

@ -8,10 +8,13 @@
import { RulesClientApi } from '@kbn/alerting-plugin/server/types';
import { ElasticsearchClient } from '@kbn/core/server';
import {
getSLOSummaryPipelineId,
getSLOSummaryTransformId,
getSLOTransformId,
SLO_DESTINATION_INDEX_PATTERN,
SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
} from '../../../common/slo/constants';
import { retryTransientEsErrors } from '../../utils/retry';
import { SLORepository } from './slo_repository';
import { TransformManager } from './transform_manager';
@ -19,6 +22,7 @@ export class DeleteSLO {
constructor(
private repository: SLORepository,
private transformManager: TransformManager,
private summaryTransformManager: TransformManager,
private esClient: ElasticsearchClient,
private rulesClient: RulesClientApi
) {}
@ -26,9 +30,20 @@ export class DeleteSLO {
public async execute(sloId: string): Promise<void> {
const slo = await this.repository.findById(sloId);
const sloTransformId = getSLOTransformId(slo.id, slo.revision);
await this.transformManager.stop(sloTransformId);
await this.transformManager.uninstall(sloTransformId);
const summaryTransformId = getSLOSummaryTransformId(slo.id, slo.revision);
await this.summaryTransformManager.stop(summaryTransformId);
await this.summaryTransformManager.uninstall(summaryTransformId);
const rollupTransformId = getSLOTransformId(slo.id, slo.revision);
await this.transformManager.stop(rollupTransformId);
await this.transformManager.uninstall(rollupTransformId);
await retryTransientEsErrors(() =>
this.esClient.ingest.deletePipeline(
{ id: getSLOSummaryPipelineId(slo.id, slo.revision) },
{ ignore: [404] }
)
);
await this.deleteRollupData(slo.id);
await this.deleteSummaryData(slo.id);

View file

@ -43,7 +43,7 @@ describe('DeleteSLOInstances', () => {
expect(mockEsClient.deleteByQuery).toHaveBeenCalledTimes(2);
expect(mockEsClient.deleteByQuery.mock.calls[0][0]).toMatchInlineSnapshot(`
Object {
"index": ".slo-observability.sli-v2*",
"index": ".slo-observability.sli-v3*",
"query": Object {
"bool": Object {
"should": Array [
@ -103,7 +103,7 @@ describe('DeleteSLOInstances', () => {
`);
expect(mockEsClient.deleteByQuery.mock.calls[1][0]).toMatchInlineSnapshot(`
Object {
"index": ".slo-observability.summary-v2*",
"index": ".slo-observability.summary-v3*",
"query": Object {
"bool": Object {
"should": Array [

View file

@ -5,13 +5,14 @@
* 2.0.
*/
import { ALL_VALUE } from '@kbn/slo-schema';
import { ALL_VALUE, Paginated } from '@kbn/slo-schema';
import { SLO_MODEL_VERSION } from '../../../common/slo/constants';
import { SLO } from '../../domain/models';
import { FindSLO } from './find_slo';
import { createSLO } from './fixtures/slo';
import { createSLORepositoryMock, createSummarySearchClientMock } from './mocks';
import { SLORepository } from './slo_repository';
import { Paginated, SLOSummary, SummarySearchClient } from './summary_search_client';
import { SLOSummary, SummarySearchClient } from './summary_search_client';
describe('FindSLO', () => {
let mockRepository: jest.Mocked<SLORepository>;
@ -95,6 +96,7 @@ describe('FindSLO', () => {
revision: slo.revision,
groupBy: slo.groupBy,
instanceId: ALL_VALUE,
version: SLO_MODEL_VERSION,
},
],
});
@ -147,7 +149,7 @@ describe('FindSLO', () => {
await expect(findSLO.execute({ perPage: '5000' })).resolves.not.toThrow();
await expect(findSLO.execute({ perPage: '5001' })).rejects.toThrowError(
'perPage limit to 5000'
'perPage limit set to 5000'
);
});
});

View file

@ -5,11 +5,11 @@
* 2.0.
*/
import { FindSLOParams, FindSLOResponse, findSLOResponseSchema } from '@kbn/slo-schema';
import { FindSLOParams, FindSLOResponse, findSLOResponseSchema, Pagination } from '@kbn/slo-schema';
import { SLO, SLOWithSummary } from '../../domain/models';
import { IllegalArgumentError } from '../../errors';
import { SLORepository } from './slo_repository';
import { Pagination, SLOSummary, Sort, SummarySearchClient } from './summary_search_client';
import { SLOSummary, Sort, SummarySearchClient } from './summary_search_client';
const DEFAULT_PAGE = 1;
const DEFAULT_PER_PAGE = 25;
@ -55,7 +55,7 @@ function toPagination(params: FindSLOParams): Pagination {
const perPage = Number(params.perPage);
if (!isNaN(perPage) && perPage > MAX_PER_PAGE) {
throw new IllegalArgumentError('perPage limit to 5000');
throw new IllegalArgumentError(`perPage limit set to ${MAX_PER_PAGE}`);
}
return {

View file

@ -5,14 +5,40 @@
* 2.0.
*/
import { FindSloDefinitionsResponse, findSloDefinitionsResponseSchema } from '@kbn/slo-schema';
import {
FindSLODefinitionsParams,
FindSLODefinitionsResponse,
findSloDefinitionsResponseSchema,
Pagination,
} from '@kbn/slo-schema';
import { IllegalArgumentError } from '../../errors';
import { SLORepository } from './slo_repository';
const MAX_PER_PAGE = 1000;
const DEFAULT_PER_PAGE = 100;
const DEFAULT_PAGE = 1;
export class FindSLODefinitions {
constructor(private repository: SLORepository) {}
public async execute(search: string): Promise<FindSloDefinitionsResponse> {
const sloList = await this.repository.search(search);
return findSloDefinitionsResponseSchema.encode(sloList);
public async execute(params: FindSLODefinitionsParams): Promise<FindSLODefinitionsResponse> {
const result = await this.repository.search(params.search ?? '', toPagination(params), {
includeOutdatedOnly: params.includeOutdatedOnly === true ? true : false,
});
return findSloDefinitionsResponseSchema.encode(result);
}
}
function toPagination(params: FindSLODefinitionsParams): Pagination {
const page = Number(params.page);
const perPage = Number(params.perPage);
if (!isNaN(perPage) && perPage > MAX_PER_PAGE) {
throw new IllegalArgumentError(`perPage limit set to ${MAX_PER_PAGE}`);
}
return {
page: !isNaN(page) && page >= 1 ? page : DEFAULT_PAGE,
perPage: !isNaN(perPage) && perPage >= 1 ? perPage : DEFAULT_PER_PAGE,
};
}

View file

@ -15,6 +15,7 @@ import {
} from '@kbn/slo-schema';
import { cloneDeep } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { SLO_MODEL_VERSION } from '../../../../common/slo/constants';
import {
APMTransactionDurationIndicator,
APMTransactionErrorRateIndicator,
@ -139,7 +140,7 @@ export const createHistogramIndicator = (
},
});
const defaultSLO: Omit<SLO, 'id' | 'revision' | 'createdAt' | 'updatedAt'> = {
const defaultSLO: Omit<SLO, 'id' | 'revision' | 'createdAt' | 'updatedAt' | 'version'> = {
name: 'irrelevant',
description: 'irrelevant',
timeWindow: sevenDaysRolling(),
@ -190,6 +191,7 @@ export const createSLO = (params: Partial<SLO> = {}): SLO => {
revision: 1,
createdAt: now,
updatedAt: now,
version: SLO_MODEL_VERSION,
...params,
});
};

View file

@ -6,6 +6,7 @@
*/
import { ALL_VALUE } from '@kbn/slo-schema';
import { SLO_MODEL_VERSION } from '../../../common/slo/constants';
import { createAPMTransactionErrorRateIndicator, createSLO } from './fixtures/slo';
import { GetSLO } from './get_slo';
import { createSummaryClientMock, createSLORepositoryMock } from './mocks';
@ -84,6 +85,7 @@ describe('GetSLO', () => {
revision: slo.revision,
groupBy: slo.groupBy,
instanceId: ALL_VALUE,
version: SLO_MODEL_VERSION,
});
});
});

View file

@ -14,10 +14,10 @@ export * from './get_slo';
export * from './historical_summary_client';
export * from './resource_installer';
export * from './slo_installer';
export * from './summary_transform/summary_transform_installer';
export * from './sli_client';
export * from './slo_repository';
export * from './transform_manager';
export * from './summay_transform_manager';
export * from './update_slo';
export * from './summary_client';
export * from './get_slo_instances';

View file

@ -7,19 +7,26 @@
import { createSLO } from './fixtures/slo';
import { ManageSLO } from './manage_slo';
import { createSLORepositoryMock, createTransformManagerMock } from './mocks';
import {
createSLORepositoryMock,
createSummaryTransformManagerMock,
createTransformManagerMock,
} from './mocks';
import { SLORepository } from './slo_repository';
import { TransformManager } from './transform_manager';
describe('ManageSLO', () => {
let mockRepository: jest.Mocked<SLORepository>;
let mockTransformManager: jest.Mocked<TransformManager>;
let mockSummaryTransformManager: jest.Mocked<TransformManager>;
let manageSLO: ManageSLO;
beforeEach(() => {
mockRepository = createSLORepositoryMock();
mockTransformManager = createTransformManagerMock();
manageSLO = new ManageSLO(mockRepository, mockTransformManager);
mockSummaryTransformManager = createSummaryTransformManagerMock();
manageSLO = new ManageSLO(mockRepository, mockTransformManager, mockSummaryTransformManager);
});
describe('Enable', () => {
@ -30,16 +37,18 @@ describe('ManageSLO', () => {
await manageSLO.enable(slo.id);
expect(mockTransformManager.start).not.toHaveBeenCalled();
expect(mockSummaryTransformManager.start).not.toHaveBeenCalled();
expect(mockRepository.save).not.toHaveBeenCalled();
});
it('enables the slo when disabled', async () => {
const slo = createSLO({ enabled: false });
const slo = createSLO({ id: 'irrelevant', enabled: false });
mockRepository.findById.mockResolvedValue(slo);
await manageSLO.enable(slo.id);
expect(mockTransformManager.start).toHaveBeenCalled();
expect(mockTransformManager.start).toMatchSnapshot();
expect(mockSummaryTransformManager.start).toMatchSnapshot();
expect(mockRepository.save).toHaveBeenCalledWith(expect.objectContaining({ enabled: true }));
});
});
@ -52,16 +61,18 @@ describe('ManageSLO', () => {
await manageSLO.disable(slo.id);
expect(mockTransformManager.stop).not.toHaveBeenCalled();
expect(mockSummaryTransformManager.stop).not.toHaveBeenCalled();
expect(mockRepository.save).not.toHaveBeenCalled();
});
it('disables the slo when enabled', async () => {
const slo = createSLO({ enabled: true });
const slo = createSLO({ id: 'irrelevant', enabled: true });
mockRepository.findById.mockResolvedValue(slo);
await manageSLO.disable(slo.id);
expect(mockTransformManager.stop).toHaveBeenCalled();
expect(mockTransformManager.stop).toMatchSnapshot();
expect(mockSummaryTransformManager.stop).toMatchSnapshot();
expect(mockRepository.save).toHaveBeenCalledWith(expect.objectContaining({ enabled: false }));
});
});

View file

@ -5,12 +5,16 @@
* 2.0.
*/
import { getSLOTransformId } from '../../../common/slo/constants';
import { getSLOSummaryTransformId, getSLOTransformId } from '../../../common/slo/constants';
import { SLORepository } from './slo_repository';
import { TransformManager } from './transform_manager';
export class ManageSLO {
constructor(private repository: SLORepository, private transformManager: TransformManager) {}
constructor(
private repository: SLORepository,
private transformManager: TransformManager,
private summaryTransformManager: TransformManager
) {}
async enable(sloId: string) {
const slo = await this.repository.findById(sloId);
@ -18,6 +22,7 @@ export class ManageSLO {
return;
}
await this.summaryTransformManager.start(getSLOSummaryTransformId(slo.id, slo.revision));
await this.transformManager.start(getSLOTransformId(slo.id, slo.revision));
slo.enabled = true;
slo.updatedAt = new Date();
@ -30,6 +35,7 @@ export class ManageSLO {
return;
}
await this.summaryTransformManager.stop(getSLOSummaryTransformId(slo.id, slo.revision));
await this.transformManager.stop(getSLOTransformId(slo.id, slo.revision));
slo.enabled = false;
slo.updatedAt = new Date();

View file

@ -10,7 +10,6 @@ import { SLIClient } from '../sli_client';
import { SLORepository } from '../slo_repository';
import { SummaryClient } from '../summary_client';
import { SummarySearchClient } from '../summary_search_client';
import { SummaryTransformInstaller } from '../summary_transform/summary_transform_installer';
import { TransformManager } from '../transform_manager';
const createResourceInstallerMock = (): jest.Mocked<ResourceInstaller> => {
@ -19,13 +18,17 @@ const createResourceInstallerMock = (): jest.Mocked<ResourceInstaller> => {
};
};
const createSummaryTransformInstallerMock = (): jest.Mocked<SummaryTransformInstaller> => {
const createTransformManagerMock = (): jest.Mocked<TransformManager> => {
return {
installAndStart: jest.fn(),
install: jest.fn(),
preview: jest.fn(),
uninstall: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
};
const createTransformManagerMock = (): jest.Mocked<TransformManager> => {
const createSummaryTransformManagerMock = (): jest.Mocked<TransformManager> => {
return {
install: jest.fn(),
preview: jest.fn(),
@ -65,8 +68,8 @@ const createSLIClientMock = (): jest.Mocked<SLIClient> => {
export {
createResourceInstallerMock,
createSummaryTransformInstallerMock,
createTransformManagerMock,
createSummaryTransformManagerMock,
createSLORepositoryMock,
createSummaryClientMock,
createSummarySearchClientMock,

View file

@ -0,0 +1,87 @@
/*
* 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 { ElasticsearchClient } from '@kbn/core/server';
import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks';
import { MockedLogger } from '@kbn/logging-mocks';
import { SLO_MODEL_VERSION } from '../../../common/slo/constants';
import { createSLO } from './fixtures/slo';
import {
createSLORepositoryMock,
createSummaryTransformManagerMock,
createTransformManagerMock,
} from './mocks';
import { ResetSLO } from './reset_slo';
import { SLORepository } from './slo_repository';
import { TransformManager } from './transform_manager';
const TEST_DATE = new Date('2023-01-01T00:00:00.000Z');
describe('ResetSLO', () => {
let mockRepository: jest.Mocked<SLORepository>;
let mockTransformManager: jest.Mocked<TransformManager>;
let mockSummaryTransformManager: jest.Mocked<TransformManager>;
let mockEsClient: jest.Mocked<ElasticsearchClient>;
let loggerMock: jest.Mocked<MockedLogger>;
let resetSLO: ResetSLO;
beforeEach(() => {
loggerMock = loggingSystemMock.createLogger();
mockRepository = createSLORepositoryMock();
mockTransformManager = createTransformManagerMock();
mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
mockSummaryTransformManager = createSummaryTransformManagerMock();
resetSLO = new ResetSLO(
mockEsClient,
mockRepository,
mockTransformManager,
mockSummaryTransformManager,
loggerMock,
'some-space'
);
jest.useFakeTimers().setSystemTime(TEST_DATE);
});
afterAll(() => {
jest.useRealTimers();
});
it('resets all associated resources', async () => {
const slo = createSLO({ id: 'irrelevant', version: 1 });
mockRepository.findById.mockResolvedValueOnce(slo);
mockRepository.save.mockImplementation((v) => Promise.resolve(v));
await resetSLO.execute(slo.id);
// delete existing resources and data
expect(mockSummaryTransformManager.stop).toMatchSnapshot();
expect(mockSummaryTransformManager.uninstall).toMatchSnapshot();
expect(mockTransformManager.stop).toMatchSnapshot();
expect(mockTransformManager.uninstall).toMatchSnapshot();
expect(mockEsClient.deleteByQuery).toMatchSnapshot();
// install resources
expect(mockSummaryTransformManager.install).toMatchSnapshot();
expect(mockSummaryTransformManager.start).toMatchSnapshot();
expect(mockEsClient.ingest.putPipeline).toMatchSnapshot();
expect(mockTransformManager.install).toMatchSnapshot();
expect(mockTransformManager.start).toMatchSnapshot();
expect(mockEsClient.index).toMatchSnapshot();
expect(mockRepository.save).toHaveBeenCalledWith({
...slo,
version: SLO_MODEL_VERSION,
updatedAt: expect.anything(),
});
});
});

View file

@ -0,0 +1,130 @@
/*
* 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 { ElasticsearchClient, Logger } from '@kbn/core/server';
import { resetSLOResponseSchema } from '@kbn/slo-schema';
import {
getSLOSummaryPipelineId,
getSLOSummaryTransformId,
getSLOTransformId,
SLO_DESTINATION_INDEX_PATTERN,
SLO_MODEL_VERSION,
SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
SLO_SUMMARY_TEMP_INDEX_NAME,
} from '../../../common/slo/constants';
import { getSLOSummaryPipelineTemplate } from '../../assets/ingest_templates/slo_summary_pipeline_template';
import { retryTransientEsErrors } from '../../utils/retry';
import { SLORepository } from './slo_repository';
import { createTempSummaryDocument } from './summary_transform_generator/helpers/create_temp_summary';
import { TransformManager } from './transform_manager';
export class ResetSLO {
constructor(
private esClient: ElasticsearchClient,
private repository: SLORepository,
private transformManager: TransformManager,
private summaryTransformManager: TransformManager,
private logger: Logger,
private spaceId: string
) {}
public async execute(sloId: string) {
const slo = await this.repository.findById(sloId);
const summaryTransformId = getSLOSummaryTransformId(slo.id, slo.revision);
await this.summaryTransformManager.stop(summaryTransformId);
await this.summaryTransformManager.uninstall(summaryTransformId);
const rollupTransformId = getSLOTransformId(slo.id, slo.revision);
await this.transformManager.stop(rollupTransformId);
await this.transformManager.uninstall(rollupTransformId);
await Promise.all([this.deleteRollupData(slo.id), this.deleteSummaryData(slo.id)]);
try {
await this.transformManager.install(slo);
await this.transformManager.start(rollupTransformId);
await retryTransientEsErrors(
() => this.esClient.ingest.putPipeline(getSLOSummaryPipelineTemplate(slo, this.spaceId)),
{ logger: this.logger }
);
await this.summaryTransformManager.install(slo);
await this.summaryTransformManager.start(summaryTransformId);
await retryTransientEsErrors(
() =>
this.esClient.index({
index: SLO_SUMMARY_TEMP_INDEX_NAME,
id: `slo-${slo.id}`,
document: createTempSummaryDocument(slo, this.spaceId),
refresh: true,
}),
{ logger: this.logger }
);
} catch (err) {
this.logger.error(
`Cannot reset the SLO [id: ${slo.id}, revision: ${slo.revision}]. Rolling back.`
);
await this.summaryTransformManager.stop(summaryTransformId);
await this.summaryTransformManager.uninstall(summaryTransformId);
await this.transformManager.stop(rollupTransformId);
await this.transformManager.uninstall(rollupTransformId);
await this.esClient.ingest.deletePipeline(
{ id: getSLOSummaryPipelineId(slo.id, slo.revision) },
{ ignore: [404] }
);
throw err;
}
const updatedSlo = await this.repository.save({
...slo,
version: SLO_MODEL_VERSION,
updatedAt: new Date(),
});
return resetSLOResponseSchema.encode(updatedSlo);
}
/**
* Deleting all SLI rollup data matching the sloId. All revision will be deleted in case of
* residual documents.
*
* @param sloId
*/
private async deleteRollupData(sloId: string): Promise<void> {
await this.esClient.deleteByQuery({
index: SLO_DESTINATION_INDEX_PATTERN,
refresh: true,
query: {
bool: {
filter: [{ term: { 'slo.id': sloId } }],
},
},
});
}
/**
* Deleting the summary documents matching the sloId. All revision will be deleted in case of
* residual documents.
*
* @param sloId
*/
private async deleteSummaryData(sloId: string): Promise<void> {
await this.esClient.deleteByQuery({
index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
refresh: true,
query: {
bool: {
filter: [{ term: { 'slo.id': sloId } }],
},
},
});
}
}

View file

@ -15,7 +15,6 @@ import {
SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME,
SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME,
SLO_SUMMARY_INDEX_TEMPLATE_NAME,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
} from '../../../common/slo/constants';
import { DefaultResourceInstaller } from './resource_installer';
@ -54,14 +53,10 @@ describe('resourceInstaller', () => {
expect.objectContaining({ name: SLO_SUMMARY_INDEX_TEMPLATE_NAME })
);
expect(mockClusterClient.ingest.putPipeline).toHaveBeenCalledTimes(2);
expect(mockClusterClient.ingest.putPipeline).toHaveBeenCalledTimes(1);
expect(mockClusterClient.ingest.putPipeline).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ id: SLO_INGEST_PIPELINE_NAME })
);
expect(mockClusterClient.ingest.putPipeline).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ id: SLO_SUMMARY_INGEST_PIPELINE_NAME })
);
});
});

View file

@ -28,13 +28,11 @@ import {
SLO_SUMMARY_DESTINATION_INDEX_NAME,
SLO_SUMMARY_INDEX_TEMPLATE_NAME,
SLO_SUMMARY_INDEX_TEMPLATE_PATTERN,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
SLO_SUMMARY_TEMP_INDEX_NAME,
} from '../../../common/slo/constants';
import { getSLOIndexTemplate } from '../../assets/index_templates/slo_index_templates';
import { getSLOSummaryIndexTemplate } from '../../assets/index_templates/slo_summary_index_templates';
import { getSLOPipelineTemplate } from '../../assets/ingest_templates/slo_pipeline_template';
import { getSLOSummaryPipelineTemplate } from '../../assets/ingest_templates/slo_summary_pipeline_template';
import { retryTransientEsErrors } from '../../utils/retry';
export interface ResourceInstaller {
@ -87,10 +85,6 @@ export class DefaultResourceInstaller implements ResourceInstaller {
await this.createOrUpdateIngestPipelineTemplate(
getSLOPipelineTemplate(SLO_INGEST_PIPELINE_NAME, SLO_INGEST_PIPELINE_INDEX_NAME_PREFIX)
);
await this.createOrUpdateIngestPipelineTemplate(
getSLOSummaryPipelineTemplate(SLO_SUMMARY_INGEST_PIPELINE_NAME)
);
} catch (err) {
this.logger.error(`Error installing resources shared for SLO: ${err.message}`);
throw err;

View file

@ -7,7 +7,7 @@
import { loggingSystemMock } from '@kbn/core/server/mocks';
import { MockedLogger } from '@kbn/logging-mocks';
import { createResourceInstallerMock, createSummaryTransformInstallerMock } from './mocks';
import { createResourceInstallerMock } from './mocks';
import { DefaultSLOInstaller } from './slo_installer';
describe('SLO Installer', () => {
@ -19,16 +19,10 @@ describe('SLO Installer', () => {
it.skip('handles concurrent installation', async () => {
const resourceInstaller = createResourceInstallerMock();
const summaryTransformInstaller = createSummaryTransformInstallerMock();
const service = new DefaultSLOInstaller(
resourceInstaller,
summaryTransformInstaller,
loggerMock
);
const service = new DefaultSLOInstaller(resourceInstaller, loggerMock);
await Promise.all([service.install(), service.install()]);
expect(resourceInstaller.ensureCommonResourcesInstalled).toHaveBeenCalledTimes(1);
expect(summaryTransformInstaller.installAndStart).toHaveBeenCalledTimes(1);
});
});

View file

@ -6,7 +6,7 @@
*/
import { Logger } from '@kbn/core/server';
import { ResourceInstaller, SummaryTransformInstaller } from '.';
import { ResourceInstaller } from '.';
export interface SLOInstaller {
install(): Promise<void>;
@ -15,11 +15,7 @@ export interface SLOInstaller {
export class DefaultSLOInstaller implements SLOInstaller {
private isInstalling: boolean = false;
constructor(
private sloResourceInstaller: ResourceInstaller,
private sloSummaryInstaller: SummaryTransformInstaller,
private logger: Logger
) {}
constructor(private sloResourceInstaller: ResourceInstaller, private logger: Logger) {}
public async install() {
if (this.isInstalling) {
@ -32,9 +28,8 @@ export class DefaultSLOInstaller implements SLOInstaller {
installTimeout = setTimeout(() => (this.isInstalling = false), 60000);
await this.sloResourceInstaller.ensureCommonResourcesInstalled();
await this.sloSummaryInstaller.installAndStart();
} catch (error) {
this.logger.error('Failed to install SLO common resources and summary transforms');
this.logger.error('Failed to install SLO common resources');
} finally {
this.isInstalling = false;
clearTimeout(installTimeout);

View file

@ -8,6 +8,7 @@
import { SavedObjectsClientContract, SavedObjectsFindResponse } from '@kbn/core/server';
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
import { sloSchema } from '@kbn/slo-schema';
import { SLO_MODEL_VERSION } from '../../../common/slo/constants';
import { SLO, StoredSLO } from '../../domain/models';
import { SLOIdConflict, SLONotFound } from '../../errors';
import { SO_SLO_TYPE } from '../../saved_objects';
@ -164,19 +165,42 @@ describe('KibanaSavedObjectsSLORepository', () => {
expect(soClientMock.delete).toHaveBeenCalledWith(SO_SLO_TYPE, SOME_SLO.id);
});
it('searches by name', async () => {
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
soClientMock.find.mockResolvedValueOnce(soFindResponse([SOME_SLO, ANOTHER_SLO]));
describe('search', () => {
it('searches by name', async () => {
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
soClientMock.find.mockResolvedValueOnce(soFindResponse([SOME_SLO, ANOTHER_SLO]));
const results = await repository.search(SOME_SLO.name);
const results = await repository.search(SOME_SLO.name, { page: 1, perPage: 100 });
expect(results).toEqual([SOME_SLO, ANOTHER_SLO]);
expect(soClientMock.find).toHaveBeenCalledWith({
type: SO_SLO_TYPE,
page: 1,
perPage: 25,
search: SOME_SLO.name,
searchFields: ['name'],
expect(results.results).toEqual([SOME_SLO, ANOTHER_SLO]);
expect(soClientMock.find).toHaveBeenCalledWith({
type: SO_SLO_TYPE,
page: 1,
perPage: 100,
search: SOME_SLO.name,
searchFields: ['name'],
});
});
it('searches only the outdated ones', async () => {
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
soClientMock.find.mockResolvedValueOnce(soFindResponse([SOME_SLO, ANOTHER_SLO]));
const results = await repository.search(
SOME_SLO.name,
{ page: 1, perPage: 100 },
{ includeOutdatedOnly: true }
);
expect(results.results).toEqual([SOME_SLO, ANOTHER_SLO]);
expect(soClientMock.find).toHaveBeenCalledWith({
type: SO_SLO_TYPE,
page: 1,
perPage: 100,
search: SOME_SLO.name,
searchFields: ['name'],
filter: `slo.attributes.version < ${SLO_MODEL_VERSION}`,
});
});
});
});

View file

@ -7,10 +7,11 @@
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server';
import { sloSchema } from '@kbn/slo-schema';
import { Paginated, Pagination, sloSchema } from '@kbn/slo-schema';
import { fold } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/pipeable';
import * as t from 'io-ts';
import { SLO_MODEL_VERSION } from '../../../common/slo/constants';
import { SLO, StoredSLO } from '../../domain/models';
import { SLOIdConflict, SLONotFound } from '../../errors';
import { SO_SLO_TYPE } from '../../saved_objects';
@ -20,7 +21,11 @@ export interface SLORepository {
findAllByIds(ids: string[]): Promise<SLO[]>;
findById(id: string): Promise<SLO>;
deleteById(id: string): Promise<void>;
search(search: string): Promise<SLO[]>;
search(
search: string,
pagination: Pagination,
options?: { includeOutdatedOnly?: boolean }
): Promise<Paginated<SLO>>;
}
export class KibanaSavedObjectsSLORepository implements SLORepository {
@ -99,19 +104,28 @@ export class KibanaSavedObjectsSLORepository implements SLORepository {
}
}
async search(search: string): Promise<SLO[]> {
try {
const response = await this.soClient.find<StoredSLO>({
type: SO_SLO_TYPE,
page: 1,
perPage: 25,
search,
searchFields: ['name'],
});
return response.saved_objects.map((slo) => toSLO(slo.attributes));
} catch (err) {
throw err;
}
async search(
search: string,
pagination: Pagination,
options: { includeOutdatedOnly?: boolean } = { includeOutdatedOnly: false }
): Promise<Paginated<SLO>> {
const response = await this.soClient.find<StoredSLO>({
type: SO_SLO_TYPE,
page: pagination.page,
perPage: pagination.perPage,
search,
searchFields: ['name'],
...(!!options.includeOutdatedOnly && {
filter: `slo.attributes.version < ${SLO_MODEL_VERSION}`,
}),
});
return {
total: response.total,
perPage: response.per_page,
page: response.page,
results: response.saved_objects.map((slo) => toSLO(slo.attributes)),
};
}
}
@ -121,7 +135,13 @@ function toStoredSLO(slo: SLO): StoredSLO {
function toSLO(storedSLO: StoredSLO): SLO {
return pipe(
sloSchema.decode(storedSLO),
sloSchema.decode({
...storedSLO,
// version was added in 8.12.0. This is a safeguard against SO migration issue.
// if not present, we considered the version to be 1, e.g. not migrated.
// We would need to call the _reset api on this SLO.
version: storedSLO.version ?? 1,
}),
fold(() => {
throw new Error('Invalid Stored SLO');
}, t.identity)

View file

@ -7,17 +7,13 @@
import { ElasticsearchClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
import { loggerMock } from '@kbn/logging-mocks';
import { Pagination } from '@kbn/slo-schema/src/models/pagination';
import {
aHitFromSummaryIndex,
aHitFromTempSummaryIndex,
aSummaryDocument,
} from './fixtures/summary_search_document';
import {
DefaultSummarySearchClient,
Pagination,
Sort,
SummarySearchClient,
} from './summary_search_client';
import { DefaultSummarySearchClient, Sort, SummarySearchClient } from './summary_search_client';
const defaultSort: Sort = {
field: 'sli_value',
@ -34,7 +30,7 @@ describe('Summary Search Client', () => {
beforeEach(() => {
esClientMock = elasticsearchServiceMock.createElasticsearchClient();
service = new DefaultSummarySearchClient(esClientMock, loggerMock.create());
service = new DefaultSummarySearchClient(esClientMock, loggerMock.create(), 'some-space');
});
it('returns an empty response on error', async () => {

View file

@ -6,7 +6,7 @@
*/
import { ElasticsearchClient, Logger } from '@kbn/core/server';
import { ALL_VALUE } from '@kbn/slo-schema';
import { ALL_VALUE, Paginated, Pagination } from '@kbn/slo-schema';
import { assertNever } from '@kbn/std';
import _ from 'lodash';
import { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types';
@ -31,13 +31,6 @@ interface EsSummaryDocument {
isTempDoc: boolean;
}
export interface Paginated<T> {
total: number;
page: number;
perPage: number;
results: T[];
}
export interface SLOSummary {
id: SLOId;
instanceId: string;
@ -50,17 +43,16 @@ export interface Sort {
direction: 'asc' | 'desc';
}
export interface Pagination {
page: number;
perPage: number;
}
export interface SummarySearchClient {
search(kqlQuery: string, sort: Sort, pagination: Pagination): Promise<Paginated<SLOSummary>>;
}
export class DefaultSummarySearchClient implements SummarySearchClient {
constructor(private esClient: ElasticsearchClient, private logger: Logger) {}
constructor(
private esClient: ElasticsearchClient,
private logger: Logger,
private spaceId: string
) {}
async search(
kqlQuery: string,
@ -71,7 +63,11 @@ export class DefaultSummarySearchClient implements SummarySearchClient {
const summarySearch = await this.esClient.search<EsSummaryDocument>({
index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
track_total_hits: true,
query: getElastichsearchQueryOrThrow(kqlQuery),
query: {
bool: {
filter: [{ term: { spaceId: this.spaceId } }, getElastichsearchQueryOrThrow(kqlQuery)],
},
},
sort: {
// non-temp first, then temp documents
isTempDoc: {

View file

@ -1,103 +0,0 @@
/*
* 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 {
ElasticsearchClientMock,
elasticsearchServiceMock,
loggingSystemMock,
} from '@kbn/core/server/mocks';
import { MockedLogger } from '@kbn/logging-mocks';
import { DefaultSummaryTransformInstaller } from './summary_transform_installer';
import { ALL_TRANSFORM_TEMPLATES } from './templates';
describe('Summary Transform Installer', () => {
let esClientMock: ElasticsearchClientMock;
let loggerMock: jest.Mocked<MockedLogger>;
beforeEach(() => {
esClientMock = elasticsearchServiceMock.createElasticsearchClient();
loggerMock = loggingSystemMock.createLogger();
});
it('skips the installation when latest version already installed', async () => {
esClientMock.transform.getTransform.mockResolvedValue({
count: ALL_TRANSFORM_TEMPLATES.length,
// @ts-ignore
transforms: ALL_TRANSFORM_TEMPLATES.map((transform) => ({
id: transform.transform_id,
_meta: transform._meta,
})),
});
const installer = new DefaultSummaryTransformInstaller(esClientMock, loggerMock);
await installer.installAndStart();
expect(esClientMock.transform.stopTransform).not.toHaveBeenCalled();
expect(esClientMock.transform.deleteTransform).not.toHaveBeenCalled();
expect(esClientMock.transform.putTransform).not.toHaveBeenCalled();
expect(esClientMock.transform.startTransform).not.toHaveBeenCalled();
});
it('installs every summary transforms when none are already installed', async () => {
esClientMock.transform.getTransform.mockResolvedValue({ count: 0, transforms: [] });
const installer = new DefaultSummaryTransformInstaller(esClientMock, loggerMock);
await installer.installAndStart();
const nbOfTransforms = ALL_TRANSFORM_TEMPLATES.length;
expect(esClientMock.transform.stopTransform).not.toHaveBeenCalled();
expect(esClientMock.transform.deleteTransform).not.toHaveBeenCalled();
expect(esClientMock.transform.putTransform).toHaveBeenCalledTimes(nbOfTransforms);
expect(esClientMock.transform.startTransform).toHaveBeenCalledTimes(nbOfTransforms);
});
it('desinstalls previous summary transforms prior to installing the new ones', async () => {
esClientMock.transform.getTransform.mockResolvedValue({
count: ALL_TRANSFORM_TEMPLATES.length,
// @ts-ignore
transforms: ALL_TRANSFORM_TEMPLATES.map((transform) => ({
id: transform.transform_id,
_meta: { ...transform._meta, version: -1 },
})),
});
const installer = new DefaultSummaryTransformInstaller(esClientMock, loggerMock);
await installer.installAndStart();
const nbOfTransforms = ALL_TRANSFORM_TEMPLATES.length;
expect(esClientMock.transform.stopTransform).toHaveBeenCalledTimes(nbOfTransforms);
expect(esClientMock.transform.deleteTransform).toHaveBeenCalledTimes(nbOfTransforms);
expect(esClientMock.transform.putTransform).toHaveBeenCalledTimes(nbOfTransforms);
expect(esClientMock.transform.startTransform).toHaveBeenCalledTimes(nbOfTransforms);
});
it('installs only the missing summary transforms', async () => {
const occurrencesSummaryTransforms = ALL_TRANSFORM_TEMPLATES.filter((transform) =>
transform.transform_id.includes('-occurrences-')
);
esClientMock.transform.getTransform.mockResolvedValue({
count: occurrencesSummaryTransforms.length,
// @ts-ignore
transforms: occurrencesSummaryTransforms.map((transform) => ({
id: transform.transform_id,
_meta: transform._meta,
})),
});
const installer = new DefaultSummaryTransformInstaller(esClientMock, loggerMock);
await installer.installAndStart();
const nbOfTransforms = ALL_TRANSFORM_TEMPLATES.length - occurrencesSummaryTransforms.length;
expect(esClientMock.transform.stopTransform).not.toHaveBeenCalled();
expect(esClientMock.transform.deleteTransform).not.toHaveBeenCalled();
expect(esClientMock.transform.putTransform).toHaveBeenCalledTimes(nbOfTransforms);
expect(esClientMock.transform.startTransform).toHaveBeenCalledTimes(nbOfTransforms);
expect(esClientMock.transform.putTransform.mock.calls).toMatchSnapshot();
});
});

View file

@ -1,105 +0,0 @@
/*
* 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
import type { ElasticsearchClient, Logger } from '@kbn/core/server';
import {
SLO_SUMMARY_TRANSFORMS_VERSION,
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
} from '../../../../common/slo/constants';
import { retryTransientEsErrors } from '../../../utils/retry';
import { ALL_TRANSFORM_TEMPLATES } from './templates';
export interface SummaryTransformInstaller {
installAndStart(): Promise<void>;
}
export class DefaultSummaryTransformInstaller implements SummaryTransformInstaller {
constructor(private esClient: ElasticsearchClient, private logger: Logger) {}
public async installAndStart(): Promise<void> {
const allTransformIds = ALL_TRANSFORM_TEMPLATES.map((transform) => transform.transform_id);
const summaryTransforms = await this.execute(() =>
this.esClient.transform.getTransform(
{ transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}*`, allow_no_match: true },
{ ignore: [404] }
)
);
const alreadyInstalled =
summaryTransforms.count === allTransformIds.length &&
summaryTransforms.transforms.every(
(transform) => transform._meta?.version === SLO_SUMMARY_TRANSFORMS_VERSION
) &&
summaryTransforms.transforms.every((transform) => allTransformIds.includes(transform.id));
if (alreadyInstalled) {
this.logger.info(`SLO summary transforms already installed - skipping`);
return;
}
for (const transformTemplate of ALL_TRANSFORM_TEMPLATES) {
const transformId = transformTemplate.transform_id;
const transform = summaryTransforms.transforms.find((t) => t.id === transformId);
const transformAlreadyInstalled =
!!transform && transform._meta?.version === SLO_SUMMARY_TRANSFORMS_VERSION;
const previousTransformAlreadyInstalled =
!!transform && transform._meta?.version !== SLO_SUMMARY_TRANSFORMS_VERSION;
if (transformAlreadyInstalled) {
this.logger.info(`SLO summary transform [${transformId}] already installed - skipping`);
continue;
}
if (previousTransformAlreadyInstalled) {
await this.deletePreviousTransformVersion(transformId);
}
await this.installTransform(transformId, transformTemplate);
await this.startTransform(transformId);
}
this.logger.info(`SLO summary transforms installed and started`);
}
private async installTransform(
transformId: string,
transformTemplate: TransformPutTransformRequest
) {
this.logger.info(`Installing SLO summary transform [${transformId}]`);
await this.execute(() =>
this.esClient.transform.putTransform(transformTemplate, { ignore: [409] })
);
}
private async deletePreviousTransformVersion(transformId: string) {
this.logger.info(`Deleting previous SLO summary transform [${transformId}]`);
await this.execute(() =>
this.esClient.transform.stopTransform(
{ transform_id: transformId, allow_no_match: true, force: true },
{ ignore: [409, 404] }
)
);
await this.execute(() =>
this.esClient.transform.deleteTransform(
{ transform_id: transformId, force: true },
{ ignore: [409, 404] }
)
);
}
private async startTransform(transformId: string) {
this.logger.info(`Starting SLO summary transform [${transformId}]`);
await this.execute(() =>
this.esClient.transform.startTransform({ transform_id: transformId }, { ignore: [409] })
);
}
private async execute<T>(esCall: () => Promise<T>): Promise<T> {
return await retryTransientEsErrors(esCall, { logger: this.logger });
}
}

View file

@ -1,100 +0,0 @@
/*
* 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.
*/
export const groupBy = {
'slo.id': {
terms: {
field: 'slo.id',
},
},
'slo.revision': {
terms: {
field: 'slo.revision',
},
},
'slo.groupBy': {
terms: {
field: 'slo.groupBy',
},
},
'slo.instanceId': {
terms: {
field: 'slo.instanceId',
},
},
'slo.name': {
terms: {
field: 'slo.name',
},
},
'slo.description': {
terms: {
field: 'slo.description',
},
},
'slo.tags': {
terms: {
field: 'slo.tags',
},
},
'slo.indicator.type': {
terms: {
field: 'slo.indicator.type',
},
},
'slo.budgetingMethod': {
terms: {
field: 'slo.budgetingMethod',
},
},
'slo.timeWindow.duration': {
terms: {
field: 'slo.timeWindow.duration',
},
},
'slo.timeWindow.type': {
terms: {
field: 'slo.timeWindow.type',
},
},
errorBudgetEstimated: {
terms: {
field: 'errorBudgetEstimated',
},
},
// Differentiate the temporary document from the summary one
isTempDoc: {
terms: {
field: 'isTempDoc',
},
},
// optional fields: only specified for APM indicators. Must include missing_bucket:true
'service.name': {
terms: {
field: 'service.name',
missing_bucket: true,
},
},
'service.environment': {
terms: {
field: 'service.environment',
missing_bucket: true,
},
},
'transaction.name': {
terms: {
field: 'transaction.name',
missing_bucket: true,
},
},
'transaction.type': {
terms: {
field: 'transaction.type',
missing_bucket: true,
},
},
};

View file

@ -1,30 +0,0 @@
/*
* 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 { SUMMARY_OCCURRENCES_7D_ROLLING } from './summary_occurrences_7d_rolling';
import { SUMMARY_OCCURRENCES_30D_ROLLING } from './summary_occurrences_30d_rolling';
import { SUMMARY_OCCURRENCES_90D_ROLLING } from './summary_occurrences_90d_rolling';
import { SUMMARY_TIMESLICES_7D_ROLLING } from './summary_timeslices_7d_rolling';
import { SUMMARY_TIMESLICES_30D_ROLLING } from './summary_timeslices_30d_rolling';
import { SUMMARY_TIMESLICES_90D_ROLLING } from './summary_timeslices_90d_rolling';
import { SUMMARY_OCCURRENCES_WEEKLY_ALIGNED } from './summary_occurrences_weekly_aligned';
import { SUMMARY_OCCURRENCES_MONTHLY_ALIGNED } from './summary_occurrences_monthly_aligned';
import { SUMMARY_TIMESLICES_WEEKLY_ALIGNED } from './summary_timeslices_weekly_aligned';
import { SUMMARY_TIMESLICES_MONTHLY_ALIGNED } from './summary_timeslices_monthly_aligned';
export const ALL_TRANSFORM_TEMPLATES = [
SUMMARY_OCCURRENCES_7D_ROLLING,
SUMMARY_OCCURRENCES_30D_ROLLING,
SUMMARY_OCCURRENCES_90D_ROLLING,
SUMMARY_OCCURRENCES_WEEKLY_ALIGNED,
SUMMARY_OCCURRENCES_MONTHLY_ALIGNED,
SUMMARY_TIMESLICES_7D_ROLLING,
SUMMARY_TIMESLICES_30D_ROLLING,
SUMMARY_TIMESLICES_90D_ROLLING,
SUMMARY_TIMESLICES_WEEKLY_ALIGNED,
SUMMARY_TIMESLICES_MONTHLY_ALIGNED,
];

View file

@ -1,153 +0,0 @@
/*
* 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
import {
SLO_DESTINATION_INDEX_PATTERN,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
SLO_SUMMARY_TRANSFORMS_VERSION,
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
} from '../../../../../common/slo/constants';
import { groupBy } from './common';
export const SUMMARY_OCCURRENCES_30D_ROLLING: TransformPutTransformRequest = {
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}occurrences-30d-rolling`,
dest: {
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
runtime_mappings: {
errorBudgetEstimated: {
type: 'boolean',
script: 'emit(false)',
},
isTempDoc: {
type: 'boolean',
script: 'emit(false)',
},
},
query: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: 'now-30d/m',
lte: 'now/m',
},
},
},
{
term: {
'slo.budgetingMethod': 'occurrences',
},
},
{
term: {
'slo.timeWindow.type': 'rolling',
},
},
{
term: {
'slo.timeWindow.duration': '30d',
},
},
],
},
},
},
pivot: {
group_by: groupBy,
aggregations: {
goodEvents: {
sum: {
field: 'slo.numerator',
},
},
totalEvents: {
sum: {
field: 'slo.denominator',
},
},
_objectiveTarget: {
max: {
field: 'slo.objective.target',
},
},
sliValue: {
bucket_script: {
buckets_path: {
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 }',
},
},
errorBudgetInitial: {
bucket_script: {
buckets_path: {
objectiveTarget: '_objectiveTarget',
},
script: '1 - params.objectiveTarget',
},
},
errorBudgetConsumed: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
errorBudgetInitial: 'errorBudgetInitial',
},
script:
'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }',
},
},
errorBudgetRemaining: {
bucket_script: {
buckets_path: {
errorBudgetConsummed: 'errorBudgetConsumed',
},
script: '1 - params.errorBudgetConsummed',
},
},
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
objectiveTarget: '_objectiveTarget',
errorBudgetRemaining: 'errorBudgetRemaining',
},
script: {
source:
'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objectiveTarget) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }',
},
},
},
},
},
description:
'Summarize every SLO with occurrences budgeting method and a 30 days rolling time window',
frequency: '1m',
sync: {
time: {
field: '@timestamp',
delay: '125s',
},
},
settings: {
deduce_mappings: false,
unattended: true,
},
_meta: {
version: SLO_SUMMARY_TRANSFORMS_VERSION,
managed: true,
managed_by: 'observability',
},
};

View file

@ -1,153 +0,0 @@
/*
* 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
import {
SLO_DESTINATION_INDEX_PATTERN,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
SLO_SUMMARY_TRANSFORMS_VERSION,
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
} from '../../../../../common/slo/constants';
import { groupBy } from './common';
export const SUMMARY_OCCURRENCES_7D_ROLLING: TransformPutTransformRequest = {
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}occurrences-7d-rolling`,
dest: {
pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME,
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
runtime_mappings: {
errorBudgetEstimated: {
type: 'boolean',
script: 'emit(false)',
},
isTempDoc: {
type: 'boolean',
script: 'emit(false)',
},
},
query: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: 'now-7d/m',
lte: 'now/m',
},
},
},
{
term: {
'slo.budgetingMethod': 'occurrences',
},
},
{
term: {
'slo.timeWindow.type': 'rolling',
},
},
{
term: {
'slo.timeWindow.duration': '7d',
},
},
],
},
},
},
pivot: {
group_by: groupBy,
aggregations: {
goodEvents: {
sum: {
field: 'slo.numerator',
},
},
totalEvents: {
sum: {
field: 'slo.denominator',
},
},
_objectiveTarget: {
max: {
field: 'slo.objective.target',
},
},
sliValue: {
bucket_script: {
buckets_path: {
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 }',
},
},
errorBudgetInitial: {
bucket_script: {
buckets_path: {
objectiveTarget: '_objectiveTarget',
},
script: '1 - params.objectiveTarget',
},
},
errorBudgetConsumed: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
errorBudgetInitial: 'errorBudgetInitial',
},
script:
'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }',
},
},
errorBudgetRemaining: {
bucket_script: {
buckets_path: {
errorBudgetConsummed: 'errorBudgetConsumed',
},
script: '1 - params.errorBudgetConsummed',
},
},
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
objectiveTarget: '_objectiveTarget',
errorBudgetRemaining: 'errorBudgetRemaining',
},
script: {
source:
'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objectiveTarget) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }',
},
},
},
},
},
description:
'Summarize every SLO with occurrences budgeting method and a 7 days rolling time window',
frequency: '1m',
sync: {
time: {
field: '@timestamp',
delay: '125s',
},
},
settings: {
deduce_mappings: false,
unattended: true,
},
_meta: {
version: SLO_SUMMARY_TRANSFORMS_VERSION,
managed: true,
managed_by: 'observability',
},
};

View file

@ -1,153 +0,0 @@
/*
* 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
import {
SLO_DESTINATION_INDEX_PATTERN,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
SLO_SUMMARY_TRANSFORMS_VERSION,
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
} from '../../../../../common/slo/constants';
import { groupBy } from './common';
export const SUMMARY_OCCURRENCES_90D_ROLLING: TransformPutTransformRequest = {
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}occurrences-90d-rolling`,
dest: {
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
runtime_mappings: {
errorBudgetEstimated: {
type: 'boolean',
script: 'emit(false)',
},
isTempDoc: {
type: 'boolean',
script: 'emit(false)',
},
},
query: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: 'now-90d/m',
lte: 'now/m',
},
},
},
{
term: {
'slo.budgetingMethod': 'occurrences',
},
},
{
term: {
'slo.timeWindow.type': 'rolling',
},
},
{
term: {
'slo.timeWindow.duration': '90d',
},
},
],
},
},
},
pivot: {
group_by: groupBy,
aggregations: {
goodEvents: {
sum: {
field: 'slo.numerator',
},
},
totalEvents: {
sum: {
field: 'slo.denominator',
},
},
_objectiveTarget: {
max: {
field: 'slo.objective.target',
},
},
sliValue: {
bucket_script: {
buckets_path: {
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 }',
},
},
errorBudgetInitial: {
bucket_script: {
buckets_path: {
objectiveTarget: '_objectiveTarget',
},
script: '1 - params.objectiveTarget',
},
},
errorBudgetConsumed: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
errorBudgetInitial: 'errorBudgetInitial',
},
script:
'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }',
},
},
errorBudgetRemaining: {
bucket_script: {
buckets_path: {
errorBudgetConsummed: 'errorBudgetConsumed',
},
script: '1 - params.errorBudgetConsummed',
},
},
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
objectiveTarget: '_objectiveTarget',
errorBudgetRemaining: 'errorBudgetRemaining',
},
script: {
source:
'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objectiveTarget) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }',
},
},
},
},
},
description:
'Summarize every SLO with occurrences budgeting method and a 90 days rolling time window',
frequency: '1m',
sync: {
time: {
field: '@timestamp',
delay: '125s',
},
},
settings: {
deduce_mappings: false,
unattended: true,
},
_meta: {
version: SLO_SUMMARY_TRANSFORMS_VERSION,
managed: true,
managed_by: 'observability',
},
};

View file

@ -1,151 +0,0 @@
/*
* 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
import {
SLO_DESTINATION_INDEX_PATTERN,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
SLO_SUMMARY_TRANSFORMS_VERSION,
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
} from '../../../../../common/slo/constants';
import { groupBy } from './common';
export const SUMMARY_OCCURRENCES_MONTHLY_ALIGNED: TransformPutTransformRequest = {
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}occurrences-monthly-aligned`,
dest: {
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
runtime_mappings: {
errorBudgetEstimated: {
type: 'boolean',
script: 'emit(true)',
},
isTempDoc: {
type: 'boolean',
script: 'emit(false)',
},
},
query: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: 'now/M',
lte: 'now/m',
},
},
},
{
term: {
'slo.budgetingMethod': 'occurrences',
},
},
{
term: {
'slo.timeWindow.type': 'calendarAligned',
},
},
{
term: {
'slo.timeWindow.duration': '1M',
},
},
],
},
},
},
pivot: {
group_by: groupBy,
aggregations: {
_objectiveTarget: {
max: {
field: 'slo.objective.target',
},
},
goodEvents: {
sum: {
field: 'slo.numerator',
},
},
totalEvents: {
sum: {
field: 'slo.denominator',
},
},
sliValue: {
bucket_script: {
buckets_path: {
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 }',
},
},
errorBudgetInitial: {
bucket_script: {
buckets_path: {
objective: '_objectiveTarget',
},
script: '1 - params.objective',
},
},
errorBudgetConsumed: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
errorBudgetInitial: 'errorBudgetInitial',
},
script:
'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }',
},
},
errorBudgetRemaining: {
bucket_script: {
buckets_path: {
errorBudgetConsumed: 'errorBudgetConsumed',
},
script: '1 - params.errorBudgetConsumed',
},
},
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
objective: '_objectiveTarget',
errorBudgetRemaining: 'errorBudgetRemaining',
},
script:
'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objective) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }',
},
},
},
},
description:
'Summarize every SLO with occurrences budgeting method and a monthly calendar aligned time window',
frequency: '1m',
sync: {
time: {
field: '@timestamp',
delay: '125s',
},
},
settings: {
deduce_mappings: false,
unattended: true,
},
_meta: {
version: SLO_SUMMARY_TRANSFORMS_VERSION,
managed: true,
managed_by: 'observability',
},
};

View file

@ -1,151 +0,0 @@
/*
* 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
import {
SLO_DESTINATION_INDEX_PATTERN,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
SLO_SUMMARY_TRANSFORMS_VERSION,
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
} from '../../../../../common/slo/constants';
import { groupBy } from './common';
export const SUMMARY_OCCURRENCES_WEEKLY_ALIGNED: TransformPutTransformRequest = {
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}occurrences-weekly-aligned`,
dest: {
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
runtime_mappings: {
errorBudgetEstimated: {
type: 'boolean',
script: 'emit(true)',
},
isTempDoc: {
type: 'boolean',
script: 'emit(false)',
},
},
query: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: 'now/w',
lte: 'now/m',
},
},
},
{
term: {
'slo.budgetingMethod': 'occurrences',
},
},
{
term: {
'slo.timeWindow.type': 'calendarAligned',
},
},
{
term: {
'slo.timeWindow.duration': '1w',
},
},
],
},
},
},
pivot: {
group_by: groupBy,
aggregations: {
_objectiveTarget: {
max: {
field: 'slo.objective.target',
},
},
goodEvents: {
sum: {
field: 'slo.numerator',
},
},
totalEvents: {
sum: {
field: 'slo.denominator',
},
},
sliValue: {
bucket_script: {
buckets_path: {
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 }',
},
},
errorBudgetInitial: {
bucket_script: {
buckets_path: {
objective: '_objectiveTarget',
},
script: '1 - params.objective',
},
},
errorBudgetConsumed: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
errorBudgetInitial: 'errorBudgetInitial',
},
script:
'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }',
},
},
errorBudgetRemaining: {
bucket_script: {
buckets_path: {
errorBudgetConsumed: 'errorBudgetConsumed',
},
script: '1 - params.errorBudgetConsumed',
},
},
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
objective: '_objectiveTarget',
errorBudgetRemaining: 'errorBudgetRemaining',
},
script:
'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objective) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }',
},
},
},
},
description:
'Summarize every SLO with occurrences budgeting method and a weekly calendar aligned time window',
frequency: '1m',
sync: {
time: {
field: '@timestamp',
delay: '125s',
},
},
settings: {
deduce_mappings: false,
unattended: true,
},
_meta: {
version: SLO_SUMMARY_TRANSFORMS_VERSION,
managed: true,
managed_by: 'observability',
},
};

View file

@ -1,153 +0,0 @@
/*
* 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
import {
SLO_DESTINATION_INDEX_PATTERN,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
SLO_SUMMARY_TRANSFORMS_VERSION,
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
} from '../../../../../common/slo/constants';
import { groupBy } from './common';
export const SUMMARY_TIMESLICES_30D_ROLLING: TransformPutTransformRequest = {
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}timeslices-30d-rolling`,
dest: {
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
runtime_mappings: {
errorBudgetEstimated: {
type: 'boolean',
script: 'emit(false)',
},
isTempDoc: {
type: 'boolean',
script: 'emit(false)',
},
},
query: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: 'now-30d/m',
lte: 'now/m',
},
},
},
{
term: {
'slo.budgetingMethod': 'timeslices',
},
},
{
term: {
'slo.timeWindow.type': 'rolling',
},
},
{
term: {
'slo.timeWindow.duration': '30d',
},
},
],
},
},
},
pivot: {
group_by: groupBy,
aggregations: {
goodEvents: {
sum: {
field: 'slo.isGoodSlice',
},
},
totalEvents: {
value_count: {
field: 'slo.isGoodSlice',
},
},
_objectiveTarget: {
max: {
field: 'slo.objective.target',
},
},
sliValue: {
bucket_script: {
buckets_path: {
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 }',
},
},
errorBudgetInitial: {
bucket_script: {
buckets_path: {
objectiveTarget: '_objectiveTarget',
},
script: '1 - params.objectiveTarget',
},
},
errorBudgetConsumed: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
errorBudgetInitial: 'errorBudgetInitial',
},
script:
'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }',
},
},
errorBudgetRemaining: {
bucket_script: {
buckets_path: {
errorBudgetConsummed: 'errorBudgetConsumed',
},
script: '1 - params.errorBudgetConsummed',
},
},
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
objectiveTarget: '_objectiveTarget',
errorBudgetRemaining: 'errorBudgetRemaining',
},
script: {
source:
'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objectiveTarget) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }',
},
},
},
},
},
description:
'Summarize every SLO with timeslices budgeting method and a 30 days rolling time window',
frequency: '1m',
sync: {
time: {
field: '@timestamp',
delay: '125s',
},
},
settings: {
deduce_mappings: false,
unattended: true,
},
_meta: {
version: SLO_SUMMARY_TRANSFORMS_VERSION,
managed: true,
managed_by: 'observability',
},
};

View file

@ -1,153 +0,0 @@
/*
* 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
import {
SLO_DESTINATION_INDEX_PATTERN,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
SLO_SUMMARY_TRANSFORMS_VERSION,
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
} from '../../../../../common/slo/constants';
import { groupBy } from './common';
export const SUMMARY_TIMESLICES_7D_ROLLING: TransformPutTransformRequest = {
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}timeslices-7d-rolling`,
dest: {
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
runtime_mappings: {
errorBudgetEstimated: {
type: 'boolean',
script: 'emit(false)',
},
isTempDoc: {
type: 'boolean',
script: 'emit(false)',
},
},
query: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: 'now-7d/m',
lte: 'now/m',
},
},
},
{
term: {
'slo.budgetingMethod': 'timeslices',
},
},
{
term: {
'slo.timeWindow.type': 'rolling',
},
},
{
term: {
'slo.timeWindow.duration': '7d',
},
},
],
},
},
},
pivot: {
group_by: groupBy,
aggregations: {
goodEvents: {
sum: {
field: 'slo.isGoodSlice',
},
},
totalEvents: {
value_count: {
field: 'slo.isGoodSlice',
},
},
_objectiveTarget: {
max: {
field: 'slo.objective.target',
},
},
sliValue: {
bucket_script: {
buckets_path: {
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 }',
},
},
errorBudgetInitial: {
bucket_script: {
buckets_path: {
objectiveTarget: '_objectiveTarget',
},
script: '1 - params.objectiveTarget',
},
},
errorBudgetConsumed: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
errorBudgetInitial: 'errorBudgetInitial',
},
script:
'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }',
},
},
errorBudgetRemaining: {
bucket_script: {
buckets_path: {
errorBudgetConsummed: 'errorBudgetConsumed',
},
script: '1 - params.errorBudgetConsummed',
},
},
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
objectiveTarget: '_objectiveTarget',
errorBudgetRemaining: 'errorBudgetRemaining',
},
script: {
source:
'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objectiveTarget) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }',
},
},
},
},
},
description:
'Summarize every SLO with timeslices budgeting method and a 7 days rolling time window',
frequency: '1m',
sync: {
time: {
field: '@timestamp',
delay: '125s',
},
},
settings: {
deduce_mappings: false,
unattended: true,
},
_meta: {
version: SLO_SUMMARY_TRANSFORMS_VERSION,
managed: true,
managed_by: 'observability',
},
};

View file

@ -1,153 +0,0 @@
/*
* 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
import {
SLO_DESTINATION_INDEX_PATTERN,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
SLO_SUMMARY_TRANSFORMS_VERSION,
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
} from '../../../../../common/slo/constants';
import { groupBy } from './common';
export const SUMMARY_TIMESLICES_90D_ROLLING: TransformPutTransformRequest = {
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}timeslices-90d-rolling`,
dest: {
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
runtime_mappings: {
errorBudgetEstimated: {
type: 'boolean',
script: 'emit(false)',
},
isTempDoc: {
type: 'boolean',
script: 'emit(false)',
},
},
query: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: 'now-90d/m',
lte: 'now/m',
},
},
},
{
term: {
'slo.budgetingMethod': 'timeslices',
},
},
{
term: {
'slo.timeWindow.type': 'rolling',
},
},
{
term: {
'slo.timeWindow.duration': '90d',
},
},
],
},
},
},
pivot: {
group_by: groupBy,
aggregations: {
goodEvents: {
sum: {
field: 'slo.isGoodSlice',
},
},
totalEvents: {
value_count: {
field: 'slo.isGoodSlice',
},
},
_objectiveTarget: {
max: {
field: 'slo.objective.target',
},
},
sliValue: {
bucket_script: {
buckets_path: {
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 }',
},
},
errorBudgetInitial: {
bucket_script: {
buckets_path: {
objectiveTarget: '_objectiveTarget',
},
script: '1 - params.objectiveTarget',
},
},
errorBudgetConsumed: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
errorBudgetInitial: 'errorBudgetInitial',
},
script:
'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }',
},
},
errorBudgetRemaining: {
bucket_script: {
buckets_path: {
errorBudgetConsummed: 'errorBudgetConsumed',
},
script: '1 - params.errorBudgetConsummed',
},
},
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
objectiveTarget: '_objectiveTarget',
errorBudgetRemaining: 'errorBudgetRemaining',
},
script: {
source:
'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objectiveTarget) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }',
},
},
},
},
},
description:
'Summarize every SLO with timeslices budgeting method and a 90 days rolling time window',
frequency: '1m',
sync: {
time: {
field: '@timestamp',
delay: '125s',
},
},
settings: {
deduce_mappings: false,
unattended: true,
},
_meta: {
version: SLO_SUMMARY_TRANSFORMS_VERSION,
managed: true,
managed_by: 'observability',
},
};

View file

@ -1,181 +0,0 @@
/*
* 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
import {
SLO_DESTINATION_INDEX_PATTERN,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
SLO_SUMMARY_TRANSFORMS_VERSION,
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
} from '../../../../../common/slo/constants';
import { groupBy } from './common';
export const SUMMARY_TIMESLICES_MONTHLY_ALIGNED: TransformPutTransformRequest = {
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}timeslices-monthly-aligned`,
dest: {
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
runtime_mappings: {
errorBudgetEstimated: {
type: 'boolean',
script: 'emit(false)',
},
isTempDoc: {
type: 'boolean',
script: 'emit(false)',
},
},
query: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: 'now/M',
lte: 'now/m',
},
},
},
{
term: {
'slo.budgetingMethod': 'timeslices',
},
},
{
term: {
'slo.timeWindow.type': 'calendarAligned',
},
},
{
term: {
'slo.timeWindow.duration': '1M',
},
},
],
},
},
},
pivot: {
group_by: groupBy,
aggregations: {
_sliceDurationInSeconds: {
max: {
field: 'slo.objective.sliceDurationInSeconds',
},
},
_totalSlicesInPeriod: {
bucket_script: {
buckets_path: {
sliceDurationInSeconds: '_sliceDurationInSeconds',
},
script: {
source: `
Date d = new Date();
Instant instant = Instant.ofEpochMilli(d.getTime());
LocalDateTime now = LocalDateTime.ofInstant(instant, ZoneOffset.UTC);
LocalDateTime startOfMonth = now
.withDayOfMonth(1)
.withHour(0)
.withMinute(0)
.withSecond(0);
LocalDateTime startOfNextMonth = startOfMonth.plusMonths(1);
double sliceDurationInMinutes = params.sliceDurationInSeconds / 60;
return Math.ceil(Duration.between(startOfMonth, startOfNextMonth).toMinutes() / sliceDurationInMinutes);
`,
},
},
},
_objectiveTarget: {
max: {
field: 'slo.objective.target',
},
},
goodEvents: {
sum: {
field: 'slo.isGoodSlice',
},
},
totalEvents: {
value_count: {
field: 'slo.isGoodSlice',
},
},
sliValue: {
bucket_script: {
buckets_path: {
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 }',
},
},
errorBudgetInitial: {
bucket_script: {
buckets_path: {
objective: '_objectiveTarget',
},
script: '1 - params.objective',
},
},
errorBudgetConsumed: {
bucket_script: {
buckets_path: {
goodEvents: 'goodEvents',
totalEvents: 'totalEvents',
totalSlicesInPeriod: '_totalSlicesInPeriod',
errorBudgetInitial: 'errorBudgetInitial',
},
script:
'if (params.totalEvents == 0) { return 0 } else { return (params.totalEvents - params.goodEvents) / (params.totalSlicesInPeriod * params.errorBudgetInitial) }',
},
},
errorBudgetRemaining: {
bucket_script: {
buckets_path: {
errorBudgetConsumed: 'errorBudgetConsumed',
},
script: '1 - params.errorBudgetConsumed',
},
},
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
objective: '_objectiveTarget',
errorBudgetRemaining: 'errorBudgetRemaining',
},
script:
'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objective) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }',
},
},
},
},
description:
'Summarize every SLO with timeslices budgeting method and a monthly calendar aligned time window',
frequency: '1m',
sync: {
time: {
field: '@timestamp',
delay: '125s',
},
},
settings: {
deduce_mappings: false,
unattended: true,
},
_meta: {
version: SLO_SUMMARY_TRANSFORMS_VERSION,
managed: true,
managed_by: 'observability',
},
};

View file

@ -1,166 +0,0 @@
/*
* 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
import {
SLO_DESTINATION_INDEX_PATTERN,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
SLO_SUMMARY_INGEST_PIPELINE_NAME,
SLO_SUMMARY_TRANSFORMS_VERSION,
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
} from '../../../../../common/slo/constants';
import { groupBy } from './common';
export const SUMMARY_TIMESLICES_WEEKLY_ALIGNED: TransformPutTransformRequest = {
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}timeslices-weekly-aligned`,
dest: {
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
runtime_mappings: {
errorBudgetEstimated: {
type: 'boolean',
script: 'emit(false)',
},
isTempDoc: {
type: 'boolean',
script: 'emit(false)',
},
},
query: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: 'now/w',
lte: 'now/m',
},
},
},
{
term: {
'slo.budgetingMethod': 'timeslices',
},
},
{
term: {
'slo.timeWindow.type': 'calendarAligned',
},
},
{
term: {
'slo.timeWindow.duration': '1w',
},
},
],
},
},
},
pivot: {
group_by: groupBy,
aggregations: {
_sliceDurationInSeconds: {
max: {
field: 'slo.objective.sliceDurationInSeconds',
},
},
_totalSlicesInPeriod: {
bucket_script: {
buckets_path: {
sliceDurationInSeconds: '_sliceDurationInSeconds',
},
script: 'Math.ceil(7 * 24 * 60 * 60 / params.sliceDurationInSeconds)',
},
},
_objectiveTarget: {
max: {
field: 'slo.objective.target',
},
},
goodEvents: {
sum: {
field: 'slo.isGoodSlice',
},
},
totalEvents: {
value_count: {
field: 'slo.isGoodSlice',
},
},
sliValue: {
bucket_script: {
buckets_path: {
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 }',
},
},
errorBudgetInitial: {
bucket_script: {
buckets_path: {
objective: '_objectiveTarget',
},
script: '1 - params.objective',
},
},
errorBudgetConsumed: {
bucket_script: {
buckets_path: {
goodEvents: 'goodEvents',
totalEvents: 'totalEvents',
totalSlicesInPeriod: '_totalSlicesInPeriod',
errorBudgetInitial: 'errorBudgetInitial',
},
script:
'if (params.totalEvents == 0) { return 0 } else { return (params.totalEvents - params.goodEvents) / (params.totalSlicesInPeriod * params.errorBudgetInitial) }',
},
},
errorBudgetRemaining: {
bucket_script: {
buckets_path: {
errorBudgetConsumed: 'errorBudgetConsumed',
},
script: '1 - params.errorBudgetConsumed',
},
},
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
objective: '_objectiveTarget',
errorBudgetRemaining: 'errorBudgetRemaining',
},
script:
'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objective) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }',
},
},
},
},
description:
'Summarize every SLO with timeslices budgeting method and a weekly calendar aligned time window',
frequency: '1m',
sync: {
time: {
field: '@timestamp',
delay: '125s',
},
},
settings: {
deduce_mappings: false,
unattended: true,
},
_meta: {
version: SLO_SUMMARY_TRANSFORMS_VERSION,
managed: true,
managed_by: 'observability',
},
};

View file

@ -0,0 +1,69 @@
/*
* 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 { ALL_VALUE } from '@kbn/slo-schema';
import { SLO } from '../../../../domain/models/slo';
export const getGroupBy = (slo: SLO) => {
const groupings =
slo.groupBy !== '' && slo.groupBy !== ALL_VALUE
? [slo.groupBy].flat().reduce((acc, field) => {
return {
...acc,
[`slo.groupings.${field}`]: {
terms: {
field: `slo.groupings.${field}`,
},
},
};
}, {})
: {};
return {
'slo.id': {
terms: {
field: 'slo.id',
},
},
'slo.revision': {
terms: {
field: 'slo.revision',
},
},
'slo.instanceId': {
terms: {
field: 'slo.instanceId',
},
},
...groupings,
// optional fields: only specified for APM indicators. Must include missing_bucket:true
'service.name': {
terms: {
field: 'service.name',
missing_bucket: true,
},
},
'service.environment': {
terms: {
field: 'service.environment',
missing_bucket: true,
},
},
'transaction.name': {
terms: {
field: 'transaction.name',
missing_bucket: true,
},
},
'transaction.type': {
terms: {
field: 'transaction.type',
missing_bucket: true,
},
},
};
};

View file

@ -0,0 +1,136 @@
/*
* 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
import { SLO } from '../../../../domain/models';
import {
getSLOSummaryPipelineId,
getSLOSummaryTransformId,
SLO_DESTINATION_INDEX_PATTERN,
SLO_RESOURCES_VERSION,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
} from '../../../../../common/slo/constants';
import { getGroupBy } from './common';
export function generateSummaryTransformForOccurrences(slo: SLO): TransformPutTransformRequest {
return {
transform_id: getSLOSummaryTransformId(slo.id, slo.revision),
dest: {
pipeline: getSLOSummaryPipelineId(slo.id, slo.revision),
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
query: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: `now-${slo.timeWindow.duration.format()}/m`,
lte: 'now/m',
},
},
},
{
term: {
'slo.id': slo.id,
},
},
{
term: {
'slo.revision': slo.revision,
},
},
],
},
},
},
pivot: {
group_by: getGroupBy(slo),
aggregations: {
goodEvents: {
sum: {
field: 'slo.numerator',
},
},
totalEvents: {
sum: {
field: 'slo.denominator',
},
},
sliValue: {
bucket_script: {
buckets_path: {
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 }',
},
},
errorBudgetInitial: {
bucket_script: {
buckets_path: {},
script: `1 - ${slo.objective.target}`,
},
},
errorBudgetConsumed: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
errorBudgetInitial: 'errorBudgetInitial',
},
script:
'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }',
},
},
errorBudgetRemaining: {
bucket_script: {
buckets_path: {
errorBudgetConsumed: 'errorBudgetConsumed',
},
script: '1 - params.errorBudgetConsumed',
},
},
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
errorBudgetRemaining: 'errorBudgetRemaining',
},
script: {
source: `if (params.sliValue == -1) { return 0 } else if (params.sliValue >= ${slo.objective.target}) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }`,
},
},
},
latestSliTimestamp: {
max: {
field: '@timestamp',
},
},
},
},
description: `Summarise the rollup data of SLO: ${slo.name} [id: ${slo.id}, revision: ${slo.revision}].`,
frequency: '1m',
sync: {
time: {
field: 'event.ingested',
delay: '65s',
},
},
settings: {
deduce_mappings: false,
unattended: true,
},
_meta: {
version: SLO_RESOURCES_VERSION,
managed: true,
managed_by: 'observability',
},
};
}

View file

@ -0,0 +1,166 @@
/*
* 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
import { DurationUnit, SLO } from '../../../../domain/models';
import {
getSLOSummaryPipelineId,
getSLOSummaryTransformId,
SLO_DESTINATION_INDEX_PATTERN,
SLO_RESOURCES_VERSION,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
} from '../../../../../common/slo/constants';
import { getGroupBy } from './common';
export function generateSummaryTransformForTimeslicesAndCalendarAligned(
slo: SLO
): TransformPutTransformRequest {
const isWeeklyAligned = slo.timeWindow.duration.unit === DurationUnit.Week;
const sliceDurationInSeconds = slo.objective.timesliceWindow!.asSeconds();
return {
transform_id: getSLOSummaryTransformId(slo.id, slo.revision),
dest: {
pipeline: getSLOSummaryPipelineId(slo.id, slo.revision),
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
query: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: isWeeklyAligned ? `now/w` : `now/M`,
lte: 'now/m',
},
},
},
{
term: {
'slo.id': slo.id,
},
},
{
term: {
'slo.revision': slo.revision,
},
},
],
},
},
},
pivot: {
group_by: getGroupBy(slo),
aggregations: {
_totalSlicesInPeriod: {
bucket_script: {
buckets_path: {},
script: {
source: `
if (${isWeeklyAligned} == true) {
return Math.ceil(7 * 24 * 60 * 60 / ${sliceDurationInSeconds});
} else {
Date d = new Date();
Instant instant = Instant.ofEpochMilli(d.getTime());
LocalDateTime now = LocalDateTime.ofInstant(instant, ZoneOffset.UTC);
LocalDateTime startOfMonth = now
.withDayOfMonth(1)
.withHour(0)
.withMinute(0)
.withSecond(0);
LocalDateTime startOfNextMonth = startOfMonth.plusMonths(1);
double sliceDurationInMinutes = ${sliceDurationInSeconds} / 60;
return Math.ceil(Duration.between(startOfMonth, startOfNextMonth).toMinutes() / sliceDurationInMinutes);
}
`,
},
},
},
goodEvents: {
sum: {
field: 'slo.isGoodSlice',
},
},
totalEvents: {
value_count: {
field: 'slo.isGoodSlice',
},
},
sliValue: {
bucket_script: {
buckets_path: {
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 }',
},
},
errorBudgetInitial: {
bucket_script: {
buckets_path: {},
script: `1 - ${slo.objective.target}`,
},
},
errorBudgetConsumed: {
bucket_script: {
buckets_path: {
goodEvents: 'goodEvents',
totalEvents: 'totalEvents',
totalSlicesInPeriod: '_totalSlicesInPeriod',
errorBudgetInitial: 'errorBudgetInitial',
},
script:
'if (params.totalEvents == 0) { return 0 } else { return (params.totalEvents - params.goodEvents) / (params.totalSlicesInPeriod * params.errorBudgetInitial) }',
},
},
errorBudgetRemaining: {
bucket_script: {
buckets_path: {
errorBudgetConsumed: 'errorBudgetConsumed',
},
script: '1 - params.errorBudgetConsumed',
},
},
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
errorBudgetRemaining: 'errorBudgetRemaining',
},
script: `if (params.sliValue == -1) { return 0 } else if (params.sliValue >= ${slo.objective.target}) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }`,
},
},
latestSliTimestamp: {
max: {
field: '@timestamp',
},
},
},
},
description: `Summarise the rollup data of SLO: ${slo.name} [id: ${slo.id}, revision: ${slo.revision}].`,
frequency: '1m',
sync: {
time: {
field: 'event.ingested',
delay: '65s',
},
},
settings: {
deduce_mappings: false,
unattended: true,
},
_meta: {
version: SLO_RESOURCES_VERSION,
managed: true,
managed_by: 'observability',
},
};
}

View file

@ -0,0 +1,138 @@
/*
* 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
import { SLO } from '../../../../domain/models';
import {
getSLOSummaryPipelineId,
getSLOSummaryTransformId,
SLO_DESTINATION_INDEX_PATTERN,
SLO_RESOURCES_VERSION,
SLO_SUMMARY_DESTINATION_INDEX_NAME,
} from '../../../../../common/slo/constants';
import { getGroupBy } from './common';
export function generateSummaryTransformForTimeslicesAndRolling(
slo: SLO
): TransformPutTransformRequest {
return {
transform_id: getSLOSummaryTransformId(slo.id, slo.revision),
dest: {
pipeline: getSLOSummaryPipelineId(slo.id, slo.revision),
index: SLO_SUMMARY_DESTINATION_INDEX_NAME,
},
source: {
index: SLO_DESTINATION_INDEX_PATTERN,
query: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: `now-${slo.timeWindow.duration.format()}/m`,
lte: 'now/m',
},
},
},
{
term: {
'slo.id': slo.id,
},
},
{
term: {
'slo.revision': slo.revision,
},
},
],
},
},
},
pivot: {
group_by: getGroupBy(slo),
aggregations: {
goodEvents: {
sum: {
field: 'slo.isGoodSlice',
},
},
totalEvents: {
value_count: {
field: 'slo.isGoodSlice',
},
},
sliValue: {
bucket_script: {
buckets_path: {
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 }',
},
},
errorBudgetInitial: {
bucket_script: {
buckets_path: {},
script: `1 - ${slo.objective.target}`,
},
},
errorBudgetConsumed: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
errorBudgetInitial: 'errorBudgetInitial',
},
script:
'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }',
},
},
errorBudgetRemaining: {
bucket_script: {
buckets_path: {
errorBudgetConsumed: 'errorBudgetConsumed',
},
script: '1 - params.errorBudgetConsumed',
},
},
statusCode: {
bucket_script: {
buckets_path: {
sliValue: 'sliValue',
errorBudgetRemaining: 'errorBudgetRemaining',
},
script: {
source: `if (params.sliValue == -1) { return 0 } else if (params.sliValue >= ${slo.objective.target}) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }`,
},
},
},
latestSliTimestamp: {
max: {
field: '@timestamp',
},
},
},
},
description: `Summarise the rollup data of SLO: ${slo.name} [id: ${slo.id}, revision: ${slo.revision}].`,
frequency: '1m',
sync: {
time: {
field: 'event.ingested',
delay: '65s',
},
},
settings: {
deduce_mappings: false,
unattended: true,
},
_meta: {
version: SLO_RESOURCES_VERSION,
managed: true,
managed_by: 'observability',
},
};
}

View file

@ -8,7 +8,7 @@
import { ALL_VALUE } from '@kbn/slo-schema';
import { SLO } from '../../../../domain/models';
export function createTempSummaryDocument(slo: SLO) {
export function createTempSummaryDocument(slo: SLO, spaceId: string) {
return {
service: {
environment: null,
@ -33,6 +33,11 @@ export function createTempSummaryDocument(slo: SLO) {
id: slo.id,
budgetingMethod: slo.budgetingMethod,
revision: slo.revision,
objective: {
target: slo.objective.target,
timesliceTarget: slo.objective.timesliceTarget ?? null,
timesliceWindow: slo.objective.timesliceWindow?.format() ?? null,
},
tags: slo.tags,
},
goodEvents: 0,
@ -45,5 +50,6 @@ export function createTempSummaryDocument(slo: SLO) {
statusCode: 0,
status: 'NO_DATA',
isTempDoc: true,
spaceId,
};
}

View file

@ -0,0 +1,30 @@
/*
* 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
import { SLO } from '../../../domain/models';
import { generateSummaryTransformForOccurrences } from './generators/occurrences';
import { generateSummaryTransformForTimeslicesAndRolling } from './generators/timeslices_rolling';
import { generateSummaryTransformForTimeslicesAndCalendarAligned } from './generators/timeslices_calendar_aligned';
export interface SummaryTransformGenerator {
generate(slo: SLO): TransformPutTransformRequest;
}
export class DefaultSummaryTransformGenerator implements SummaryTransformGenerator {
public generate(slo: SLO): TransformPutTransformRequest {
if (slo.budgetingMethod === 'occurrences') {
return generateSummaryTransformForOccurrences(slo);
} else if (slo.budgetingMethod === 'timeslices' && slo.timeWindow.type === 'rolling') {
return generateSummaryTransformForTimeslicesAndRolling(slo);
} else if (slo.budgetingMethod === 'timeslices' && slo.timeWindow.type === 'calendarAligned') {
return generateSummaryTransformForTimeslicesAndCalendarAligned(slo);
}
throw new Error('Not supported SLO');
}
}

View file

@ -0,0 +1,99 @@
/*
* 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 { ElasticsearchClient, Logger } from '@kbn/core/server';
import { SLO } from '../../domain/models';
import { SecurityException } from '../../errors';
import { retryTransientEsErrors } from '../../utils/retry';
import { SummaryTransformGenerator } from './summary_transform_generator/summary_transform_generator';
import { TransformManager } from './transform_manager';
type TransformId = string;
export class DefaultSummaryTransformManager implements TransformManager {
constructor(
private generator: SummaryTransformGenerator,
private esClient: ElasticsearchClient,
private logger: Logger
) {}
async install(slo: SLO): Promise<TransformId> {
const transformParams = this.generator.generate(slo);
try {
await retryTransientEsErrors(() => this.esClient.transform.putTransform(transformParams), {
logger: this.logger,
});
} catch (err) {
this.logger.error(`Cannot create summary transform for SLO [${slo.id}]`);
if (err.meta?.body?.error?.type === 'security_exception') {
throw new SecurityException(err.meta.body.error.reason);
}
throw err;
}
return transformParams.transform_id;
}
async preview(transformId: string): Promise<void> {
try {
await retryTransientEsErrors(
() => this.esClient.transform.previewTransform({ transform_id: transformId }),
{ logger: this.logger }
);
} catch (err) {
this.logger.error(`Cannot preview SLO summary transform [${transformId}]`);
throw err;
}
}
async start(transformId: TransformId): Promise<void> {
try {
await retryTransientEsErrors(
() =>
this.esClient.transform.startTransform({ transform_id: transformId }, { ignore: [409] }),
{ logger: this.logger }
);
} catch (err) {
this.logger.error(`Cannot start SLO summary transform [${transformId}]`);
throw err;
}
}
async stop(transformId: TransformId): Promise<void> {
try {
await retryTransientEsErrors(
() =>
this.esClient.transform.stopTransform(
{ transform_id: transformId, wait_for_completion: true, force: true },
{ ignore: [404] }
),
{ logger: this.logger }
);
} catch (err) {
this.logger.error(`Cannot stop SLO summary transform [${transformId}]`);
throw err;
}
}
async uninstall(transformId: TransformId): Promise<void> {
try {
await retryTransientEsErrors(
() =>
this.esClient.transform.deleteTransform(
{ transform_id: transformId, force: true },
{ ignore: [404] }
),
{ logger: this.logger }
);
} catch (err) {
this.logger.error(`Cannot delete SLO summary transform [${transformId}]`);
throw err;
}
}
}

View file

@ -190,66 +190,21 @@ Object {
"field": "service.environment",
},
},
"slo.budgetingMethod": Object {
"terms": Object {
"field": "slo.budgetingMethod",
},
},
"slo.description": Object {
"terms": Object {
"field": "slo.description",
},
},
"slo.groupBy": Object {
"terms": Object {
"field": "slo.groupBy",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
},
},
"slo.indicator.type": Object {
"terms": Object {
"field": "slo.indicator.type",
},
},
"slo.instanceId": Object {
"terms": Object {
"field": "slo.instanceId",
},
},
"slo.name": Object {
"terms": Object {
"field": "slo.name",
},
},
"slo.objective.target": Object {
"terms": Object {
"field": "slo.objective.target",
},
},
"slo.revision": Object {
"terms": Object {
"field": "slo.revision",
},
},
"slo.tags": Object {
"terms": Object {
"field": "slo.tags",
},
},
"slo.timeWindow.duration": Object {
"terms": Object {
"field": "slo.timeWindow.duration",
},
},
"slo.timeWindow.type": Object {
"terms": Object {
"field": "slo.timeWindow.type",
},
},
}
`;
@ -304,66 +259,21 @@ Object {
"field": "service.name",
},
},
"slo.budgetingMethod": Object {
"terms": Object {
"field": "slo.budgetingMethod",
},
},
"slo.description": Object {
"terms": Object {
"field": "slo.description",
},
},
"slo.groupBy": Object {
"terms": Object {
"field": "slo.groupBy",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
},
},
"slo.indicator.type": Object {
"terms": Object {
"field": "slo.indicator.type",
},
},
"slo.instanceId": Object {
"terms": Object {
"field": "slo.instanceId",
},
},
"slo.name": Object {
"terms": Object {
"field": "slo.name",
},
},
"slo.objective.target": Object {
"terms": Object {
"field": "slo.objective.target",
},
},
"slo.revision": Object {
"terms": Object {
"field": "slo.revision",
},
},
"slo.tags": Object {
"terms": Object {
"field": "slo.tags",
},
},
"slo.timeWindow.duration": Object {
"terms": Object {
"field": "slo.timeWindow.duration",
},
},
"slo.timeWindow.type": Object {
"terms": Object {
"field": "slo.timeWindow.type",
},
},
}
`;
@ -413,66 +323,21 @@ Object {
"fixed_interval": "1m",
},
},
"slo.budgetingMethod": Object {
"terms": Object {
"field": "slo.budgetingMethod",
},
},
"slo.description": Object {
"terms": Object {
"field": "slo.description",
},
},
"slo.groupBy": Object {
"terms": Object {
"field": "slo.groupBy",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
},
},
"slo.indicator.type": Object {
"terms": Object {
"field": "slo.indicator.type",
},
},
"slo.instanceId": Object {
"terms": Object {
"field": "slo.instanceId",
},
},
"slo.name": Object {
"terms": Object {
"field": "slo.name",
},
},
"slo.objective.target": Object {
"terms": Object {
"field": "slo.objective.target",
},
},
"slo.revision": Object {
"terms": Object {
"field": "slo.revision",
},
},
"slo.tags": Object {
"terms": Object {
"field": "slo.tags",
},
},
"slo.timeWindow.duration": Object {
"terms": Object {
"field": "slo.timeWindow.duration",
},
},
"slo.timeWindow.type": Object {
"terms": Object {
"field": "slo.timeWindow.type",
},
},
"transaction.name": Object {
"terms": Object {
"field": "transaction.name",
@ -527,66 +392,21 @@ Object {
"fixed_interval": "1m",
},
},
"slo.budgetingMethod": Object {
"terms": Object {
"field": "slo.budgetingMethod",
},
},
"slo.description": Object {
"terms": Object {
"field": "slo.description",
},
},
"slo.groupBy": Object {
"terms": Object {
"field": "slo.groupBy",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
},
},
"slo.indicator.type": Object {
"terms": Object {
"field": "slo.indicator.type",
},
},
"slo.instanceId": Object {
"terms": Object {
"field": "slo.instanceId",
},
},
"slo.name": Object {
"terms": Object {
"field": "slo.name",
},
},
"slo.objective.target": Object {
"terms": Object {
"field": "slo.objective.target",
},
},
"slo.revision": Object {
"terms": Object {
"field": "slo.revision",
},
},
"slo.tags": Object {
"terms": Object {
"field": "slo.tags",
},
},
"slo.timeWindow.duration": Object {
"terms": Object {
"field": "slo.timeWindow.duration",
},
},
"slo.timeWindow.type": Object {
"terms": Object {
"field": "slo.timeWindow.type",
},
},
"transaction.type": Object {
"terms": Object {
"field": "transaction.type",
@ -600,12 +420,12 @@ Object {
"_meta": Object {
"managed": true,
"managed_by": "observability",
"version": 2,
"version": 3,
},
"description": "Rolled-up SLI data for SLO: irrelevant",
"description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]",
"dest": Object {
"index": ".slo-observability.sli-v2",
"pipeline": ".slo-observability.sli.pipeline",
"index": ".slo-observability.sli-v3",
"pipeline": ".slo-observability.sli.pipeline-v3",
},
"frequency": "1m",
"pivot": Object {
@ -660,71 +480,21 @@ Object {
"field": "service.name",
},
},
"slo.budgetingMethod": Object {
"terms": Object {
"field": "slo.budgetingMethod",
},
},
"slo.description": Object {
"terms": Object {
"field": "slo.description",
},
},
"slo.groupBy": Object {
"terms": Object {
"field": "slo.groupBy",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
},
},
"slo.indicator.type": Object {
"terms": Object {
"field": "slo.indicator.type",
},
},
"slo.instanceId": Object {
"terms": Object {
"field": "slo.instanceId",
},
},
"slo.name": Object {
"terms": Object {
"field": "slo.name",
},
},
"slo.objective.sliceDurationInSeconds": Object {
"terms": Object {
"field": "slo.objective.sliceDurationInSeconds",
},
},
"slo.objective.target": Object {
"terms": Object {
"field": "slo.objective.target",
},
},
"slo.revision": Object {
"terms": Object {
"field": "slo.revision",
},
},
"slo.tags": Object {
"terms": Object {
"field": "slo.tags",
},
},
"slo.timeWindow.duration": Object {
"terms": Object {
"field": "slo.timeWindow.duration",
},
},
"slo.timeWindow.type": Object {
"terms": Object {
"field": "slo.timeWindow.type",
},
},
"transaction.name": Object {
"terms": Object {
"field": "transaction.name",
@ -794,33 +564,9 @@ Object {
},
},
"runtime_mappings": Object {
"slo.budgetingMethod": Object {
"script": Object {
"source": "emit('timeslices')",
},
"type": "keyword",
},
"slo.description": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.groupBy": Object {
"script": Object {
"source": "emit('*')",
},
"type": "keyword",
},
"slo.id": Object {
"script": Object {
"source": Any<String>,
},
"type": "keyword",
},
"slo.indicator.type": Object {
"script": Object {
"source": "emit('sli.apm.transactionDuration')",
"source": "emit('irrelevant')",
},
"type": "keyword",
},
@ -830,48 +576,12 @@ Object {
},
"type": "keyword",
},
"slo.name": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.objective.sliceDurationInSeconds": Object {
"script": Object {
"source": "emit(120)",
},
"type": "long",
},
"slo.objective.target": Object {
"script": Object {
"source": "emit(0.98)",
},
"type": "double",
},
"slo.revision": Object {
"script": Object {
"source": "emit(1)",
},
"type": "long",
},
"slo.tags": Object {
"script": Object {
"source": "emit('critical,k8s')",
},
"type": "keyword",
},
"slo.timeWindow.duration": Object {
"script": Object {
"source": "emit('7d')",
},
"type": "keyword",
},
"slo.timeWindow.type": Object {
"script": Object {
"source": "emit('rolling')",
},
"type": "keyword",
},
},
},
"sync": Object {
@ -880,7 +590,7 @@ Object {
"field": "@timestamp",
},
},
"transform_id": Any<String>,
"transform_id": "slo-irrelevant-1",
}
`;
@ -889,12 +599,12 @@ Object {
"_meta": Object {
"managed": true,
"managed_by": "observability",
"version": 2,
"version": 3,
},
"description": "Rolled-up SLI data for SLO: irrelevant",
"description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]",
"dest": Object {
"index": ".slo-observability.sli-v2",
"pipeline": ".slo-observability.sli.pipeline",
"index": ".slo-observability.sli-v3",
"pipeline": ".slo-observability.sli.pipeline-v3",
},
"frequency": "1m",
"pivot": Object {
@ -940,66 +650,21 @@ Object {
"field": "service.name",
},
},
"slo.budgetingMethod": Object {
"terms": Object {
"field": "slo.budgetingMethod",
},
},
"slo.description": Object {
"terms": Object {
"field": "slo.description",
},
},
"slo.groupBy": Object {
"terms": Object {
"field": "slo.groupBy",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
},
},
"slo.indicator.type": Object {
"terms": Object {
"field": "slo.indicator.type",
},
},
"slo.instanceId": Object {
"terms": Object {
"field": "slo.instanceId",
},
},
"slo.name": Object {
"terms": Object {
"field": "slo.name",
},
},
"slo.objective.target": Object {
"terms": Object {
"field": "slo.objective.target",
},
},
"slo.revision": Object {
"terms": Object {
"field": "slo.revision",
},
},
"slo.tags": Object {
"terms": Object {
"field": "slo.tags",
},
},
"slo.timeWindow.duration": Object {
"terms": Object {
"field": "slo.timeWindow.duration",
},
},
"slo.timeWindow.type": Object {
"terms": Object {
"field": "slo.timeWindow.type",
},
},
"transaction.name": Object {
"terms": Object {
"field": "transaction.name",
@ -1069,33 +734,9 @@ Object {
},
},
"runtime_mappings": Object {
"slo.budgetingMethod": Object {
"script": Object {
"source": "emit('occurrences')",
},
"type": "keyword",
},
"slo.description": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.groupBy": Object {
"script": Object {
"source": "emit('*')",
},
"type": "keyword",
},
"slo.id": Object {
"script": Object {
"source": Any<String>,
},
"type": "keyword",
},
"slo.indicator.type": Object {
"script": Object {
"source": "emit('sli.apm.transactionDuration')",
"source": "emit('irrelevant')",
},
"type": "keyword",
},
@ -1105,42 +746,12 @@ Object {
},
"type": "keyword",
},
"slo.name": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.objective.target": Object {
"script": Object {
"source": "emit(0.999)",
},
"type": "double",
},
"slo.revision": Object {
"script": Object {
"source": "emit(1)",
},
"type": "long",
},
"slo.tags": Object {
"script": Object {
"source": "emit('critical,k8s')",
},
"type": "keyword",
},
"slo.timeWindow.duration": Object {
"script": Object {
"source": "emit('7d')",
},
"type": "keyword",
},
"slo.timeWindow.type": Object {
"script": Object {
"source": "emit('rolling')",
},
"type": "keyword",
},
},
},
"sync": Object {
@ -1149,6 +760,6 @@ Object {
"field": "@timestamp",
},
},
"transform_id": Any<String>,
"transform_id": "slo-irrelevant-1",
}
`;

View file

@ -178,66 +178,21 @@ Object {
"field": "service.environment",
},
},
"slo.budgetingMethod": Object {
"terms": Object {
"field": "slo.budgetingMethod",
},
},
"slo.description": Object {
"terms": Object {
"field": "slo.description",
},
},
"slo.groupBy": Object {
"terms": Object {
"field": "slo.groupBy",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
},
},
"slo.indicator.type": Object {
"terms": Object {
"field": "slo.indicator.type",
},
},
"slo.instanceId": Object {
"terms": Object {
"field": "slo.instanceId",
},
},
"slo.name": Object {
"terms": Object {
"field": "slo.name",
},
},
"slo.objective.target": Object {
"terms": Object {
"field": "slo.objective.target",
},
},
"slo.revision": Object {
"terms": Object {
"field": "slo.revision",
},
},
"slo.tags": Object {
"terms": Object {
"field": "slo.tags",
},
},
"slo.timeWindow.duration": Object {
"terms": Object {
"field": "slo.timeWindow.duration",
},
},
"slo.timeWindow.type": Object {
"terms": Object {
"field": "slo.timeWindow.type",
},
},
}
`;
@ -288,66 +243,21 @@ Object {
"field": "service.name",
},
},
"slo.budgetingMethod": Object {
"terms": Object {
"field": "slo.budgetingMethod",
},
},
"slo.description": Object {
"terms": Object {
"field": "slo.description",
},
},
"slo.groupBy": Object {
"terms": Object {
"field": "slo.groupBy",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
},
},
"slo.indicator.type": Object {
"terms": Object {
"field": "slo.indicator.type",
},
},
"slo.instanceId": Object {
"terms": Object {
"field": "slo.instanceId",
},
},
"slo.name": Object {
"terms": Object {
"field": "slo.name",
},
},
"slo.objective.target": Object {
"terms": Object {
"field": "slo.objective.target",
},
},
"slo.revision": Object {
"terms": Object {
"field": "slo.revision",
},
},
"slo.tags": Object {
"terms": Object {
"field": "slo.tags",
},
},
"slo.timeWindow.duration": Object {
"terms": Object {
"field": "slo.timeWindow.duration",
},
},
"slo.timeWindow.type": Object {
"terms": Object {
"field": "slo.timeWindow.type",
},
},
}
`;
@ -393,66 +303,21 @@ Object {
"fixed_interval": "1m",
},
},
"slo.budgetingMethod": Object {
"terms": Object {
"field": "slo.budgetingMethod",
},
},
"slo.description": Object {
"terms": Object {
"field": "slo.description",
},
},
"slo.groupBy": Object {
"terms": Object {
"field": "slo.groupBy",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
},
},
"slo.indicator.type": Object {
"terms": Object {
"field": "slo.indicator.type",
},
},
"slo.instanceId": Object {
"terms": Object {
"field": "slo.instanceId",
},
},
"slo.name": Object {
"terms": Object {
"field": "slo.name",
},
},
"slo.objective.target": Object {
"terms": Object {
"field": "slo.objective.target",
},
},
"slo.revision": Object {
"terms": Object {
"field": "slo.revision",
},
},
"slo.tags": Object {
"terms": Object {
"field": "slo.tags",
},
},
"slo.timeWindow.duration": Object {
"terms": Object {
"field": "slo.timeWindow.duration",
},
},
"slo.timeWindow.type": Object {
"terms": Object {
"field": "slo.timeWindow.type",
},
},
"transaction.name": Object {
"terms": Object {
"field": "transaction.name",
@ -503,66 +368,21 @@ Object {
"fixed_interval": "1m",
},
},
"slo.budgetingMethod": Object {
"terms": Object {
"field": "slo.budgetingMethod",
},
},
"slo.description": Object {
"terms": Object {
"field": "slo.description",
},
},
"slo.groupBy": Object {
"terms": Object {
"field": "slo.groupBy",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
},
},
"slo.indicator.type": Object {
"terms": Object {
"field": "slo.indicator.type",
},
},
"slo.instanceId": Object {
"terms": Object {
"field": "slo.instanceId",
},
},
"slo.name": Object {
"terms": Object {
"field": "slo.name",
},
},
"slo.objective.target": Object {
"terms": Object {
"field": "slo.objective.target",
},
},
"slo.revision": Object {
"terms": Object {
"field": "slo.revision",
},
},
"slo.tags": Object {
"terms": Object {
"field": "slo.tags",
},
},
"slo.timeWindow.duration": Object {
"terms": Object {
"field": "slo.timeWindow.duration",
},
},
"slo.timeWindow.type": Object {
"terms": Object {
"field": "slo.timeWindow.type",
},
},
"transaction.type": Object {
"terms": Object {
"field": "transaction.type",
@ -576,12 +396,12 @@ Object {
"_meta": Object {
"managed": true,
"managed_by": "observability",
"version": 2,
"version": 3,
},
"description": "Rolled-up SLI data for SLO: irrelevant",
"description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]",
"dest": Object {
"index": ".slo-observability.sli-v2",
"pipeline": ".slo-observability.sli.pipeline",
"index": ".slo-observability.sli-v3",
"pipeline": ".slo-observability.sli.pipeline-v3",
},
"frequency": "1m",
"pivot": Object {
@ -629,71 +449,21 @@ Object {
"field": "service.name",
},
},
"slo.budgetingMethod": Object {
"terms": Object {
"field": "slo.budgetingMethod",
},
},
"slo.description": Object {
"terms": Object {
"field": "slo.description",
},
},
"slo.groupBy": Object {
"terms": Object {
"field": "slo.groupBy",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
},
},
"slo.indicator.type": Object {
"terms": Object {
"field": "slo.indicator.type",
},
},
"slo.instanceId": Object {
"terms": Object {
"field": "slo.instanceId",
},
},
"slo.name": Object {
"terms": Object {
"field": "slo.name",
},
},
"slo.objective.sliceDurationInSeconds": Object {
"terms": Object {
"field": "slo.objective.sliceDurationInSeconds",
},
},
"slo.objective.target": Object {
"terms": Object {
"field": "slo.objective.target",
},
},
"slo.revision": Object {
"terms": Object {
"field": "slo.revision",
},
},
"slo.tags": Object {
"terms": Object {
"field": "slo.tags",
},
},
"slo.timeWindow.duration": Object {
"terms": Object {
"field": "slo.timeWindow.duration",
},
},
"slo.timeWindow.type": Object {
"terms": Object {
"field": "slo.timeWindow.type",
},
},
"transaction.name": Object {
"terms": Object {
"field": "transaction.name",
@ -759,33 +529,9 @@ Object {
},
},
"runtime_mappings": Object {
"slo.budgetingMethod": Object {
"script": Object {
"source": "emit('timeslices')",
},
"type": "keyword",
},
"slo.description": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.groupBy": Object {
"script": Object {
"source": "emit('*')",
},
"type": "keyword",
},
"slo.id": Object {
"script": Object {
"source": Any<String>,
},
"type": "keyword",
},
"slo.indicator.type": Object {
"script": Object {
"source": "emit('sli.apm.transactionErrorRate')",
"source": "emit('irrelevant')",
},
"type": "keyword",
},
@ -795,48 +541,12 @@ Object {
},
"type": "keyword",
},
"slo.name": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.objective.sliceDurationInSeconds": Object {
"script": Object {
"source": "emit(120)",
},
"type": "long",
},
"slo.objective.target": Object {
"script": Object {
"source": "emit(0.98)",
},
"type": "double",
},
"slo.revision": Object {
"script": Object {
"source": "emit(1)",
},
"type": "long",
},
"slo.tags": Object {
"script": Object {
"source": "emit('critical,k8s')",
},
"type": "keyword",
},
"slo.timeWindow.duration": Object {
"script": Object {
"source": "emit('7d')",
},
"type": "keyword",
},
"slo.timeWindow.type": Object {
"script": Object {
"source": "emit('rolling')",
},
"type": "keyword",
},
},
},
"sync": Object {
@ -845,7 +555,7 @@ Object {
"field": "@timestamp",
},
},
"transform_id": Any<String>,
"transform_id": "slo-irrelevant-1",
}
`;
@ -854,12 +564,12 @@ Object {
"_meta": Object {
"managed": true,
"managed_by": "observability",
"version": 2,
"version": 3,
},
"description": "Rolled-up SLI data for SLO: irrelevant",
"description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]",
"dest": Object {
"index": ".slo-observability.sli-v2",
"pipeline": ".slo-observability.sli.pipeline",
"index": ".slo-observability.sli-v3",
"pipeline": ".slo-observability.sli.pipeline-v3",
},
"frequency": "1m",
"pivot": Object {
@ -898,66 +608,21 @@ Object {
"field": "service.name",
},
},
"slo.budgetingMethod": Object {
"terms": Object {
"field": "slo.budgetingMethod",
},
},
"slo.description": Object {
"terms": Object {
"field": "slo.description",
},
},
"slo.groupBy": Object {
"terms": Object {
"field": "slo.groupBy",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
},
},
"slo.indicator.type": Object {
"terms": Object {
"field": "slo.indicator.type",
},
},
"slo.instanceId": Object {
"terms": Object {
"field": "slo.instanceId",
},
},
"slo.name": Object {
"terms": Object {
"field": "slo.name",
},
},
"slo.objective.target": Object {
"terms": Object {
"field": "slo.objective.target",
},
},
"slo.revision": Object {
"terms": Object {
"field": "slo.revision",
},
},
"slo.tags": Object {
"terms": Object {
"field": "slo.tags",
},
},
"slo.timeWindow.duration": Object {
"terms": Object {
"field": "slo.timeWindow.duration",
},
},
"slo.timeWindow.type": Object {
"terms": Object {
"field": "slo.timeWindow.type",
},
},
"transaction.name": Object {
"terms": Object {
"field": "transaction.name",
@ -1023,33 +688,9 @@ Object {
},
},
"runtime_mappings": Object {
"slo.budgetingMethod": Object {
"script": Object {
"source": "emit('occurrences')",
},
"type": "keyword",
},
"slo.description": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.groupBy": Object {
"script": Object {
"source": "emit('*')",
},
"type": "keyword",
},
"slo.id": Object {
"script": Object {
"source": Any<String>,
},
"type": "keyword",
},
"slo.indicator.type": Object {
"script": Object {
"source": "emit('sli.apm.transactionErrorRate')",
"source": "emit('irrelevant')",
},
"type": "keyword",
},
@ -1059,42 +700,12 @@ Object {
},
"type": "keyword",
},
"slo.name": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.objective.target": Object {
"script": Object {
"source": "emit(0.999)",
},
"type": "double",
},
"slo.revision": Object {
"script": Object {
"source": "emit(1)",
},
"type": "long",
},
"slo.tags": Object {
"script": Object {
"source": "emit('critical,k8s')",
},
"type": "keyword",
},
"slo.timeWindow.duration": Object {
"script": Object {
"source": "emit('7d')",
},
"type": "keyword",
},
"slo.timeWindow.type": Object {
"script": Object {
"source": "emit('rolling')",
},
"type": "keyword",
},
},
},
"sync": Object {
@ -1103,6 +714,6 @@ Object {
"field": "@timestamp",
},
},
"transform_id": Any<String>,
"transform_id": "slo-irrelevant-1",
}
`;

View file

@ -77,12 +77,12 @@ Object {
"_meta": Object {
"managed": true,
"managed_by": "observability",
"version": 2,
"version": 3,
},
"description": "Rolled-up SLI data for SLO: irrelevant",
"description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]",
"dest": Object {
"index": ".slo-observability.sli-v2",
"pipeline": ".slo-observability.sli.pipeline",
"index": ".slo-observability.sli-v3",
"pipeline": ".slo-observability.sli.pipeline-v3",
},
"frequency": "1m",
"pivot": Object {
@ -151,71 +151,21 @@ Object {
"fixed_interval": "2m",
},
},
"slo.budgetingMethod": Object {
"terms": Object {
"field": "slo.budgetingMethod",
},
},
"slo.description": Object {
"terms": Object {
"field": "slo.description",
},
},
"slo.groupBy": Object {
"terms": Object {
"field": "slo.groupBy",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
},
},
"slo.indicator.type": Object {
"terms": Object {
"field": "slo.indicator.type",
},
},
"slo.instanceId": Object {
"terms": Object {
"field": "slo.instanceId",
},
},
"slo.name": Object {
"terms": Object {
"field": "slo.name",
},
},
"slo.objective.sliceDurationInSeconds": Object {
"terms": Object {
"field": "slo.objective.sliceDurationInSeconds",
},
},
"slo.objective.target": Object {
"terms": Object {
"field": "slo.objective.target",
},
},
"slo.revision": Object {
"terms": Object {
"field": "slo.revision",
},
},
"slo.tags": Object {
"terms": Object {
"field": "slo.tags",
},
},
"slo.timeWindow.duration": Object {
"terms": Object {
"field": "slo.timeWindow.duration",
},
},
"slo.timeWindow.type": Object {
"terms": Object {
"field": "slo.timeWindow.type",
},
},
},
},
"settings": Object {
@ -253,33 +203,9 @@ Object {
},
},
"runtime_mappings": Object {
"slo.budgetingMethod": Object {
"script": Object {
"source": "emit('timeslices')",
},
"type": "keyword",
},
"slo.description": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.groupBy": Object {
"script": Object {
"source": "emit('*')",
},
"type": "keyword",
},
"slo.id": Object {
"script": Object {
"source": Any<String>,
},
"type": "keyword",
},
"slo.indicator.type": Object {
"script": Object {
"source": "emit('sli.histogram.custom')",
"source": "emit('irrelevant')",
},
"type": "keyword",
},
@ -289,48 +215,12 @@ Object {
},
"type": "keyword",
},
"slo.name": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.objective.sliceDurationInSeconds": Object {
"script": Object {
"source": "emit(120)",
},
"type": "long",
},
"slo.objective.target": Object {
"script": Object {
"source": "emit(0.98)",
},
"type": "double",
},
"slo.revision": Object {
"script": Object {
"source": "emit(1)",
},
"type": "long",
},
"slo.tags": Object {
"script": Object {
"source": "emit('critical,k8s')",
},
"type": "keyword",
},
"slo.timeWindow.duration": Object {
"script": Object {
"source": "emit('7d')",
},
"type": "keyword",
},
"slo.timeWindow.type": Object {
"script": Object {
"source": "emit('rolling')",
},
"type": "keyword",
},
},
},
"sync": Object {
@ -339,7 +229,7 @@ Object {
"field": "log_timestamp",
},
},
"transform_id": Any<String>,
"transform_id": "slo-irrelevant-1",
}
`;
@ -348,12 +238,12 @@ Object {
"_meta": Object {
"managed": true,
"managed_by": "observability",
"version": 2,
"version": 3,
},
"description": "Rolled-up SLI data for SLO: irrelevant",
"description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]",
"dest": Object {
"index": ".slo-observability.sli-v2",
"pipeline": ".slo-observability.sli.pipeline",
"index": ".slo-observability.sli-v3",
"pipeline": ".slo-observability.sli.pipeline-v3",
},
"frequency": "1m",
"pivot": Object {
@ -413,66 +303,21 @@ Object {
"fixed_interval": "1m",
},
},
"slo.budgetingMethod": Object {
"terms": Object {
"field": "slo.budgetingMethod",
},
},
"slo.description": Object {
"terms": Object {
"field": "slo.description",
},
},
"slo.groupBy": Object {
"terms": Object {
"field": "slo.groupBy",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
},
},
"slo.indicator.type": Object {
"terms": Object {
"field": "slo.indicator.type",
},
},
"slo.instanceId": Object {
"terms": Object {
"field": "slo.instanceId",
},
},
"slo.name": Object {
"terms": Object {
"field": "slo.name",
},
},
"slo.objective.target": Object {
"terms": Object {
"field": "slo.objective.target",
},
},
"slo.revision": Object {
"terms": Object {
"field": "slo.revision",
},
},
"slo.tags": Object {
"terms": Object {
"field": "slo.tags",
},
},
"slo.timeWindow.duration": Object {
"terms": Object {
"field": "slo.timeWindow.duration",
},
},
"slo.timeWindow.type": Object {
"terms": Object {
"field": "slo.timeWindow.type",
},
},
},
},
"settings": Object {
@ -510,33 +355,9 @@ Object {
},
},
"runtime_mappings": Object {
"slo.budgetingMethod": Object {
"script": Object {
"source": "emit('occurrences')",
},
"type": "keyword",
},
"slo.description": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.groupBy": Object {
"script": Object {
"source": "emit('*')",
},
"type": "keyword",
},
"slo.id": Object {
"script": Object {
"source": Any<String>,
},
"type": "keyword",
},
"slo.indicator.type": Object {
"script": Object {
"source": "emit('sli.histogram.custom')",
"source": "emit('irrelevant')",
},
"type": "keyword",
},
@ -546,42 +367,12 @@ Object {
},
"type": "keyword",
},
"slo.name": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.objective.target": Object {
"script": Object {
"source": "emit(0.999)",
},
"type": "double",
},
"slo.revision": Object {
"script": Object {
"source": "emit(1)",
},
"type": "long",
},
"slo.tags": Object {
"script": Object {
"source": "emit('critical,k8s')",
},
"type": "keyword",
},
"slo.timeWindow.duration": Object {
"script": Object {
"source": "emit('7d')",
},
"type": "keyword",
},
"slo.timeWindow.type": Object {
"script": Object {
"source": "emit('rolling')",
},
"type": "keyword",
},
},
},
"sync": Object {
@ -590,6 +381,6 @@ Object {
"field": "log_timestamp",
},
},
"transform_id": Any<String>,
"transform_id": "slo-irrelevant-1",
}
`;

View file

@ -118,12 +118,12 @@ Object {
"_meta": Object {
"managed": true,
"managed_by": "observability",
"version": 2,
"version": 3,
},
"description": "Rolled-up SLI data for SLO: irrelevant",
"description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]",
"dest": Object {
"index": ".slo-observability.sli-v2",
"pipeline": ".slo-observability.sli.pipeline",
"index": ".slo-observability.sli-v3",
"pipeline": ".slo-observability.sli.pipeline-v3",
},
"frequency": "1m",
"pivot": Object {
@ -166,71 +166,21 @@ Object {
"fixed_interval": "2m",
},
},
"slo.budgetingMethod": Object {
"terms": Object {
"field": "slo.budgetingMethod",
},
},
"slo.description": Object {
"terms": Object {
"field": "slo.description",
},
},
"slo.groupBy": Object {
"terms": Object {
"field": "slo.groupBy",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
},
},
"slo.indicator.type": Object {
"terms": Object {
"field": "slo.indicator.type",
},
},
"slo.instanceId": Object {
"terms": Object {
"field": "slo.instanceId",
},
},
"slo.name": Object {
"terms": Object {
"field": "slo.name",
},
},
"slo.objective.sliceDurationInSeconds": Object {
"terms": Object {
"field": "slo.objective.sliceDurationInSeconds",
},
},
"slo.objective.target": Object {
"terms": Object {
"field": "slo.objective.target",
},
},
"slo.revision": Object {
"terms": Object {
"field": "slo.revision",
},
},
"slo.tags": Object {
"terms": Object {
"field": "slo.tags",
},
},
"slo.timeWindow.duration": Object {
"terms": Object {
"field": "slo.timeWindow.duration",
},
},
"slo.timeWindow.type": Object {
"terms": Object {
"field": "slo.timeWindow.type",
},
},
},
},
"settings": Object {
@ -268,33 +218,9 @@ Object {
},
},
"runtime_mappings": Object {
"slo.budgetingMethod": Object {
"script": Object {
"source": "emit('timeslices')",
},
"type": "keyword",
},
"slo.description": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.groupBy": Object {
"script": Object {
"source": "emit('*')",
},
"type": "keyword",
},
"slo.id": Object {
"script": Object {
"source": Any<String>,
},
"type": "keyword",
},
"slo.indicator.type": Object {
"script": Object {
"source": "emit('sli.kql.custom')",
"source": "emit('irrelevant')",
},
"type": "keyword",
},
@ -304,48 +230,12 @@ Object {
},
"type": "keyword",
},
"slo.name": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.objective.sliceDurationInSeconds": Object {
"script": Object {
"source": "emit(120)",
},
"type": "long",
},
"slo.objective.target": Object {
"script": Object {
"source": "emit(0.98)",
},
"type": "double",
},
"slo.revision": Object {
"script": Object {
"source": "emit(1)",
},
"type": "long",
},
"slo.tags": Object {
"script": Object {
"source": "emit('critical,k8s')",
},
"type": "keyword",
},
"slo.timeWindow.duration": Object {
"script": Object {
"source": "emit('7d')",
},
"type": "keyword",
},
"slo.timeWindow.type": Object {
"script": Object {
"source": "emit('rolling')",
},
"type": "keyword",
},
},
},
"sync": Object {
@ -354,7 +244,7 @@ Object {
"field": "log_timestamp",
},
},
"transform_id": Any<String>,
"transform_id": "slo-irrelevant-1",
}
`;
@ -363,12 +253,12 @@ Object {
"_meta": Object {
"managed": true,
"managed_by": "observability",
"version": 2,
"version": 3,
},
"description": "Rolled-up SLI data for SLO: irrelevant",
"description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]",
"dest": Object {
"index": ".slo-observability.sli-v2",
"pipeline": ".slo-observability.sli.pipeline",
"index": ".slo-observability.sli-v3",
"pipeline": ".slo-observability.sli.pipeline-v3",
},
"frequency": "1m",
"pivot": Object {
@ -402,66 +292,21 @@ Object {
"fixed_interval": "1m",
},
},
"slo.budgetingMethod": Object {
"terms": Object {
"field": "slo.budgetingMethod",
},
},
"slo.description": Object {
"terms": Object {
"field": "slo.description",
},
},
"slo.groupBy": Object {
"terms": Object {
"field": "slo.groupBy",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
},
},
"slo.indicator.type": Object {
"terms": Object {
"field": "slo.indicator.type",
},
},
"slo.instanceId": Object {
"terms": Object {
"field": "slo.instanceId",
},
},
"slo.name": Object {
"terms": Object {
"field": "slo.name",
},
},
"slo.objective.target": Object {
"terms": Object {
"field": "slo.objective.target",
},
},
"slo.revision": Object {
"terms": Object {
"field": "slo.revision",
},
},
"slo.tags": Object {
"terms": Object {
"field": "slo.tags",
},
},
"slo.timeWindow.duration": Object {
"terms": Object {
"field": "slo.timeWindow.duration",
},
},
"slo.timeWindow.type": Object {
"terms": Object {
"field": "slo.timeWindow.type",
},
},
},
},
"settings": Object {
@ -499,33 +344,9 @@ Object {
},
},
"runtime_mappings": Object {
"slo.budgetingMethod": Object {
"script": Object {
"source": "emit('occurrences')",
},
"type": "keyword",
},
"slo.description": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.groupBy": Object {
"script": Object {
"source": "emit('*')",
},
"type": "keyword",
},
"slo.id": Object {
"script": Object {
"source": Any<String>,
},
"type": "keyword",
},
"slo.indicator.type": Object {
"script": Object {
"source": "emit('sli.kql.custom')",
"source": "emit('irrelevant')",
},
"type": "keyword",
},
@ -535,42 +356,12 @@ Object {
},
"type": "keyword",
},
"slo.name": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.objective.target": Object {
"script": Object {
"source": "emit(0.999)",
},
"type": "double",
},
"slo.revision": Object {
"script": Object {
"source": "emit(1)",
},
"type": "long",
},
"slo.tags": Object {
"script": Object {
"source": "emit('critical,k8s')",
},
"type": "keyword",
},
"slo.timeWindow.duration": Object {
"script": Object {
"source": "emit('7d')",
},
"type": "keyword",
},
"slo.timeWindow.type": Object {
"script": Object {
"source": "emit('rolling')",
},
"type": "keyword",
},
},
},
"sync": Object {
@ -579,6 +370,6 @@ Object {
"field": "log_timestamp",
},
},
"transform_id": Any<String>,
"transform_id": "slo-irrelevant-1",
}
`;

View file

@ -117,12 +117,12 @@ Object {
"_meta": Object {
"managed": true,
"managed_by": "observability",
"version": 2,
"version": 3,
},
"description": "Rolled-up SLI data for SLO: irrelevant",
"description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]",
"dest": Object {
"index": ".slo-observability.sli-v2",
"pipeline": ".slo-observability.sli.pipeline",
"index": ".slo-observability.sli-v3",
"pipeline": ".slo-observability.sli.pipeline-v3",
},
"frequency": "1m",
"pivot": Object {
@ -203,71 +203,21 @@ Object {
"fixed_interval": "2m",
},
},
"slo.budgetingMethod": Object {
"terms": Object {
"field": "slo.budgetingMethod",
},
},
"slo.description": Object {
"terms": Object {
"field": "slo.description",
},
},
"slo.groupBy": Object {
"terms": Object {
"field": "slo.groupBy",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
},
},
"slo.indicator.type": Object {
"terms": Object {
"field": "slo.indicator.type",
},
},
"slo.instanceId": Object {
"terms": Object {
"field": "slo.instanceId",
},
},
"slo.name": Object {
"terms": Object {
"field": "slo.name",
},
},
"slo.objective.sliceDurationInSeconds": Object {
"terms": Object {
"field": "slo.objective.sliceDurationInSeconds",
},
},
"slo.objective.target": Object {
"terms": Object {
"field": "slo.objective.target",
},
},
"slo.revision": Object {
"terms": Object {
"field": "slo.revision",
},
},
"slo.tags": Object {
"terms": Object {
"field": "slo.tags",
},
},
"slo.timeWindow.duration": Object {
"terms": Object {
"field": "slo.timeWindow.duration",
},
},
"slo.timeWindow.type": Object {
"terms": Object {
"field": "slo.timeWindow.type",
},
},
},
},
"settings": Object {
@ -305,33 +255,9 @@ Object {
},
},
"runtime_mappings": Object {
"slo.budgetingMethod": Object {
"script": Object {
"source": "emit('timeslices')",
},
"type": "keyword",
},
"slo.description": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.groupBy": Object {
"script": Object {
"source": "emit('*')",
},
"type": "keyword",
},
"slo.id": Object {
"script": Object {
"source": Any<String>,
},
"type": "keyword",
},
"slo.indicator.type": Object {
"script": Object {
"source": "emit('sli.metric.custom')",
"source": "emit('irrelevant')",
},
"type": "keyword",
},
@ -341,48 +267,12 @@ Object {
},
"type": "keyword",
},
"slo.name": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.objective.sliceDurationInSeconds": Object {
"script": Object {
"source": "emit(120)",
},
"type": "long",
},
"slo.objective.target": Object {
"script": Object {
"source": "emit(0.98)",
},
"type": "double",
},
"slo.revision": Object {
"script": Object {
"source": "emit(1)",
},
"type": "long",
},
"slo.tags": Object {
"script": Object {
"source": "emit('critical,k8s')",
},
"type": "keyword",
},
"slo.timeWindow.duration": Object {
"script": Object {
"source": "emit('7d')",
},
"type": "keyword",
},
"slo.timeWindow.type": Object {
"script": Object {
"source": "emit('rolling')",
},
"type": "keyword",
},
},
},
"sync": Object {
@ -391,7 +281,7 @@ Object {
"field": "log_timestamp",
},
},
"transform_id": Any<String>,
"transform_id": "slo-irrelevant-1",
}
`;
@ -400,12 +290,12 @@ Object {
"_meta": Object {
"managed": true,
"managed_by": "observability",
"version": 2,
"version": 3,
},
"description": "Rolled-up SLI data for SLO: irrelevant",
"description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]",
"dest": Object {
"index": ".slo-observability.sli-v2",
"pipeline": ".slo-observability.sli.pipeline",
"index": ".slo-observability.sli-v3",
"pipeline": ".slo-observability.sli.pipeline-v3",
},
"frequency": "1m",
"pivot": Object {
@ -477,66 +367,21 @@ Object {
"fixed_interval": "1m",
},
},
"slo.budgetingMethod": Object {
"terms": Object {
"field": "slo.budgetingMethod",
},
},
"slo.description": Object {
"terms": Object {
"field": "slo.description",
},
},
"slo.groupBy": Object {
"terms": Object {
"field": "slo.groupBy",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
},
},
"slo.indicator.type": Object {
"terms": Object {
"field": "slo.indicator.type",
},
},
"slo.instanceId": Object {
"terms": Object {
"field": "slo.instanceId",
},
},
"slo.name": Object {
"terms": Object {
"field": "slo.name",
},
},
"slo.objective.target": Object {
"terms": Object {
"field": "slo.objective.target",
},
},
"slo.revision": Object {
"terms": Object {
"field": "slo.revision",
},
},
"slo.tags": Object {
"terms": Object {
"field": "slo.tags",
},
},
"slo.timeWindow.duration": Object {
"terms": Object {
"field": "slo.timeWindow.duration",
},
},
"slo.timeWindow.type": Object {
"terms": Object {
"field": "slo.timeWindow.type",
},
},
},
},
"settings": Object {
@ -574,33 +419,9 @@ Object {
},
},
"runtime_mappings": Object {
"slo.budgetingMethod": Object {
"script": Object {
"source": "emit('occurrences')",
},
"type": "keyword",
},
"slo.description": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.groupBy": Object {
"script": Object {
"source": "emit('*')",
},
"type": "keyword",
},
"slo.id": Object {
"script": Object {
"source": Any<String>,
},
"type": "keyword",
},
"slo.indicator.type": Object {
"script": Object {
"source": "emit('sli.metric.custom')",
"source": "emit('irrelevant')",
},
"type": "keyword",
},
@ -610,42 +431,12 @@ Object {
},
"type": "keyword",
},
"slo.name": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.objective.target": Object {
"script": Object {
"source": "emit(0.999)",
},
"type": "double",
},
"slo.revision": Object {
"script": Object {
"source": "emit(1)",
},
"type": "long",
},
"slo.tags": Object {
"script": Object {
"source": "emit('critical,k8s')",
},
"type": "keyword",
},
"slo.timeWindow.duration": Object {
"script": Object {
"source": "emit('7d')",
},
"type": "keyword",
},
"slo.timeWindow.type": Object {
"script": Object {
"source": "emit('rolling')",
},
"type": "keyword",
},
},
},
"sync": Object {
@ -654,7 +445,7 @@ Object {
"field": "log_timestamp",
},
},
"transform_id": Any<String>,
"transform_id": "slo-irrelevant-1",
}
`;

View file

@ -33,12 +33,12 @@ Object {
"_meta": Object {
"managed": true,
"managed_by": "observability",
"version": 2,
"version": 3,
},
"description": "Rolled-up SLI data for SLO: irrelevant",
"description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]",
"dest": Object {
"index": ".slo-observability.sli-v2",
"pipeline": ".slo-observability.sli.pipeline",
"index": ".slo-observability.sli-v3",
"pipeline": ".slo-observability.sli.pipeline-v3",
},
"frequency": "1m",
"pivot": Object {
@ -173,71 +173,21 @@ Object {
"fixed_interval": "2m",
},
},
"slo.budgetingMethod": Object {
"terms": Object {
"field": "slo.budgetingMethod",
},
},
"slo.description": Object {
"terms": Object {
"field": "slo.description",
},
},
"slo.groupBy": Object {
"terms": Object {
"field": "slo.groupBy",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
},
},
"slo.indicator.type": Object {
"terms": Object {
"field": "slo.indicator.type",
},
},
"slo.instanceId": Object {
"terms": Object {
"field": "slo.instanceId",
},
},
"slo.name": Object {
"terms": Object {
"field": "slo.name",
},
},
"slo.objective.sliceDurationInSeconds": Object {
"terms": Object {
"field": "slo.objective.sliceDurationInSeconds",
},
},
"slo.objective.target": Object {
"terms": Object {
"field": "slo.objective.target",
},
},
"slo.revision": Object {
"terms": Object {
"field": "slo.revision",
},
},
"slo.tags": Object {
"terms": Object {
"field": "slo.tags",
},
},
"slo.timeWindow.duration": Object {
"terms": Object {
"field": "slo.timeWindow.duration",
},
},
"slo.timeWindow.type": Object {
"terms": Object {
"field": "slo.timeWindow.type",
},
},
},
},
"settings": Object {
@ -272,33 +222,9 @@ Object {
},
},
"runtime_mappings": Object {
"slo.budgetingMethod": Object {
"script": Object {
"source": "emit('timeslices')",
},
"type": "keyword",
},
"slo.description": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.groupBy": Object {
"script": Object {
"source": "emit('*')",
},
"type": "keyword",
},
"slo.id": Object {
"script": Object {
"source": Any<String>,
},
"type": "keyword",
},
"slo.indicator.type": Object {
"script": Object {
"source": "emit('sli.metric.timeslice')",
"source": "emit('irrelevant')",
},
"type": "keyword",
},
@ -308,48 +234,12 @@ Object {
},
"type": "keyword",
},
"slo.name": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.objective.sliceDurationInSeconds": Object {
"script": Object {
"source": "emit(120)",
},
"type": "long",
},
"slo.objective.target": Object {
"script": Object {
"source": "emit(0.98)",
},
"type": "double",
},
"slo.revision": Object {
"script": Object {
"source": "emit(1)",
},
"type": "long",
},
"slo.tags": Object {
"script": Object {
"source": "emit('critical,k8s')",
},
"type": "keyword",
},
"slo.timeWindow.duration": Object {
"script": Object {
"source": "emit('7d')",
},
"type": "keyword",
},
"slo.timeWindow.type": Object {
"script": Object {
"source": "emit('rolling')",
},
"type": "keyword",
},
},
},
"sync": Object {
@ -358,7 +248,7 @@ Object {
"field": "@timestamp",
},
},
"transform_id": Any<String>,
"transform_id": "slo-irrelevant-1",
}
`;
@ -367,12 +257,12 @@ Object {
"_meta": Object {
"managed": true,
"managed_by": "observability",
"version": 2,
"version": 3,
},
"description": "Rolled-up SLI data for SLO: irrelevant",
"description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]",
"dest": Object {
"index": ".slo-observability.sli-v2",
"pipeline": ".slo-observability.sli.pipeline",
"index": ".slo-observability.sli-v3",
"pipeline": ".slo-observability.sli.pipeline-v3",
},
"frequency": "1m",
"pivot": Object {
@ -507,71 +397,21 @@ Object {
"fixed_interval": "2m",
},
},
"slo.budgetingMethod": Object {
"terms": Object {
"field": "slo.budgetingMethod",
},
},
"slo.description": Object {
"terms": Object {
"field": "slo.description",
},
},
"slo.groupBy": Object {
"terms": Object {
"field": "slo.groupBy",
},
},
"slo.id": Object {
"terms": Object {
"field": "slo.id",
},
},
"slo.indicator.type": Object {
"terms": Object {
"field": "slo.indicator.type",
},
},
"slo.instanceId": Object {
"terms": Object {
"field": "slo.instanceId",
},
},
"slo.name": Object {
"terms": Object {
"field": "slo.name",
},
},
"slo.objective.sliceDurationInSeconds": Object {
"terms": Object {
"field": "slo.objective.sliceDurationInSeconds",
},
},
"slo.objective.target": Object {
"terms": Object {
"field": "slo.objective.target",
},
},
"slo.revision": Object {
"terms": Object {
"field": "slo.revision",
},
},
"slo.tags": Object {
"terms": Object {
"field": "slo.tags",
},
},
"slo.timeWindow.duration": Object {
"terms": Object {
"field": "slo.timeWindow.duration",
},
},
"slo.timeWindow.type": Object {
"terms": Object {
"field": "slo.timeWindow.type",
},
},
},
},
"settings": Object {
@ -606,33 +446,9 @@ Object {
},
},
"runtime_mappings": Object {
"slo.budgetingMethod": Object {
"script": Object {
"source": "emit('timeslices')",
},
"type": "keyword",
},
"slo.description": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.groupBy": Object {
"script": Object {
"source": "emit('*')",
},
"type": "keyword",
},
"slo.id": Object {
"script": Object {
"source": Any<String>,
},
"type": "keyword",
},
"slo.indicator.type": Object {
"script": Object {
"source": "emit('sli.metric.timeslice')",
"source": "emit('irrelevant')",
},
"type": "keyword",
},
@ -642,48 +458,12 @@ Object {
},
"type": "keyword",
},
"slo.name": Object {
"script": Object {
"source": "emit('irrelevant')",
},
"type": "keyword",
},
"slo.objective.sliceDurationInSeconds": Object {
"script": Object {
"source": "emit(120)",
},
"type": "long",
},
"slo.objective.target": Object {
"script": Object {
"source": "emit(0.98)",
},
"type": "double",
},
"slo.revision": Object {
"script": Object {
"source": "emit(1)",
},
"type": "long",
},
"slo.tags": Object {
"script": Object {
"source": "emit('critical,k8s')",
},
"type": "keyword",
},
"slo.timeWindow.duration": Object {
"script": Object {
"source": "emit('7d')",
},
"type": "keyword",
},
"slo.timeWindow.type": Object {
"script": Object {
"source": "emit('rolling')",
},
"type": "keyword",
},
},
},
"sync": Object {
@ -692,6 +472,6 @@ Object {
"field": "@timestamp",
},
},
"transform_id": Any<String>,
"transform_id": "slo-irrelevant-1",
}
`;

View file

@ -17,32 +17,20 @@ const generator = new ApmTransactionDurationTransformGenerator();
describe('APM Transaction Duration Transform Generator', () => {
it('returns the expected transform params with every specified indicator params', () => {
const slo = createSLO({ indicator: createAPMTransactionDurationIndicator() });
const slo = createSLO({ id: 'irrelevant', indicator: createAPMTransactionDurationIndicator() });
const transform = generator.getTransformParams(slo);
expect(transform).toMatchSnapshot({
transform_id: expect.any(String),
source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } },
});
expect(transform.transform_id).toEqual(`slo-${slo.id}-${slo.revision}`);
expect(transform.source.runtime_mappings!['slo.id']).toMatchObject({
script: { source: `emit('${slo.id}')` },
});
expect(transform.source.runtime_mappings!['slo.revision']).toMatchObject({
script: { source: `emit(${slo.revision})` },
});
expect(transform).toMatchSnapshot();
});
it('returns the expected transform params for timeslices slo', () => {
const slo = createSLOWithTimeslicesBudgetingMethod({
id: 'irrelevant',
indicator: createAPMTransactionDurationIndicator(),
});
const transform = generator.getTransformParams(slo);
expect(transform).toMatchSnapshot({
transform_id: expect.any(String),
source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } },
});
expect(transform).toMatchSnapshot();
});
it("does not include the query filter when params are '*'", () => {

View file

@ -17,32 +17,23 @@ const generator = new ApmTransactionErrorRateTransformGenerator();
describe('APM Transaction Error Rate Transform Generator', () => {
it('returns the expected transform params with every specified indicator params', async () => {
const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator() });
const transform = generator.getTransformParams(slo);
expect(transform).toMatchSnapshot({
transform_id: expect.any(String),
source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } },
});
expect(transform.transform_id).toEqual(`slo-${slo.id}-${slo.revision}`);
expect(transform.source.runtime_mappings!['slo.id']).toMatchObject({
script: { source: `emit('${slo.id}')` },
});
expect(transform.source.runtime_mappings!['slo.revision']).toMatchObject({
script: { source: `emit(${slo.revision})` },
});
});
it('returns the expected transform params for timeslices slo', async () => {
const slo = createSLOWithTimeslicesBudgetingMethod({
const slo = createSLO({
id: 'irrelevant',
indicator: createAPMTransactionErrorRateIndicator(),
});
const transform = generator.getTransformParams(slo);
expect(transform).toMatchSnapshot({
transform_id: expect.any(String),
source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } },
expect(transform).toMatchSnapshot();
});
it('returns the expected transform params for timeslices slo', async () => {
const slo = createSLOWithTimeslicesBudgetingMethod({
id: 'irrelevant',
indicator: createAPMTransactionErrorRateIndicator(),
});
const transform = generator.getTransformParams(slo);
expect(transform).toMatchSnapshot();
});
it("does not include the query filter when params are '*'", async () => {

View file

@ -30,6 +30,7 @@ describe('Histogram Transform Generator', () => {
});
expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid KQL: foo:/);
});
it('throws when the total filter is invalid', () => {
const anSLO = createSLO({
indicator: createHistogramIndicator({
@ -42,6 +43,7 @@ describe('Histogram Transform Generator', () => {
});
expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid KQL: foo:/);
});
it('throws when the query_filter is invalid', () => {
const anSLO = createSLO({
indicator: createHistogramIndicator({ filter: '{ kql.query: invalid' }),
@ -51,32 +53,20 @@ describe('Histogram Transform Generator', () => {
});
it('returns the expected transform params with every specified indicator params', async () => {
const anSLO = createSLO({ indicator: createHistogramIndicator() });
const anSLO = createSLO({ id: 'irrelevant', indicator: createHistogramIndicator() });
const transform = generator.getTransformParams(anSLO);
expect(transform).toMatchSnapshot({
transform_id: expect.any(String),
source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } },
});
expect(transform.transform_id).toEqual(`slo-${anSLO.id}-${anSLO.revision}`);
expect(transform.source.runtime_mappings!['slo.id']).toMatchObject({
script: { source: `emit('${anSLO.id}')` },
});
expect(transform.source.runtime_mappings!['slo.revision']).toMatchObject({
script: { source: `emit(${anSLO.revision})` },
});
expect(transform).toMatchSnapshot();
});
it('returns the expected transform params for timeslices slo', async () => {
const anSLO = createSLOWithTimeslicesBudgetingMethod({
id: 'irrelevant',
indicator: createHistogramIndicator(),
});
const transform = generator.getTransformParams(anSLO);
expect(transform).toMatchSnapshot({
transform_id: expect.any(String),
source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } },
});
expect(transform).toMatchSnapshot();
});
it('filters the source using the kql query', async () => {

View file

@ -37,32 +37,20 @@ describe('KQL Custom Transform Generator', () => {
});
it('returns the expected transform params with every specified indicator params', async () => {
const anSLO = createSLO({ indicator: createKQLCustomIndicator() });
const anSLO = createSLO({ id: 'irrelevant', indicator: createKQLCustomIndicator() });
const transform = generator.getTransformParams(anSLO);
expect(transform).toMatchSnapshot({
transform_id: expect.any(String),
source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } },
});
expect(transform.transform_id).toEqual(`slo-${anSLO.id}-${anSLO.revision}`);
expect(transform.source.runtime_mappings!['slo.id']).toMatchObject({
script: { source: `emit('${anSLO.id}')` },
});
expect(transform.source.runtime_mappings!['slo.revision']).toMatchObject({
script: { source: `emit(${anSLO.revision})` },
});
expect(transform).toMatchSnapshot();
});
it('returns the expected transform params for timeslices slo', async () => {
const anSLO = createSLOWithTimeslicesBudgetingMethod({
id: 'irrelevant',
indicator: createKQLCustomIndicator(),
});
const transform = generator.getTransformParams(anSLO);
expect(transform).toMatchSnapshot({
transform_id: expect.any(String),
source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } },
});
expect(transform).toMatchSnapshot();
});
it('filters the source using the kql query', async () => {

View file

@ -69,32 +69,20 @@ describe('Metric Custom Transform Generator', () => {
});
it('returns the expected transform params with every specified indicator params', async () => {
const anSLO = createSLO({ indicator: createMetricCustomIndicator() });
const anSLO = createSLO({ id: 'irrelevant', indicator: createMetricCustomIndicator() });
const transform = generator.getTransformParams(anSLO);
expect(transform).toMatchSnapshot({
transform_id: expect.any(String),
source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } },
});
expect(transform.transform_id).toEqual(`slo-${anSLO.id}-${anSLO.revision}`);
expect(transform.source.runtime_mappings!['slo.id']).toMatchObject({
script: { source: `emit('${anSLO.id}')` },
});
expect(transform.source.runtime_mappings!['slo.revision']).toMatchObject({
script: { source: `emit(${anSLO.revision})` },
});
expect(transform).toMatchSnapshot();
});
it('returns the expected transform params for timeslices slo', async () => {
const anSLO = createSLOWithTimeslicesBudgetingMethod({
id: 'irrelevant',
indicator: createMetricCustomIndicator(),
});
const transform = generator.getTransformParams(anSLO);
expect(transform).toMatchSnapshot({
transform_id: expect.any(String),
source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } },
});
expect(transform).toMatchSnapshot();
});
it('filters the source using the kql query', async () => {

Some files were not shown because too many files have changed in this diff Show more