Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RAC] [Metrics UI] Register Inventory rule types with new RAC rules registry #105706

Merged
merged 18 commits into from
Jul 22, 2021
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
9 changes: 6 additions & 3 deletions x-pack/plugins/infra/public/alerting/inventory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,18 @@ import {
METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../server/lib/alerting/inventory_metric_threshold/types';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types';

import { ObservabilityRuleTypeModel } from '../../../../observability/public';

import { AlertTypeParams } from '../../../../alerting/common';
import { validateMetricThreshold } from './components/validation';
import { formatReason } from './rule_data_formatters';

interface InventoryMetricAlertTypeParams extends AlertTypeParams {
criteria: InventoryMetricConditions[];
}

export function createInventoryMetricAlertType(): AlertTypeModel<InventoryMetricAlertTypeParams> {
export function createInventoryMetricAlertType(): ObservabilityRuleTypeModel<InventoryMetricAlertTypeParams> {
return {
id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
description: i18n.translate('xpack.infra.metrics.inventory.alertFlyout.alertDescription', {
Expand All @@ -44,5 +46,6 @@ Reason:
}
),
requiresAppContext: false,
format: formatReason,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { ALERT_ID } from '@kbn/rule-data-utils';
import { ObservabilityRuleTypeFormatter } from '../../../../observability/public';

export const formatReason: ObservabilityRuleTypeFormatter = ({ fields }) => {
const groupName = fields[ALERT_ID];
const reason = i18n.translate('xpack.infra.metrics.alerting.inventory.alertReasonDescription', {
defaultMessage: 'Inventory alert for {groupName}.', // TEMP reason message, will be deleted once we index the reason field
values: {
groupName,
},
});

const link = '/app/metrics/inventory';

return {
reason,
link,
};
};
7 changes: 5 additions & 2 deletions x-pack/plugins/infra/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,15 @@ export class Plugin implements InfraClientPluginClass {
registerFeatures(pluginsSetup.home);
}

pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createInventoryMetricAlertType());
pluginsSetup.observability.observabilityRuleTypeRegistry.register(
createInventoryMetricAlertType()
);

pluginsSetup.observability.observabilityRuleTypeRegistry.register(
createLogThresholdAlertType()
);
pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createMetricThresholdAlertType());

pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createMetricThresholdAlertType());
pluginsSetup.observability.dashboard.register({
appName: 'infra_logs',
hasData: getLogsHasDataFetcher(core.getStartServices),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_m
import { toMetricOpt } from '../../../../common/snapshot_metric_i18n';
import { AlertStates, InventoryMetricConditions } from './types';
import {
ActionGroupIdsOf,
ActionGroup,
AlertInstanceContext,
AlertInstanceState,
RecoveredActionGroup,
} from '../../../../../alerting/common';
import { AlertExecutorOptions } from '../../../../../alerting/server';
import { AlertInstance, AlertTypeState } from '../../../../../alerting/server';
import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types';
import { InfraBackendLibs } from '../../infra_types';
import { METRIC_FORMATTERS } from '../../../../common/formatters/snapshot_metric_formats';
Expand All @@ -30,7 +31,6 @@ import {
stateToAlertMessage,
} from '../common/messages';
import { evaluateCondition } from './evaluate_condition';
import { InventoryMetricThresholdAllowedActionGroups } from './register_inventory_metric_threshold_alert_type';

interface InventoryMetricThresholdParams {
criteria: InventoryMetricConditions[];
Expand All @@ -40,145 +40,163 @@ interface InventoryMetricThresholdParams {
alertOnNoData?: boolean;
}

export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) => async ({
services,
params,
}: AlertExecutorOptions<
/**
* TODO: Remove this use of `any` by utilizing a proper type
*/
Record<string, any>,
Record<string, any>,
AlertInstanceState,
AlertInstanceContext,
type InventoryMetricThresholdAllowedActionGroups = ActionGroupIdsOf<
typeof FIRED_ACTIONS | typeof WARNING_ACTIONS
>;

export type InventoryMetricThresholdAlertTypeParams = Record<string, any>;
export type InventoryMetricThresholdAlertTypeState = AlertTypeState; // no specific state used
export type InventoryMetricThresholdAlertInstanceState = AlertInstanceState; // no specific state used
export type InventoryMetricThresholdAlertInstanceContext = AlertInstanceContext; // no specific instance context used

type InventoryMetricThresholdAlertInstance = AlertInstance<
InventoryMetricThresholdAlertInstanceState,
InventoryMetricThresholdAlertInstanceContext,
InventoryMetricThresholdAllowedActionGroups
>) => {
const {
criteria,
filterQuery,
sourceId,
nodeType,
alertOnNoData,
} = params as InventoryMetricThresholdParams;

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

const source = await libs.sources.getSourceConfiguration(
services.savedObjectsClient,
sourceId || 'default'
);

const logQueryFields = await libs
.getLogQueryFields(
sourceId || 'default',
services.savedObjectsClient,
services.scopedClusterClient.asCurrentUser
)
.catch(() => undefined);

const compositeSize = libs.configuration.inventory.compositeSize;

const results = await Promise.all(
criteria.map((condition) =>
evaluateCondition({
condition,
nodeType,
source,
logQueryFields,
esClient: services.scopedClusterClient.asCurrentUser,
compositeSize,
filterQuery,
})
)
);

const inventoryItems = Object.keys(first(results)!);
for (const item of inventoryItems) {
// AND logic; all criteria must be across the threshold
const shouldAlertFire = results.every((result) =>
// Grab the result of the most recent bucket
last(result[item].shouldFire)
>;
type InventoryMetricThresholdAlertInstanceFactory = (
id: string,
threshold?: number | undefined,
value?: number | undefined
) => InventoryMetricThresholdAlertInstance;

export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =>
libs.metricsRules.createLifecycleRuleExecutor<
InventoryMetricThresholdAlertTypeParams,
InventoryMetricThresholdAlertTypeState,
InventoryMetricThresholdAlertInstanceState,
InventoryMetricThresholdAlertInstanceContext,
InventoryMetricThresholdAllowedActionGroups
>(async ({ services, params }) => {
const {
criteria,
filterQuery,
sourceId,
nodeType,
alertOnNoData,
} = params as InventoryMetricThresholdParams;
if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions');
const { alertWithLifecycle, savedObjectsClient } = services;
const alertInstanceFactory: InventoryMetricThresholdAlertInstanceFactory = (id) =>
alertWithLifecycle({
id,
fields: {},
});

const source = await libs.sources.getSourceConfiguration(
savedObjectsClient,
sourceId || 'default'
);
const shouldAlertWarn = results.every((result) => last(result[item].shouldWarn));

// AND logic; because we need to evaluate all criteria, if one of them reports no data then the
// whole alert is in a No Data/Error state
const isNoData = results.some((result) => last(result[item].isNoData));
const isError = results.some((result) => result[item].isError);

const nextState = isError
? AlertStates.ERROR
: isNoData
? AlertStates.NO_DATA
: shouldAlertFire
? AlertStates.ALERT
: shouldAlertWarn
? AlertStates.WARNING
: AlertStates.OK;

let reason;
if (nextState === AlertStates.ALERT || nextState === AlertStates.WARNING) {
reason = results
.map((result) =>
buildReasonWithVerboseMetricName(
result[item],
buildFiredAlertReason,
nextState === AlertStates.WARNING
)
)
.join('\n');
/*
* Custom recovery actions aren't yet available in the alerting framework
* Uncomment the code below once they've been implemented
* Reference: https://github.com/elastic/kibana/issues/87048
*/
// } else if (nextState === AlertStates.OK && prevState?.alertState === AlertStates.ALERT) {
// reason = results
// .map((result) => buildReasonWithVerboseMetricName(result[item], buildRecoveredAlertReason))
// .join('\n');
}
if (alertOnNoData) {
if (nextState === AlertStates.NO_DATA) {
reason = results
.filter((result) => result[item].isNoData)
.map((result) => buildReasonWithVerboseMetricName(result[item], buildNoDataAlertReason))
.join('\n');
} else if (nextState === AlertStates.ERROR) {

const logQueryFields = await libs
.getLogQueryFields(
sourceId || 'default',
services.savedObjectsClient,
services.scopedClusterClient.asCurrentUser
)
.catch(() => undefined);

const compositeSize = libs.configuration.inventory.compositeSize;
const results = await Promise.all(
criteria.map((condition) =>
evaluateCondition({
condition,
nodeType,
source,
logQueryFields,
esClient: services.scopedClusterClient.asCurrentUser,
compositeSize,
filterQuery,
})
)
);
const inventoryItems = Object.keys(first(results)!);
for (const item of inventoryItems) {
// AND logic; all criteria must be across the threshold
const shouldAlertFire = results.every((result) => {
// Grab the result of the most recent bucket
return last(result[item].shouldFire);
});
const shouldAlertWarn = results.every((result) => last(result[item].shouldWarn));

// AND logic; because we need to evaluate all criteria, if one of them reports no data then the
// whole alert is in a No Data/Error state
const isNoData = results.some((result) => last(result[item].isNoData));
const isError = results.some((result) => result[item].isError);

const nextState = isError
? AlertStates.ERROR
: isNoData
? AlertStates.NO_DATA
: shouldAlertFire
? AlertStates.ALERT
: shouldAlertWarn
? AlertStates.WARNING
: AlertStates.OK;
let reason;
if (nextState === AlertStates.ALERT || nextState === AlertStates.WARNING) {
reason = results
.filter((result) => result[item].isError)
.map((result) => buildReasonWithVerboseMetricName(result[item], buildErrorAlertReason))
.map((result) =>
buildReasonWithVerboseMetricName(
result[item],
buildFiredAlertReason,
nextState === AlertStates.WARNING
)
)
.join('\n');
}
}
if (reason) {
const actionGroupId =
nextState === AlertStates.OK
? RecoveredActionGroup.id
: nextState === AlertStates.WARNING
? WARNING_ACTIONS.id
: FIRED_ACTIONS.id;
const alertInstance = services.alertInstanceFactory(`${item}`);
alertInstance.scheduleActions(
/**
* TODO: We're lying to the compiler here as explicitly calling `scheduleActions` on
* the RecoveredActionGroup isn't allowed
/*
* Custom recovery actions aren't yet available in the alerting framework
* Uncomment the code below once they've been implemented
* Reference: https://github.com/elastic/kibana/issues/87048
*/
(actionGroupId as unknown) as InventoryMetricThresholdAllowedActionGroups,
{
group: item,
alertState: stateToAlertMessage[nextState],
reason,
timestamp: moment().toISOString(),
value: mapToConditionsLookup(results, (result) =>
formatMetric(result[item].metric, result[item].currentValue)
),
threshold: mapToConditionsLookup(criteria, (c) => c.threshold),
metric: mapToConditionsLookup(criteria, (c) => c.metric),
// } else if (nextState === AlertStates.OK && prevState?.alertState === AlertStates.ALERT) {
// reason = results
// .map((result) => buildReasonWithVerboseMetricName(result[item], buildRecoveredAlertReason))
// .join('\n');
}
if (alertOnNoData) {
if (nextState === AlertStates.NO_DATA) {
reason = results
.filter((result) => result[item].isNoData)
.map((result) => buildReasonWithVerboseMetricName(result[item], buildNoDataAlertReason))
.join('\n');
} else if (nextState === AlertStates.ERROR) {
reason = results
.filter((result) => result[item].isError)
.map((result) => buildReasonWithVerboseMetricName(result[item], buildErrorAlertReason))
.join('\n');
}
);
}
if (reason) {
const actionGroupId =
nextState === AlertStates.OK
? RecoveredActionGroup.id
: nextState === AlertStates.WARNING
? WARNING_ACTIONS.id
: FIRED_ACTIONS.id;

const alertInstance = alertInstanceFactory(`${item}`);
alertInstance.scheduleActions(
/**
* TODO: We're lying to the compiler here as explicitly calling `scheduleActions` on
* the RecoveredActionGroup isn't allowed
*/
(actionGroupId as unknown) as InventoryMetricThresholdAllowedActionGroups,
{
group: item,
alertState: stateToAlertMessage[nextState],
reason,
timestamp: moment().toISOString(),
value: mapToConditionsLookup(results, (result) =>
formatMetric(result[item].metric, result[item].currentValue)
),
threshold: mapToConditionsLookup(criteria, (c) => c.threshold),
metric: mapToConditionsLookup(criteria, (c) => c.metric),
}
);
}
}
}
};
});

const buildReasonWithVerboseMetricName = (
resultItem: any,
Expand Down
Loading