Skip to content

Commit

Permalink
[RAC] [Metrics UI] Register Inventory rule types with new RAC rules r…
Browse files Browse the repository at this point in the history
…egistry (#105706)

* WIP: register inventory metric threshold as lifecycle rule

* fix inventory executor error

* save alerts into ES

* temp

* basic format reason for inventory threshold

* clean up, fix i18n error and temporarily remove types

* delete serialized params

* include group name in the reason

* cleanup

* link to default metrics page

* grab the value and threshold for the inventory item

* fix typo

* fix check types

* remove threshold and currentValue, the reason field will contain this info for combined conditions

* remove thereshold and value from the reason, soon will be replaced by indexed reason field

* remove unnecessary export

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
mgiota and kibanamachine committed Jul 22, 2021
1 parent 4c7037b commit eaa6dcb
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 199 deletions.
9 changes: 6 additions & 3 deletions x-pack/plugins/infra/public/alerting/inventory/index.ts
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,
};
}
@@ -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
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
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

0 comments on commit eaa6dcb

Please sign in to comment.