[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({
path: t.type({ id: t.string }),
});
@ -229,6 +247,13 @@ 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 GetPreviewDataParams = t.TypeOf<typeof getPreviewDataParamsSchema.props.body>;
type GetPreviewDataResponse = t.OutputOf<typeof getPreviewDataResponseSchema>;
@ -257,6 +282,8 @@ export {
getSLOResponseSchema,
fetchHistoricalSummaryParamsSchema,
fetchHistoricalSummaryResponseSchema,
findSloDefinitionsParamsSchema,
findSloDefinitionsResponseSchema,
manageSLOParamsSchema,
sloResponseSchema,
sloWithSummaryResponseSchema,
@ -281,6 +308,7 @@ export type {
FetchHistoricalSummaryParams,
FetchHistoricalSummaryResponse,
HistoricalSummaryResponse,
FindSloDefinitionsResponse,
ManageSLOParams,
SLOResponse,
SLOWithSummaryResponse,

View file

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

View file

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

View file

@ -11,6 +11,7 @@ import {
createSLOParamsSchema,
deleteSLOParamsSchema,
fetchHistoricalSummaryParamsSchema,
findSloDefinitionsParamsSchema,
findSLOParamsSchema,
getPreviewDataParamsSchema,
getSLOBurnRatesParamsSchema,
@ -32,6 +33,7 @@ import {
UpdateSLO,
} from '../../services/slo';
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 { getGlobalDiagnosis, getSloDiagnosis } from '../../services/slo/get_diagnosis';
import { GetPreviewData } from '../../services/slo/get_preview_data';
@ -58,9 +60,13 @@ const transformGenerators: Record<IndicatorTypes, TransformGenerator> = {
'sli.histogram.custom': new HistogramTransformGenerator(),
};
const isLicenseAtLeastPlatinum = async (context: ObservabilityRequestHandlerContext) => {
const assertPlatinumLicense = async (context: ObservabilityRequestHandlerContext) => {
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({
@ -70,11 +76,7 @@ const createSLORoute = createObservabilityServerRoute({
},
params: createSLOParamsSchema,
handler: async ({ context, params, logger }) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context);
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
await assertPlatinumLicense(context);
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
const soClient = (await context.core).savedObjects.client;
@ -95,11 +97,7 @@ const updateSLORoute = createObservabilityServerRoute({
},
params: updateSLOParamsSchema,
handler: async ({ context, params, logger }) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context);
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
await assertPlatinumLicense(context);
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
const soClient = (await context.core).savedObjects.client;
@ -127,11 +125,7 @@ const deleteSLORoute = createObservabilityServerRoute({
logger,
dependencies: { getRulesClientWithRequest },
}) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context);
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
await assertPlatinumLicense(context);
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
const soClient = (await context.core).savedObjects.client;
@ -153,11 +147,7 @@ const getSLORoute = createObservabilityServerRoute({
},
params: getSLOParamsSchema,
handler: async ({ context, params }) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context);
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
await assertPlatinumLicense(context);
const soClient = (await context.core).savedObjects.client;
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
@ -178,11 +168,7 @@ const enableSLORoute = createObservabilityServerRoute({
},
params: manageSLOParamsSchema,
handler: async ({ context, params, logger }) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context);
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
await assertPlatinumLicense(context);
const soClient = (await context.core).savedObjects.client;
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
@ -204,11 +190,7 @@ const disableSLORoute = createObservabilityServerRoute({
},
params: manageSLOParamsSchema,
handler: async ({ context, params, logger }) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context);
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
await assertPlatinumLicense(context);
const soClient = (await context.core).savedObjects.client;
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
@ -230,11 +212,7 @@ const findSLORoute = createObservabilityServerRoute({
},
params: findSLOParamsSchema,
handler: async ({ context, params, logger }) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context);
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
await assertPlatinumLicense(context);
const soClient = (await context.core).savedObjects.client;
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({
endpoint: 'POST /internal/observability/slos/_historical_summary',
options: {
@ -255,11 +252,7 @@ const fetchHistoricalSummary = createObservabilityServerRoute({
},
params: fetchHistoricalSummaryParamsSchema,
handler: async ({ context, params }) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context);
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
await assertPlatinumLicense(context);
const soClient = (await context.core).savedObjects.client;
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
@ -281,11 +274,7 @@ const getSLOInstancesRoute = createObservabilityServerRoute({
},
params: getSLOInstancesParamsSchema,
handler: async ({ context, params }) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context);
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
await assertPlatinumLicense(context);
const soClient = (await context.core).savedObjects.client;
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
@ -343,11 +332,7 @@ const getSloBurnRates = createObservabilityServerRoute({
},
params: getSLOBurnRatesParamsSchema,
handler: async ({ context, params }) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context);
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
await assertPlatinumLicense(context);
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
const soClient = (await context.core).savedObjects.client;
@ -371,11 +356,7 @@ const getPreviewData = createObservabilityServerRoute({
},
params: getPreviewDataParamsSchema,
handler: async ({ context, params }) => {
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context);
if (!hasCorrectLicense) {
throw forbidden('Platinum license or higher is needed to make use of this feature.');
}
await assertPlatinumLicense(context);
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
const service = new GetPreviewData(esClient);
@ -389,6 +370,7 @@ export const sloRouteRepository = {
...disableSLORoute,
...enableSLORoute,
...fetchHistoricalSummary,
...findSloDefinitionsRoute,
...findSLORoute,
...getSLORoute,
...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(),
findAllByIds: 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);
});
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[]>;
findById(id: string): Promise<SLO>;
deleteById(id: string): Promise<void>;
search(search: string): Promise<SLO[]>;
}
export class KibanaSavedObjectsSLORepository implements SLORepository {
@ -97,6 +98,21 @@ export class KibanaSavedObjectsSLORepository implements SLORepository {
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 {