[Custom threshold] Add viewInApp URL to the custom threshold rule type (#171985)

Closes #171613

## Summary

This PR adds the viewInApp URL to the custom threshold rule type. This
URL will send the user to the log explorer with the selected data view
and the rule's query filter. If there is only one document aggregation,
then the filter related to this aggregation will be added as shown
below:

|Rule|Discover with pre-fill data|
|---|---|

|![image](2f08b4f4-e6cc-4d25-a48a-098db63b9ce6)|

For the ad-hoc data view, you should be able to see the selected index
pattern in discover similar to this:

<img
src="046493ae-ba59-46b7-a40f-68d1836d43f1"
width=400 />

### 🧪 How to test
- Check the viewInApp URL both in action variables and the alert table
for the following scenarios:
    - A rule with a persisted data view
    - A rule with an ad-hoc data view
    - A rule with count aggregation and filter
    - A rule with an optional query filter
    - A rule with non-count aggregation

In all the above scenarios, the starting time in the Discover should be
before the alert's start time.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Maryam Saeidi 2023-12-01 16:44:10 +01:00 committed by GitHub
parent d83955dcdd
commit 59982bfa5c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 307 additions and 89 deletions

1
.github/CODEOWNERS vendored
View file

@ -555,6 +555,7 @@ x-pack/plugins/observability_ai_assistant @elastic/obs-knowledge-team
x-pack/packages/observability/alert_details @elastic/obs-ux-management-team
x-pack/packages/observability/alerting_test_data @elastic/obs-ux-management-team
x-pack/test/cases_api_integration/common/plugins/observability @elastic/response-ops
x-pack/packages/observability/get_padded_alert_time_range_util @elastic/obs-ux-management-team
x-pack/plugins/observability_log_explorer @elastic/obs-ux-logs-team
x-pack/plugins/observability_onboarding @elastic/obs-ux-logs-team
x-pack/plugins/observability @elastic/obs-ux-management-team

View file

@ -573,6 +573,7 @@
"@kbn/observability-alert-details": "link:x-pack/packages/observability/alert_details",
"@kbn/observability-alerting-test-data": "link:x-pack/packages/observability/alerting_test_data",
"@kbn/observability-fixtures-plugin": "link:x-pack/test/cases_api_integration/common/plugins/observability",
"@kbn/observability-get-padded-alert-time-range-util": "link:x-pack/packages/observability/get_padded_alert_time_range_util",
"@kbn/observability-log-explorer-plugin": "link:x-pack/plugins/observability_log_explorer",
"@kbn/observability-onboarding-plugin": "link:x-pack/plugins/observability_onboarding",
"@kbn/observability-plugin": "link:x-pack/plugins/observability",

View file

@ -1104,6 +1104,8 @@
"@kbn/observability-alerting-test-data/*": ["x-pack/packages/observability/alerting_test_data/*"],
"@kbn/observability-fixtures-plugin": ["x-pack/test/cases_api_integration/common/plugins/observability"],
"@kbn/observability-fixtures-plugin/*": ["x-pack/test/cases_api_integration/common/plugins/observability/*"],
"@kbn/observability-get-padded-alert-time-range-util": ["x-pack/packages/observability/get_padded_alert_time_range_util"],
"@kbn/observability-get-padded-alert-time-range-util/*": ["x-pack/packages/observability/get_padded_alert_time_range_util/*"],
"@kbn/observability-log-explorer-plugin": ["x-pack/plugins/observability_log_explorer"],
"@kbn/observability-log-explorer-plugin/*": ["x-pack/plugins/observability_log_explorer/*"],
"@kbn/observability-onboarding-plugin": ["x-pack/plugins/observability_onboarding"],

View file

@ -9,5 +9,4 @@ export { AlertAnnotation } from './src/components/alert_annotation';
export { AlertActiveTimeRangeAnnotation } from './src/components/alert_active_time_range_annotation';
export { AlertThresholdTimeRangeRect } from './src/components/alert_threshold_time_range_rect';
export { AlertThresholdAnnotation } from './src/components/alert_threshold_annotation';
export { getPaddedAlertTimeRange } from './src/helpers/get_padded_alert_time_range';
export { useAlertsHistory } from './src/hooks/use_alerts_history';

View file

@ -1,7 +1,6 @@
{
"name": "@kbn/observability-alert-details",
"descriptio": "Helper and components related to alert details",
"author": "Actionable Observability",
"private": true,
"version": "1.0.0",
"license": "Elastic License 2.0"

View file

@ -0,0 +1,3 @@
# @kbn/get-padded-alert-time-range-util
A utility to get padded alert time range based on alert's start and end time

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export { getPaddedAlertTimeRange } from './src/get_padded_alert_time_range';

View file

@ -0,0 +1,12 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../..',
roots: ['<rootDir>/x-pack/packages/observability/get_padded_alert_time_range_util'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/observability-get-padded-alert-time-range-util",
"owner": "@elastic/obs-ux-management-team"
}

View file

@ -0,0 +1,7 @@
{
"name": "@kbn/observability-get-padded-alert-time-range-util",
"descriptio": "An util to get padded alert time range",
"private": true,
"version": "1.0.0",
"license": "Elastic License 2.0"
}

View file

@ -0,0 +1,17 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node"
]
},
"include": [
"**/*.ts",
],
"exclude": [
"target/**/*"
],
"kbn_references": []
}

