From 464d361cc72b1e66e3fcd1655021bc5becc894b3 Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Wed, 8 Jan 2025 11:07:23 -0500 Subject: [PATCH] fix(slo): introduce cursor pagination in Find SLO API (#203712) --- .../src/rest_specs/routes/find.test.ts | 46 ++++++++ .../src/rest_specs/routes/find.ts | 55 +++++++-- .../plugins/slo/server/routes/slo/route.ts | 2 +- .../slo/server/services/find_slo.test.ts | 15 ++- .../plugins/slo/server/services/find_slo.ts | 39 +++++-- .../fixtures/summary_search_document.ts | 4 +- .../plugins/slo/server/services/index.ts | 1 + .../slo/server/services/mocks/index.ts | 2 +- .../summary_search_client.test.ts.snap | 0 .../summary_search_client.test.ts | 30 ++++- .../summary_search_client.ts | 106 +++++++++--------- .../services/summary_search_client/types.ts | 90 +++++++++++++++ .../services/transform_generators/common.ts | 3 +- .../apis/observability/slo/find_slo.ts | 14 ++- 14 files changed, 323 insertions(+), 84 deletions(-) create mode 100644 x-pack/platform/packages/shared/kbn-slo-schema/src/rest_specs/routes/find.test.ts rename x-pack/solutions/observability/plugins/slo/server/services/{ => summary_search_client}/__snapshots__/summary_search_client.test.ts.snap (100%) rename x-pack/solutions/observability/plugins/slo/server/services/{ => summary_search_client}/summary_search_client.test.ts (90%) rename x-pack/solutions/observability/plugins/slo/server/services/{ => summary_search_client}/summary_search_client.ts (76%) create mode 100644 x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/types.ts diff --git a/x-pack/platform/packages/shared/kbn-slo-schema/src/rest_specs/routes/find.test.ts b/x-pack/platform/packages/shared/kbn-slo-schema/src/rest_specs/routes/find.test.ts new file mode 100644 index 0000000000000..9aaef0a65b093 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-slo-schema/src/rest_specs/routes/find.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { merge } from 'lodash'; +import { findSLOParamsSchema } from './find'; + +const BASE_REQUEST = { + query: { + filters: 'irrelevant', + kqlQuery: 'irrelevant', + page: '1', + perPage: '25', + sortBy: 'error_budget_consumed', + sortDirection: 'asc', + hideStale: true, + }, +}; + +describe('FindSLO schema validation', () => { + it.each(['not_an_array', 42, [], [42, 'ok']])( + 'returns an error when searchAfter is not a valid JSON array (%s)', + (searchAfter) => { + const request = merge(BASE_REQUEST, { + query: { + searchAfter, + }, + }); + const result = findSLOParamsSchema.decode(request); + expect(result._tag === 'Left').toBe(true); + } + ); + + it('parses searchAfter correctly', () => { + const request = merge(BASE_REQUEST, { + query: { + searchAfter: JSON.stringify([1, 'ok']), + }, + }); + const result = findSLOParamsSchema.decode(request); + expect(result._tag === 'Right').toBe(true); + }); +}); diff --git a/x-pack/platform/packages/shared/kbn-slo-schema/src/rest_specs/routes/find.ts b/x-pack/platform/packages/shared/kbn-slo-schema/src/rest_specs/routes/find.ts index be7e063ecc80f..bf2e05bf8a982 100644 --- a/x-pack/platform/packages/shared/kbn-slo-schema/src/rest_specs/routes/find.ts +++ b/x-pack/platform/packages/shared/kbn-slo-schema/src/rest_specs/routes/find.ts @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import * as t from 'io-ts'; import { toBooleanRt } from '@kbn/io-ts-utils'; +import { either, isRight } from 'fp-ts/lib/Either'; +import * as t from 'io-ts'; import { sloWithDataResponseSchema } from '../slo'; const sortDirectionSchema = t.union([t.literal('asc'), t.literal('desc')]); @@ -19,24 +20,64 @@ const sortBySchema = t.union([ t.literal('burn_rate_1d'), ]); +const searchAfterArraySchema = t.array(t.union([t.string, t.number])); +type SearchAfterArray = t.TypeOf; + +const searchAfterSchema = new t.Type( + 'SearchAfter', + (input: unknown): input is SearchAfterArray => + Array.isArray(input) && + input.length > 0 && + input.every((item) => typeof item === 'string' || typeof item === 'number'), + (input: unknown, context: t.Context) => + either.chain(t.string.validate(input, context), (value: string) => { + try { + const parsedValue = JSON.parse(value); + const decoded = searchAfterArraySchema.decode(parsedValue); + if (isRight(decoded)) { + return t.success(decoded.right); + } + return t.failure( + input, + context, + 'Invalid searchAfter value, must be a JSON array of strings or numbers' + ); + } catch (err) { + return t.failure( + input, + context, + 'Invalid searchAfter value, must be a JSON array of strings or numbers' + ); + } + }), + (input: SearchAfterArray): string => JSON.stringify(input) +); + const findSLOParamsSchema = t.partial({ query: t.partial({ filters: t.string, kqlQuery: t.string, + // Used for page pagination page: t.string, perPage: t.string, sortBy: sortBySchema, sortDirection: sortDirectionSchema, hideStale: toBooleanRt, + // Used for cursor pagination, searchAfter is a JSON array + searchAfter: searchAfterSchema, + size: t.string, }), }); -const findSLOResponseSchema = t.type({ - page: t.number, - perPage: t.number, - total: t.number, - results: t.array(sloWithDataResponseSchema), -}); +const findSLOResponseSchema = t.intersection([ + t.type({ + page: t.number, + perPage: t.number, + total: t.number, + results: t.array(sloWithDataResponseSchema), + }), + t.partial({ searchAfter: searchAfterArraySchema, size: t.number }), +]); type FindSLOParams = t.TypeOf; type FindSLOResponse = t.OutputOf; diff --git a/x-pack/solutions/observability/plugins/slo/server/routes/slo/route.ts b/x-pack/solutions/observability/plugins/slo/server/routes/slo/route.ts index a7589de5d0909..b93076ace737f 100644 --- a/x-pack/solutions/observability/plugins/slo/server/routes/slo/route.ts +++ b/x-pack/solutions/observability/plugins/slo/server/routes/slo/route.ts @@ -57,7 +57,7 @@ import { ManageSLO } from '../../services/manage_slo'; import { ResetSLO } from '../../services/reset_slo'; import { SloDefinitionClient } from '../../services/slo_definition_client'; import { getSloSettings, storeSloSettings } from '../../services/slo_settings'; -import { DefaultSummarySearchClient } from '../../services/summary_search_client'; +import { DefaultSummarySearchClient } from '../../services/summary_search_client/summary_search_client'; import { DefaultSummaryTransformGenerator } from '../../services/summary_transform_generator/summary_transform_generator'; import { createTransformGenerators } from '../../services/transform_generators'; import { createSloServerRoute } from '../create_slo_server_route'; diff --git a/x-pack/solutions/observability/plugins/slo/server/services/find_slo.test.ts b/x-pack/solutions/observability/plugins/slo/server/services/find_slo.test.ts index bb26ab235c9f4..b4e3656bb8e39 100644 --- a/x-pack/solutions/observability/plugins/slo/server/services/find_slo.test.ts +++ b/x-pack/solutions/observability/plugins/slo/server/services/find_slo.test.ts @@ -12,7 +12,7 @@ import { FindSLO } from './find_slo'; import { createSLO } from './fixtures/slo'; import { createSLORepositoryMock, createSummarySearchClientMock } from './mocks'; import { SLORepository } from './slo_repository'; -import { SummaryResult, SummarySearchClient } from './summary_search_client'; +import type { SummaryResult, SummarySearchClient } from './summary_search_client/types'; describe('FindSLO', () => { let mockRepository: jest.Mocked; @@ -151,16 +151,27 @@ describe('FindSLO', () => { }); describe('validation', () => { - it("throws an error when 'perPage > 5000'", async () => { + beforeEach(() => { const slo = createSLO(); mockSummarySearchClient.search.mockResolvedValueOnce(summarySearchResult(slo)); mockRepository.findAllByIds.mockResolvedValueOnce([slo]); + }); + it("throws an error when 'perPage' > 5000", async () => { await expect(findSLO.execute({ perPage: '5000' })).resolves.not.toThrow(); await expect(findSLO.execute({ perPage: '5001' })).rejects.toThrowError( 'perPage limit set to 5000' ); }); + + describe('Cursor Pagination', () => { + it("throws an error when 'size' > 5000", async () => { + await expect(findSLO.execute({ size: '5000' })).resolves.not.toThrow(); + await expect(findSLO.execute({ size: '5001' })).rejects.toThrowError( + 'size limit set to 5000' + ); + }); + }); }); }); diff --git a/x-pack/solutions/observability/plugins/slo/server/services/find_slo.ts b/x-pack/solutions/observability/plugins/slo/server/services/find_slo.ts index dcd7fe44d0783..4c240dae77af7 100644 --- a/x-pack/solutions/observability/plugins/slo/server/services/find_slo.ts +++ b/x-pack/solutions/observability/plugins/slo/server/services/find_slo.ts @@ -5,16 +5,22 @@ * 2.0. */ -import { FindSLOParams, FindSLOResponse, findSLOResponseSchema, Pagination } from '@kbn/slo-schema'; +import { FindSLOParams, FindSLOResponse, findSLOResponseSchema } from '@kbn/slo-schema'; import { keyBy } from 'lodash'; import { SLODefinition } from '../domain/models'; import { IllegalArgumentError } from '../errors'; import { SLORepository } from './slo_repository'; -import { Sort, SummaryResult, SummarySearchClient } from './summary_search_client'; +import type { + Pagination, + Sort, + SummaryResult, + SummarySearchClient, +} from './summary_search_client/types'; const DEFAULT_PAGE = 1; const DEFAULT_PER_PAGE = 25; -const MAX_PER_PAGE = 5000; +const DEFAULT_SIZE = 100; +const MAX_PER_PAGE_OR_SIZE = 5000; export class FindSLO { constructor( @@ -38,8 +44,10 @@ export class FindSLO { ); return findSLOResponseSchema.encode({ - page: summaryResults.page, - perPage: summaryResults.perPage, + page: 'page' in summaryResults ? summaryResults.page : DEFAULT_PAGE, + perPage: 'perPage' in summaryResults ? summaryResults.perPage : DEFAULT_PER_PAGE, + size: 'size' in summaryResults ? summaryResults.size : undefined, + searchAfter: 'searchAfter' in summaryResults ? summaryResults.searchAfter : undefined, total: summaryResults.total, results: mergeSloWithSummary(localSloDefinitions, summaryResults.results), }); @@ -78,16 +86,29 @@ function mergeSloWithSummary( } function toPagination(params: FindSLOParams): Pagination { + const isCursorBased = !!params.searchAfter || !!params.size; + + if (isCursorBased) { + const size = Number(params.size); + if (!isNaN(size) && size > MAX_PER_PAGE_OR_SIZE) { + throw new IllegalArgumentError('size limit set to 5000'); + } + + return { + searchAfter: params.searchAfter, + size: !isNaN(size) && size > 0 ? size : DEFAULT_SIZE, + }; + } + 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}`); + if (!isNaN(perPage) && perPage > MAX_PER_PAGE_OR_SIZE) { + throw new IllegalArgumentError('perPage limit set to 5000'); } return { page: !isNaN(page) && page >= 1 ? page : DEFAULT_PAGE, - perPage: !isNaN(perPage) && perPage >= 0 ? perPage : DEFAULT_PER_PAGE, + perPage: !isNaN(perPage) && perPage > 0 ? perPage : DEFAULT_PER_PAGE, }; } diff --git a/x-pack/solutions/observability/plugins/slo/server/services/fixtures/summary_search_document.ts b/x-pack/solutions/observability/plugins/slo/server/services/fixtures/summary_search_document.ts index 8837e567c97cc..1f59a964a3e1b 100644 --- a/x-pack/solutions/observability/plugins/slo/server/services/fixtures/summary_search_document.ts +++ b/x-pack/solutions/observability/plugins/slo/server/services/fixtures/summary_search_document.ts @@ -25,7 +25,7 @@ export const aSummaryDocument = ( export const aHitFromSummaryIndex = (_source: any) => { return { - _index: '.slo-observability.summary-v2', + _index: '.slo-observability.summary-v3.3', _id: uuidv4(), _score: 1, _source, @@ -34,7 +34,7 @@ export const aHitFromSummaryIndex = (_source: any) => { export const aHitFromTempSummaryIndex = (_source: any) => { return { - _index: '.slo-observability.summary-v2.temp', + _index: '.slo-observability.summary-v3.3.temp', _id: uuidv4(), _score: 1, _source, diff --git a/x-pack/solutions/observability/plugins/slo/server/services/index.ts b/x-pack/solutions/observability/plugins/slo/server/services/index.ts index 4688a34740c63..c229226a290d2 100644 --- a/x-pack/solutions/observability/plugins/slo/server/services/index.ts +++ b/x-pack/solutions/observability/plugins/slo/server/services/index.ts @@ -22,3 +22,4 @@ export * from './summary_client'; export * from './get_slo_groupings'; export * from './find_slo_groups'; export * from './get_slo_health'; +export * from './summary_search_client/summary_search_client'; diff --git a/x-pack/solutions/observability/plugins/slo/server/services/mocks/index.ts b/x-pack/solutions/observability/plugins/slo/server/services/mocks/index.ts index ab8230cfec463..c6fa4a3d949f3 100644 --- a/x-pack/solutions/observability/plugins/slo/server/services/mocks/index.ts +++ b/x-pack/solutions/observability/plugins/slo/server/services/mocks/index.ts @@ -9,7 +9,7 @@ import { ResourceInstaller } from '../resource_installer'; import { BurnRatesClient } from '../burn_rates_client'; import { SLORepository } from '../slo_repository'; import { SummaryClient } from '../summary_client'; -import { SummarySearchClient } from '../summary_search_client'; +import { SummarySearchClient } from '../summary_search_client/types'; import { TransformManager } from '../transform_manager'; const createResourceInstallerMock = (): jest.Mocked => { diff --git a/x-pack/solutions/observability/plugins/slo/server/services/__snapshots__/summary_search_client.test.ts.snap b/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/__snapshots__/summary_search_client.test.ts.snap similarity index 100% rename from x-pack/solutions/observability/plugins/slo/server/services/__snapshots__/summary_search_client.test.ts.snap rename to x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/__snapshots__/summary_search_client.test.ts.snap diff --git a/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client.test.ts b/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/summary_search_client.test.ts similarity index 90% rename from x-pack/solutions/observability/plugins/slo/server/services/summary_search_client.test.ts rename to x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/summary_search_client.test.ts index a522bd287d045..d1fd1ffe0ce25 100644 --- a/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client.test.ts +++ b/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/summary_search_client.test.ts @@ -8,13 +8,14 @@ import { ElasticsearchClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks'; import { loggerMock } from '@kbn/logging-mocks'; import { Pagination } from '@kbn/slo-schema/src/models/pagination'; -import { createSLO } from './fixtures/slo'; +import { createSLO } from '../fixtures/slo'; import { aHitFromSummaryIndex, aHitFromTempSummaryIndex, aSummaryDocument, -} from './fixtures/summary_search_document'; -import { DefaultSummarySearchClient, Sort, SummarySearchClient } from './summary_search_client'; +} from '../fixtures/summary_search_document'; +import { DefaultSummarySearchClient } from './summary_search_client'; +import type { Sort, SummarySearchClient } from './types'; const defaultSort: Sort = { field: 'sli_value', @@ -169,6 +170,12 @@ describe('Summary Search Client', () => { sliValue: { order: 'asc', }, + 'slo.id': { + order: 'asc', + }, + 'slo.instanceId': { + order: 'asc', + }, }, track_total_hits: true, }, @@ -202,6 +209,12 @@ describe('Summary Search Client', () => { sliValue: { order: 'asc', }, + 'slo.id': { + order: 'asc', + }, + 'slo.instanceId': { + order: 'asc', + }, }, track_total_hits: true, }, @@ -229,7 +242,16 @@ describe('Summary Search Client', () => { }, }, size: 40, - sort: { isTempDoc: { order: 'asc' }, sliValue: { order: 'asc' } }, + sort: { + isTempDoc: { order: 'asc' }, + sliValue: { order: 'asc' }, + 'slo.id': { + order: 'asc', + }, + 'slo.instanceId': { + order: 'asc', + }, + }, track_total_hits: true, }, ]); diff --git a/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client.ts b/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/summary_search_client.ts similarity index 76% rename from x-pack/solutions/observability/plugins/slo/server/services/summary_search_client.ts rename to x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/summary_search_client.ts index c4f0de5b38b25..5c1c0e9e780cb 100644 --- a/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client.ts +++ b/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/summary_search_client.ts @@ -5,58 +5,30 @@ * 2.0. */ -import { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ElasticsearchClient, Logger, SavedObjectsClientContract } from '@kbn/core/server'; import { isCCSRemoteIndexName } from '@kbn/es-query'; -import { ALL_VALUE, Paginated, Pagination } from '@kbn/slo-schema'; +import { ALL_VALUE } from '@kbn/slo-schema'; import { assertNever } from '@kbn/std'; import { partition } from 'lodash'; -import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../common/constants'; -import { Groupings, SLODefinition, SLOId, StoredSLOSettings, Summary } from '../domain/models'; -import { toHighPrecision } from '../utils/number'; -import { createEsParams, typedSearch } from '../utils/queries'; -import { getListOfSummaryIndices, getSloSettings } from './slo_settings'; -import { EsSummaryDocument } from './summary_transform_generator/helpers/create_temp_summary'; -import { getElasticsearchQueryOrThrow, parseStringFilters } from './transform_generators'; -import { fromRemoteSummaryDocumentToSloDefinition } from './unsafe_federated/remote_summary_doc_to_slo'; -import { getFlattenedGroupings } from './utils'; - -export interface SummaryResult { - sloId: SLOId; - instanceId: string; - summary: Summary; - groupings: Groupings; - remote?: { - kibanaUrl: string; - remoteName: string; - slo: SLODefinition; - }; -} - -type SortField = - | 'error_budget_consumed' - | 'error_budget_remaining' - | 'sli_value' - | 'status' - | 'burn_rate_5m' - | 'burn_rate_1h' - | 'burn_rate_1d'; - -export interface Sort { - field: SortField; - direction: 'asc' | 'desc'; -} - -export interface SummarySearchClient { - search( - kqlQuery: string, - filters: string, - sort: Sort, - pagination: Pagination, - hideStale?: boolean - ): Promise>; -} +import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../../common/constants'; +import { StoredSLOSettings } from '../../domain/models'; +import { toHighPrecision } from '../../utils/number'; +import { createEsParams, typedSearch } from '../../utils/queries'; +import { getListOfSummaryIndices, getSloSettings } from '../slo_settings'; +import { EsSummaryDocument } from '../summary_transform_generator/helpers/create_temp_summary'; +import { getElasticsearchQueryOrThrow, parseStringFilters } from '../transform_generators'; +import { fromRemoteSummaryDocumentToSloDefinition } from '../unsafe_federated/remote_summary_doc_to_slo'; +import { getFlattenedGroupings } from '../utils'; +import type { + Paginated, + Pagination, + Sort, + SortField, + SummaryResult, + SummarySearchClient, +} from './types'; +import { isCursorPagination } from './types'; export class DefaultSummarySearchClient implements SummarySearchClient { constructor( @@ -76,6 +48,7 @@ export class DefaultSummarySearchClient implements SummarySearchClient { const parsedFilters = parseStringFilters(filters, this.logger); const settings = await getSloSettings(this.soClient); const { indices } = await getListOfSummaryIndices(this.esClient, settings); + const esParams = createEsParams({ index: indices, track_total_hits: true, @@ -98,9 +71,14 @@ export class DefaultSummarySearchClient implements SummarySearchClient { [toDocumentSortField(sort.field)]: { order: sort.direction, }, + 'slo.id': { + order: 'asc', + }, + 'slo.instanceId': { + order: 'asc', + }, }, - from: (pagination.page - 1) * pagination.perPage, - size: pagination.perPage * 2, // twice as much as we return, in case they are all duplicate temp/non-temp summary + ...toPaginationQuery(pagination), }); try { @@ -109,9 +87,9 @@ export class DefaultSummarySearchClient implements SummarySearchClient { esParams ); - const total = (summarySearch.hits.total as SearchTotalHits).value ?? 0; + const total = summarySearch.hits.total.value ?? 0; if (total === 0) { - return { total: 0, perPage: pagination.perPage, page: pagination.page, results: [] }; + return { total: 0, ...pagination, results: [] }; } const [tempSummaryDocuments, summaryDocuments] = partition( @@ -129,12 +107,16 @@ export class DefaultSummarySearchClient implements SummarySearchClient { const finalResults = summaryDocuments .concat(tempSummaryDocumentsDeduped) - .slice(0, pagination.perPage); + .slice(0, isCursorPagination(pagination) ? pagination.size : pagination.perPage); const finalTotal = total - (tempSummaryDocuments.length - tempSummaryDocumentsDeduped.length); + const paginationResults = isCursorPagination(pagination) + ? { searchAfter: finalResults[finalResults.length - 1].sort, size: pagination.size } + : pagination; + return { - ...pagination, + ...paginationResults, total: finalTotal, results: finalResults.map((doc) => { const summaryDoc = doc._source; @@ -179,7 +161,7 @@ export class DefaultSummarySearchClient implements SummarySearchClient { }; } catch (err) { this.logger.error(`Error while searching SLO summary documents. ${err}`); - return { total: 0, perPage: pagination.perPage, page: pagination.page, results: [] }; + return { total: 0, ...pagination, results: [] }; } } @@ -251,3 +233,19 @@ function toDocumentSortField(field: SortField) { assertNever(field); } } + +function toPaginationQuery( + pagination: Pagination +): { size: number; search_after?: Array } | { size: number; from: number } { + if (isCursorPagination(pagination)) { + return { + size: pagination.size * 2, // Potential duplicates between temp and non-temp summaries + search_after: pagination.searchAfter, + }; + } + + return { + size: pagination.perPage * 2, // Potential duplicates between temp and non-temp summaries + from: (pagination.page - 1) * pagination.perPage, + }; +} diff --git a/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/types.ts b/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/types.ts new file mode 100644 index 0000000000000..098184de99077 --- /dev/null +++ b/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/types.ts @@ -0,0 +1,90 @@ +/* + * 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 { Groupings, SLODefinition, SLOId, Summary } from '../../domain/models'; + +interface SummaryResult { + sloId: SLOId; + instanceId: string; + summary: Summary; + groupings: Groupings; + remote?: { + kibanaUrl: string; + remoteName: string; + slo: SLODefinition; + }; +} + +type SortField = + | 'error_budget_consumed' + | 'error_budget_remaining' + | 'sli_value' + | 'status' + | 'burn_rate_5m' + | 'burn_rate_1h' + | 'burn_rate_1d'; + +interface Sort { + field: SortField; + direction: 'asc' | 'desc'; +} + +type Pagination = CursorPagination | OffsetPagination; + +interface CursorPagination { + searchAfter?: Array; + size: number; +} + +function isCursorPagination(pagination: Pagination): pagination is CursorPagination { + return (pagination as CursorPagination).size !== undefined; +} + +interface OffsetPagination { + page: number; + perPage: number; +} + +type Paginated = CursorPaginated | OffsetPaginated; + +interface CursorPaginated { + total: number; + searchAfter?: Array; + size: number; + results: T[]; +} + +interface OffsetPaginated { + total: number; + page: number; + perPage: number; + results: T[]; +} + +interface SummarySearchClient { + search( + kqlQuery: string, + filters: string, + sort: Sort, + pagination: Pagination, + hideStale?: boolean + ): Promise>; +} + +export type { + SummaryResult, + SortField, + Sort, + Pagination, + CursorPagination, + OffsetPagination as PagePagination, + Paginated, + CursorPaginated, + OffsetPaginated as PagePaginated, + SummarySearchClient, +}; +export { isCursorPagination }; diff --git a/x-pack/solutions/observability/plugins/slo/server/services/transform_generators/common.ts b/x-pack/solutions/observability/plugins/slo/server/services/transform_generators/common.ts index 4958a65a17a15..a663b24780953 100644 --- a/x-pack/solutions/observability/plugins/slo/server/services/transform_generators/common.ts +++ b/x-pack/solutions/observability/plugins/slo/server/services/transform_generators/common.ts @@ -40,9 +40,8 @@ export function parseStringFilters(filters: string, logger: Logger) { return JSON.parse(filters); } catch (e) { logger.info(`Failed to parse filters: ${e}`); + return {}; } - - return {}; } export function parseIndex(index: string): string | string[] { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/find_slo.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/find_slo.ts index 6c49cb9df751e..98b0bd1d237f2 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/find_slo.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/find_slo.ts @@ -65,24 +65,34 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { await retry.tryForTime(180 * 1000, async () => { let response = await supertestWithoutAuth .get(`/api/observability/slos`) + .query({ page: 1, perPage: 333 }) .set(adminRoleAuthc.apiKeyHeader) .set(internalHeaders) .send(); expect(response.body.results).length(2); + expect(response.body.page).eql(1); + expect(response.body.perPage).eql(333); + expect(response.body.total).eql(2); response = await supertestWithoutAuth - .get(`/api/observability/slos?kqlQuery=slo.name%3Airrelevant`) + .get(`/api/observability/slos`) + .query({ size: 222, kqlQuery: 'slo.name:irrelevant' }) .set(adminRoleAuthc.apiKeyHeader) .set(internalHeaders) .send() .expect(200); + expect(response.body.page).eql(1); // always return page with default value + expect(response.body.perPage).eql(25); // always return perPage with default value + expect(response.body.size).eql(222); + expect(response.body.searchAfter).ok(); expect(response.body.results).length(1); expect(response.body.results[0].id).eql(sloId2); response = await supertestWithoutAuth - .get(`/api/observability/slos?kqlQuery=slo.name%3Aintegration`) + .get(`/api/observability/slos`) + .query({ kqlQuery: 'slo.name:integration' }) .set(adminRoleAuthc.apiKeyHeader) .set(internalHeaders) .send()