From 30706f8bf5f99559f38ebde6019a03c42715ccfa Mon Sep 17 00:00:00 2001 From: mgiota Date: Thu, 18 Jan 2024 11:07:29 +0100 Subject: [PATCH] paginated api for groups --- .../kbn-slo-schema/src/rest_specs/slo.ts | 26 ++++ .../observability/common/slo/constants.ts | 2 + .../public/hooks/slo/query_key_factory.ts | 7 +- .../public/hooks/slo/use_fetch_slo_groups.ts | 115 ++++++++---------- .../grouped_slos/group_list_view.tsx | 2 +- .../components/grouped_slos/group_view.tsx | 32 ++++- .../public/pages/slos/components/slo_list.tsx | 72 +++++------ .../observability/server/routes/slo/route.ts | 23 +++- .../server/services/slo/find_slo_groups.ts | 101 +++++++++++++++ .../server/services/slo/index.ts | 1 + 10 files changed, 274 insertions(+), 107 deletions(-) create mode 100644 x-pack/plugins/observability/server/services/slo/find_slo_groups.ts diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts index 4f72827957ef7..ea56c1ddeca10 100644 --- a/x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts @@ -96,6 +96,16 @@ const findSLOParamsSchema = t.partial({ }), }); +const groupBySchema = t.literal('tags'); // TODO add t.union when the rest options will be added + +const findSLOGroupsParamsSchema = t.partial({ + query: t.partial({ + page: t.string, + perPage: t.string, + groupBy: groupBySchema, + }), +}); + const sloResponseSchema = t.intersection([ t.type({ id: sloIdSchema, @@ -124,6 +134,8 @@ const sloWithSummaryResponseSchema = t.intersection([ t.type({ summary: summarySchema }), ]); +const sloGroupsResponseSchema = t.record(t.string, t.number); + const getSLOQuerySchema = t.partial({ query: t.partial({ instanceId: allOrAnyString, @@ -176,6 +188,13 @@ const findSLOResponseSchema = t.type({ results: t.array(sloWithSummaryResponseSchema), }); +const findSLOGroupsResponseSchema = t.type({ + page: t.number, + perPage: t.number, + total: t.number, + results: sloGroupsResponseSchema, +}); + const deleteSLOInstancesParamsSchema = t.type({ body: t.type({ list: t.array(t.type({ sloId: sloIdSchema, instanceId: t.string })) }), }); @@ -262,6 +281,9 @@ type UpdateSLOResponse = t.OutputOf; type FindSLOParams = t.TypeOf; type FindSLOResponse = t.OutputOf; +type FindSLOGroupsParams = t.TypeOf; +type FindSLOGroupsResponse = t.OutputOf; + type DeleteSLOInstancesInput = t.OutputOf; type DeleteSLOInstancesParams = t.TypeOf; @@ -298,6 +320,8 @@ export { deleteSLOInstancesParamsSchema, findSLOParamsSchema, findSLOResponseSchema, + findSLOGroupsParamsSchema, + findSLOGroupsResponseSchema, getPreviewDataParamsSchema, getPreviewDataResponseSchema, getSLOParamsSchema, @@ -327,6 +351,8 @@ export type { DeleteSLOInstancesParams, FindSLOParams, FindSLOResponse, + FindSLOGroupsParams, + FindSLOGroupsResponse, GetPreviewDataParams, GetPreviewDataResponse, GetSLOParams, diff --git a/x-pack/plugins/observability/common/slo/constants.ts b/x-pack/plugins/observability/common/slo/constants.ts index d49b1a805e192..7ef3ebb292e4d 100644 --- a/x-pack/plugins/observability/common/slo/constants.ts +++ b/x-pack/plugins/observability/common/slo/constants.ts @@ -33,6 +33,8 @@ export const getSLOTransformId = (sloId: string, sloRevision: number) => `slo-${sloId}-${sloRevision}`; export const DEFAULT_SLO_PAGE_SIZE = 25; +export const DEFAULT_SLO_GROUPS_PAGE_SIZE = 25; + export const getSLOSummaryTransformId = (sloId: string, sloRevision: number) => `slo-summary-${sloId}-${sloRevision}`; diff --git a/x-pack/plugins/observability/public/hooks/slo/query_key_factory.ts b/x-pack/plugins/observability/public/hooks/slo/query_key_factory.ts index 4f355ee0a8c28..14a4d7efca1f7 100644 --- a/x-pack/plugins/observability/public/hooks/slo/query_key_factory.ts +++ b/x-pack/plugins/observability/public/hooks/slo/query_key_factory.ts @@ -17,11 +17,16 @@ interface SloListFilter { lastRefresh?: number; } +interface SloGroupListFilter { + page: number; + perPage: number; +} + export const sloKeys = { all: ['slo'] as const, lists: () => [...sloKeys.all, 'list'] as const, list: (filters: SloListFilter) => [...sloKeys.lists(), filters] as const, - groups: () => [...sloKeys.all, 'groups'], + groups: (filters: SloGroupListFilter) => [...sloKeys.all, filters] as const, details: () => [...sloKeys.all, 'details'] as const, detail: (sloId?: string) => [...sloKeys.details(), sloId] as const, rules: () => [...sloKeys.all, 'rules'] as const, diff --git a/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_groups.ts b/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_groups.ts index e5c62c4016b25..a23f87f0fca6d 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_groups.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_groups.ts @@ -4,84 +4,71 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { - IEsSearchRequest, - IKibanaSearchResponse, - isRunningResponse, -} from '@kbn/data-plugin/common'; -import type { ISearchStart } from '@kbn/data-plugin/public'; import { useQuery } from '@tanstack/react-query'; +import { i18n } from '@kbn/i18n'; +import { FindSLOGroupsResponse } from '@kbn/slo-schema'; import { useKibana } from '../../utils/kibana_react'; -import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../../common/slo/constants'; import { sloKeys } from './query_key_factory'; +import { DEFAULT_SLO_GROUPS_PAGE_SIZE } from '../../../common/slo/constants'; -interface Aggregation { - doc_count: number; - key: string; +interface SLOGroupsParams { + page?: number; + perPage?: number; } -interface GroupAggregationsResponse { - aggregations: { - groupByTags: { - buckets: Aggregation[]; - }; - }; +interface UseFetchSloGroupsResponse { + isLoading: boolean; + isRefetching: boolean; + isSuccess: boolean; + isError: boolean; + data: FindSLOGroupsResponse | undefined; } -const createFetchAggregations = async (searchService: ISearchStart) => { - const search = async (): Promise => { - return new Promise((resolve, reject) => { - searchService - .search>({ - params: { - index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, - body: { - size: 0, - aggs: { - groupByTags: { - terms: { - field: 'slo.tags', - }, - }, - }, - }, - }, - }) - .subscribe({ - next: (response) => { - if (!isRunningResponse(response)) { - resolve(response.rawResponse); - } - }, - error: (requestError) => { - searchService.showError(requestError); - reject(requestError); - }, - }); - }); - }; - const { aggregations } = await search(); - return aggregations; -}; - -export function useFetchSloGroups() { +export function useFetchSloGroups({ + page = 1, + perPage = DEFAULT_SLO_GROUPS_PAGE_SIZE, +}: SLOGroupsParams = {}): UseFetchSloGroupsResponse { const { - data: { search: searchService }, + http, + notifications: { toasts }, } = useKibana().services; - - const { data, isLoading, isFetching } = useQuery({ - queryKey: sloKeys.groups(), - queryFn: async () => { - const response = await createFetchAggregations(searchService); - return response.groupByTags.buckets.reduce((acc, bucket) => { - return { ...acc, [bucket.key]: bucket.doc_count ?? 0 }; - }, {} as Record); + const { data, isLoading, isSuccess, isError, isRefetching } = useQuery({ + queryKey: sloKeys.groups({ page, perPage }), + queryFn: async ({ signal }) => { + const response = await http.get( + '/internal/api/observability/slos/_groups', + { + query: { + ...(page && { page }), + ...(perPage && { perPage }), + }, + signal, + } + ); + return response; + }, + cacheTime: 0, + refetchOnWindowFocus: false, + retry: (failureCount, error) => { + if (String(error) === 'Error: Forbidden') { + return false; + } + return failureCount < 4; + }, + onError: (error: Error) => { + toasts.addError(error, { + title: i18n.translate('xpack.observability.slo.groups.list.errorNotification', { + defaultMessage: 'Something went wrong while fetching SLO Groups', + }), + }); }, }); return { - data: data || {}, + data, isLoading, - isFetching, + isSuccess, + isError, + isRefetching, }; } diff --git a/x-pack/plugins/observability/public/pages/slos/components/grouped_slos/group_list_view.tsx b/x-pack/plugins/observability/public/pages/slos/components/grouped_slos/group_list_view.tsx index 301f8358ece76..38e6bf9781bff 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/grouped_slos/group_list_view.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/grouped_slos/group_list_view.tsx @@ -46,7 +46,7 @@ export function GroupListView({ isCompact, group, kqlQuery, sloView, sort, direc return ( - + <> { + setPage(pageNumber); + }; if (isLoading) { return ( @@ -33,10 +41,11 @@ export function GroupView({ isCompact, kqlQuery, sloView, sort, direction }: Pro } return ( <> - {data && - Object.keys(data).map((group) => { + {results && + Object.keys(results).map((group) => { return ( ); })} + + {total > 0 ? ( + + { + setPerPage(newPerPage); + }} + /> + + ) : null} ); } diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx index fc73cd5395ef8..4ed3c9ee61da4 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx @@ -81,57 +81,57 @@ export function SloList() { loading={isLoading || isCreatingSlo || isCloningSlo || isUpdatingSlo || isDeletingSlo} /> - - - onStateChange({ view: newView })} - onToggleCompactView={() => onStateChange({ compact: !isCompact })} - isCompact={isCompact} - /> - - {groupBy === 'ungrouped' && ( - + onStateChange({ view: newView })} + onToggleCompactView={() => onStateChange({ compact: !isCompact })} + isCompact={isCompact} /> + + {groupBy === 'ungrouped' && ( + <> + + {total > 0 ? ( + + { + onStateChange({ page: newPage }); + }} + itemsPerPage={perPage} + itemsPerPageOptions={[10, 25, 50, 100]} + onChangeItemsPerPage={(newPerPage) => { + storeState({ perPage: newPerPage, page: 0 }); + }} + /> + + ) : null} + )} - {groupBy !== 'ungrouped' && ( <> )} - - {total > 0 ? ( - - { - onStateChange({ page: newPage }); - }} - itemsPerPage={perPage} - itemsPerPageOptions={[10, 25, 50, 100]} - onChangeItemsPerPage={(newPerPage) => { - storeState({ perPage: newPerPage, page: 0 }); - }} - /> - - ) : null} ); } diff --git a/x-pack/plugins/observability/server/routes/slo/route.ts b/x-pack/plugins/observability/server/routes/slo/route.ts index 0ce51e284e949..3abb3670d0267 100644 --- a/x-pack/plugins/observability/server/routes/slo/route.ts +++ b/x-pack/plugins/observability/server/routes/slo/route.ts @@ -14,6 +14,7 @@ import { fetchHistoricalSummaryParamsSchema, findSloDefinitionsParamsSchema, findSLOParamsSchema, + findSLOGroupsParamsSchema, getPreviewDataParamsSchema, getSLOBurnRatesParamsSchema, getSLOInstancesParamsSchema, @@ -34,6 +35,7 @@ import { GetSLO, KibanaSavedObjectsSLORepository, UpdateSLO, + FindSLOGroups, } from '../../services/slo'; import { FetchHistoricalSummary } from '../../services/slo/fetch_historical_summary'; import { FindSLODefinitions } from '../../services/slo/find_slo_definitions'; @@ -359,7 +361,6 @@ const findSLORoute = createObservabilityServerRoute({ 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); @@ -372,6 +373,25 @@ const findSLORoute = createObservabilityServerRoute({ }, }); +const findSLOGroupsRoute = createObservabilityServerRoute({ + endpoint: 'GET /internal/api/observability/slos/_groups', + options: { + tags: ['access:slo_read'], + access: 'internal', + }, + params: findSLOGroupsParamsSchema, + handler: async ({ context, request, params, logger, dependencies }) => { + await assertPlatinumLicense(context); + const spaceId = + (await dependencies.spaces?.spacesService.getActiveSpace(request))?.id ?? 'default'; + const coreContext = context.core; + const esClient = (await coreContext).elasticsearch.client.asCurrentUser; + const findSLOGroups = new FindSLOGroups(esClient, spaceId); + const response = await findSLOGroups.execute(params?.query ?? {}); + return response; + }, +}); + const deleteSloInstancesRoute = createObservabilityServerRoute({ endpoint: 'POST /api/observability/slos/_delete_instances 2023-10-31', options: { @@ -532,4 +552,5 @@ export const sloRouteRepository = { ...getPreviewData, ...getSLOInstancesRoute, ...resetSLORoute, + ...findSLOGroupsRoute, }; diff --git a/x-pack/plugins/observability/server/services/slo/find_slo_groups.ts b/x-pack/plugins/observability/server/services/slo/find_slo_groups.ts new file mode 100644 index 0000000000000..38af808da2bd8 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/find_slo_groups.ts @@ -0,0 +1,101 @@ +/* + * 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 { FindSLOGroupsParams, FindSLOGroupsResponse, Pagination } from '@kbn/slo-schema'; +import { ElasticsearchClient } from '@kbn/core/server'; +import { IllegalArgumentError } from '../../errors'; +import { + SLO_SUMMARY_DESTINATION_INDEX_PATTERN, + DEFAULT_SLO_GROUPS_PAGE_SIZE, +} from '../../../common/slo/constants'; + +const DEFAULT_PAGE = 1; +const MAX_PER_PAGE = 5000; + +function toPagination(params: FindSLOGroupsParams): 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_SLO_GROUPS_PAGE_SIZE, + }; +} + +interface Aggregation { + doc_count: number; + key: string; +} + +interface GroupAggregationsResponse { + groupByTags: { + buckets: Aggregation[]; + }; + distinct_tags: { + value: number; + }; +} + +export class FindSLOGroups { + constructor(private esClient: ElasticsearchClient, private spaceId: string) {} + public async execute(params: FindSLOGroupsParams): Promise { + const pagination = toPagination(params); + const response = await this.esClient.search({ + index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, + size: 0, + query: { + bool: { + filter: [{ term: { spaceId: this.spaceId } }], + }, + }, + body: { + aggs: { + groupByTags: { + terms: { + field: 'slo.tags', + size: 10000, + }, + aggs: { + bucket_sort: { + bucket_sort: { + sort: [ + { + _key: { + order: 'asc', + }, + }, + ], + from: (pagination.page - 1) * pagination.perPage, + size: pagination.perPage, + }, + }, + }, + }, + distinct_tags: { + cardinality: { + field: 'slo.tags', + }, + }, + }, + }, + }); + const total = response.aggregations?.distinct_tags?.value ?? 0; + const results = + response.aggregations?.groupByTags?.buckets.reduce((acc, bucket) => { + return { ...acc, [bucket.key]: bucket.doc_count ?? 0 }; + }, {} as Record) ?? {}; + return { + page: pagination.page, + perPage: pagination.perPage, + total, + results, + }; + } +} diff --git a/x-pack/plugins/observability/server/services/slo/index.ts b/x-pack/plugins/observability/server/services/slo/index.ts index 2a939c56fde4b..c9c1ebfc7991c 100644 --- a/x-pack/plugins/observability/server/services/slo/index.ts +++ b/x-pack/plugins/observability/server/services/slo/index.ts @@ -21,3 +21,4 @@ export * from './summay_transform_manager'; export * from './update_slo'; export * from './summary_client'; export * from './get_slo_instances'; +export * from './find_slo_groups';