[AO] Add alertDetailsUrl to SLO rule type (#158920)

Closes #158662

## Summary

This PR adds alertDetailsUrl to the SLO burn rate rule type

|Action|Result|
|---|---|

|![image](647ce4a8-16e9-4884-a8f4-3e7b5f128ebf)|

#### Filtered alert
<img
src="7ac8f529-d18e-4940-a8fe-3d148fa21d82"
width="800"/>

## 🧪 How to test
- Ensure that `server.publicBaseUrl` is configured in kibana.dev.yml
- Create an SLO rule
- Add an action to this rule and specify `context.alertDetailsUrl`
variable there
- After the alert is triggered, open the value of `alertDetailsUrl` in
the browser
- you should be able to see the related alert on the `Observability >
Alerts` page
This commit is contained in:
Maryam Saeidi 2023-06-05 11:08:41 +02:00 committed by GitHub
parent 94fb44ae0c
commit 3b3a409021
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 92 additions and 11 deletions

View file

@ -8,14 +8,19 @@
import { PluginSetupContract } from '@kbn/alerting-plugin/server';
import { IBasePath, Logger } from '@kbn/core/server';
import { createLifecycleExecutor, IRuleDataClient } from '@kbn/rule-registry-plugin/server';
import { LocatorPublic } from '@kbn/share-plugin/common';
import { AlertsLocatorParams } from '../../../common';
import { sloBurnRateRuleType } from './slo_burn_rate';
export function registerRuleTypes(
alertingPlugin: PluginSetupContract,
logger: Logger,
ruleDataClient: IRuleDataClient,
basePath: IBasePath
basePath: IBasePath,
alertsLocator?: LocatorPublic<AlertsLocatorParams>
) {
const createLifecycleRuleExecutor = createLifecycleExecutor(logger.get('rules'), ruleDataClient);
alertingPlugin.registerType(sloBurnRateRuleType(createLifecycleRuleExecutor, basePath));
alertingPlugin.registerType(
sloBurnRateRuleType(createLifecycleRuleExecutor, basePath, alertsLocator)
);
}

View file

@ -30,6 +30,8 @@ import {
ALERT_EVALUATION_VALUE,
ALERT_REASON,
} from '@kbn/rule-data-utils';
import { LocatorPublic } from '@kbn/share-plugin/common';
import type { AlertsLocatorParams } from '../../../../common';
import { getRuleExecutor } from './executor';
import { createSLO } from '../../../services/slo/fixtures/slo';
import { SLO, StoredSLO } from '../../../domain/models';
@ -85,7 +87,15 @@ describe('BurnRateRuleExecutor', () => {
let esClientMock: ElasticsearchClientMock;
let soClientMock: jest.Mocked<SavedObjectsClientContract>;
let loggerMock: jest.Mocked<MockedLogger>;
const alertUuid = 'mockedAlertUuid';
const basePathMock = { publicBaseUrl: 'https://kibana.dev' } as IBasePath;
const alertsLocatorMock = {
getLocation: jest.fn().mockImplementation(() => ({
path: 'mockedAlertsLocator > getLocation',
})),
} as any as LocatorPublic<AlertsLocatorParams>;
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<LifecycleAlertService>;
let alertFactoryMock: jest.Mocked<
PublicAlertFactory<BurnRateAlertState, BurnRateAlertContext, BurnRateAllowedActionGroups>
@ -119,7 +129,7 @@ describe('BurnRateRuleExecutor', () => {
shouldWriteAlerts: jest.fn(),
shouldStopExecution: jest.fn(),
getAlertStartedDate: jest.fn(),
getAlertUuid: jest.fn(),
getAlertUuid: jest.fn().mockImplementation(() => alertUuid),
getAlertByAlertUuid: jest.fn(),
share: {} as SharePluginStart,
dataViews: dataViewPluginMocks.createStartContract(),
@ -231,7 +241,10 @@ describe('BurnRateRuleExecutor', () => {
alertWithLifecycleMock.mockImplementation(() => alertMock as any);
alertFactoryMock.done.mockReturnValueOnce({ getRecoveredAlerts: () => [] });
const executor = getRuleExecutor({ basePath: basePathMock });
const executor = getRuleExecutor({
basePath: basePathMock,
alertsLocator: alertsLocatorMock,
});
await executor({
params: someRuleParamsWithWindows({ sloId: slo.id }),
startedAt: new Date(),
@ -264,9 +277,16 @@ describe('BurnRateRuleExecutor', () => {
burnRateThreshold: 2,
reason:
'CRITICAL: The burn rate for the past 1h is 2 and for the past 5m is 2. 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"',
rangeFrom: expect.stringMatching(ISO_DATE_REGEX),
spaceId: 'irrelevant',
});
});
it('schedules an alert when both windows of second window definition burn rate have reached the threshold', async () => {
@ -329,7 +349,10 @@ describe('BurnRateRuleExecutor', () => {
};
alertFactoryMock.done.mockReturnValueOnce({ getRecoveredAlerts: () => [alertMock] as any });
const executor = getRuleExecutor({ basePath: basePathMock });
const executor = getRuleExecutor({
basePath: basePathMock,
alertsLocator: alertsLocatorMock,
});
await executor({
params: someRuleParamsWithWindows({ sloId: slo.id }),
@ -350,8 +373,15 @@ describe('BurnRateRuleExecutor', () => {
longWindow: { burnRate: 0.9, duration: '6h' },
shortWindow: { burnRate: 0.9, duration: '30m' },
burnRateThreshold: 1,
alertDetailsUrl: 'mockedAlertsLocator > getLocation',
})
);
expect(alertsLocatorMock.getLocation).toBeCalledWith({
baseUrl: 'https://kibana.dev',
kuery: 'kibana.alert.uuid: "mockedAlertUuid"',
rangeFrom: expect.stringMatching(ISO_DATE_REGEX),
spaceId: 'irrelevant',
});
});
});
});

View file

@ -15,9 +15,11 @@ import {
import { LifecycleRuleExecutor } from '@kbn/rule-registry-plugin/server';
import { ExecutorType } from '@kbn/alerting-plugin/server';
import { IBasePath } from '@kbn/core/server';
import { LocatorPublic } from '@kbn/share-plugin/common';
import { memoize, last, upperCase } from 'lodash';
import { addSpaceIdToPath } from '@kbn/spaces-plugin/server';
import { AlertsLocatorParams, getAlertUrl } from '../../../../common';
import { SLO_ID_FIELD, SLO_REVISION_FIELD } from '../../../../common/field_names/infra_metrics';
import { Duration, SLO, toDurationUnit } from '../../../domain/models';
import { DefaultSLIClient, KibanaSavedObjectsSLORepository } from '../../../services/slo';
@ -91,8 +93,10 @@ async function evaluate(slo: SLO, summaryClient: DefaultSLIClient, params: BurnR
export const getRuleExecutor = ({
basePath,
alertsLocator,
}: {
basePath: IBasePath;
alertsLocator?: LocatorPublic<AlertsLocatorParams>;
}): LifecycleRuleExecutor<
BurnRateRuleParams,
BurnRateRuleTypeState,
@ -119,6 +123,8 @@ export const getRuleExecutor = ({
savedObjectsClient: soClient,
scopedClusterClient: esClient,
alertFactory,
getAlertStartedDate,
getAlertUuid,
} = services;
const sloRepository = new KibanaSavedObjectsSLORepository(soClient);
@ -157,9 +163,21 @@ export const getRuleExecutor = ({
windowDef
);
const alertId = `alert-${slo.id}-${slo.revision}`;
const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString();
const alertUuid = getAlertUuid(alertId);
const alertDetailsUrl = await getAlertUrl(
alertUuid,
spaceId,
indexedStartedAt,
alertsLocator,
basePath.publicBaseUrl
);
const context = {
longWindow: { burnRate: longWindowBurnRate, duration: longWindowDuration.format() },
alertDetailsUrl,
reason,
longWindow: { burnRate: longWindowBurnRate, duration: longWindowDuration.format() },
shortWindow: { burnRate: shortWindowBurnRate, duration: shortWindowDuration.format() },
burnRateThreshold: windowDef.burnRateThreshold,
timestamp: startedAt.toISOString(),
@ -169,7 +187,7 @@ export const getRuleExecutor = ({
};
const alert = alertWithLifecycle({
id: `alert-${slo.id}-${slo.revision}`,
id: alertId,
fields: {
[ALERT_REASON]: reason,
[ALERT_EVALUATION_THRESHOLD]: windowDef.burnRateThreshold,
@ -186,12 +204,23 @@ export const getRuleExecutor = ({
const { getRecoveredAlerts } = alertFactory.done();
const recoveredAlerts = getRecoveredAlerts();
for (const recoveredAlert of recoveredAlerts) {
const alertId = `alert-${slo.id}-${slo.revision}`;
const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString();
const alertUuid = getAlertUuid(alertId);
const alertDetailsUrl = await getAlertUrl(
alertUuid,
spaceId,
indexedStartedAt,
alertsLocator,
basePath.publicBaseUrl
);
const context = {
longWindow: { burnRate: longWindowBurnRate, duration: longWindowDuration.format() },
shortWindow: { burnRate: shortWindowBurnRate, duration: shortWindowDuration.format() },
burnRateThreshold: windowDef.burnRateThreshold,
timestamp: startedAt.toISOString(),
viewInAppUrl,
alertDetailsUrl,
sloId: slo.id,
sloName: slo.name,
};

View file

@ -11,7 +11,8 @@ 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 { sloFeatureId } from '../../../../common';
import { LocatorPublic } from '@kbn/share-plugin/common';
import { AlertsLocatorParams, sloFeatureId } from '../../../../common';
import { SLO_RULE_REGISTRATION_CONTEXT } from '../../../common/constants';
import {
@ -43,7 +44,8 @@ type CreateLifecycleExecutor = ReturnType<typeof createLifecycleExecutor>;
export function sloBurnRateRuleType(
createLifecycleRuleExecutor: CreateLifecycleExecutor,
basePath: IBasePath
basePath: IBasePath,
alertsLocator?: LocatorPublic<AlertsLocatorParams>
) {
return {
id: SLO_BURN_RATE_RULE_ID,
@ -61,7 +63,7 @@ export function sloBurnRateRuleType(
producer: sloFeatureId,
minimumLicenseRequired: 'platinum' as LicenseType,
isExportable: true,
executor: createLifecycleRuleExecutor(getRuleExecutor({ basePath })),
executor: createLifecycleRuleExecutor(getRuleExecutor({ basePath, alertsLocator })),
doesSetRecoveryContext: true,
actionVariables: {
context: [
@ -71,6 +73,7 @@ export function sloBurnRateRuleType(
{ name: 'longWindow', description: windowActionVariableDescription },
{ name: 'shortWindow', description: windowActionVariableDescription },
{ name: 'viewInAppUrl', description: viewInAppUrlActionVariableDescription },
{ name: 'alertDetailsUrl', description: alertDetailsUrlActionVariableDescription },
{ name: 'sloId', description: sloIdActionVariableDescription },
{ name: 'sloName', description: sloNameActionVariableDescription },
],
@ -118,6 +121,14 @@ export const viewInAppUrlActionVariableDescription = i18n.translate(
}
);
export const alertDetailsUrlActionVariableDescription = i18n.translate(
'xpack.observability.slo.alerting.alertDetailsUrlDescription',
{
defaultMessage:
'Link to the alert troubleshooting view for further context and details. This will be an empty string if the server.publicBaseUrl is not configured.',
}
);
export const sloIdActionVariableDescription = i18n.translate(
'xpack.observability.slo.alerting.sloIdDescription',
{

View file

@ -244,7 +244,13 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
},
],
});
registerRuleTypes(plugins.alerting, this.logger, ruleDataClient, core.http.basePath);
registerRuleTypes(
plugins.alerting,
this.logger,
ruleDataClient,
core.http.basePath,
alertsLocator
);
registerSloUsageCollector(plugins.usageCollection);
core.getStartServices().then(([coreStart, pluginStart]) => {