Skip to content

Commit

Permalink
Add context.alertDetailsUrl to connector template when configuring a …
Browse files Browse the repository at this point in the history
…Rule (#142854)
  • Loading branch information
CoenWarmer authored Oct 27, 2022
1 parent 460cf89 commit 7bc63e0
Show file tree
Hide file tree
Showing 15 changed files with 250 additions and 123 deletions.
18 changes: 9 additions & 9 deletions x-pack/plugins/alerting/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,21 +90,21 @@ export interface RuleExecutorOptions<
InstanceContext extends AlertInstanceContext = never,
ActionGroupIds extends string = never
> {
alertId: string;
alertId: string; // Is actually the Rule ID. Will be updated as part of https://github.com/elastic/kibana/issues/100115
createdBy: string | null;
executionId: string;
startedAt: Date;
previousStartedAt: Date | null;
services: RuleExecutorServices<InstanceState, InstanceContext, ActionGroupIds>;
logger: Logger;
name: string;
params: Params;
state: State;
previousStartedAt: Date | null;
rule: SanitizedRuleConfig;
services: RuleExecutorServices<InstanceState, InstanceContext, ActionGroupIds>;
spaceId: string;
namespace?: string;
name: string;
startedAt: Date;
state: State;
tags: string[];
createdBy: string | null;
updatedBy: string | null;
logger: Logger;
namespace?: string;
}

export interface RuleParamsAndRefs<Params extends RuleTypeParams> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,18 @@ import { SpacesPluginSetup } from '@kbn/spaces-plugin/server';
import { PluginSetupContract as AlertingPluginContract } from '@kbn/alerting-plugin/server';
import { MlPluginSetup } from '@kbn/ml-plugin/server';
import { RuleRegistryPluginSetupContract } from '@kbn/rule-registry-plugin/server';
import { ObservabilityPluginSetup } from '@kbn/observability-plugin/server';

export interface InfraServerPluginSetupDeps {
alerting: AlertingPluginContract;
data: DataPluginSetup;
home: HomeServerPluginSetup;
features: FeaturesPluginSetup;
ruleRegistry: RuleRegistryPluginSetupContract;
observability: ObservabilityPluginSetup;
spaces: SpacesPluginSetup;
usageCollection: UsageCollectionSetup;
visTypeTimeseries: VisTypeTimeseriesSetup;
features: FeaturesPluginSetup;
alerting: AlertingPluginContract;
ruleRegistry: RuleRegistryPluginSetupContract;
ml?: MlPluginSetup;
}

Expand Down
10 changes: 9 additions & 1 deletion x-pack/plugins/infra/server/lib/alerting/common/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,14 @@ export const alertStateActionVariableDescription = i18n.translate(
}
);

export const alertDetailUrlActionVariableDescription = i18n.translate(
'xpack.infra.metrics.alerting.alertDetailUrlActionVariableDescription',
{
defaultMessage:
'Link to the view within Elastic that shows further details and context surrounding this alert',
}
);

