Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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',
Comment thread
CoenWarmer marked this conversation as resolved.
Outdated
}
);

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