View file

@ -19,7 +19,7 @@ import {
import moment from 'moment';
import React, { useEffect, useMemo } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { getPaddedAlertTimeRange } from '@kbn/observability-alert-details';
import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util';
import { EuiCallOut } from '@elastic/eui';
import { SERVICE_ENVIRONMENT } from '../../../../../common/es_fields/apm';
import { ChartPointerEventContextProvider } from '../../../../context/chart_pointer_event/chart_pointer_event_context';

View file

@ -105,7 +105,8 @@
"@kbn/deeplinks-observability",
"@kbn/custom-icons",
"@kbn/elastic-agent-utils",
"@kbn/shared-ux-link-redirect-app"
"@kbn/shared-ux-link-redirect-app",
"@kbn/observability-get-padded-alert-time-range-util"
],
"exclude": ["target/**/*"]
}

View file

@ -18,7 +18,7 @@ import moment from 'moment';
import { useTheme } from '@emotion/react';
import { EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { getPaddedAlertTimeRange } from '@kbn/observability-alert-details';
import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util';
import { get, identity } from 'lodash';
import { ObservabilityAIAssistantProvider } from '@kbn/observability-ai-assistant-plugin/public';
import { useLogView } from '@kbn/logs-shared-plugin/public';

View file

@ -24,6 +24,9 @@ const mockedChartStartContract = chartPluginMock.createStartContract();
jest.mock('@kbn/observability-alert-details', () => ({
AlertAnnotation: () => {},
AlertActiveTimeRangeAnnotation: () => {},
}));
jest.mock('@kbn/observability-get-padded-alert-time-range-util', () => ({
getPaddedAlertTimeRange: () => ({
from: '2023-03-28T10:43:13.802Z',
to: '2023-03-29T13:14:09.581Z',

View file

@ -22,11 +22,8 @@ import {
import { AlertSummaryField, TopAlert } from '@kbn/observability-plugin/public';
import { ALERT_END, ALERT_START, ALERT_EVALUATION_VALUES } from '@kbn/rule-data-utils';
import { Rule } from '@kbn/alerting-plugin/common';
import {
AlertAnnotation,
getPaddedAlertTimeRange,
AlertActiveTimeRangeAnnotation,
} from '@kbn/observability-alert-details';
import { AlertAnnotation, AlertActiveTimeRangeAnnotation } from '@kbn/observability-alert-details';
import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util';
import { metricValueFormatter } from '../../../../common/alerting/metrics/metric_value_formatter';
import { TIME_LABELS } from '../../common/criterion_preview_chart/criterion_preview_chart';
import { Threshold } from '../../common/components/threshold';

View file

@ -78,7 +78,8 @@
"@kbn/custom-icons",
"@kbn/profiling-utils",
"@kbn/profiling-data-access-plugin",
"@kbn/core-http-request-handler-context-server"
"@kbn/core-http-request-handler-context-server",
"@kbn/observability-get-padded-alert-time-range-util"
],
"exclude": ["target/**/*"]
}

View file

@ -10,16 +10,14 @@ import { LogExplorerLocatorDefinition } from './log_explorer_locator';
import { LogExplorerLocatorDependencies } from './types';
const setup = async () => {
const discoverSetupContract: LogExplorerLocatorDependencies = {
discover: {
locator: sharePluginMock.createLocator(),
},
const logExplorerLocatorDependencies: LogExplorerLocatorDependencies = {
discoverAppLocator: sharePluginMock.createLocator(),
};
const logExplorerLocator = new LogExplorerLocatorDefinition(discoverSetupContract);
const logExplorerLocator = new LogExplorerLocatorDefinition(logExplorerLocatorDependencies);
return {
logExplorerLocator,
discoverGetLocation: discoverSetupContract.discover.locator?.getLocation,
discoverGetLocation: logExplorerLocatorDependencies.discoverAppLocator?.getLocation,
};
};

View file

@ -29,7 +29,7 @@ export class LogExplorerLocatorDefinition implements LocatorDefinition<LogExplor
}
: undefined;
return this.deps.discover.locator?.getLocation({
return this.deps.discoverAppLocator?.getLocation({
...params,
dataViewId: dataset,
dataViewSpec,

View file

@ -5,8 +5,9 @@
* 2.0.
*/
import type { DiscoverSetup } from '@kbn/discover-plugin/public';
import { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
import { LocatorPublic } from '@kbn/share-plugin/common';
export interface LogExplorerLocatorDependencies {
discover: DiscoverSetup;
discoverAppLocator?: LocatorPublic<DiscoverAppLocatorParams>;
}

View file

@ -6,6 +6,7 @@
*/
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public';
import { DISCOVER_APP_LOCATOR, DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
import { LogExplorerLocatorDefinition, LogExplorerLocators } from '../common/locators';
import { createLogExplorer } from './components/log_explorer';
import {
@ -21,12 +22,14 @@ export class LogExplorerPlugin implements Plugin<LogExplorerPluginSetup, LogExpl
constructor(context: PluginInitializerContext) {}
public setup(core: CoreSetup, plugins: LogExplorerSetupDeps) {
const { share, discover } = plugins;
const { share } = plugins;
const discoverAppLocator =
share.url.locators.get<DiscoverAppLocatorParams>(DISCOVER_APP_LOCATOR);
// Register Locators
const logExplorerLocator = share.url.locators.create(
new LogExplorerLocatorDefinition({
discover,
discoverAppLocator,
})
);

View file

@ -5,10 +5,34 @@
* 2.0.
*/
import { Plugin } from '@kbn/core/server';
import { Plugin, CoreSetup } from '@kbn/core/server';
import { DISCOVER_APP_LOCATOR, DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
import { LogExplorerLocatorDefinition, LogExplorerLocators } from '../common/locators';
import type { LogExplorerSetupDeps } from './types';
export class LogExplorerServerPlugin implements Plugin {
setup() {}
private locators?: LogExplorerLocators;
setup(core: CoreSetup, plugins: LogExplorerSetupDeps) {
const { share } = plugins;
const discoverAppLocator =
share.url.locators.get<DiscoverAppLocatorParams>(DISCOVER_APP_LOCATOR);
// Register Locators
const logExplorerLocator = share.url.locators.create(
new LogExplorerLocatorDefinition({
discoverAppLocator,
})
);
this.locators = {
logExplorerLocator,
};
return {
locators: this.locators,
};
}
start() {}
}

View file

@ -0,0 +1,12 @@
/*
* 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 type { SharePluginSetup } from '@kbn/share-plugin/server';
export interface LogExplorerSetupDeps {
share: SharePluginSetup;
}

View file

@ -0,0 +1,49 @@
/*
* 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 { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util';
import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
import type { TimeRange } from '@kbn/es-query';
import type { LocatorPublic } from '@kbn/share-plugin/common';
import type { CustomThresholdExpressionMetric } from './types';
export const getViewInAppUrl = (
metrics: CustomThresholdExpressionMetric[],
startedAt?: string,
logExplorerLocator?: LocatorPublic<DiscoverAppLocatorParams>,
filter?: string,
dataViewId?: string,
endedAt?: string
) => {
if (!logExplorerLocator) return '';
let timeRange: TimeRange | undefined;
if (startedAt) {
timeRange = getPaddedAlertTimeRange(startedAt, endedAt);
timeRange.to = endedAt ? timeRange.to : 'now';
}
const query = {
query: '',
language: 'kuery',
};
const isOneCountConditionWithFilter =
metrics.length === 1 && metrics[0].aggType === 'count' && metrics[0].filter;
if (filter && isOneCountConditionWithFilter) {
query.query = `${filter} and ${metrics[0].filter}`;
} else if (isOneCountConditionWithFilter) {
query.query = metrics[0].filter!;
} else if (filter) {
query.query = filter;
}
return logExplorerLocator?.getRedirectUrl({
dataset: dataViewId,
timeRange,
query,
});
};

View file

@ -35,6 +35,7 @@
"visualizations",
"dashboard",
"expressions",
"logExplorer",
"licensing"
],
"optionalPlugins": [

View file

@ -4,7 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import React, { useState, useEffect } from 'react';
import { EuiFlyoutFooter, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useKibana } from '../../utils/kibana_react';
@ -25,17 +26,22 @@ export function AlertsFlyoutFooter({ alert, isInApp }: FlyoutProps & { isInApp:
},
} = useKibana().services;
const { config } = usePluginContext();
const [viewInAppUrl, setViewInAppUrl] = useState<string>();
useEffect(() => {
if (!alert.hasBasePath) {
setViewInAppUrl(prepend(alert.link ?? ''));
} else {
setViewInAppUrl(alert.link);
}
}, [alert.hasBasePath, alert.link, prepend]);
return (
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="flexEnd">
{!alert.link || isInApp ? null : (
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="alertsFlyoutViewInAppButton"
fill
href={prepend && prepend(alert.link)}
>
<EuiButton data-test-subj="alertsFlyoutViewInAppButton" fill href={viewInAppUrl}>
{i18n.translate('xpack.observability.alertsFlyout.viewInAppButtonText', {
defaultMessage: 'View in app',
})}

View file

@ -24,6 +24,9 @@ const mockedChartStartContract = chartPluginMock.createStartContract();
jest.mock('@kbn/observability-alert-details', () => ({
AlertAnnotation: () => {},
AlertActiveTimeRangeAnnotation: () => {},
}));
jest.mock('@kbn/observability-get-padded-alert-time-range-util', () => ({
getPaddedAlertTimeRange: () => ({
from: '2023-03-28T10:43:13.802Z',
to: '2023-03-29T13:14:09.581Z',

View file

@ -22,11 +22,8 @@ import {
} from '@elastic/eui';
import { ALERT_END, ALERT_START, ALERT_EVALUATION_VALUES } from '@kbn/rule-data-utils';
import { Rule, RuleTypeParams } from '@kbn/alerting-plugin/common';
import {
AlertAnnotation,
getPaddedAlertTimeRange,
AlertActiveTimeRangeAnnotation,
} from '@kbn/observability-alert-details';
import { AlertAnnotation, AlertActiveTimeRangeAnnotation } from '@kbn/observability-alert-details';
import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util';
import { DataView } from '@kbn/data-views-plugin/common';
import { MetricsExplorerChartType } from '../../../../common/custom_threshold_rule/types';
import { useKibana } from '../../../utils/kibana_react';

View file

@ -14,19 +14,17 @@ import {
EuiToolTip,
} from '@elastic/eui';
import React, { useMemo, useState, useCallback } from 'react';
import React, { useMemo, useState, useCallback, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public';
import { AttachmentType } from '@kbn/cases-plugin/common';
import { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
import { TimelineNonEcsData } from '@kbn/timelines-plugin/common';
import {
ALERT_RULE_TYPE_ID,
ALERT_RULE_UUID,
ALERT_STATUS,
ALERT_STATUS_ACTIVE,
ALERT_UUID,
OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
} from '@kbn/rule-data-utils';
import { useBulkUntrackAlerts } from '@kbn/triggers-actions-ui-plugin/public';
import { useKibana } from '../../../utils/kibana_react';
@ -70,6 +68,7 @@ export function AlertActions({
} = useKibana().services;
const { mutateAsync: untrackAlerts } = useBulkUntrackAlerts();
const userCasesPermissions = canUseCases([observabilityFeatureId]);
const [viewInAppUrl, setViewInAppUrl] = useState<string>();
const parseObservabilityAlert = useMemo(
() => parseAlert(observabilityRuleTypeRegistry),
@ -79,6 +78,14 @@ export function AlertActions({
const dataFieldEs = data.reduce((acc, d) => ({ ...acc, [d.field]: d.value }), {});
const alert = parseObservabilityAlert(dataFieldEs);
useEffect(() => {
if (!alert.hasBasePath) {
setViewInAppUrl(prepend(alert.link ?? ''));
} else {
setViewInAppUrl(alert.link);
}
}, [alert.hasBasePath, alert.link, prepend]);
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
const ruleId = alert.fields[ALERT_RULE_UUID] ?? null;
@ -236,10 +243,7 @@ export function AlertActions({
return (
<>
{/* Hide the View In App for the Threshold alerts, temporarily https://github.com/elastic/kibana/pull/159915 */}
{alert.fields[ALERT_RULE_TYPE_ID] === OBSERVABILITY_THRESHOLD_RULE_TYPE_ID ? (
<EuiFlexItem style={{ width: 32 }} />
) : (
{viewInAppUrl ? (
<EuiFlexItem>
<EuiToolTip
content={i18n.translate('xpack.observability.alertsTable.viewInAppTextLabel', {
@ -252,12 +256,14 @@ export function AlertActions({
defaultMessage: 'View in app',
})}
color="text"
href={prepend(alert.link ?? '')}
href={viewInAppUrl}
iconType="eye"
size="s"
/>
</EuiToolTip>
</EuiFlexItem>
) : (
<EuiFlexItem style={{ width: 32 }} />
)}
<EuiFlexItem>

View file

@ -23,6 +23,7 @@ import {
import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public';
import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { LOG_EXPLORER_LOCATOR_ID, LogExplorerLocatorParams } from '@kbn/deeplinks-observability';
import type { DiscoverStart } from '@kbn/discover-plugin/public';
import type { EmbeddableStart } from '@kbn/embeddable-plugin/public';
import type { HomePublicPluginSetup, HomePublicPluginStart } from '@kbn/home-plugin/public';
@ -82,6 +83,7 @@ import {
ObservabilityRuleTypeRegistry,
} from './rules/create_observability_rule_type_registry';
import { registerObservabilityRuleTypes } from './rules/register_observability_rule_types';
export interface ConfigSchema {
unsafe: {
alertDetails: {
@ -103,9 +105,7 @@ export interface ConfigSchema {
};
};
}
export type ObservabilityPublicSetup = ReturnType<Plugin['setup']>;
export interface ObservabilityPublicPluginsSetup {
data: DataPublicPluginSetup;
observabilityShared: ObservabilitySharedPluginSetup;
@ -117,7 +117,6 @@ export interface ObservabilityPublicPluginsSetup {
embeddable: EmbeddableSetup;
licensing: LicensingPluginSetup;
}
export interface ObservabilityPublicPluginsStart {
actionTypeRegistry: ActionTypeRegistryContract;
cases: CasesUiStart;
@ -146,7 +145,6 @@ export interface ObservabilityPublicPluginsStart {
aiops: AiopsPluginStart;
serverless?: ServerlessPluginStart;
}
export type ObservabilityPublicStart = ReturnType<Plugin['start']>;
export class Plugin
@ -239,6 +237,9 @@ export class Plugin
const sloEditLocator = pluginsSetup.share.url.locators.create(new SloEditLocatorDefinition());
const sloListLocator = pluginsSetup.share.url.locators.create(new SloListLocatorDefinition());
const logExplorerLocator =
pluginsSetup.share.url.locators.get<LogExplorerLocatorParams>(LOG_EXPLORER_LOCATOR_ID);
const mount = async (params: AppMountParameters<unknown>) => {
// Load application bundle
const { renderApp } = await import('./application');
@ -292,7 +293,7 @@ export class Plugin
coreSetup.application.register(app);
registerObservabilityRuleTypes(config, this.observabilityRuleTypeRegistry);
registerObservabilityRuleTypes(config, this.observabilityRuleTypeRegistry, logExplorerLocator);
const assertPlatinumLicense = async () => {
const licensing = await pluginsSetup.licensing;

View file

@ -16,7 +16,7 @@ import { AsDuration, AsPercent } from '../../common/utils/formatters';
export type ObservabilityRuleTypeFormatter = (options: {
fields: ParsedTechnicalFields & Record<string, any>;
formatters: { asDuration: AsDuration; asPercent: AsPercent };
}) => { reason: string; link?: string };
}) => { reason: string; link?: string; hasBasePath?: boolean };
export interface ObservabilityRuleTypeModel<Params extends RuleTypeParams = RuleTypeParams>
extends RuleTypeModel<Params> {

View file

@ -7,15 +7,24 @@
import { lazy } from 'react';
import { i18n } from '@kbn/i18n';
import { ALERT_REASON, OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils';
import type { SerializedSearchSourceFields } from '@kbn/data-plugin/common';
import {
ALERT_REASON,
ALERT_RULE_PARAMETERS,
ALERT_START,
OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
} from '@kbn/rule-data-utils';
import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
import type { LocatorPublic } from '@kbn/share-plugin/common';
import type { MetricExpression } from '../components/custom_threshold/types';
import type { CustomThresholdExpressionMetric } from '../../common/custom_threshold_rule/types';
import { getViewInAppUrl } from '../../common/custom_threshold_rule/get_view_in_app_url';
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 { SLO_BURN_RATE_RULE_TYPE_ID } from '../../common/constants';
import { validateBurnRateRule } from '../components/burn_rate_rule_editor/validation';
import { validateCustomThreshold } from '../components/custom_threshold/components/validation';
import { formatReason } from '../components/custom_threshold/rule_data_formatters';
const sloBurnRateDefaultActionMessage = i18n.translate(
'xpack.observability.slo.rules.burnRate.defaultActionMessage',
@ -71,9 +80,15 @@ const thresholdDefaultRecoveryMessage = i18n.translate(
}
);
export const registerObservabilityRuleTypes = (
const getDataViewId = (searchConfiguration?: SerializedSearchSourceFields) =>
typeof searchConfiguration?.index === 'string'
? searchConfiguration.index
: searchConfiguration?.index?.title;
export const registerObservabilityRuleTypes = async (
config: ConfigSchema,
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry,
logExplorerLocator?: LocatorPublic<DiscoverAppLocatorParams>
) => {
observabilityRuleTypeRegistry.register({
id: SLO_BURN_RATE_RULE_TYPE_ID,
@ -121,7 +136,27 @@ export const registerObservabilityRuleTypes = (
defaultActionMessage: thresholdDefaultActionMessage,
defaultRecoveryMessage: thresholdDefaultRecoveryMessage,
requiresAppContext: false,
format: formatReason,
format: ({ fields }) => {
const searchConfiguration = fields[ALERT_RULE_PARAMETERS]?.searchConfiguration as
| SerializedSearchSourceFields
| undefined;
const criteria = fields[ALERT_RULE_PARAMETERS]?.criteria as MetricExpression[];
const metrics: CustomThresholdExpressionMetric[] =
criteria.length === 1 ? criteria[0].metrics : [];
const dataViewId = getDataViewId(searchConfiguration);
return {
reason: fields[ALERT_REASON] ?? '-',
link: getViewInAppUrl(
metrics,
fields[ALERT_START],
logExplorerLocator,
(searchConfiguration?.query as { query: string }).query,
dataViewId
),
hasBasePath: true,
};
},
alertDetailsAppSection: lazy(
() => import('../components/custom_threshold/components/alert_details_app_section')
),

View file

@ -15,4 +15,5 @@ export interface TopAlert<TAdditionalMetaFields extends Record<string, any> = {}
reason: string;
link?: string;
active: boolean;
hasBasePath?: boolean;
}

View file

@ -29,6 +29,9 @@ import {
} from '../../../../common/custom_threshold_rule/types';
jest.mock('./lib/evaluate_rule', () => ({ evaluateRule: jest.fn() }));
jest.mock('../../../../common/custom_threshold_rule/get_view_in_app_url', () => ({
getViewInAppUrl: () => 'mockedViewInApp',
}));
interface AlertTestInstance {
instance: AlertInstanceMock;
@ -134,7 +137,7 @@ const setEvaluationResults = (response: Array<Record<string, Evaluation>>) => {
jest.requireMock('./lib/evaluate_rule').evaluateRule.mockImplementation(() => response);
};
describe('The metric threshold alert type', () => {
describe('The custom threshold alert type', () => {
describe('querying the entire infrastructure', () => {
afterAll(() => clearInstances());
const instanceID = '*';
@ -1339,6 +1342,7 @@ describe('The metric threshold alert type', () => {
timestamp: STARTED_AT_MOCK_DATE.toISOString(),
value: ['[NO DATA]', null],
tags: [],
viewInAppUrl: 'mockedViewInApp',
});
expect(recentAction).toBeNoDataAction();
});
@ -1765,6 +1769,7 @@ const mockLibs: any = {
groupByPageSize: 10_000,
},
},
locators: {},
};
const executor = createCustomThresholdExecutor(mockLibs);
@ -1780,6 +1785,7 @@ const mockedIndex = {
};
const mockedDataView = {
getIndexPattern: () => 'mockedIndexPattern',
getName: () => 'mockedDataViewName',
...mockedIndex,
};
const mockedSearchSource = {

View file

@ -6,6 +6,7 @@
*/
import { isEqual } from 'lodash';
import { LogExplorerLocatorParams } from '@kbn/deeplinks-observability';
import {
ALERT_ACTION_GROUP,
ALERT_EVALUATION_VALUES,
@ -17,6 +18,7 @@ import { RecoveredActionGroup } from '@kbn/alerting-plugin/common';
import { IBasePath, Logger } from '@kbn/core/server';
import { LifecycleRuleExecutor } from '@kbn/rule-registry-plugin/server';
import { AlertsLocatorParams, getAlertUrl } from '../../../../common';
import { getViewInAppUrl } from '../../../../common/custom_threshold_rule/get_view_in_app_url';
import { ObservabilityConfig } from '../../..';
import { FIRED_ACTIONS_ID, NO_DATA_ACTIONS_ID, UNGROUPED_FACTORY_KEY } from './constants';
import {
@ -48,16 +50,21 @@ import { EvaluatedRuleParams, evaluateRule } from './lib/evaluate_rule';
import { MissingGroupsRecord } from './lib/check_missing_group';
import { convertStringsToMissingGroupsRecord } from './lib/convert_strings_to_missing_groups_record';
export interface CustomThresholdLocators {
alertsLocator?: LocatorPublic<AlertsLocatorParams>;
logExplorerLocator?: LocatorPublic<LogExplorerLocatorParams>;
}
export const createCustomThresholdExecutor = ({
alertsLocator,
basePath,
logger,
config,
locators: { alertsLocator, logExplorerLocator },
}: {
basePath: IBasePath;
logger: Logger;
config: ObservabilityConfig;
alertsLocator?: LocatorPublic<AlertsLocatorParams>;
locators: CustomThresholdLocators;
}): LifecycleRuleExecutor<
CustomThresholdRuleParams,
CustomThresholdRuleTypeState,
@ -132,21 +139,22 @@ export const createCustomThresholdExecutor = ({
: [];
const initialSearchSource = await searchSourceClient.create(params.searchConfiguration!);
const dataView = initialSearchSource.getField('index')!.getIndexPattern();
const dataViewName = initialSearchSource.getField('index')!.name;
const timeFieldName = initialSearchSource.getField('index')?.timeFieldName;
if (!dataView) {
const dataView = initialSearchSource.getField('index')!;
const { id: dataViewId, timeFieldName } = dataView;
const dataViewIndexPattern = dataView.getIndexPattern();
const dataViewName = dataView.getName();
if (!dataViewIndexPattern) {
throw new Error('No matched data view');
} else if (!timeFieldName) {
throw new Error('The selected data view does not have a timestamp field');
}
// Calculate initial start and end date with no time window, as each criteria has it's own time window
// Calculate initial start and end date with no time window, as each criterion has its own time window
const { dateStart, dateEnd } = getTimeRange();
const alertResults = await evaluateRule(
services.scopedClusterClient.asCurrentUser,
params as EvaluatedRuleParams,
dataView,
dataViewIndexPattern,
timeFieldName,
compositeSize,
alertOnGroupDisappear,
@ -270,13 +278,20 @@ export const createCustomThresholdExecutor = ({
group: groupByKeysObjectMapping[group],
reason,
timestamp,
value: alertResults.map((result, index) => {
value: alertResults.map((result) => {
const evaluation = result[group];
if (!evaluation) {
return null;
}
return formatAlertResult(evaluation).currentValue;
}),
viewInAppUrl: getViewInAppUrl(
alertResults.length === 1 ? alertResults[0][group].metrics : [],
indexedStartedAt,
logExplorerLocator,
params.searchConfiguration.query.query,
params.searchConfiguration?.index?.title ?? dataViewId
),
...additionalContext,
});
}

View file

@ -16,13 +16,8 @@ import { legacyExperimentalFieldMap } from '@kbn/alerts-as-data-utils';
import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils';
import { createLifecycleExecutor, IRuleDataClient } from '@kbn/rule-registry-plugin/server';
import { LicenseType } from '@kbn/licensing-plugin/server';
import { LocatorPublic } from '@kbn/share-plugin/common';
import { EsQueryRuleParamsExtractedParams } from '@kbn/stack-alerts-plugin/server/rule_types/es_query/rule_type_params';
import {
AlertsLocatorParams,
observabilityFeatureId,
observabilityPaths,
} from '../../../../common';
import { observabilityFeatureId, observabilityPaths } from '../../../../common';
import { Comparator } from '../../../../common/custom_threshold_rule/types';
import { THRESHOLD_RULE_REGISTRATION_CONTEXT } from '../../../common/constants';
@ -38,9 +33,13 @@ import {
tagsActionVariableDescription,
timestampActionVariableDescription,
valueActionVariableDescription,
viewInAppUrlActionVariableDescription,
} from './translations';
import { oneOfLiterals, validateKQLStringFilter } from './utils';
import { createCustomThresholdExecutor } from './custom_threshold_executor';
import {
createCustomThresholdExecutor,
CustomThresholdLocators,
} from './custom_threshold_executor';
import { FIRED_ACTION, NO_DATA_ACTION } from './constants';
import { ObservabilityConfig } from '../../..';
@ -69,7 +68,7 @@ export function thresholdRuleType(
config: ObservabilityConfig,
logger: Logger,
ruleDataClient: IRuleDataClient,
alertsLocator?: LocatorPublic<AlertsLocatorParams>
locators: CustomThresholdLocators
) {
const baseCriterion = {
threshold: schema.arrayOf(schema.number()),
@ -128,7 +127,7 @@ export function thresholdRuleType(
minimumLicenseRequired: 'basic' as LicenseType,
isExportable: true,
executor: createLifecycleRuleExecutor(
createCustomThresholdExecutor({ alertsLocator, basePath, logger, config })
createCustomThresholdExecutor({ basePath, logger, config, locators })
),
doesSetRecoveryContext: true,
actionVariables: {
@ -148,6 +147,7 @@ export function thresholdRuleType(
{ name: 'orchestrator', description: orchestratorActionVariableDescription },
{ name: 'labels', description: labelsActionVariableDescription },
{ name: 'tags', description: tagsActionVariableDescription },
{ name: 'viewInAppUrl', description: viewInAppUrlActionVariableDescription },
],
},
useSavedObjectReferences: {

View file

@ -7,7 +7,6 @@
import { PluginSetupContract } from '@kbn/alerting-plugin/server';
import { IBasePath, Logger } from '@kbn/core/server';
import { LocatorPublic } from '@kbn/share-plugin/common';
import {
createLifecycleExecutor,
Dataset,
@ -15,7 +14,8 @@ import {
} from '@kbn/rule-registry-plugin/server';
import { mappingFromFieldMap } from '@kbn/alerting-plugin/common';
import { legacyExperimentalFieldMap } from '@kbn/alerts-as-data-utils';
import { sloFeatureId, AlertsLocatorParams, observabilityFeatureId } from '../../../common';
import { CustomThresholdLocators } from './custom_threshold/custom_threshold_executor';
import { sloFeatureId, observabilityFeatureId } from '../../../common';
import { ObservabilityConfig } from '../..';
import {
SLO_RULE_REGISTRATION_CONTEXT,
@ -27,11 +27,11 @@ import { sloRuleFieldMap } from './slo_burn_rate/field_map';
export function registerRuleTypes(
alertingPlugin: PluginSetupContract,
logger: Logger,
ruleDataService: IRuleDataService,
basePath: IBasePath,
config: ObservabilityConfig,
alertsLocator?: LocatorPublic<AlertsLocatorParams>
logger: Logger,
ruleDataService: IRuleDataService,
locators: CustomThresholdLocators
) {
// SLO RULE
const ruleDataClientSLO = ruleDataService.initializeIndex({
@ -55,7 +55,7 @@ export function registerRuleTypes(
ruleDataClientSLO
);
alertingPlugin.registerType(
sloBurnRateRuleType(createLifecycleRuleExecutorSLO, basePath, alertsLocator)
sloBurnRateRuleType(createLifecycleRuleExecutorSLO, basePath, locators.alertsLocator)
);
// Threshold RULE
@ -85,7 +85,7 @@ export function registerRuleTypes(
config,
logger,
ruleDataClientThreshold,
alertsLocator
locators
)
);
}

View file

@ -18,6 +18,7 @@ import {
Plugin,
PluginInitializerContext,
} from '@kbn/core/server';
import { LOG_EXPLORER_LOCATOR_ID, LogExplorerLocatorParams } from '@kbn/deeplinks-observability';
import { PluginSetupContract as FeaturesSetup } from '@kbn/features-plugin/server';
import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects';
import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server';
@ -101,6 +102,8 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
const config = this.initContext.config.get<ObservabilityConfig>();
const alertsLocator = plugins.share.url.locators.create(new AlertsLocatorDefinition());
const logExplorerLocator =
plugins.share.url.locators.get<LogExplorerLocatorParams>(LOG_EXPLORER_LOCATOR_ID);
plugins.features.registerKibanaFeature({
id: casesFeatureId,
@ -332,14 +335,10 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
core.savedObjects.registerType(slo);
core.savedObjects.registerType(threshold);
registerRuleTypes(
plugins.alerting,
this.logger,
ruleDataService,
core.http.basePath,
config,
alertsLocator
);
registerRuleTypes(plugins.alerting, core.http.basePath, config, this.logger, ruleDataService, {
alertsLocator,
logExplorerLocator,
});
registerSloUsageCollector(plugins.usageCollection);
core.getStartServices().then(([coreStart, pluginStart]) => {

View file

@ -71,6 +71,7 @@
"@kbn/rison",
"@kbn/io-ts-utils",
"@kbn/observability-alert-details",
"@kbn/observability-get-padded-alert-time-range-util",
"@kbn/ui-actions-plugin",
"@kbn/field-types",
"@kbn/safer-lodash-set",

View file

@ -5143,6 +5143,10 @@
version "0.0.0"
uid ""
"@kbn/observability-get-padded-alert-time-range-util@link:x-pack/packages/observability/get_padded_alert_time_range_util":
version "0.0.0"
uid ""
"@kbn/observability-log-explorer-plugin@link:x-pack/plugins/observability_log_explorer":
version "0.0.0"
uid ""