export const reasonActionVariableDescription = i18n.translate(
'xpack.infra.metrics.alerting.reasonActionVariableDescription',
{
Expand Down Expand Up @@ -211,7 +219,7 @@ export const viewInAppUrlActionVariableDescription = i18n.translate(
'xpack.infra.metrics.alerting.viewInAppUrlActionVariableDescription',
{
defaultMessage:
'Link to the view or feature within Elastic that can be used to investigate the alert and its context further',
'Link to the view or feature within Elastic that can assist with further investigation',
}
);

Expand Down
51 changes: 39 additions & 12 deletions x-pack/plugins/infra/server/lib/alerting/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import { isEmpty, isError } from 'lodash';
import { schema } from '@kbn/config-schema';
import { Logger, LogMeta } from '@kbn/logging';
import type { 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 { parseTechnicalFields } from '@kbn/rule-registry-plugin/common/parse_technical_fields';
import { LINK_TO_METRICS_EXPLORER } from '../../../../common/alerting/metrics';
import { getInventoryViewInAppUrl } from '../../../../common/alerting/metrics/alert_link';
import {
AlertExecutionDetails,
Expand Down Expand Up @@ -83,18 +86,30 @@ export const createScopedLogger = (
};
};

export const getViewInAppUrl = (basePath: IBasePath, relativeViewInAppUrl: string) =>
basePath.publicBaseUrl
? new URL(basePath.prepend(relativeViewInAppUrl), basePath.publicBaseUrl).toString()
: relativeViewInAppUrl;
export const getAlertDetailsPageEnabledForApp = (
config: ObservabilityConfig['unsafe']['alertDetails'] | null,
appName: keyof ObservabilityConfig['unsafe']['alertDetails']
): boolean => {
if (!config) return false;

export const getViewInAppUrlInventory = (
criteria: InventoryMetricConditions[],
nodeType: string,
timestamp: string,
basePath: IBasePath
) => {
return config[appName].enabled;
};

export const getViewInInventoryAppUrl = ({
basePath,
criteria,
nodeType,
spaceId,
timestamp,
}: {
basePath: IBasePath;
criteria: InventoryMetricConditions[];
nodeType: string;
spaceId: string;
timestamp: string;
}) => {
const { metric, customMetric } = criteria[0];

const fields = {
[`${ALERT_RULE_PARAMETERS}.criteria.metric`]: [metric],
[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.id`]: [customMetric?.id],
Expand All @@ -104,6 +119,18 @@ export const getViewInAppUrlInventory = (
[TIMESTAMP]: timestamp,
};

const relativeViewInAppUrl = getInventoryViewInAppUrl(parseTechnicalFields(fields, true));
return getViewInAppUrl(basePath, relativeViewInAppUrl);
return addSpaceIdToPath(
basePath.publicBaseUrl,
spaceId,
getInventoryViewInAppUrl(parseTechnicalFields(fields, true))
);
};

export const getViewInMetricsAppUrl = (basePath: IBasePath, spaceId: string) =>
addSpaceIdToPath(basePath.publicBaseUrl, spaceId, LINK_TO_METRICS_EXPLORER);

export const getAlertDetailsUrl = (
basePath: IBasePath,
spaceId: string,
alertUuid: string | null
) => addSpaceIdToPath(basePath.publicBaseUrl, spaceId, `/app/observability/alerts/${alertUuid}`);
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ import {
buildNoDataAlertReason,
stateToAlertMessage,
} from '../common/messages';
import { createScopedLogger, getViewInAppUrlInventory } from '../common/utils';
import {
createScopedLogger,
getAlertDetailsUrl,
getViewInInventoryAppUrl,
UNGROUPED_FACTORY_KEY,
} from '../common/utils';
import { evaluateCondition, ConditionResult } from './evaluate_condition';

type InventoryMetricThresholdAllowedActionGroups = ActionGroupIdsOf<
Expand Down Expand Up @@ -61,12 +66,18 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
InventoryMetricThresholdAlertState,
InventoryMetricThresholdAlertContext,
InventoryMetricThresholdAllowedActionGroups
>(async ({ services, params, alertId, executionId, startedAt }) => {
>(async ({ services, params, alertId, executionId, spaceId, startedAt }) => {
const startTime = Date.now();

const { criteria, filterQuery, sourceId = 'default', nodeType, alertOnNoData } = params;

if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions');

const logger = createScopedLogger(libs.logger, 'inventoryRule', { alertId, executionId });
const { alertWithLifecycle, savedObjectsClient, getAlertStartedDate } = services;

const esClient = services.scopedClusterClient.asCurrentUser;

const { alertWithLifecycle, savedObjectsClient, getAlertStartedDate, getAlertUuid } = services;
const alertFactory: InventoryMetricThresholdAlertFactory = (id, reason, additionalContext) =>
alertWithLifecycle({
id,
Expand All @@ -85,31 +96,36 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
logger.error(e.message);
const actionGroupId = FIRED_ACTIONS.id; // Change this to an Error action group when able
const reason = buildInvalidQueryAlertReason(params.filterQueryText);
const alert = alertFactory('*', reason);
const indexedStartedDate = getAlertStartedDate('*') ?? startedAt.toISOString();
const viewInAppUrl = getViewInAppUrlInventory(
criteria,
nodeType,
indexedStartedDate,
libs.basePath
);
const alert = alertFactory(UNGROUPED_FACTORY_KEY, reason);
const indexedStartedDate =
getAlertStartedDate(UNGROUPED_FACTORY_KEY) ?? startedAt.toISOString();
const alertUuid = getAlertUuid(UNGROUPED_FACTORY_KEY);

alert.scheduleActions(actionGroupId, {
group: '*',
alertDetailsUrl: getAlertDetailsUrl(libs.basePath, spaceId, alertUuid),
alertState: stateToAlertMessage[AlertStates.ERROR],
group: UNGROUPED_FACTORY_KEY,
metric: mapToConditionsLookup(criteria, (c) => c.metric),
reason,
timestamp: startedAt.toISOString(),
viewInAppUrl,
value: null,
metric: mapToConditionsLookup(criteria, (c) => c.metric),
viewInAppUrl: getViewInInventoryAppUrl({
basePath: libs.basePath,
criteria,
nodeType,
timestamp: indexedStartedDate,
spaceId,
}),
});

return {};
}
}
const source = await libs.sources.getSourceConfiguration(savedObjectsClient, sourceId);

const [, , { logViews }] = await libs.getStartServices();
const logQueryFields: LogQueryFields | undefined = await logViews
.getClient(savedObjectsClient, services.scopedClusterClient.asCurrentUser)
.getClient(savedObjectsClient, esClient)
.getResolvedLogView(sourceId)
.then(
({ indices }) => ({ indexPattern: indices }),
Expand All @@ -120,18 +136,19 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
const results = await Promise.all(
criteria.map((condition) =>
evaluateCondition({
condition,
nodeType,
source,
logQueryFields,
esClient: services.scopedClusterClient.asCurrentUser,
compositeSize,
filterQuery,
condition,
esClient,
executionTimestamp: startedAt,
filterQuery,
logger,
logQueryFields,
nodeType,
source,
})
)
);

let scheduledActionsCount = 0;
const inventoryItems = Object.keys(first(results)!);
for (const group of inventoryItems) {
Expand Down Expand Up @@ -190,25 +207,28 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =

const alert = alertFactory(group, reason, additionalContext);
const indexedStartedDate = getAlertStartedDate(group) ?? startedAt.toISOString();
const viewInAppUrl = getViewInAppUrlInventory(
criteria,
nodeType,
indexedStartedDate,
libs.basePath
);
const alertUuid = getAlertUuid(group);

scheduledActionsCount++;

const context = {
group,
alertDetailsUrl: getAlertDetailsUrl(libs.basePath, spaceId, alertUuid),
alertState: stateToAlertMessage[nextState],
group,
reason,
metric: mapToConditionsLookup(criteria, (c) => c.metric),
timestamp: startedAt.toISOString(),
viewInAppUrl,
threshold: mapToConditionsLookup(criteria, (c) => c.threshold),
value: mapToConditionsLookup(results, (result) =>
formatMetric(result[group].metric, result[group].currentValue)
),
threshold: mapToConditionsLookup(criteria, (c) => c.threshold),
metric: mapToConditionsLookup(criteria, (c) => c.metric),
viewInAppUrl: getViewInInventoryAppUrl({
basePath: libs.basePath,
criteria,
nodeType,
timestamp: indexedStartedDate,
spaceId,
}),
...additionalContext,
};
alert.scheduleActions(actionGroupId, context);
Expand All @@ -217,24 +237,27 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =

const { getRecoveredAlerts } = services.alertFactory.done();
const recoveredAlerts = getRecoveredAlerts();

for (const alert of recoveredAlerts) {
const recoveredAlertId = alert.getId();
const indexedStartedDate = getAlertStartedDate(recoveredAlertId) ?? startedAt.toISOString();
const viewInAppUrl = getViewInAppUrlInventory(
criteria,
nodeType,
indexedStartedDate,
libs.basePath
);
const context = {
group: recoveredAlertId,
const alertUuid = getAlertUuid(recoveredAlertId);

alert.setContext({
alertDetailsUrl: getAlertDetailsUrl(libs.basePath, spaceId, alertUuid),
alertState: stateToAlertMessage[AlertStates.OK],
timestamp: startedAt.toISOString(),
viewInAppUrl,
threshold: mapToConditionsLookup(criteria, (c) => c.threshold),
group: recoveredAlertId,
metric: mapToConditionsLookup(criteria, (c) => c.metric),
};
alert.setContext(context);
threshold: mapToConditionsLookup(criteria, (c) => c.threshold),
timestamp: startedAt.toISOString(),
viewInAppUrl: getViewInInventoryAppUrl({
basePath: libs.basePath,
criteria,
nodeType,
timestamp: indexedStartedDate,
spaceId,
}),
});
}

const stopTime = Date.now();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from '../../../../common/inventory_models/types';
import { InfraBackendLibs } from '../../infra_types';
import {
alertDetailUrlActionVariableDescription,
alertStateActionVariableDescription,
cloudActionVariableDescription,
containerActionVariableDescription,
Expand All @@ -39,7 +40,11 @@ import {
valueActionVariableDescription,
viewInAppUrlActionVariableDescription,
} from '../common/messages';
import { oneOfLiterals, validateIsStringElasticsearchJSONFilter } from '../common/utils';
import {
getAlertDetailsPageEnabledForApp,
oneOfLiterals,
validateIsStringElasticsearchJSONFilter,
} from '../common/utils';
import {
createInventoryMetricThresholdExecutor,
FIRED_ACTIONS,
Expand Down Expand Up @@ -72,6 +77,8 @@ 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', {
Expand Down Expand Up @@ -102,6 +109,9 @@ export async function registerMetricInventoryThresholdRuleType(
context: [
{ name: 'group', description: groupActionVariableDescription },
{ name: 'alertState', description: alertStateActionVariableDescription },
...(getAlertDetailsPageEnabledForApp(config, 'metrics')
? [{ name: 'alertDetailsUrl', description: alertDetailUrlActionVariableDescription }]
: []),
{ name: 'reason', description: reasonActionVariableDescription },
{ name: 'timestamp', description: timestampActionVariableDescription },
{ name: 'value', description: valueActionVariableDescription },
Expand Down
Loading

0 comments on commit 7bc63e0

Please sign in to comment.