[8.10] feat(slo): refactor fetch slo definitions hook (#164466) (#164697)

# Backport

This will backport the following commits from `main` to `8.10`:
- [feat(slo): refactor fetch slo definitions hook
(#164466)](https://github.com/elastic/kibana/pull/164466)

<!--- 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-08-24T12:07:02Z","message":"feat(slo):
refactor fetch slo definitions hook
(#164466)","sha":"b270602601229c5afafa997db99cc4e59ff97a13","branchLabelMapping":{"^v8.11.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["backport","release_note:skip","Team:
Actionable
Observability","v8.10.0","v8.11.0"],"number":164466,"url":"https://github.com/elastic/kibana/pull/164466","mergeCommit":{"message":"feat(slo):
refactor fetch slo definitions hook
(#164466)","sha":"b270602601229c5afafa997db99cc4e59ff97a13"}},"sourceBranch":"main","suggestedTargetBranches":["8.10"],"targetPullRequestStates":[{"branch":"8.10","label":"v8.10.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.11.0","labelRegex":"^v8.11.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/164466","number":164466,"mergeCommit":{"message":"feat(slo):
refactor fetch slo definitions hook
(#164466)","sha":"b270602601229c5afafa997db99cc4e59ff97a13"}}]}]
BACKPORT-->

Co-authored-by: Kevin Delemme <kevin.delemme@elastic.co>
This commit is contained in:
Kibana Machine 2023-08-24 09:33:27 -04:00 committed by GitHub
parent af08fbdcd1
commit d27124a99c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 138 additions and 73 deletions

View file

@ -170,6 +170,24 @@ const fetchHistoricalSummaryResponseSchema = t.array(
}) })
); );
/**
* The query params schema for /internal/observability/slo/_definitions
*
* @private
*/
const findSloDefinitionsParamsSchema = t.type({
query: t.type({
search: t.string,
}),
});
/**
* The response schema for /internal/observability/slo/_definitions
*
* @private
*/
const findSloDefinitionsResponseSchema = t.array(sloResponseSchema);
const getSLODiagnosisParamsSchema = t.type({ const getSLODiagnosisParamsSchema = t.type({
path: t.type({ id: t.string }), path: t.type({ id: t.string }),
}); });
@ -229,6 +247,13 @@ type FetchHistoricalSummaryParams = t.TypeOf<typeof fetchHistoricalSummaryParams
type FetchHistoricalSummaryResponse = t.OutputOf<typeof fetchHistoricalSummaryResponseSchema>; type FetchHistoricalSummaryResponse = t.OutputOf<typeof fetchHistoricalSummaryResponseSchema>;
type HistoricalSummaryResponse = t.OutputOf<typeof historicalSummarySchema>; type HistoricalSummaryResponse = t.OutputOf<typeof historicalSummarySchema>;
/**
* The response type for /internal/observability/slo/_definitions
*
* @private
*/
type FindSloDefinitionsResponse = t.OutputOf<typeof findSloDefinitionsResponseSchema>;
type GetPreviewDataParams = t.TypeOf<typeof getPreviewDataParamsSchema.props.body>; type GetPreviewDataParams = t.TypeOf<typeof getPreviewDataParamsSchema.props.body>;
type GetPreviewDataResponse = t.OutputOf<typeof getPreviewDataResponseSchema>; type GetPreviewDataResponse = t.OutputOf<typeof getPreviewDataResponseSchema>;
@ -257,6 +282,8 @@ export {
getSLOResponseSchema, getSLOResponseSchema,
fetchHistoricalSummaryParamsSchema, fetchHistoricalSummaryParamsSchema,
fetchHistoricalSummaryResponseSchema, fetchHistoricalSummaryResponseSchema,
findSloDefinitionsParamsSchema,
findSloDefinitionsResponseSchema,
manageSLOParamsSchema, manageSLOParamsSchema,
sloResponseSchema, sloResponseSchema,
sloWithSummaryResponseSchema, sloWithSummaryResponseSchema,
@ -281,6 +308,7 @@ export type {
FetchHistoricalSummaryParams, FetchHistoricalSummaryParams,
FetchHistoricalSummaryResponse, FetchHistoricalSummaryResponse,
HistoricalSummaryResponse, HistoricalSummaryResponse,
FindSloDefinitionsResponse,
ManageSLOParams, ManageSLOParams,
SLOResponse, SLOResponse,
SLOWithSummaryResponse, SLOWithSummaryResponse,

View file

@ -34,6 +34,7 @@ export const sloKeys = {
historicalSummaries: () => [...sloKeys.all, 'historicalSummary'] as const, historicalSummaries: () => [...sloKeys.all, 'historicalSummary'] as const,
historicalSummary: (list: Array<{ sloId: string; instanceId: string }>) => historicalSummary: (list: Array<{ sloId: string; instanceId: string }>) =>
[...sloKeys.historicalSummaries(), list] as const, [...sloKeys.historicalSummaries(), list] as const,
definitions: (search: string) => [...sloKeys.all, 'definitions', search] as const,
globalDiagnosis: () => [...sloKeys.all, 'globalDiagnosis'] as const, globalDiagnosis: () => [...sloKeys.all, 'globalDiagnosis'] as const,
burnRates: (sloId: string, instanceId: string | undefined) => burnRates: (sloId: string, instanceId: string | undefined) =>
[...sloKeys.all, 'burnRates', sloId, instanceId] as const, [...sloKeys.all, 'burnRates', sloId, instanceId] as const,

View file

@ -5,14 +5,15 @@
* 2.0. * 2.0.
*/ */
import { FindSloDefinitionsResponse, SLOResponse } from '@kbn/slo-schema';
import { import {
QueryObserverResult, QueryObserverResult,
RefetchOptions, RefetchOptions,
RefetchQueryFilters, RefetchQueryFilters,
useQuery, useQuery,
} from '@tanstack/react-query'; } from '@tanstack/react-query';
import { SLOResponse } from '@kbn/slo-schema';
import { useKibana } from '../../utils/kibana_react'; import { useKibana } from '../../utils/kibana_react';
import { sloKeys } from './query_key_factory';
export interface UseFetchSloDefinitionsResponse { export interface UseFetchSloDefinitionsResponse {
isLoading: boolean; isLoading: boolean;
@ -26,31 +27,33 @@ export interface UseFetchSloDefinitionsResponse {
interface Params { interface Params {
name?: string; name?: string;
size?: number;
} }
export function useFetchSloDefinitions({ export function useFetchSloDefinitions({ name = '' }: Params): UseFetchSloDefinitionsResponse {
name = '', const { http } = useKibana().services;
size = 10,
}: Params): UseFetchSloDefinitionsResponse {
const { savedObjects } = useKibana().services;
const search = name.endsWith('*') ? name : `${name}*`; const search = name.endsWith('*') ? name : `${name}*`;
const { isLoading, isError, isSuccess, data, refetch } = useQuery({ const { isLoading, isError, isSuccess, data, refetch } = useQuery({
queryKey: ['fetchSloDefinitions', search], queryKey: sloKeys.definitions(search),
queryFn: async () => { queryFn: async ({ signal }) => {
try { try {
const response = await savedObjects.client.find<SLOResponse>({ const response = await http.get<FindSloDefinitionsResponse>(
type: 'slo', '/internal/observability/slos/_definitions',
{
query: {
search, search,
searchFields: ['name'], },
perPage: size, signal,
}); }
return response.savedObjects.map((so) => so.attributes); );
return response;
} catch (error) { } catch (error) {
throw new Error(`Something went wrong. Error: ${error}`); throw new Error(`Something went wrong. Error: ${error}`);
} }
}, },
retry: false,
refetchOnWindowFocus: false,
}); });
return { isLoading, isError, isSuccess, data, refetch }; return { isLoading, isError, isSuccess, data, refetch };

View file

@ -11,6 +11,7 @@ import {
createSLOParamsSchema, createSLOParamsSchema,
deleteSLOParamsSchema, deleteSLOParamsSchema,
fetchHistoricalSummaryParamsSchema, fetchHistoricalSummaryParamsSchema,
findSloDefinitionsParamsSchema,
findSLOParamsSchema, findSLOParamsSchema,
getPreviewDataParamsSchema, getPreviewDataParamsSchema,
getSLOBurnRatesParamsSchema, getSLOBurnRatesParamsSchema,
@ -32,6 +33,7 @@ import {
UpdateSLO, UpdateSLO,
} from '../../services/slo'; } from '../../services/slo';
import { FetchHistoricalSummary } from '../../services/slo/fetch_historical_summary'; import { FetchHistoricalSummary } from '../../services/slo/fetch_historical_summary';
import { FindSLODefinitions } from '../../services/slo/find_slo_definitions';
import { getBurnRates } from '../../services/slo/get_burn_rates'; import { getBurnRates } from '../../services/slo/get_burn_rates';
import { getGlobalDiagnosis, getSloDiagnosis } from '../../services/slo/get_diagnosis'; import { getGlobalDiagnosis, getSloDiagnosis } from '../../services/slo/get_diagnosis';
import { GetPreviewData } from '../../services/slo/get_preview_data'; import { GetPreviewData } from '../../services/slo/get_preview_data';
@ -58,9 +60,13 @@ const transformGenerators: Record<IndicatorTypes, TransformGenerator> = {
'sli.histogram.custom': new HistogramTransformGenerator(), 'sli.histogram.custom': new HistogramTransformGenerator(),
}; };
const isLicenseAtLeastPlatinum = async (context: ObservabilityRequestHandlerContext) => { const assertPlatinumLicense = async (context: ObservabilityRequestHandlerContext) => {
const licensing = await context.licensing; const licensing = await context.licensing;
return licensing.license.hasAtLeast('platinum'); const hasCorrectLicense = licensing.license.hasAtLeast('platinum');
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
}; };
const createSLORoute = createObservabilityServerRoute({ const createSLORoute = createObservabilityServerRoute({
@ -70,11 +76,7 @@ const createSLORoute = createObservabilityServerRoute({
}, },
params: createSLOParamsSchema, params: createSLOParamsSchema,
handler: async ({ context, params, logger }) => { handler: async ({ context, params, logger }) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context); await assertPlatinumLicense(context);
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
const esClient = (await context.core).elasticsearch.client.asCurrentUser; const esClient = (await context.core).elasticsearch.client.asCurrentUser;
const soClient = (await context.core).savedObjects.client; const soClient = (await context.core).savedObjects.client;
@ -95,11 +97,7 @@ const updateSLORoute = createObservabilityServerRoute({
}, },
params: updateSLOParamsSchema, params: updateSLOParamsSchema,
handler: async ({ context, params, logger }) => { handler: async ({ context, params, logger }) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context); await assertPlatinumLicense(context);
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
const esClient = (await context.core).elasticsearch.client.asCurrentUser; const esClient = (await context.core).elasticsearch.client.asCurrentUser;
const soClient = (await context.core).savedObjects.client; const soClient = (await context.core).savedObjects.client;
@ -127,11 +125,7 @@ const deleteSLORoute = createObservabilityServerRoute({
logger, logger,
dependencies: { getRulesClientWithRequest }, dependencies: { getRulesClientWithRequest },
}) => { }) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context); await assertPlatinumLicense(context);
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
const esClient = (await context.core).elasticsearch.client.asCurrentUser; const esClient = (await context.core).elasticsearch.client.asCurrentUser;
const soClient = (await context.core).savedObjects.client; const soClient = (await context.core).savedObjects.client;
@ -153,11 +147,7 @@ const getSLORoute = createObservabilityServerRoute({
}, },
params: getSLOParamsSchema, params: getSLOParamsSchema,
handler: async ({ context, params }) => { handler: async ({ context, params }) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context); await assertPlatinumLicense(context);
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
const soClient = (await context.core).savedObjects.client; const soClient = (await context.core).savedObjects.client;
const esClient = (await context.core).elasticsearch.client.asCurrentUser; const esClient = (await context.core).elasticsearch.client.asCurrentUser;
@ -178,11 +168,7 @@ const enableSLORoute = createObservabilityServerRoute({
}, },
params: manageSLOParamsSchema, params: manageSLOParamsSchema,
handler: async ({ context, params, logger }) => { handler: async ({ context, params, logger }) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context); await assertPlatinumLicense(context);
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
const soClient = (await context.core).savedObjects.client; const soClient = (await context.core).savedObjects.client;
const esClient = (await context.core).elasticsearch.client.asCurrentUser; const esClient = (await context.core).elasticsearch.client.asCurrentUser;
@ -204,11 +190,7 @@ const disableSLORoute = createObservabilityServerRoute({
}, },
params: manageSLOParamsSchema, params: manageSLOParamsSchema,
handler: async ({ context, params, logger }) => { handler: async ({ context, params, logger }) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context); await assertPlatinumLicense(context);
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
const soClient = (await context.core).savedObjects.client; const soClient = (await context.core).savedObjects.client;
const esClient = (await context.core).elasticsearch.client.asCurrentUser; const esClient = (await context.core).elasticsearch.client.asCurrentUser;
@ -230,11 +212,7 @@ const findSLORoute = createObservabilityServerRoute({
}, },
params: findSLOParamsSchema, params: findSLOParamsSchema,
handler: async ({ context, params, logger }) => { handler: async ({ context, params, logger }) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context); await assertPlatinumLicense(context);
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
const soClient = (await context.core).savedObjects.client; const soClient = (await context.core).savedObjects.client;
const esClient = (await context.core).elasticsearch.client.asCurrentUser; const esClient = (await context.core).elasticsearch.client.asCurrentUser;
@ -248,6 +226,25 @@ const findSLORoute = createObservabilityServerRoute({
}, },
}); });
const findSloDefinitionsRoute = createObservabilityServerRoute({
endpoint: 'GET /internal/observability/slos/_definitions',
options: {
tags: ['access:slo_read'],
},
params: findSloDefinitionsParamsSchema,
handler: async ({ context, params }) => {
await assertPlatinumLicense(context);
const soClient = (await context.core).savedObjects.client;
const repository = new KibanaSavedObjectsSLORepository(soClient);
const findSloDefinitions = new FindSLODefinitions(repository);
const response = await findSloDefinitions.execute(params.query.search);
return response;
},
});
const fetchHistoricalSummary = createObservabilityServerRoute({ const fetchHistoricalSummary = createObservabilityServerRoute({
endpoint: 'POST /internal/observability/slos/_historical_summary', endpoint: 'POST /internal/observability/slos/_historical_summary',
options: { options: {
@ -255,11 +252,7 @@ const fetchHistoricalSummary = createObservabilityServerRoute({
}, },
params: fetchHistoricalSummaryParamsSchema, params: fetchHistoricalSummaryParamsSchema,
handler: async ({ context, params }) => { handler: async ({ context, params }) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context); await assertPlatinumLicense(context);
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
const soClient = (await context.core).savedObjects.client; const soClient = (await context.core).savedObjects.client;
const esClient = (await context.core).elasticsearch.client.asCurrentUser; const esClient = (await context.core).elasticsearch.client.asCurrentUser;
@ -281,11 +274,7 @@ const getSLOInstancesRoute = createObservabilityServerRoute({
}, },
params: getSLOInstancesParamsSchema, params: getSLOInstancesParamsSchema,
handler: async ({ context, params }) => { handler: async ({ context, params }) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context); await assertPlatinumLicense(context);
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
const soClient = (await context.core).savedObjects.client; const soClient = (await context.core).savedObjects.client;
const esClient = (await context.core).elasticsearch.client.asCurrentUser; const esClient = (await context.core).elasticsearch.client.asCurrentUser;
@ -343,11 +332,7 @@ const getSloBurnRates = createObservabilityServerRoute({
}, },
params: getSLOBurnRatesParamsSchema, params: getSLOBurnRatesParamsSchema,
handler: async ({ context, params }) => { handler: async ({ context, params }) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context); await assertPlatinumLicense(context);
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
const esClient = (await context.core).elasticsearch.client.asCurrentUser; const esClient = (await context.core).elasticsearch.client.asCurrentUser;
const soClient = (await context.core).savedObjects.client; const soClient = (await context.core).savedObjects.client;
@ -371,11 +356,7 @@ const getPreviewData = createObservabilityServerRoute({
}, },
params: getPreviewDataParamsSchema, params: getPreviewDataParamsSchema,
handler: async ({ context, params }) => { handler: async ({ context, params }) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context); await assertPlatinumLicense(context);
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
const esClient = (await context.core).elasticsearch.client.asCurrentUser; const esClient = (await context.core).elasticsearch.client.asCurrentUser;
const service = new GetPreviewData(esClient); const service = new GetPreviewData(esClient);
@ -389,6 +370,7 @@ export const sloRouteRepository = {
...disableSLORoute, ...disableSLORoute,
...enableSLORoute, ...enableSLORoute,
...fetchHistoricalSummary, ...fetchHistoricalSummary,
...findSloDefinitionsRoute,
...findSLORoute, ...findSLORoute,
...getSLORoute, ...getSLORoute,
...updateSLORoute, ...updateSLORoute,

View file

@ -0,0 +1,18 @@
/*
* 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 { FindSloDefinitionsResponse, findSloDefinitionsResponseSchema } from '@kbn/slo-schema';
import { SLORepository } from './slo_repository';
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);
}
}

View file

@ -40,6 +40,7 @@ const createSLORepositoryMock = (): jest.Mocked<SLORepository> => {
findById: jest.fn(), findById: jest.fn(),
findAllByIds: jest.fn(), findAllByIds: jest.fn(),
deleteById: jest.fn(), deleteById: jest.fn(),
search: jest.fn(),
}; };
}; };

View file

@ -163,4 +163,20 @@ describe('KibanaSavedObjectsSLORepository', () => {
}); });
expect(soClientMock.delete).toHaveBeenCalledWith(SO_SLO_TYPE, SOME_SLO.id); 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]));
const results = await repository.search(SOME_SLO.name);
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'],
});
});
}); });

View file

@ -20,6 +20,7 @@ export interface SLORepository {
findAllByIds(ids: string[]): Promise<SLO[]>; findAllByIds(ids: string[]): Promise<SLO[]>;
findById(id: string): Promise<SLO>; findById(id: string): Promise<SLO>;
deleteById(id: string): Promise<void>; deleteById(id: string): Promise<void>;
search(search: string): Promise<SLO[]>;
} }
export class KibanaSavedObjectsSLORepository implements SLORepository { export class KibanaSavedObjectsSLORepository implements SLORepository {
@ -97,6 +98,21 @@ export class KibanaSavedObjectsSLORepository implements SLORepository {
throw err; throw err;
} }
} }
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;
}
}
} }
function toStoredSLO(slo: SLO): StoredSLO { function toStoredSLO(slo: SLO): StoredSLO {