diff --git a/x-pack/plugins/observability_solution/slo/server/lib/rules/register_burn_rate_rule.ts b/x-pack/plugins/observability_solution/slo/server/lib/rules/register_burn_rate_rule.ts index 8ddaffdc05fad..d432dc5c52d34 100644 --- a/x-pack/plugins/observability_solution/slo/server/lib/rules/register_burn_rate_rule.ts +++ b/x-pack/plugins/observability_solution/slo/server/lib/rules/register_burn_rate_rule.ts @@ -7,18 +7,9 @@ import { PluginSetupContract } from '@kbn/alerting-plugin/server'; import { IBasePath, Logger } from '@kbn/core/server'; -import { - createLifecycleExecutor, - Dataset, - IRuleDataService, -} from '@kbn/rule-registry-plugin/server'; -import { mappingFromFieldMap } from '@kbn/alerting-plugin/common'; -import { legacyExperimentalFieldMap } from '@kbn/alerts-as-data-utils'; +import { IRuleDataService } from '@kbn/rule-registry-plugin/server'; import { CustomThresholdLocators } from '@kbn/observability-plugin/server'; -import { sloFeatureId } from '@kbn/observability-plugin/common'; -import { SLO_RULE_REGISTRATION_CONTEXT } from '../../common/constants'; import { sloBurnRateRuleType } from './slo_burn_rate'; -import { sloRuleFieldMap } from './slo_burn_rate/field_map'; export function registerBurnRateRule( alertingPlugin: PluginSetupContract, @@ -28,27 +19,5 @@ export function registerBurnRateRule( locators: CustomThresholdLocators // TODO move this somewhere else, or use only alertsLocator ) { // SLO RULE - const ruleDataClientSLO = ruleDataService.initializeIndex({ - feature: sloFeatureId, - registrationContext: SLO_RULE_REGISTRATION_CONTEXT, - dataset: Dataset.alerts, - componentTemplateRefs: [], - componentTemplates: [ - { - name: 'mappings', - mappings: mappingFromFieldMap( - { ...legacyExperimentalFieldMap, ...sloRuleFieldMap }, - 'strict' - ), - }, - ], - }); - - const createLifecycleRuleExecutorSLO = createLifecycleExecutor( - logger.get('rules'), - ruleDataClientSLO - ); - alertingPlugin.registerType( - sloBurnRateRuleType(createLifecycleRuleExecutorSLO, basePath, locators.alertsLocator) - ); + alertingPlugin.registerType(sloBurnRateRuleType(basePath, locators.alertsLocator)); } diff --git a/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/executor.test.ts b/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/executor.test.ts index 4796a6c976205..7cfa6bc17600e 100644 --- a/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/executor.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/executor.test.ts @@ -18,12 +18,10 @@ import { loggingSystemMock, savedObjectsClientMock, } from '@kbn/core/server/mocks'; -import { LifecycleAlertService, LifecycleAlertServices } from '@kbn/rule-registry-plugin/server'; -import { PublicAlertFactory } from '@kbn/alerting-plugin/server/alert/create_alert_factory'; import { ISearchStartSearchSource } from '@kbn/data-plugin/public'; import { MockedLogger } from '@kbn/logging-mocks'; import { SanitizedRuleConfig } from '@kbn/alerting-plugin/common'; -import { Alert, RuleExecutorServices } from '@kbn/alerting-plugin/server'; +import { RuleExecutorServices } from '@kbn/alerting-plugin/server'; import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings'; import { LocatorPublic } from '@kbn/share-plugin/common'; import { AlertsLocatorParams } from '@kbn/observability-plugin/common'; @@ -67,6 +65,8 @@ import { SHORT_WINDOW, } from './lib/build_query'; import { get } from 'lodash'; +import { ObservabilitySloAlert } from '@kbn/alerts-as-data-utils'; +import { publicAlertsClientMock } from '@kbn/alerting-plugin/server/alerts_client/alerts_client.mock'; const commonEsResponse = { took: 100, @@ -106,9 +106,6 @@ describe('BurnRateRuleExecutor', () => { let esClientMock: ElasticsearchClientMock; let soClientMock: jest.Mocked; let loggerMock: jest.Mocked; - let alertUuidMap: Map; - let alertMock: Partial; - const alertUuid = 'mockedAlertUuid'; const basePathMock = { publicBaseUrl: 'https://kibana.dev' } as IBasePath; const alertsLocatorMock = { getLocation: jest.fn().mockImplementation(() => ({ @@ -117,50 +114,35 @@ describe('BurnRateRuleExecutor', () => { } as any as LocatorPublic; const ISO_DATE_REGEX = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)((-(\d{2}):(\d{2})|Z)?)$/; - let alertWithLifecycleMock: jest.MockedFn; - let alertFactoryMock: jest.Mocked< - PublicAlertFactory - >; + let searchSourceClientMock: jest.Mocked; let uiSettingsClientMock: jest.Mocked; - let servicesMock: RuleExecutorServices< - BurnRateAlertState, - BurnRateAlertContext, - BurnRateAllowedActionGroups - > & - LifecycleAlertServices; + let servicesMock: jest.Mocked< + RuleExecutorServices< + BurnRateAlertState, + BurnRateAlertContext, + BurnRateAllowedActionGroups, + ObservabilitySloAlert + > + >; beforeEach(() => { - alertUuidMap = new Map(); - alertMock = { - scheduleActions: jest.fn(), - replaceState: jest.fn(), - }; esClientMock = elasticsearchServiceMock.createElasticsearchClient(); soClientMock = savedObjectsClientMock.create(); - alertWithLifecycleMock = jest.fn().mockImplementation(({ id }) => { - alertUuidMap.set(id, alertUuid); - return alertMock as any; - }); - alertFactoryMock = { - create: jest.fn(), - done: jest.fn(), - alertLimit: { getValue: jest.fn(), setLimitReached: jest.fn() }, - }; loggerMock = loggingSystemMock.createLogger(); servicesMock = { - alertWithLifecycle: alertWithLifecycleMock, savedObjectsClient: soClientMock, scopedClusterClient: { asCurrentUser: esClientMock, asInternalUser: esClientMock }, - alertsClient: null, - alertFactory: alertFactoryMock, + alertsClient: publicAlertsClientMock.create(), + alertFactory: { + create: jest.fn(), + done: jest.fn(), + alertLimit: { getValue: jest.fn(), setLimitReached: jest.fn() }, + }, searchSourceClient: searchSourceClientMock, uiSettingsClient: uiSettingsClientMock, shouldWriteAlerts: jest.fn(), shouldStopExecution: jest.fn(), - getAlertStartedDate: jest.fn(), - getAlertUuid: jest.fn().mockImplementation((id) => alertUuidMap.get(id) || 'bad-uuid'), - getAlertByAlertUuid: jest.fn(), share: {} as SharePluginStart, dataViews: dataViewPluginMocks.createStartContract(), }; @@ -208,8 +190,9 @@ describe('BurnRateRuleExecutor', () => { }); expect(esClientMock.search).not.toHaveBeenCalled(); - expect(alertWithLifecycleMock).not.toHaveBeenCalled(); - expect(alertFactoryMock.done).not.toHaveBeenCalled(); + expect(servicesMock.alertsClient!.report).not.toHaveBeenCalled(); + expect(servicesMock.alertsClient!.setAlertData).not.toHaveBeenCalled(); + expect(servicesMock.alertsClient!.getRecoveredAlerts).not.toHaveBeenCalled(); expect(result).toEqual({ state: {} }); }); @@ -239,7 +222,6 @@ describe('BurnRateRuleExecutor', () => { esClientMock.search.mockResolvedValueOnce( generateEsResponse(ruleParams, [], { instanceId: 'bar' }) ); - alertFactoryMock.done.mockReturnValueOnce({ getRecoveredAlerts: () => [] }); const executor = getRuleExecutor({ basePath: basePathMock }); await executor({ @@ -256,7 +238,8 @@ describe('BurnRateRuleExecutor', () => { getTimeRange, }); - expect(alertWithLifecycleMock).not.toBeCalled(); + expect(servicesMock.alertsClient?.report).not.toBeCalled(); + expect(servicesMock.alertsClient?.setAlertData).not.toBeCalled(); }); it('does not schedule an alert when the short window burn rate is below the threshold', async () => { @@ -285,7 +268,6 @@ describe('BurnRateRuleExecutor', () => { esClientMock.search.mockResolvedValueOnce( generateEsResponse(ruleParams, [], { instanceId: 'bar' }) ); - alertFactoryMock.done.mockReturnValueOnce({ getRecoveredAlerts: () => [] }); const executor = getRuleExecutor({ basePath: basePathMock }); await executor({ @@ -302,7 +284,8 @@ describe('BurnRateRuleExecutor', () => { getTimeRange, }); - expect(alertWithLifecycleMock).not.toBeCalled(); + expect(servicesMock.alertsClient?.report).not.toBeCalled(); + expect(servicesMock.alertsClient?.setAlertData).not.toBeCalled(); }); it('schedules an alert when both windows of first window definition burn rate have reached the threshold', async () => { @@ -331,12 +314,18 @@ describe('BurnRateRuleExecutor', () => { esClientMock.search.mockResolvedValueOnce( generateEsResponse(ruleParams, [], { instanceId: 'bar' }) ); - alertFactoryMock.done.mockReturnValueOnce({ getRecoveredAlerts: () => [] }); + + // @ts-ignore + servicesMock.alertsClient!.report.mockImplementation(({ id }: { id: string }) => ({ + uuid: `uuid-${id}`, + start: new Date().toISOString(), + })); const executor = getRuleExecutor({ basePath: basePathMock, alertsLocator: alertsLocatorMock, }); + await executor({ params: ruleParams, startedAt: new Date(), @@ -351,9 +340,13 @@ describe('BurnRateRuleExecutor', () => { getTimeRange, }); - expect(alertWithLifecycleMock).toBeCalledWith({ + expect(servicesMock.alertsClient?.report).toBeCalledWith({ id: 'foo', - fields: { + actionGroup: ALERT_ACTION.id, + state: { + alertState: AlertStates.ALERT, + }, + payload: { [ALERT_REASON]: 'CRITICAL: The burn rate for the past 1h is 2.3 and for the past 5m is 2.1 for foo. Alert when above 2 for both windows', [ALERT_EVALUATION_THRESHOLD]: 2, @@ -363,9 +356,13 @@ describe('BurnRateRuleExecutor', () => { [SLO_INSTANCE_ID_FIELD]: 'foo', }, }); - expect(alertWithLifecycleMock).toBeCalledWith({ + expect(servicesMock.alertsClient?.report).toBeCalledWith({ id: 'bar', - fields: { + actionGroup: ALERT_ACTION.id, + state: { + alertState: AlertStates.ALERT, + }, + payload: { [ALERT_REASON]: 'CRITICAL: The burn rate for the past 1h is 2.5 and for the past 5m is 2.2 for bar. Alert when above 2 for both windows', [ALERT_EVALUATION_THRESHOLD]: 2, @@ -375,32 +372,32 @@ describe('BurnRateRuleExecutor', () => { [SLO_INSTANCE_ID_FIELD]: 'bar', }, }); - expect(alertMock.scheduleActions).toBeCalledWith( - ALERT_ACTION.id, - expect.objectContaining({ + expect(servicesMock.alertsClient?.setAlertData).toHaveBeenNthCalledWith(1, { + id: 'foo', + context: expect.objectContaining({ longWindow: { burnRate: 2.3, duration: '1h' }, shortWindow: { burnRate: 2.1, duration: '5m' }, burnRateThreshold: 2, reason: 'CRITICAL: The burn rate for the past 1h is 2.3 and for the past 5m is 2.1 for foo. Alert when above 2 for both windows', alertDetailsUrl: 'mockedAlertsLocator > getLocation', - }) - ); - expect(alertMock.scheduleActions).toBeCalledWith( - ALERT_ACTION.id, - expect.objectContaining({ + }), + }); + expect(servicesMock.alertsClient?.setAlertData).toHaveBeenNthCalledWith(2, { + id: 'bar', + context: expect.objectContaining({ longWindow: { burnRate: 2.5, duration: '1h' }, shortWindow: { burnRate: 2.2, duration: '5m' }, burnRateThreshold: 2, reason: 'CRITICAL: The burn rate for the past 1h is 2.5 and for the past 5m is 2.2 for bar. Alert when above 2 for both windows', alertDetailsUrl: 'mockedAlertsLocator > getLocation', - }) - ); - expect(alertMock.replaceState).toBeCalledWith({ alertState: AlertStates.ALERT }); + }), + }); + expect(alertsLocatorMock.getLocation).toBeCalledWith({ baseUrl: 'https://kibana.dev', - kuery: 'kibana.alert.uuid: "mockedAlertUuid"', + kuery: 'kibana.alert.uuid: "uuid-foo"', rangeFrom: expect.stringMatching(ISO_DATE_REGEX), spaceId: 'irrelevant', }); @@ -432,7 +429,12 @@ describe('BurnRateRuleExecutor', () => { esClientMock.search.mockResolvedValueOnce( generateEsResponse(ruleParams, [], { instanceId: 'bar' }) ); - alertFactoryMock.done.mockReturnValueOnce({ getRecoveredAlerts: () => [] }); + + // @ts-ignore + servicesMock.alertsClient!.report.mockImplementation(({ id }: { id: string }) => ({ + uuid: `uuid-${id}`, + start: new Date().toISOString(), + })); const executor = getRuleExecutor({ basePath: basePathMock }); await executor({ @@ -449,9 +451,13 @@ describe('BurnRateRuleExecutor', () => { getTimeRange, }); - expect(alertWithLifecycleMock).toBeCalledWith({ + expect(servicesMock.alertsClient!.report).toBeCalledWith({ id: 'foo', - fields: { + actionGroup: HIGH_PRIORITY_ACTION_ID, + state: { + alertState: AlertStates.ALERT, + }, + payload: { [ALERT_REASON]: 'HIGH: The burn rate for the past 6h is 1.2 and for the past 30m is 1.9 for foo. Alert when above 1 for both windows', [ALERT_EVALUATION_THRESHOLD]: 1, @@ -461,9 +467,13 @@ describe('BurnRateRuleExecutor', () => { [SLO_INSTANCE_ID_FIELD]: 'foo', }, }); - expect(alertWithLifecycleMock).toBeCalledWith({ + expect(servicesMock.alertsClient!.report).toBeCalledWith({ id: 'bar', - fields: { + actionGroup: HIGH_PRIORITY_ACTION_ID, + state: { + alertState: AlertStates.ALERT, + }, + payload: { [ALERT_REASON]: 'HIGH: The burn rate for the past 6h is 1.1 and for the past 30m is 1.5 for bar. Alert when above 1 for both windows', [ALERT_EVALUATION_THRESHOLD]: 1, @@ -473,27 +483,28 @@ describe('BurnRateRuleExecutor', () => { [SLO_INSTANCE_ID_FIELD]: 'bar', }, }); - expect(alertMock.scheduleActions).toBeCalledWith( - HIGH_PRIORITY_ACTION_ID, - expect.objectContaining({ + + expect(servicesMock.alertsClient?.setAlertData).toHaveBeenNthCalledWith(1, { + id: 'foo', + context: expect.objectContaining({ longWindow: { burnRate: 1.2, duration: '6h' }, shortWindow: { burnRate: 1.9, duration: '30m' }, burnRateThreshold: 1, reason: 'HIGH: The burn rate for the past 6h is 1.2 and for the past 30m is 1.9 for foo. Alert when above 1 for both windows', - }) - ); - expect(alertMock.scheduleActions).toBeCalledWith( - HIGH_PRIORITY_ACTION_ID, - expect.objectContaining({ + }), + }); + + expect(servicesMock.alertsClient?.setAlertData).toHaveBeenNthCalledWith(2, { + id: 'bar', + context: expect.objectContaining({ longWindow: { burnRate: 1.1, duration: '6h' }, shortWindow: { burnRate: 1.5, duration: '30m' }, burnRateThreshold: 1, reason: 'HIGH: The burn rate for the past 6h is 1.1 and for the past 30m is 1.5 for bar. Alert when above 1 for both windows', - }) - ); - expect(alertMock.replaceState).toBeCalledWith({ alertState: AlertStates.ALERT }); + }), + }); }); }); }); diff --git a/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/executor.ts b/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/executor.ts index 3a8e153c76663..88d16b5e12db3 100644 --- a/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/executor.ts +++ b/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/executor.ts @@ -12,8 +12,7 @@ import { ALERT_EVALUATION_VALUE, ALERT_REASON, } from '@kbn/rule-data-utils'; -import { LifecycleRuleExecutor } from '@kbn/rule-registry-plugin/server'; -import { ExecutorType } from '@kbn/alerting-plugin/server'; +import { AlertsClientError, RuleExecutorOptions } from '@kbn/alerting-plugin/server'; import { IBasePath } from '@kbn/core/server'; import { LocatorPublic } from '@kbn/share-plugin/common'; @@ -21,6 +20,8 @@ import { upperCase } from 'lodash'; import { addSpaceIdToPath } from '@kbn/spaces-plugin/server'; import { ALL_VALUE } from '@kbn/slo-schema'; import { AlertsLocatorParams, getAlertUrl } from '@kbn/observability-plugin/common'; +import { ObservabilitySloAlert } from '@kbn/alerts-as-data-utils'; +import { ExecutorType } from '@kbn/alerting-plugin/server'; import { SLO_ID_FIELD, SLO_INSTANCE_ID_FIELD, @@ -51,21 +52,17 @@ export const getRuleExecutor = ({ }: { basePath: IBasePath; alertsLocator?: LocatorPublic; -}): LifecycleRuleExecutor< - BurnRateRuleParams, - BurnRateRuleTypeState, - BurnRateAlertState, - BurnRateAlertContext, - BurnRateAllowedActionGroups -> => - async function executor({ - services, - params, - logger, - startedAt, - spaceId, - getTimeRange, - }): ReturnType< +}) => + async function executor( + options: RuleExecutorOptions< + BurnRateRuleParams, + BurnRateRuleTypeState, + BurnRateAlertState, + BurnRateAlertContext, + BurnRateAllowedActionGroups, + ObservabilitySloAlert + > + ): ReturnType< ExecutorType< BurnRateRuleParams, BurnRateRuleTypeState, @@ -74,14 +71,13 @@ export const getRuleExecutor = ({ BurnRateAllowedActionGroups > > { - const { - alertWithLifecycle, - savedObjectsClient: soClient, - scopedClusterClient: esClient, - alertFactory, - getAlertStartedDate, - getAlertUuid, - } = services; + const { services, params, logger, startedAt, spaceId, getTimeRange } = options; + + const { savedObjectsClient: soClient, scopedClusterClient: esClient, alertsClient } = services; + + if (!alertsClient) { + throw new AlertsClientError(); + } const sloRepository = new KibanaSavedObjectsSLORepository(soClient, logger); const slo = await sloRepository.findById(params.sloId); @@ -96,7 +92,7 @@ export const getRuleExecutor = ({ const results = await evaluate(esClient.asCurrentUser, slo, params, new Date(dateEnd)); if (results.length > 0) { - const alertLimit = alertFactory.alertLimit.getValue(); + const alertLimit = alertsClient.getAlertLimitValue(); let hasReachedLimit = false; let scheduledActionsCount = 0; for (const result of results) { @@ -133,9 +129,14 @@ export const getRuleExecutor = ({ ); const alertId = instanceId; - const alert = alertWithLifecycle({ + + const { uuid, start } = alertsClient.report({ id: alertId, - fields: { + actionGroup: windowDef.actionGroup, + state: { + alertState: AlertStates.ALERT, + }, + payload: { [ALERT_REASON]: reason, [ALERT_EVALUATION_THRESHOLD]: windowDef.burnRateThreshold, [ALERT_EVALUATION_VALUE]: Math.min(longWindowBurnRate, shortWindowBurnRate), @@ -144,10 +145,10 @@ export const getRuleExecutor = ({ [SLO_INSTANCE_ID_FIELD]: instanceId, }, }); - const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString(); - const alertUuid = getAlertUuid(alertId); + + const indexedStartedAt = start ?? startedAt.toISOString(); const alertDetailsUrl = await getAlertUrl( - alertUuid, + uuid, spaceId, indexedStartedAt, alertsLocator, @@ -168,20 +169,18 @@ export const getRuleExecutor = ({ slo, }; - alert.scheduleActions(windowDef.actionGroup, context); - alert.replaceState({ alertState: AlertStates.ALERT }); + alertsClient.setAlertData({ id: alertId, context }); scheduledActionsCount++; } } - alertFactory.alertLimit.setLimitReached(hasReachedLimit); + alertsClient.setAlertLimitReached(hasReachedLimit); } - const { getRecoveredAlerts } = alertFactory.done(); - const recoveredAlerts = getRecoveredAlerts(); + const recoveredAlerts = alertsClient.getRecoveredAlerts() ?? []; for (const recoveredAlert of recoveredAlerts) { - const alertId = recoveredAlert.getId(); - const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString(); - const alertUuid = recoveredAlert.getUuid(); + const alertId = recoveredAlert.alert.getId(); + const indexedStartedAt = recoveredAlert.alert.getStart() ?? startedAt.toISOString(); + const alertUuid = recoveredAlert.alert.getUuid(); const alertDetailsUrl = await getAlertUrl( alertUuid, spaceId, @@ -206,7 +205,10 @@ export const getRuleExecutor = ({ sloInstanceId: alertId, }; - recoveredAlert.setContext(context); + alertsClient.setAlertData({ + id: alertId, + context, + }); } return { state: {} }; diff --git a/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/register.ts b/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/register.ts index 8c87d3ee71bbd..c3936de825f32 100644 --- a/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/register.ts +++ b/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/register.ts @@ -10,7 +10,6 @@ import { GetViewInAppRelativeUrlFnOpts } from '@kbn/alerting-plugin/server'; import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { LicenseType } from '@kbn/licensing-plugin/server'; -import { createLifecycleExecutor } from '@kbn/rule-registry-plugin/server'; import { legacyExperimentalFieldMap } from '@kbn/alerts-as-data-utils'; import { IBasePath } from '@kbn/core/server'; import { LocatorPublic } from '@kbn/share-plugin/common'; @@ -44,10 +43,7 @@ const windowSchema = schema.object({ actionGroup: schema.string(), }); -type CreateLifecycleExecutor = ReturnType; - export function sloBurnRateRuleType( - createLifecycleRuleExecutor: CreateLifecycleExecutor, basePath: IBasePath, alertsLocator?: LocatorPublic ) { @@ -76,7 +72,7 @@ export function sloBurnRateRuleType( producer: sloFeatureId, minimumLicenseRequired: 'platinum' as LicenseType, isExportable: true, - executor: createLifecycleRuleExecutor(getRuleExecutor({ basePath, alertsLocator })), + executor: getRuleExecutor({ basePath, alertsLocator }), doesSetRecoveryContext: true, actionVariables: { context: [ @@ -97,6 +93,7 @@ export function sloBurnRateRuleType( mappings: { fieldMap: { ...legacyExperimentalFieldMap, ...sloRuleFieldMap } }, useEcs: false, useLegacyAlerts: true, + shouldWrite: true, }, getViewInAppRelativeUrl: ({ rule }: GetViewInAppRelativeUrlFnOpts<{}>) => observabilityPaths.ruleDetails(rule.id),