[8.10] chore(slo): general enhancement (#164723) (#165032)

# Backport

This will backport the following commits from `main` to `8.10`:
- [chore(slo): general enhancement
(#164723)](https://github.com/elastic/kibana/pull/164723)

<!--- 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-28T18:50:38Z","message":"chore(slo):
general enhancement
(#164723)","sha":"733869e9e5774c4813126c80e8c00532ba8659ed","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":164723,"url":"https://github.com/elastic/kibana/pull/164723","mergeCommit":{"message":"chore(slo):
general enhancement
(#164723)","sha":"733869e9e5774c4813126c80e8c00532ba8659ed"}},"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/164723","number":164723,"mergeCommit":{"message":"chore(slo):
general enhancement
(#164723)","sha":"733869e9e5774c4813126c80e8c00532ba8659ed"}}]}]
BACKPORT-->

Co-authored-by: Kevin Delemme <kevin.delemme@elastic.co>
This commit is contained in:
Kibana Machine 2023-08-28 16:05:26 -04:00 committed by GitHub
parent ee5c73d84d
commit c47cdbf1ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 79 additions and 223 deletions

View file

@ -188,10 +188,6 @@ const findSloDefinitionsParamsSchema = t.type({
*/
const findSloDefinitionsResponseSchema = t.array(sloResponseSchema);
const getSLODiagnosisParamsSchema = t.type({
path: t.type({ id: t.string }),
});
const getSLOBurnRatesResponseSchema = t.type({
burnRates: t.array(
t.type({
@ -277,7 +273,6 @@ export {
findSLOResponseSchema,
getPreviewDataParamsSchema,
getPreviewDataResponseSchema,
getSLODiagnosisParamsSchema,
getSLOParamsSchema,
getSLOResponseSchema,
fetchHistoricalSummaryParamsSchema,

View file

@ -135,6 +135,7 @@ export function useFetchActiveAlerts({ sloIdsAndInstanceIds = [] }: Params): Use
}
},
refetchOnWindowFocus: false,
enabled: Boolean(sloIdsAndInstanceIds.length),
});
return {

View file

@ -5,32 +5,17 @@
* 2.0.
*/
import {
QueryObserverResult,
RefetchOptions,
RefetchQueryFilters,
useQuery,
} from '@tanstack/react-query';
import type { SecurityHasPrivilegesResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { i18n } from '@kbn/i18n';
import type { PublicLicenseJSON } from '@kbn/licensing-plugin/public';
import type {
SecurityGetUserPrivilegesResponse,
TransformGetTransformStatsResponse,
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { useQuery } from '@tanstack/react-query';
import { useKibana } from '../../utils/kibana_react';
import { convertErrorForUseInToast } from './helpers/convert_error_for_use_in_toast';
import { sloKeys } from './query_key_factory';
interface SloGlobalDiagnosisResponse {
licenseAndFeatures: PublicLicenseJSON;
userPrivileges: SecurityGetUserPrivilegesResponse;
sloResources: {
[x: string]: 'OK' | 'NOT_OK';
};
sloSummaryResources: {
[x: string]: 'OK' | 'NOT_OK';
};
sloSummaryTransformsStats: TransformGetTransformStatsResponse;
userPrivileges: { write: SecurityHasPrivilegesResponse; read: SecurityHasPrivilegesResponse };
}
export interface UseFetchSloGlobalDiagnoseResponse {
@ -39,10 +24,7 @@ export interface UseFetchSloGlobalDiagnoseResponse {
isRefetching: boolean;
isSuccess: boolean;
isError: boolean;
globalSloDiagnosis: SloGlobalDiagnosisResponse | undefined;
refetch: <TPageData>(
options?: (RefetchOptions & RefetchQueryFilters<TPageData>) | undefined
) => Promise<QueryObserverResult<SloGlobalDiagnosisResponse | undefined, unknown>>;
data: SloGlobalDiagnosisResponse | undefined;
}
export function useFetchSloGlobalDiagnosis(): UseFetchSloGlobalDiagnoseResponse {
@ -51,44 +33,41 @@ export function useFetchSloGlobalDiagnosis(): UseFetchSloGlobalDiagnoseResponse
notifications: { toasts },
} = useKibana().services;
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data, refetch } = useQuery(
{
queryKey: sloKeys.globalDiagnosis(),
queryFn: async ({ signal }) => {
try {
const response = await http.get<SloGlobalDiagnosisResponse>(
'/internal/observability/slos/_diagnosis',
{
query: {},
signal,
}
);
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({
queryKey: sloKeys.globalDiagnosis(),
queryFn: async ({ signal }) => {
try {
const response = await http.get<SloGlobalDiagnosisResponse>(
'/internal/observability/slos/_diagnosis',
{
query: {},
signal,
}
);
return response;
} catch (error) {
throw convertErrorForUseInToast(error);
}
},
keepPreviousData: true,
refetchOnWindowFocus: false,
retry: false,
onError: (error: Error) => {
toasts.addError(error, {
title: i18n.translate('xpack.observability.slo.globalDiagnosis.errorNotification', {
defaultMessage: 'You do not have the right permissions to use this feature.',
}),
});
},
}
);
return response;
} catch (error) {
throw convertErrorForUseInToast(error);
}
},
keepPreviousData: true,
refetchOnWindowFocus: false,
retry: false,
onError: (error: Error) => {
toasts.addError(error, {
title: i18n.translate('xpack.observability.slo.globalDiagnosis.errorNotification', {
defaultMessage: 'You do not have the right permissions to use this feature.',
}),
});
},
});
return {
globalSloDiagnosis: data,
data,
isLoading,
isInitialLoading,
isRefetching,
isSuccess,
isError,
refetch,
};
}

View file

@ -34,7 +34,7 @@ export function useFetchIndexPatternFields(
return [];
}
try {
return await dataViews.getFieldsForWildcard({ pattern: indexPattern });
return await dataViews.getFieldsForWildcard({ pattern: indexPattern, allowNoIndex: true });
} catch (error) {
throw new Error(`Something went wrong. Error: ${error}`);
}

View file

@ -52,8 +52,7 @@ export function useFetchRulesForSlo({ sloIds = [] }: Params): UseFetchRulesForSl
queryFn: async () => {
try {
const body = JSON.stringify({
filter:
sloIds?.map((sloId) => `alert.attributes.params.sloId:${sloId}`).join(' or ') ?? '',
filter: sloIds.map((sloId) => `alert.attributes.params.sloId:${sloId}`).join(' or '),
fields: ['params.sloId', 'name'],
per_page: 1000,
});
@ -62,7 +61,7 @@ export function useFetchRulesForSlo({ sloIds = [] }: Params): UseFetchRulesForSl
body,
});
const init = sloIds?.reduce((acc, sloId) => ({ ...acc, [sloId]: [] }), {});
const init = sloIds.reduce((acc, sloId) => ({ ...acc, [sloId]: [] }), {});
return response.data.reduce(
(acc, rule) => ({
@ -75,7 +74,7 @@ export function useFetchRulesForSlo({ sloIds = [] }: Params): UseFetchRulesForSl
// ignore error for retrieving slos
}
},
enabled: Boolean(sloIds?.length),
enabled: Boolean(sloIds.length),
refetchOnWindowFocus: false,
keepPreviousData: true,
}

View file

@ -59,7 +59,9 @@ describe('SLOs Welcome Page', () => {
useFetchSloListMock.mockReturnValue({ isLoading: false, sloList: emptySloList });
useLicenseMock.mockReturnValue({ hasAtLeast: () => false });
useGlobalDiagnosisMock.mockReturnValue({
isError: false,
data: {
userPrivileges: { write: { has_all_requested: true }, read: { has_all_requested: true } },
},
});
render(<SlosWelcomePage />);
@ -104,7 +106,12 @@ describe('SLOs Welcome Page', () => {
hasReadCapabilities: true,
});
useGlobalDiagnosisMock.mockReturnValue({
isError: true,
data: {
userPrivileges: {
write: { has_all_requested: false },
read: { has_all_requested: true },
},
},
});
render(<SlosWelcomePage />);
@ -116,7 +123,12 @@ describe('SLOs Welcome Page', () => {
it('should display the welcome message with a Create new SLO button which should navigate to the SLO Creation page', async () => {
useGlobalDiagnosisMock.mockReturnValue({
isError: false,
data: {
userPrivileges: {
write: { has_all_requested: true },
read: { has_all_requested: true },
},
},
});
render(<SlosWelcomePage />);
@ -136,7 +148,12 @@ describe('SLOs Welcome Page', () => {
beforeEach(() => {
useFetchSloListMock.mockReturnValue({ isLoading: false, sloList });
useGlobalDiagnosisMock.mockReturnValue({
isError: false,
data: {
userPrivileges: {
write: { has_all_requested: true },
read: { has_all_requested: true },
},
},
});
});

View file

@ -34,7 +34,7 @@ export function SlosWelcomePage() {
http: { basePath },
} = useKibana().services;
const { hasWriteCapabilities } = useCapabilities();
const { isError: hasErrorInGlobalDiagnosis } = useFetchSloGlobalDiagnosis();
const { data: globalDiagnosis } = useFetchSloGlobalDiagnosis();
const { ObservabilityPageTemplate } = usePluginContext();
const { hasAtLeast } = useLicense();
@ -43,12 +43,15 @@ export function SlosWelcomePage() {
const { isLoading, sloList } = useFetchSloList();
const { total } = sloList || { total: 0 };
const hasRequiredWritePrivileges = !!globalDiagnosis?.userPrivileges.write.has_all_requested;
const hasRequiredReadPrivileges = !!globalDiagnosis?.userPrivileges.read.has_all_requested;
const handleClickCreateSlo = () => {
navigateToUrl(basePath.prepend(paths.observability.sloCreate));
};
const hasSlosAndHasPermissions =
total > 0 && hasAtLeast('platinum') === true && !hasErrorInGlobalDiagnosis;
total > 0 && hasAtLeast('platinum') === true && hasRequiredReadPrivileges;
useEffect(() => {
if (hasSlosAndHasPermissions) {
@ -115,7 +118,7 @@ export function SlosWelcomePage() {
fill
color="primary"
onClick={handleClickCreateSlo}
disabled={!hasWriteCapabilities || hasErrorInGlobalDiagnosis}
disabled={!hasWriteCapabilities || !hasRequiredWritePrivileges}
>
{i18n.translate('xpack.observability.slo.sloList.welcomePrompt.buttonLabel', {
defaultMessage: 'Create SLO',

View file

@ -9,7 +9,7 @@ import { lazy } from 'react';
import { i18n } from '@kbn/i18n';
import { ALERT_REASON } from '@kbn/rule-data-utils';
import { SLO_ID_FIELD } from '../../common/field_names/slo';
import { SLO_ID_FIELD, SLO_INSTANCE_ID_FIELD } from '../../common/field_names/slo';
import { ConfigSchema } from '../plugin';
import { ObservabilityRuleTypeRegistry } from './create_observability_rule_type_registry';
import {
@ -86,7 +86,9 @@ export const registerObservabilityRuleTypes = (
format: ({ fields }) => {
return {
reason: fields[ALERT_REASON] ?? '-',
link: `/app/observability/slos/${fields[SLO_ID_FIELD]}`,
link: `/app/observability/slos/${fields[SLO_ID_FIELD]}?instanceId=${
fields[SLO_INSTANCE_ID_FIELD] ?? '*'
}`,
};
},
iconClass: 'bell',

View file

@ -11,7 +11,7 @@ export const getSLOSummarySettingsTemplate = (name: string) => ({
name,
template: {
settings: {
auto_expand_replicas: '0-all',
auto_expand_replicas: '0-1',
hidden: true,
},
},

View file

@ -15,7 +15,6 @@ import {
findSLOParamsSchema,
getPreviewDataParamsSchema,
getSLOBurnRatesParamsSchema,
getSLODiagnosisParamsSchema,
getSLOInstancesParamsSchema,
getSLOParamsSchema,
manageSLOParamsSchema,
@ -35,7 +34,7 @@ import {
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 { getGlobalDiagnosis } from '../../services/slo/get_diagnosis';
import { GetPreviewData } from '../../services/slo/get_preview_data';
import { GetSLOInstances } from '../../services/slo/get_slo_instances';
import { DefaultHistoricalSummaryClient } from '../../services/slo/historical_summary_client';
@ -310,21 +309,6 @@ const getDiagnosisRoute = createObservabilityServerRoute({
},
});
const getSloDiagnosisRoute = createObservabilityServerRoute({
endpoint: 'GET /internal/observability/slos/{id}/_diagnosis',
options: {
tags: [],
},
params: getSLODiagnosisParamsSchema,
handler: async ({ context, params }) => {
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
const soClient = (await context.core).savedObjects.client;
const repository = new KibanaSavedObjectsSLORepository(soClient);
return getSloDiagnosis(params.path.id, { esClient, repository });
},
});
const getSloBurnRates = createObservabilityServerRoute({
endpoint: 'POST /internal/observability/slos/{id}/_burn_rates',
options: {
@ -375,7 +359,6 @@ export const sloRouteRepository = {
...getSLORoute,
...updateSLORoute,
...getDiagnosisRoute,
...getSloDiagnosisRoute,
...getSloBurnRates,
...getPreviewData,
...getSLOInstancesRoute,

View file

@ -57,7 +57,7 @@ export const slo: SavedObjectsType = {
},
management: {
displayName: 'SLO',
importableAndExportable: true,
importableAndExportable: false,
getTitle(sloSavedObject: SavedObject<StoredSLO>) {
return `SLO: [${sloSavedObject.attributes.name}]`;
},

View file

@ -5,147 +5,24 @@
* 2.0.
*/
import { errors } from '@elastic/elasticsearch';
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import { LicensingApiRequestHandlerContext } from '@kbn/licensing-plugin/server';
import {
getSLOTransformId,
SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME,
SLO_COMPONENT_TEMPLATE_SETTINGS_NAME,
SLO_INDEX_TEMPLATE_NAME,
SLO_INGEST_PIPELINE_NAME,
SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME,
SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME,
SLO_SUMMARY_INDEX_TEMPLATE_NAME,
SLO_SUMMARY_TRANSFORM_NAME_PREFIX,
} from '../../assets/constants';
import { SLO } from '../../domain/models';
import { SLORepository } from './slo_repository';
const OK = 'OK';
const NOT_OK = 'NOT_OK';
export async function getGlobalDiagnosis(
esClient: ElasticsearchClient,
licensing: LicensingApiRequestHandlerContext
) {
const licenseInfo = licensing.license.toJSON();
const userPrivileges = await esClient.security.getUserPrivileges();
const sloResources = await getSloResourcesDiagnosis(esClient);
const sloSummaryResources = await getSloSummaryResourcesDiagnosis(esClient);
const sloSummaryTransformsStats = await esClient.transform.getTransformStats({
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}*`,
allow_no_match: true,
const userWritePrivileges = await esClient.security.hasPrivileges({
cluster: ['manage_transform'],
index: [{ names: '.slo-*', privileges: ['all'] }],
});
const userReadPrivileges = await esClient.security.hasPrivileges({
index: [{ names: '.slo-*', privileges: ['read'] }],
});
return {
licenseAndFeatures: licenseInfo,
userPrivileges,
sloResources,
sloSummaryResources,
sloSummaryTransformsStats,
userPrivileges: { write: userWritePrivileges, read: userReadPrivileges },
};
}
export async function getSloDiagnosis(
sloId: string,
services: { esClient: ElasticsearchClient; repository: SLORepository }
) {
const { esClient, repository } = services;
const sloResources = await getSloResourcesDiagnosis(esClient);
const sloSummaryResources = await getSloSummaryResourcesDiagnosis(esClient);
let slo: SLO | undefined;
try {
slo = await repository.findById(sloId);
} catch (err) {
// noop
}
const sloTransformStats = await esClient.transform.getTransformStats({
transform_id: getSLOTransformId(sloId, slo?.revision ?? 1),
allow_no_match: true,
});
const sloSummaryTransformsStats = await esClient.transform.getTransformStats({
transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}*`,
allow_no_match: true,
});
return {
sloResources,
sloSummaryResources,
slo: slo ?? NOT_OK,
sloTransformStats,
sloSummaryTransformsStats,
};
}
async function getSloResourcesDiagnosis(esClient: ElasticsearchClient) {
try {
const indexTemplateExists = await esClient.indices.existsIndexTemplate({
name: SLO_INDEX_TEMPLATE_NAME,
});
const mappingsTemplateExists = await esClient.cluster.existsComponentTemplate({
name: SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME,
});
const settingsTemplateExists = await esClient.cluster.existsComponentTemplate({
name: SLO_COMPONENT_TEMPLATE_SETTINGS_NAME,
});
let ingestPipelineExists = true;
try {
await esClient.ingest.getPipeline({ id: SLO_INGEST_PIPELINE_NAME });
} catch (err) {
ingestPipelineExists = false;
throw err;
}
return {
[SLO_INDEX_TEMPLATE_NAME]: indexTemplateExists ? OK : NOT_OK,
[SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME]: mappingsTemplateExists ? OK : NOT_OK,
[SLO_COMPONENT_TEMPLATE_SETTINGS_NAME]: settingsTemplateExists ? OK : NOT_OK,
[SLO_INGEST_PIPELINE_NAME]: ingestPipelineExists ? OK : NOT_OK,
};
} catch (err) {
if (
err instanceof errors.ResponseError &&
(err.statusCode === 403 || err.meta.statusCode === 403)
) {
throw new Error('Insufficient permissions to access Elasticsearch Cluster', { cause: err });
}
}
}
async function getSloSummaryResourcesDiagnosis(esClient: ElasticsearchClient) {
try {
const indexTemplateExists = await esClient.indices.existsIndexTemplate({
name: SLO_SUMMARY_INDEX_TEMPLATE_NAME,
});
const mappingsTemplateExists = await esClient.cluster.existsComponentTemplate({
name: SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME,
});
const settingsTemplateExists = await esClient.cluster.existsComponentTemplate({
name: SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME,
});
return {
[SLO_SUMMARY_INDEX_TEMPLATE_NAME]: indexTemplateExists ? OK : NOT_OK,
[SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME]: mappingsTemplateExists ? OK : NOT_OK,
[SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME]: settingsTemplateExists ? OK : NOT_OK,
};
} catch (err) {
if (
err instanceof errors.ResponseError &&
(err.statusCode === 403 || err.meta.statusCode === 403)
) {
throw new Error('Insufficient permissions to access Elasticsearch Cluster', { cause: err });
}
}
}

View file

@ -65,7 +65,7 @@ export class DefaultTransformManager implements TransformManager {
await retryTransientEsErrors(
() =>
this.esClient.transform.stopTransform(
{ transform_id: transformId, wait_for_completion: true },
{ transform_id: transformId, wait_for_completion: true, force: true },
{ ignore: [404] }
),
{ logger: this.logger }