mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
feat(slo): introduce slo feature (#150554)
This commit is contained in:
parent
833456e1b8
commit
aff771c287
14 changed files with 157 additions and 28 deletions
|
@ -20,6 +20,7 @@ export const AlertConsumers = {
|
|||
LOGS: 'logs',
|
||||
INFRASTRUCTURE: 'infrastructure',
|
||||
OBSERVABILITY: 'observability',
|
||||
SLO: 'slo',
|
||||
SIEM: 'siem',
|
||||
UPTIME: 'uptime',
|
||||
} as const;
|
||||
|
|
|
@ -38,6 +38,8 @@ export {
|
|||
getProbabilityFromProgressiveLoadingQuality,
|
||||
} from './progressive_loading';
|
||||
|
||||
export const sloFeatureId = 'slo';
|
||||
|
||||
export const casesFeatureId = 'observabilityCases';
|
||||
|
||||
// The ID of the observability app. Should more appropriately be called
|
||||
|
|
|
@ -13,4 +13,5 @@ export const observabilityAlertFeatureIds: ValidFeatureId[] = [
|
|||
AlertConsumers.INFRASTRUCTURE,
|
||||
AlertConsumers.LOGS,
|
||||
AlertConsumers.UPTIME,
|
||||
AlertConsumers.SLO,
|
||||
];
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { sloFeatureId } from '../../../common';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
|
||||
export function useCapabilities() {
|
||||
const {
|
||||
application: { capabilities },
|
||||
} = useKibana().services;
|
||||
|
||||
return {
|
||||
hasReadCapabilities: !!capabilities[sloFeatureId].read ?? false,
|
||||
hasWriteCapabilities: !!capabilities[sloFeatureId].write ?? false,
|
||||
};
|
||||
}
|
|
@ -19,6 +19,7 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { HistoricalSummaryResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { useCapabilities } from '../../../hooks/slo/use_capabilities';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { useCreateOrUpdateSlo } from '../../../hooks/slo/use_create_slo';
|
||||
import { SloSummary } from './slo_summary';
|
||||
|
@ -53,6 +54,7 @@ export function SloListItem({
|
|||
application: { navigateToUrl },
|
||||
http: { basePath },
|
||||
} = useKibana().services;
|
||||
const { hasWriteCapabilities } = useCapabilities();
|
||||
|
||||
const { createSlo, loading: isCloning, success: isCloned } = useCreateOrUpdateSlo();
|
||||
|
||||
|
@ -158,6 +160,7 @@ export function SloListItem({
|
|||
<EuiContextMenuItem
|
||||
key="edit"
|
||||
icon="pencil"
|
||||
disabled={!hasWriteCapabilities}
|
||||
onClick={handleEdit}
|
||||
data-test-subj="sloActionsEdit"
|
||||
>
|
||||
|
@ -167,6 +170,7 @@ export function SloListItem({
|
|||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
key="clone"
|
||||
disabled={!hasWriteCapabilities}
|
||||
icon="copy"
|
||||
onClick={handleClone}
|
||||
data-test-subj="sloActionsClone"
|
||||
|
@ -178,6 +182,7 @@ export function SloListItem({
|
|||
<EuiContextMenuItem
|
||||
key="delete"
|
||||
icon="trash"
|
||||
disabled={!hasWriteCapabilities}
|
||||
onClick={handleDelete}
|
||||
data-test-subj="sloActionsDelete"
|
||||
>
|
||||
|
|
|
@ -23,6 +23,7 @@ import { emptySloList, sloList } from '../../data/slo/slo';
|
|||
import type { ConfigSchema } from '../../plugin';
|
||||
import type { Subset } from '../../typings';
|
||||
import { historicalSummaryData } from '../../data/slo/historical_summary_data';
|
||||
import { useCapabilities } from '../../hooks/slo/use_capabilities';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
|
@ -36,6 +37,7 @@ jest.mock('../../hooks/slo/use_fetch_slo_list');
|
|||
jest.mock('../../hooks/slo/use_create_slo');
|
||||
jest.mock('../../hooks/slo/use_delete_slo');
|
||||
jest.mock('../../hooks/slo/use_fetch_historical_summary');
|
||||
jest.mock('../../hooks/slo/use_capabilities');
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mock;
|
||||
const useLicenseMock = useLicense as jest.Mock;
|
||||
|
@ -43,6 +45,7 @@ const useFetchSloListMock = useFetchSloList as jest.Mock;
|
|||
const useCreateOrUpdateSloMock = useCreateOrUpdateSlo as jest.Mock;
|
||||
const useDeleteSloMock = useDeleteSlo as jest.Mock;
|
||||
const useFetchHistoricalSummaryMock = useFetchHistoricalSummary as jest.Mock;
|
||||
const useCapabilitiesMock = useCapabilities as jest.Mock;
|
||||
|
||||
const mockCreateSlo = jest.fn();
|
||||
useCreateOrUpdateSloMock.mockReturnValue({ createSlo: mockCreateSlo });
|
||||
|
@ -84,6 +87,7 @@ describe('SLOs Page', () => {
|
|||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockKibana();
|
||||
useCapabilitiesMock.mockReturnValue({ hasWriteCapabilities: true, hasReadCapabilities: true });
|
||||
});
|
||||
|
||||
describe('when the feature flag is not enabled', () => {
|
||||
|
|
|
@ -20,6 +20,7 @@ import PageNotFound from '../404';
|
|||
import { paths } from '../../config';
|
||||
import { isSloFeatureEnabled } from './helpers/is_slo_feature_enabled';
|
||||
import type { ObservabilityAppServices } from '../../application/types';
|
||||
import { useCapabilities } from '../../hooks/slo/use_capabilities';
|
||||
|
||||
export function SlosPage() {
|
||||
const {
|
||||
|
@ -27,7 +28,7 @@ export function SlosPage() {
|
|||
http: { basePath },
|
||||
} = useKibana<ObservabilityAppServices>().services;
|
||||
const { ObservabilityPageTemplate, config } = usePluginContext();
|
||||
|
||||
const { hasWriteCapabilities } = useCapabilities();
|
||||
const { hasAtLeast } = useLicense();
|
||||
|
||||
const {
|
||||
|
@ -68,6 +69,7 @@ export function SlosPage() {
|
|||
}),
|
||||
rightSideItems: [
|
||||
<EuiButton
|
||||
disabled={!hasWriteCapabilities}
|
||||
color="primary"
|
||||
fill
|
||||
onClick={handleClickCreateSlo}
|
||||
|
|
|
@ -235,6 +235,7 @@ export class Plugin
|
|||
'logs',
|
||||
'metrics',
|
||||
'apm',
|
||||
'slo',
|
||||
'performance',
|
||||
'trace',
|
||||
'agent',
|
||||
|
@ -289,7 +290,7 @@ export class Plugin
|
|||
// See https://github.com/elastic/kibana/issues/103325.
|
||||
const otherLinks: NavigationEntry[] = deepLinks
|
||||
.filter((link) => link.navLinkStatus === AppNavLinkStatus.visible)
|
||||
.filter((link) => (link.id === 'slos' ? config.unsafe.slo.enabled : link))
|
||||
.filter((link) => (link.id === 'slos' ? config.unsafe.slo.enabled : link)) // might not be useful anymore
|
||||
.map((link) => ({
|
||||
app: observabilityAppId,
|
||||
label: link.title,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { Subject } from 'rxjs';
|
||||
import { App, AppDeepLink, ApplicationStart, AppNavLinkStatus, AppUpdater } from '@kbn/core/public';
|
||||
import { casesFeatureId } from '../common';
|
||||
import { casesFeatureId, sloFeatureId } from '../common';
|
||||
import { updateGlobalNavigation } from './update_global_navigation';
|
||||
|
||||
// Used in updater callback
|
||||
|
@ -163,11 +163,45 @@ describe('updateGlobalNavigation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("hides the slo link when the capabilities don't include it", () => {
|
||||
const capabilities = {
|
||||
navLinks: { apm: true, logs: false, metrics: false, uptime: false },
|
||||
} as unknown as ApplicationStart['capabilities'];
|
||||
|
||||
const sloRoute = {
|
||||
id: 'slos',
|
||||
title: 'SLOs',
|
||||
order: 8002,
|
||||
path: '/slos',
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
};
|
||||
|
||||
const deepLinks = [sloRoute];
|
||||
|
||||
const callback = jest.fn();
|
||||
const updater$ = {
|
||||
next: (cb: AppUpdater) => callback(cb(app)),
|
||||
} as unknown as Subject<AppUpdater>;
|
||||
|
||||
updateGlobalNavigation({ capabilities, deepLinks, updater$ });
|
||||
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
deepLinks: [
|
||||
{
|
||||
...sloRoute,
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
},
|
||||
],
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when slos are enabled', () => {
|
||||
it('shows the slos deep link', () => {
|
||||
const capabilities = {
|
||||
[casesFeatureId]: { read_cases: true },
|
||||
navLinks: { apm: true, logs: false, metrics: false, uptime: false },
|
||||
[sloFeatureId]: { read: true },
|
||||
navLinks: { apm: false, logs: false, metrics: false, uptime: false },
|
||||
} as unknown as ApplicationStart['capabilities'];
|
||||
|
||||
const sloRoute = {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { Subject } from 'rxjs';
|
||||
import { AppNavLinkStatus, AppUpdater, ApplicationStart, AppDeepLink } from '@kbn/core/public';
|
||||
import { CasesDeepLinkId } from '@kbn/cases-plugin/public';
|
||||
import { casesFeatureId } from '../common';
|
||||
import { casesFeatureId, sloFeatureId } from '../common';
|
||||
|
||||
export function updateGlobalNavigation({
|
||||
capabilities,
|
||||
|
@ -20,7 +20,12 @@ export function updateGlobalNavigation({
|
|||
updater$: Subject<AppUpdater>;
|
||||
}) {
|
||||
const { apm, logs, metrics, uptime } = capabilities.navLinks;
|
||||
const someVisible = Object.values({ apm, logs, metrics, uptime }).some((visible) => visible);
|
||||
const someVisible = Object.values({
|
||||
apm,
|
||||
logs,
|
||||
metrics,
|
||||
uptime,
|
||||
}).some((visible) => visible);
|
||||
|
||||
const updatedDeepLinks = deepLinks.map((link) => {
|
||||
switch (link.id) {
|
||||
|
@ -37,16 +42,18 @@ export function updateGlobalNavigation({
|
|||
...link,
|
||||
navLinkStatus: someVisible ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden,
|
||||
};
|
||||
case 'slos':
|
||||
return {
|
||||
...link,
|
||||
navLinkStatus: someVisible ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden,
|
||||
};
|
||||
case 'rules':
|
||||
return {
|
||||
...link,
|
||||
navLinkStatus: someVisible ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden,
|
||||
};
|
||||
case 'slos':
|
||||
return {
|
||||
...link,
|
||||
navLinkStatus: !!capabilities[sloFeatureId]?.read
|
||||
? AppNavLinkStatus.visible
|
||||
: AppNavLinkStatus.hidden,
|
||||
};
|
||||
default:
|
||||
return link;
|
||||
}
|
||||
|
@ -54,6 +61,9 @@ export function updateGlobalNavigation({
|
|||
|
||||
updater$.next(() => ({
|
||||
deepLinks: updatedDeepLinks,
|
||||
navLinkStatus: someVisible ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden,
|
||||
navLinkStatus:
|
||||
someVisible || !!capabilities[sloFeatureId]?.read
|
||||
? AppNavLinkStatus.visible
|
||||
: AppNavLinkStatus.hidden,
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -5,5 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const OBSERVABILITY_FEATURE_ID = 'observability';
|
||||
export const RULE_REGISTRATION_CONTEXT = 'observability.slo';
|
||||
export const SLO_RULE_REGISTRATION_CONTEXT = 'observability.slo';
|
||||
|
|
|
@ -37,7 +37,7 @@ export function sloBurnRateRuleType(createLifecycleRuleExecutor: CreateLifecycle
|
|||
},
|
||||
defaultActionGroupId: FIRED_ACTION.id,
|
||||
actionGroups: [FIRED_ACTION],
|
||||
producer: 'observability',
|
||||
producer: 'slo',
|
||||
minimumLicenseRequired: 'basic' as LicenseType,
|
||||
isExportable: true,
|
||||
executor: createLifecycleRuleExecutor(getRuleExecutor()),
|
||||
|
|
|
@ -37,10 +37,11 @@ import {
|
|||
import { uiSettings } from './ui_settings';
|
||||
import { registerRoutes } from './routes/register_routes';
|
||||
import { getGlobalObservabilityServerRouteRepository } from './routes/get_global_observability_server_route_repository';
|
||||
import { casesFeatureId, observabilityFeatureId } from '../common';
|
||||
import { slo } from './saved_objects';
|
||||
import { OBSERVABILITY_FEATURE_ID, RULE_REGISTRATION_CONTEXT } from './common/constants';
|
||||
import { casesFeatureId, observabilityFeatureId, sloFeatureId } from '../common';
|
||||
import { slo, SO_SLO_TYPE } from './saved_objects';
|
||||
import { SLO_RULE_REGISTRATION_CONTEXT } from './common/constants';
|
||||
import { registerRuleTypes } from './lib/rules/register_rule_types';
|
||||
import { SLO_BURN_RATE_RULE_ID } from '../common/constants';
|
||||
import { registerSloUsageCollector } from './lib/collectors/register';
|
||||
|
||||
export type ObservabilityPluginSetup = ReturnType<ObservabilityPlugin['setup']>;
|
||||
|
@ -161,11 +162,61 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
|
|||
const { ruleDataService } = plugins.ruleRegistry;
|
||||
|
||||
if (config.unsafe.slo.enabled) {
|
||||
plugins.features.registerKibanaFeature({
|
||||
id: sloFeatureId,
|
||||
name: i18n.translate('xpack.observability.featureRegistry.linkSloTitle', {
|
||||
defaultMessage: 'SLOs',
|
||||
}),
|
||||
order: 1200,
|
||||
category: DEFAULT_APP_CATEGORIES.observability,
|
||||
app: [sloFeatureId, 'kibana'],
|
||||
catalogue: [sloFeatureId, 'observability'],
|
||||
alerting: [SLO_BURN_RATE_RULE_ID],
|
||||
privileges: {
|
||||
all: {
|
||||
app: [sloFeatureId, 'kibana'],
|
||||
catalogue: [sloFeatureId, 'observability'],
|
||||
api: ['slo_write', 'slo_read', 'rac'],
|
||||
savedObject: {
|
||||
all: [SO_SLO_TYPE],
|
||||
read: [],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
all: [SLO_BURN_RATE_RULE_ID],
|
||||
},
|
||||
alert: {
|
||||
all: [SLO_BURN_RATE_RULE_ID],
|
||||
},
|
||||
},
|
||||
ui: ['read', 'write'],
|
||||
},
|
||||
read: {
|
||||
app: [sloFeatureId, 'kibana'],
|
||||
catalogue: [sloFeatureId, 'observability'],
|
||||
api: ['slo_read', 'rac'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [SO_SLO_TYPE],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
read: [SLO_BURN_RATE_RULE_ID],
|
||||
},
|
||||
alert: {
|
||||
read: [SLO_BURN_RATE_RULE_ID],
|
||||
},
|
||||
},
|
||||
ui: ['read'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
core.savedObjects.registerType(slo);
|
||||
|
||||
const ruleDataClient = ruleDataService.initializeIndex({
|
||||
feature: OBSERVABILITY_FEATURE_ID,
|
||||
registrationContext: RULE_REGISTRATION_CONTEXT,
|
||||
feature: sloFeatureId,
|
||||
registrationContext: SLO_RULE_REGISTRATION_CONTEXT,
|
||||
dataset: Dataset.alerts,
|
||||
componentTemplateRefs: [ECS_COMPONENT_TEMPLATE_NAME],
|
||||
componentTemplates: [
|
||||
|
|
|
@ -52,7 +52,7 @@ const isLicenseAtLeastPlatinum = async (context: ObservabilityRequestHandlerCont
|
|||
const createSLORoute = createObservabilityServerRoute({
|
||||
endpoint: 'POST /api/observability/slos',
|
||||
options: {
|
||||
tags: [],
|
||||
tags: ['access:slo_write'],
|
||||
},
|
||||
params: createSLOParamsSchema,
|
||||
handler: async ({ context, params, logger }) => {
|
||||
|
@ -77,7 +77,7 @@ const createSLORoute = createObservabilityServerRoute({
|
|||
const updateSLORoute = createObservabilityServerRoute({
|
||||
endpoint: 'PUT /api/observability/slos/{id}',
|
||||
options: {
|
||||
tags: [],
|
||||
tags: ['access:slo_write'],
|
||||
},
|
||||
params: updateSLOParamsSchema,
|
||||
handler: async ({ context, params, logger }) => {
|
||||
|
@ -101,7 +101,7 @@ const updateSLORoute = createObservabilityServerRoute({
|
|||
const deleteSLORoute = createObservabilityServerRoute({
|
||||
endpoint: 'DELETE /api/observability/slos/{id}',
|
||||
options: {
|
||||
tags: [],
|
||||
tags: ['access:slo_write'],
|
||||
},
|
||||
params: deleteSLOParamsSchema,
|
||||
handler: async ({ context, params, logger }) => {
|
||||
|
@ -124,7 +124,7 @@ const deleteSLORoute = createObservabilityServerRoute({
|
|||
const getSLORoute = createObservabilityServerRoute({
|
||||
endpoint: 'GET /api/observability/slos/{id}',
|
||||
options: {
|
||||
tags: [],
|
||||
tags: ['access:slo_read'],
|
||||
},
|
||||
params: getSLOParamsSchema,
|
||||
handler: async ({ context, params }) => {
|
||||
|
@ -147,7 +147,7 @@ const getSLORoute = createObservabilityServerRoute({
|
|||
const enableSLORoute = createObservabilityServerRoute({
|
||||
endpoint: 'POST /api/observability/slos/{id}/enable',
|
||||
options: {
|
||||
tags: [],
|
||||
tags: ['access:slo_write'],
|
||||
},
|
||||
params: manageSLOParamsSchema,
|
||||
handler: async ({ context, params, logger }) => {
|
||||
|
@ -171,7 +171,7 @@ const enableSLORoute = createObservabilityServerRoute({
|
|||
const disableSLORoute = createObservabilityServerRoute({
|
||||
endpoint: 'POST /api/observability/slos/{id}/disable',
|
||||
options: {
|
||||
tags: [],
|
||||
tags: ['access:slo_write'],
|
||||
},
|
||||
params: manageSLOParamsSchema,
|
||||
handler: async ({ context, params, logger }) => {
|
||||
|
@ -195,7 +195,7 @@ const disableSLORoute = createObservabilityServerRoute({
|
|||
const findSLORoute = createObservabilityServerRoute({
|
||||
endpoint: 'GET /api/observability/slos',
|
||||
options: {
|
||||
tags: [],
|
||||
tags: ['access:slo_read'],
|
||||
},
|
||||
params: findSLOParamsSchema,
|
||||
handler: async ({ context, params }) => {
|
||||
|
@ -218,7 +218,7 @@ const findSLORoute = createObservabilityServerRoute({
|
|||
const fetchHistoricalSummary = createObservabilityServerRoute({
|
||||
endpoint: 'POST /internal/observability/slos/_historical_summary',
|
||||
options: {
|
||||
tags: [],
|
||||
tags: ['access:slo_read'],
|
||||
},
|
||||
params: fetchHistoricalSummaryParamsSchema,
|
||||
handler: async ({ context, params }) => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue