[AO] Add alertDetailsUrl for infra rule (#157987)

Closes #156534

## Summary

This PR adds the alertDetailsUrl to the infra rules. The value of this
variable is a link to the `observability > alerts` page filtered for
this instance of alert.


![image](409bea90-5d2b-4e60-ae4c-61223cccd41a)

Here is an example of this action variable:

|alertDetailsUrl as action variable|Result of action|
|---|---|

|![image](4f800c6d-f15f-481e-b7fc-4f85aa1085a7)|

**Note**
- I will change this field to `kibana.alert.url` in another
[ticket](https://github.com/elastic/kibana/issues/158359)

## 🧪 How to test
- Ensure that `server.publicBaseUrl` is configured in kibana.dev.yml
- Create a metric threshold/inventory/logs rule and use the
`context.alertDetailsUrl` in action for this rule
- After an alert is triggered, open the link provided by alertDetailsUrl
and make sure that the alert is filtered correctly
- Check the time range, it should be set for 5 mins before the alert
start time

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Carlos Crespo <crespocarlos@users.noreply.github.com>
This commit is contained in:
Maryam Saeidi 2023-05-26 10:14:05 +02:00 committed by GitHub
parent f4b88be5b2
commit e7ddab7e57
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 339 additions and 91 deletions

View file

@ -169,6 +169,7 @@ enabled:
- x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/config.ts
- x-pack/test/alerting_api_integration/spaces_only/tests/actions/config.ts
- x-pack/test/alerting_api_integration/spaces_only/tests/action_task_params/config.ts
- x-pack/test/alerting_api_integration/observability/config.ts
- x-pack/test/api_integration_basic/config.ts
- x-pack/test/api_integration/config_security_basic.ts
- x-pack/test/api_integration/config_security_trial.ts

1
.github/CODEOWNERS vendored
View file

@ -970,6 +970,7 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib
# Response Ops team
/x-pack/test/alerting_api_integration/ @elastic/response-ops
/x-pack/test/alerting_api_integration/observability @elastic/actionable-observability
/x-pack/test/plugin_api_integration/test_suites/task_manager/ @elastic/response-ops
/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/ @elastic/response-ops
/docs/user/alerting/ @elastic/response-ops

View file

@ -7,6 +7,7 @@
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { Lifecycle } from '@hapi/hapi';
import { SharePluginSetup } from '@kbn/share-plugin/server';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import { JsonArray, JsonValue } from '@kbn/utility-types';
import { RouteConfig, RouteMethod } from '@kbn/core/server';
@ -31,6 +32,7 @@ export interface InfraServerPluginSetupDeps {
features: FeaturesPluginSetup;
ruleRegistry: RuleRegistryPluginSetupContract;
observability: ObservabilityPluginSetup;
share: SharePluginSetup;
spaces: SpacesPluginSetup;
usageCollection: UsageCollectionSetup;
visTypeTimeseries: VisTypeTimeseriesSetup;

View file

@ -5,12 +5,14 @@
* 2.0.
*/
import moment from 'moment';
import { AlertsLocatorParams } from '@kbn/observability-plugin/common';
import { LocatorPublic } from '@kbn/share-plugin/common';
import { isEmpty, isError } from 'lodash';
import { schema } from '@kbn/config-schema';
import { Logger, LogMeta } from '@kbn/logging';
import type { ElasticsearchClient, IBasePath } from '@kbn/core/server';
import { addSpaceIdToPath } from '@kbn/spaces-plugin/common';
import { ObservabilityConfig } from '@kbn/observability-plugin/server';
import { ALERT_RULE_PARAMETERS, TIMESTAMP } from '@kbn/rule-data-utils';
import {
ParsedTechnicalFields,
@ -110,15 +112,6 @@ export const createScopedLogger = (
};
};
export const getAlertDetailsPageEnabledForApp = (
config: ObservabilityConfig['unsafe']['alertDetails'] | null,
appName: keyof ObservabilityConfig['unsafe']['alertDetails']
): boolean => {
if (!config) return false;
return config[appName].enabled;
};
export const getViewInInventoryAppUrl = ({
basePath,
criteria,
@ -153,6 +146,27 @@ export const getViewInInventoryAppUrl = ({
export const getViewInMetricsAppUrl = (basePath: IBasePath, spaceId: string) =>
addSpaceIdToPath(basePath.publicBaseUrl, spaceId, LINK_TO_METRICS_EXPLORER);
export const getAlertUrl = async (
alertUuid: string | null,
spaceId: string,
startedAt: string,
alertsLocator?: LocatorPublic<AlertsLocatorParams>,
publicBaseUrl?: string
) => {
if (!publicBaseUrl || !alertsLocator || !alertUuid) return '';
const rangeFrom = moment(startedAt).subtract('5', 'minute').toISOString();
return (
await alertsLocator.getLocation({
baseUrl: publicBaseUrl,
spaceId,
kuery: `kibana.alert.uuid: "${alertUuid}"`,
rangeFrom,
})
).path;
};
export const getAlertDetailsUrl = (
basePath: IBasePath,
spaceId: string,

View file

@ -35,7 +35,7 @@ import {
AdditionalContext,
createScopedLogger,
flattenAdditionalContext,
getAlertDetailsUrl,
getAlertUrl,
getContextForRecoveredAlerts,
getViewInInventoryAppUrl,
UNGROUPED_FACTORY_KEY,
@ -130,12 +130,18 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
const actionGroupId = FIRED_ACTIONS.id; // Change this to an Error action group when able
const reason = buildInvalidQueryAlertReason(params.filterQueryText);
const alert = alertFactory(UNGROUPED_FACTORY_KEY, reason, actionGroupId);
const indexedStartedDate =
const indexedStartedAt =
getAlertStartedDate(UNGROUPED_FACTORY_KEY) ?? startedAt.toISOString();
const alertUuid = getAlertUuid(UNGROUPED_FACTORY_KEY);
alert.scheduleActions(actionGroupId, {
alertDetailsUrl: getAlertDetailsUrl(libs.basePath, spaceId, alertUuid),
alertDetailsUrl: await getAlertUrl(
alertUuid,
spaceId,
indexedStartedAt,
libs.alertsLocator,
libs.basePath.publicBaseUrl
),
alertState: stateToAlertMessage[AlertStates.ERROR],
group: UNGROUPED_FACTORY_KEY,
metric: mapToConditionsLookup(criteria, (c) => c.metric),
@ -146,7 +152,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
basePath: libs.basePath,
criteria,
nodeType,
timestamp: indexedStartedDate,
timestamp: indexedStartedAt,
spaceId,
}),
});
@ -256,13 +262,19 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
additionalContext,
evaluationValues
);
const indexedStartedDate = getAlertStartedDate(group) ?? startedAt.toISOString();
const indexedStartedAt = getAlertStartedDate(group) ?? startedAt.toISOString();
const alertUuid = getAlertUuid(group);
scheduledActionsCount++;
const context = {
alertDetailsUrl: getAlertDetailsUrl(libs.basePath, spaceId, alertUuid),
alertDetailsUrl: await getAlertUrl(
alertUuid,
spaceId,
indexedStartedAt,
libs.alertsLocator,
libs.basePath.publicBaseUrl
),
alertState: stateToAlertMessage[nextState],
group,
reason,
@ -276,7 +288,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
basePath: libs.basePath,
criteria,
nodeType,
timestamp: indexedStartedDate,
timestamp: indexedStartedAt,
spaceId,
}),
...additionalContext,
@ -290,14 +302,20 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
for (const alert of recoveredAlerts) {
const recoveredAlertId = alert.getId();
const indexedStartedDate = getAlertStartedDate(recoveredAlertId) ?? startedAt.toISOString();
const indexedStartedAt = getAlertStartedDate(recoveredAlertId) ?? startedAt.toISOString();
const alertUuid = getAlertUuid(recoveredAlertId);
const alertHits = alertUuid ? await getAlertByAlertUuid(alertUuid) : undefined;
const additionalContext = getContextForRecoveredAlerts(alertHits);
const originalActionGroup = getOriginalActionGroup(alertHits);
alert.setContext({
alertDetailsUrl: getAlertDetailsUrl(libs.basePath, spaceId, alertUuid),
alertDetailsUrl: await getAlertUrl(
alertUuid,
spaceId,
indexedStartedAt,
libs.alertsLocator,
libs.basePath.publicBaseUrl
),
alertState: stateToAlertMessage[AlertStates.OK],
group: recoveredAlertId,
metric: mapToConditionsLookup(criteria, (c) => c.metric),
@ -307,7 +325,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
basePath: libs.basePath,
criteria,
nodeType,
timestamp: indexedStartedDate,
timestamp: indexedStartedAt,
spaceId,
}),
originalAlertState: translateActionGroupToAlertState(originalActionGroup),

View file

@ -41,11 +41,7 @@ import {
valueActionVariableDescription,
viewInAppUrlActionVariableDescription,
} from '../common/messages';
import {
getAlertDetailsPageEnabledForApp,
oneOfLiterals,
validateIsStringElasticsearchJSONFilter,
} from '../common/utils';
import { oneOfLiterals, validateIsStringElasticsearchJSONFilter } from '../common/utils';
import {
createInventoryMetricThresholdExecutor,
FIRED_ACTIONS,
@ -86,8 +82,6 @@ export async function registerMetricInventoryThresholdRuleType(
alertingPlugin: PluginSetupContract,
libs: InfraBackendLibs
) {
const config = libs.getAlertDetailsConfig();
alertingPlugin.registerType({
id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
name: i18n.translate('xpack.infra.metrics.inventory.alertName', {
@ -118,15 +112,11 @@ export async function registerMetricInventoryThresholdRuleType(
context: [
{ name: 'group', description: groupActionVariableDescription },
{ name: 'alertState', description: alertStateActionVariableDescription },
...(getAlertDetailsPageEnabledForApp(config, 'metrics')
? [
{
name: 'alertDetailsUrl',
description: alertDetailUrlActionVariableDescription,
usesPublicBaseUrl: true,
},
]
: []),
{
name: 'alertDetailsUrl',
description: alertDetailUrlActionVariableDescription,
usesPublicBaseUrl: true,
},
{ name: 'reason', description: reasonActionVariableDescription },
{ name: 'timestamp', description: timestampActionVariableDescription },
{ name: 'value', description: valueActionVariableDescription },

View file

@ -7,6 +7,7 @@
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { i18n } from '@kbn/i18n';
import { AlertsLocatorParams } from '@kbn/observability-plugin/common';
import {
ALERT_CONTEXT,
ALERT_EVALUATION_THRESHOLD,
@ -23,7 +24,9 @@ import {
RuleExecutorServices,
RuleTypeState,
} from '@kbn/alerting-plugin/server';
import { LocatorPublic } from '@kbn/share-plugin/common';
import { addSpaceIdToPath } from '@kbn/spaces-plugin/common';
import { asyncForEach } from '@kbn/std';
import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common';
import { ParsedExperimentalFields } from '@kbn/rule-registry-plugin/common/parse_experimental_fields';
@ -57,7 +60,7 @@ import { InfraBackendLibs } from '../../infra_types';
import {
AdditionalContext,
flattenAdditionalContext,
getAlertDetailsUrl,
getAlertUrl,
getContextForRecoveredAlerts,
getGroupByObject,
unflattenObject,
@ -127,7 +130,7 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) =>
getAlertUuid,
getAlertByAlertUuid,
} = services;
const { basePath } = libs;
const { basePath, alertsLocator } = libs;
const alertFactory: LogThresholdAlertFactory = (
id,
@ -168,7 +171,7 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) =>
viewInAppUrl,
};
actions.forEach((actionSet) => {
asyncForEach(actions, async (actionSet) => {
const { actionGroup, context } = actionSet;
const alertInstanceId = (context.group || id) as string;
@ -178,7 +181,13 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) =>
alert.scheduleActions(actionGroup, {
...sharedContext,
...context,
alertDetailsUrl: getAlertDetailsUrl(libs.basePath, spaceId, alertUuid),
alertDetailsUrl: await getAlertUrl(
alertUuid,
spaceId,
indexedStartedAt,
libs.alertsLocator,
libs.basePath.publicBaseUrl
),
});
});
}
@ -234,6 +243,7 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) =>
startedAt,
validatedParams,
getAlertByAlertUuid,
alertsLocator,
});
} catch (e) {
throw new Error(e);
@ -997,6 +1007,7 @@ const processRecoveredAlerts = async ({
startedAt,
validatedParams,
getAlertByAlertUuid,
alertsLocator,
}: {
basePath: IBasePath;
getAlertStartedDate: (alertId: string) => string | null;
@ -1008,6 +1019,7 @@ const processRecoveredAlerts = async ({
getAlertByAlertUuid: (
alertUuid: string
) => Promise<Partial<ParsedTechnicalFields & ParsedExperimentalFields> | null> | null;
alertsLocator?: LocatorPublic<AlertsLocatorParams>;
}) => {
const groupByKeysObjectForRecovered = getGroupByObject(
validatedParams.groupBy,
@ -1024,7 +1036,13 @@ const processRecoveredAlerts = async ({
const viewInAppUrl = addSpaceIdToPath(basePath.publicBaseUrl, spaceId, relativeViewInAppUrl);
const baseContext = {
alertDetailsUrl: getAlertDetailsUrl(basePath, spaceId, alertUuid),
alertDetailsUrl: await getAlertUrl(
alertUuid,
spaceId,
indexedStartedAt,
alertsLocator,
basePath.publicBaseUrl
),
group: hasGroupBy(validatedParams) ? recoveredAlertId : null,
groupByKeys: groupByKeysObjectForRecovered[recoveredAlertId],
timestamp: startedAt.toISOString(),

View file

@ -15,7 +15,6 @@ import {
} from '../../../../common/alerting/logs/log_threshold';
import { InfraBackendLibs } from '../../infra_types';
import { decodeOrThrow } from '../../../../common/runtime_types';
import { getAlertDetailsPageEnabledForApp } from '../common/utils';
import {
alertDetailUrlActionVariableDescription,
groupByKeysActionVariableDescription,
@ -110,8 +109,6 @@ export async function registerLogThresholdRuleType(
);
}
const config = libs.getAlertDetailsConfig();
alertingPlugin.registerType({
id: LOG_DOCUMENT_COUNT_RULE_TYPE_ID,
name: i18n.translate('xpack.infra.logs.alertName', {
@ -144,15 +141,11 @@ export async function registerLogThresholdRuleType(
name: 'denominatorConditions',
description: denominatorConditionsActionVariableDescription,
},
...(getAlertDetailsPageEnabledForApp(config, 'logs')
? [
{
name: 'alertDetailsUrl',
description: alertDetailUrlActionVariableDescription,
usesPublicBaseUrl: true,
},
]
: []),
{
name: 'alertDetailsUrl',
description: alertDetailUrlActionVariableDescription,
usesPublicBaseUrl: true,
},
{
name: 'viewInAppUrl',
description: viewInAppUrlActionVariableDescription,

View file

@ -30,7 +30,6 @@ import {
import {
createScopedLogger,
AdditionalContext,
getAlertDetailsUrl,
getContextForRecoveredAlerts,
getViewInMetricsAppUrl,
UNGROUPED_FACTORY_KEY,
@ -38,6 +37,7 @@ import {
validGroupByForContext,
flattenAdditionalContext,
getGroupByObject,
getAlertUrl,
} from '../common/utils';
import { EvaluatedRuleParams, evaluateRule } from './lib/evaluate_rule';
@ -110,7 +110,13 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) =>
executionId,
});
const { alertWithLifecycle, savedObjectsClient, getAlertUuid, getAlertByAlertUuid } = services;
const {
alertWithLifecycle,
savedObjectsClient,
getAlertUuid,
getAlertStartedDate,
getAlertByAlertUuid,
} = services;
const alertFactory: MetricThresholdAlertFactory = (
id,
@ -150,9 +156,17 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) =>
const reason = buildInvalidQueryAlertReason(params.filterQueryText);
const alert = alertFactory(UNGROUPED_FACTORY_KEY, reason, actionGroupId);
const alertUuid = getAlertUuid(UNGROUPED_FACTORY_KEY);
const indexedStartedAt =
getAlertStartedDate(UNGROUPED_FACTORY_KEY) ?? startedAt.toISOString();
alert.scheduleActions(actionGroupId, {
alertDetailsUrl: getAlertDetailsUrl(libs.basePath, spaceId, alertUuid),
alertDetailsUrl: await getAlertUrl(
alertUuid,
spaceId,
indexedStartedAt,
libs.alertsLocator,
libs.basePath.publicBaseUrl
),
alertState: stateToAlertMessage[AlertStates.ERROR],
group: UNGROUPED_FACTORY_KEY,
metric: mapToConditionsLookup(criteria, (c) => c.metric),
@ -309,10 +323,17 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) =>
evaluationValues
);
const alertUuid = getAlertUuid(group);
const indexedStartedAt = getAlertStartedDate(group) ?? startedAt.toISOString();
scheduledActionsCount++;
alert.scheduleActions(actionGroupId, {
alertDetailsUrl: getAlertDetailsUrl(libs.basePath, spaceId, alertUuid),
alertDetailsUrl: await getAlertUrl(
alertUuid,
spaceId,
indexedStartedAt,
libs.alertsLocator,
libs.basePath.publicBaseUrl
),
alertState: stateToAlertMessage[nextState],
group,
groupByKeys: groupByKeysObjectMapping[group],
@ -357,13 +378,21 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) =>
for (const alert of recoveredAlerts) {
const recoveredAlertId = alert.getId();
const alertUuid = getAlertUuid(recoveredAlertId);
const timestamp = startedAt.toISOString();
const indexedStartedAt = getAlertStartedDate(recoveredAlertId) ?? timestamp;
const alertHits = alertUuid ? await getAlertByAlertUuid(alertUuid) : undefined;
const additionalContext = getContextForRecoveredAlerts(alertHits);
const originalActionGroup = getOriginalActionGroup(alertHits);
alert.setContext({
alertDetailsUrl: getAlertDetailsUrl(libs.basePath, spaceId, alertUuid),
alertDetailsUrl: await getAlertUrl(
alertUuid,
spaceId,
indexedStartedAt,
libs.alertsLocator,
libs.basePath.publicBaseUrl
),
alertState: stateToAlertMessage[AlertStates.OK],
group: recoveredAlertId,
groupByKeys: groupByKeysObjectForRecovered[recoveredAlertId],
@ -373,7 +402,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) =>
}
return c.metric;
}),
timestamp: startedAt.toISOString(),
timestamp,
threshold: mapToConditionsLookup(criteria, (c) => c.threshold),
viewInAppUrl: getViewInMetricsAppUrl(libs.basePath, spaceId),

View file

@ -31,11 +31,7 @@ import {
valueActionVariableDescription,
viewInAppUrlActionVariableDescription,
} from '../common/messages';
import {
getAlertDetailsPageEnabledForApp,
oneOfLiterals,
validateIsStringElasticsearchJSONFilter,
} from '../common/utils';
import { oneOfLiterals, validateIsStringElasticsearchJSONFilter } from '../common/utils';
import {
createMetricThresholdExecutor,
FIRED_ACTIONS,
@ -55,8 +51,6 @@ export async function registerMetricThresholdRuleType(
alertingPlugin: PluginSetupContract,
libs: InfraBackendLibs
) {
const config = libs.getAlertDetailsConfig();
const baseCriterion = {
threshold: schema.arrayOf(schema.number()),
comparator: oneOfLiterals(Object.values(Comparator)),
@ -150,15 +144,11 @@ export async function registerMetricThresholdRuleType(
context: [
{ name: 'group', description: groupActionVariableDescription },
{ name: 'groupByKeys', description: groupByKeysActionVariableDescription },
...(getAlertDetailsPageEnabledForApp(config, 'metrics')
? [
{
name: 'alertDetailsUrl',
description: alertDetailUrlActionVariableDescription,
usesPublicBaseUrl: true,
},
]
: []),
{
name: 'alertDetailsUrl',
description: alertDetailUrlActionVariableDescription,
usesPublicBaseUrl: true,
},
{ name: 'alertState', description: alertStateActionVariableDescription },
{ name: 'reason', description: reasonActionVariableDescription },
{ name: 'timestamp', description: timestampActionVariableDescription },

View file

@ -8,7 +8,9 @@
import { Logger } from '@kbn/logging';
import type { IBasePath } from '@kbn/core/server';
import { handleEsError } from '@kbn/es-ui-shared-plugin/server';
import type { AlertsLocatorParams } from '@kbn/observability-plugin/common';
import { ObservabilityConfig } from '@kbn/observability-plugin/server';
import { LocatorPublic } from '@kbn/share-plugin/common';
import { RulesServiceSetup } from '../services/rules';
import { InfraConfig, InfraPluginStartServicesAccessor } from '../types';
import { KibanaFramework } from './adapters/framework/kibana_framework_adapter';
@ -36,4 +38,5 @@ export interface InfraBackendLibs extends InfraDomainLibs {
getStartServices: InfraPluginStartServicesAccessor;
handleEsError: typeof handleEsError;
logger: Logger;
alertsLocator?: LocatorPublic<AlertsLocatorParams>;
}

View file

@ -16,6 +16,7 @@ import {
import { handleEsError } from '@kbn/es-ui-shared-plugin/server';
import { i18n } from '@kbn/i18n';
import { Logger } from '@kbn/logging';
import { alertsLocatorID } from '@kbn/observability-plugin/common';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import {
DISCOVER_APP_TARGET,
@ -203,6 +204,7 @@ export class InfraServerPlugin
getAlertDetailsConfig: () => plugins.observability.getAlertDetailsConfig(),
logger: this.logger,
basePath: core.http.basePath,
alertsLocator: plugins.share.url.locators.get(alertsLocatorID),
};
plugins.features.registerKibanaFeature(METRICS_FEATURE);

View file

@ -8,3 +8,5 @@
export const SLO_BURN_RATE_RULE_ID = 'slo.rules.burnRate';
export const INVALID_EQUATION_REGEX = /[^A-Z|+|\-|\s|\d+|\.|\(|\)|\/|\*|>|<|=|\?|\:|&|\!|\|]+/g;
export const ALERT_STATUS_ALL = 'all';
export const ALERTS_URL_STORAGE_KEY = '_a';

View file

@ -63,10 +63,13 @@ export const uptimeOverviewLocatorID = 'UPTIME_OVERVIEW_LOCATOR';
export const syntheticsMonitorDetailLocatorID = 'SYNTHETICS_MONITOR_DETAIL_LOCATOR';
export const syntheticsEditMonitorLocatorID = 'SYNTHETICS_EDIT_MONITOR_LOCATOR';
export const syntheticsSettingsLocatorID = 'SYNTHETICS_SETTINGS';
export const alertsLocatorID = 'ALERTS_LOCATOR';
export const ruleDetailsLocatorID = 'RULE_DETAILS_LOCATOR';
export const rulesLocatorID = 'RULES_LOCATOR';
export const sloDetailsLocatorID = 'SLO_DETAILS_LOCATOR';
export type { AlertsLocatorParams } from './locators/alerts';
export {
NETWORK_TIMINGS_FIELDS,
SYNTHETICS_BLOCKED_TIMINGS,

View file

@ -0,0 +1,57 @@
/*
* 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 { ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils';
import { ALERTS_PATH, AlertsLocatorDefinition } from './alerts';
describe('RuleDetailsLocator', () => {
const locator = new AlertsLocatorDefinition();
const baseUrlMock = 'https://kibana.dev';
const spaceIdMock = 'mockedSpaceId';
it('should return correct url when only baseUrl and spaceId are provided', async () => {
const location = await locator.getLocation({ baseUrl: baseUrlMock, spaceId: spaceIdMock });
expect(location.app).toEqual('observability');
expect(location.path).toEqual(
`${baseUrlMock}/s/${spaceIdMock}${ALERTS_PATH}?_a=(kuery:%27%27%2CrangeFrom:now-15m%2CrangeTo:now%2Cstatus:all)`
);
});
it('should return correct url when spaceId is default', async () => {
const location = await locator.getLocation({ baseUrl: baseUrlMock, spaceId: 'default' });
expect(location.app).toEqual('observability');
expect(location.path).toEqual(
`${baseUrlMock}${ALERTS_PATH}?_a=(kuery:%27%27%2CrangeFrom:now-15m%2CrangeTo:now%2Cstatus:all)`
);
});
it('should return correct url when time range is provided', async () => {
const location = await locator.getLocation({
baseUrl: baseUrlMock,
spaceId: spaceIdMock,
rangeFrom: 'mockedRangeTo',
rangeTo: 'mockedRangeFrom',
});
expect(location.path).toEqual(
`${baseUrlMock}/s/${spaceIdMock}${ALERTS_PATH}?_a=(kuery:%27%27%2CrangeFrom:mockedRangeTo%2CrangeTo:mockedRangeFrom%2Cstatus:all)`
);
});
it('should return correct url when all the params are provided', async () => {
const location = await locator.getLocation({
baseUrl: baseUrlMock,
spaceId: spaceIdMock,
rangeFrom: 'mockedRangeTo',
rangeTo: 'mockedRangeFrom',
kuery: 'mockedKuery',
status: ALERT_STATUS_ACTIVE,
});
expect(location.path).toEqual(
`${baseUrlMock}/s/${spaceIdMock}${ALERTS_PATH}?_a=(kuery:mockedKuery%2CrangeFrom:mockedRangeTo%2CrangeTo:mockedRangeFrom%2Cstatus:active)`
);
});
});

View file

@ -0,0 +1,70 @@
/*
* 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 { stringify } from 'query-string';
import rison from '@kbn/rison';
import { url as urlUtils } from '@kbn/kibana-utils-plugin/common';
import { addSpaceIdToPath } from '@kbn/spaces-plugin/common';
import type { SerializableRecord } from '@kbn/utility-types';
import type { LocatorDefinition } from '@kbn/share-plugin/public';
import { alertsLocatorID } from '..';
import { ALERTS_URL_STORAGE_KEY } from '../constants';
import type { AlertStatus } from '../typings';
export interface AlertsLocatorParams extends SerializableRecord {
baseUrl: string;
spaceId: string;
rangeFrom?: string;
rangeTo?: string;
kuery?: string;
status?: AlertStatus;
}
export const ALERTS_PATH = '/app/observability/alerts';
function fromQuery(query: Record<string, any>) {
const encodedQuery = urlUtils.encodeQuery(query, (value) =>
encodeURIComponent(value).replace(/%3A/g, ':')
);
return stringify(encodedQuery, { sort: false, encode: false });
}
export class AlertsLocatorDefinition implements LocatorDefinition<AlertsLocatorParams> {
public readonly id = alertsLocatorID;
public readonly getLocation = async ({
baseUrl,
spaceId,
kuery,
rangeTo,
rangeFrom,
status,
}: AlertsLocatorParams) => {
const appState: {
rangeFrom?: string;
rangeTo?: string;
kuery?: string;
status?: AlertStatus;
} = {};
appState.rangeFrom = rangeFrom || 'now-15m';
appState.rangeTo = rangeTo || 'now';
appState.kuery = kuery || '';
appState.status = status || 'all';
const path = addSpaceIdToPath(baseUrl, spaceId, ALERTS_PATH);
const url = new URL(path);
url.search = fromQuery({ [ALERTS_URL_STORAGE_KEY]: rison.encodeUnknown(appState) });
return {
app: 'observability',
path: url.href,
state: {},
};
};
}

View file

@ -40,7 +40,13 @@ export {
enableInfrastructureHostsView,
enableAgentExplorerView,
} from '../common/ui_settings_keys';
export { uptimeOverviewLocatorID } from '../common';
export {
alertsLocatorID,
ruleDetailsLocatorID,
rulesLocatorID,
sloDetailsLocatorID,
uptimeOverviewLocatorID,
} from '../common';
export type { UXMetrics } from './components/core_web_vitals/core_vitals';
export { getCoreVitalsComponent } from './components/core_web_vitals/get_core_web_vitals_lazy';

View file

@ -15,6 +15,7 @@ import { loadRuleAggregations } from '@kbn/triggers-actions-ui-plugin/public';
import { AlertConsumers } from '@kbn/rule-data-utils';
import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public';
import { ALERTS_URL_STORAGE_KEY } from '../../../common/constants';
import { useHasData } from '../../hooks/use_has_data';
import { usePluginContext } from '../../hooks/use_plugin_context';
import { useTimeBuckets } from '../../hooks/use_time_buckets';
@ -35,7 +36,6 @@ import type { ObservabilityAppServices } from '../../application/types';
const ALERTS_SEARCH_BAR_ID = 'alerts-search-bar-o11y';
const ALERTS_PER_PAGE = 50;
const ALERTS_TABLE_ID = 'xpack.observability.alerts.alert.table';
const URL_STORAGE_KEY = '_a';
const DEFAULT_INTERVAL = '60s';
const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD HH:mm';
@ -58,7 +58,7 @@ function InternalAlertsPage() {
},
} = useKibana<ObservabilityAppServices>().services;
const { ObservabilityPageTemplate, observabilityRuleTypeRegistry } = usePluginContext();
const alertSearchBarStateProps = useAlertSearchBarStateContainer(URL_STORAGE_KEY, {
const alertSearchBarStateProps = useAlertSearchBarStateContainer(ALERTS_URL_STORAGE_KEY, {
replace: false,
});

View file

@ -22,6 +22,7 @@ import {
createUICapabilities as createCasesUICapabilities,
getApiTags as getCasesApiTags,
} from '@kbn/cases-plugin/common';
import { SharePluginSetup } from '@kbn/share-plugin/server';
import { SpacesPluginSetup } from '@kbn/spaces-plugin/server';
import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server';
@ -43,6 +44,7 @@ import { getObservabilityServerRouteRepository } from './routes/get_global_obser
import { casesFeatureId, observabilityFeatureId, sloFeatureId } from '../common';
import { compositeSlo, slo, SO_COMPOSITE_SLO_TYPE, SO_SLO_TYPE } from './saved_objects';
import { SLO_RULE_REGISTRATION_CONTEXT } from './common/constants';
import { AlertsLocatorDefinition } from '../common/locators/alerts';
import { registerRuleTypes } from './lib/rules/register_rule_types';
import { SLO_BURN_RATE_RULE_ID } from '../common/constants';
import { registerSloUsageCollector } from './lib/collectors/register';
@ -55,6 +57,7 @@ interface PluginSetup {
features: FeaturesSetup;
guidedOnboarding: GuidedOnboardingPluginSetup;
ruleRegistry: RuleRegistryPluginSetupContract;
share: SharePluginSetup;
spaces?: SpacesPluginSetup;
usageCollection?: UsageCollectionSetup;
}
@ -77,6 +80,8 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
const config = this.initContext.config.get<ObservabilityConfig>();
const alertsLocator = plugins.share.url.locators.create(new AlertsLocatorDefinition());
plugins.features.registerKibanaFeature({
id: casesFeatureId,
name: i18n.translate('xpack.observability.featureRegistry.linkObservabilityTitle', {
@ -267,6 +272,7 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
const api = await annotationsApiPromise;
return api?.getScopedAnnotationsClient(...args);
},
alertsLocator,
};
}

View file

@ -68,7 +68,8 @@
"@kbn/core-theme-browser",
"@kbn/core-elasticsearch-server",
"@kbn/observability-shared-plugin",
"@kbn/exploratory-view-plugin"
"@kbn/exploratory-view-plugin",
"@kbn/rison"
],
"exclude": ["target/**/*"]
}

View file

@ -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 { createTestConfig } from '../common/config';
// eslint-disable-next-line import/no-default-export
export default createTestConfig('observability', {
disabledPlugins: [],
license: 'trial',
ssl: true,
enableActionsProxy: true,
publicBaseUrl: true,
testFiles: [require.resolve('.')],
useDedicatedTaskRunner: true,
});

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
// eslint-disable-next-line import/no-default-export
export default function ({ loadTestFile }: any) {
describe('MetricsUI Endpoints', () => {
loadTestFile(require.resolve('./metric_threshold_rule'));
});
}

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import moment from 'moment';
import expect from '@kbn/expect';
import { cleanup, generate } from '@kbn/infra-forge';
import { Aggregators, Comparator, InfraRuleType } from '@kbn/infra-plugin/common/alerting/metrics';
@ -13,9 +14,10 @@ import {
waitForAlertInIndex,
waitForRuleStatus,
} from './helpers/alerting_wait_for_helpers';
import { FtrProviderContext } from '../../ftr_provider_context';
import { FtrProviderContext } from '../common/ftr_provider_context';
import { createIndexConnector, createMetricThresholdRule } from './helpers/alerting_api_helper';
// eslint-disable-next-line import/no-default-export
export default function ({ getService }: FtrProviderContext) {
const esClient = getService('es');
const esDeleteAllIndices = getService('esDeleteAllIndices');
@ -24,7 +26,9 @@ export default function ({ getService }: FtrProviderContext) {
describe('Metric threshold rule >', () => {
let ruleId: string;
let actionId: string | undefined;
let alertId: string;
let startedAt: string;
let actionId: string;
let infraDataIndex: string;
const METRICS_ALERTS_INDEX = '.alerts-observability.metrics.alerts-default';
@ -65,6 +69,7 @@ export default function ({ getService }: FtrProviderContext) {
documents: [
{
ruleType: '{{rule.type}}',
alertDetailsUrl: '{{context.alertDetailsUrl}}',
},
],
},
@ -106,21 +111,14 @@ export default function ({ getService }: FtrProviderContext) {
expect(executionStatus.status).to.be('active');
});
it('should set correct action parameter: ruleType', async () => {
const resp = await waitForDocumentInIndex<{ ruleType: string }>({
esClient,
indexName: ALERT_ACTION_INDEX,
});
expect(resp.hits.hits[0]._source?.ruleType).eql('metrics.alert.threshold');
});
it('should set correct information in the alert document', async () => {
const resp = await waitForAlertInIndex({
esClient,
indexName: METRICS_ALERTS_INDEX,
ruleId,
});
alertId = (resp.hits.hits[0]._source as any)['kibana.alert.uuid'];
startedAt = (resp.hits.hits[0]._source as any)['kibana.alert.start'];
expect(resp.hits.hits[0]._source).property(
'kibana.alert.rule.category',
'Metric threshold'
@ -169,6 +167,19 @@ export default function ({ getService }: FtrProviderContext) {
alertOnGroupDisappear: true,
});
});
it('should set correct action parameter: ruleType', async () => {
const rangeFrom = moment(startedAt).subtract('5', 'minute').toISOString();
const resp = await waitForDocumentInIndex<{ ruleType: string; alertDetailsUrl: string }>({
esClient,
indexName: ALERT_ACTION_INDEX,
});
expect(resp.hits.hits[0]._source?.ruleType).eql('metrics.alert.threshold');
expect(resp.hits.hits[0]._source?.alertDetailsUrl).eql(
`https://localhost:5601/app/observability/alerts?_a=(kuery:%27kibana.alert.uuid:%20%22${alertId}%22%27%2CrangeFrom:%27${rangeFrom}%27%2CrangeTo:now%2Cstatus:all)`
);
});
});
});
}

View file

@ -18,7 +18,6 @@ export default function ({ loadTestFile }) {
loadTestFile(require.resolve('./ip_to_hostname'));
loadTestFile(require.resolve('./http_source'));
loadTestFile(require.resolve('./metric_threshold_alert'));
loadTestFile(require.resolve('./metric_threshold_rule'));
loadTestFile(require.resolve('./metrics_overview_top'));
loadTestFile(require.resolve('./metrics_process_list'));
loadTestFile(require.resolve('./metrics_process_list_chart'));