"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createMetricThresholdExecutor = exports.NO_DATA_ACTIONS_ID = exports.NO_DATA_ACTIONS = exports.FIRED_ACTIONS_ID = exports.FIRED_ACTIONS = void 0; var _i18n = require("@kbn/i18n"); var _ruleDataUtils = require("@kbn/rule-data-utils"); var _lodash = require("lodash"); var _common = require("@kbn/alerting-plugin/common"); var _common2 = require("../../../../common"); var _formatters = require("../../../../common/threshold_rule/formatters"); var _types = require("./types"); var _messages = require("./messages"); var _utils = require("./utils"); var _evaluate_rule = require("./lib/evaluate_rule"); var _convert_strings_to_missing_groups_record = require("./lib/convert_strings_to_missing_groups_record"); /* * 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. */ const FIRED_ACTIONS_ID = 'threshold.fired'; exports.FIRED_ACTIONS_ID = FIRED_ACTIONS_ID; const NO_DATA_ACTIONS_ID = 'threshold.nodata'; exports.NO_DATA_ACTIONS_ID = NO_DATA_ACTIONS_ID; const createMetricThresholdExecutor = ({ alertsLocator, basePath, logger, config }) => async function (options) { var _initialSearchSource$; const startTime = Date.now(); const { services, params, state, startedAt, executionId, spaceId, rule: { id: ruleId } } = options; const { criteria } = params; if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); const thresholdLogger = (0, _utils.createScopedLogger)(logger, 'thresholdRule', { alertId: ruleId, executionId }); // TODO: check if we need to use "savedObjectsClient"=> https://github.com/elastic/kibana/issues/159340 const { alertWithLifecycle, getAlertUuid, getAlertByAlertUuid, getAlertStartedDate, searchSourceClient } = services; const alertFactory = (id, reason, actionGroup, additionalContext, evaluationValues) => alertWithLifecycle({ id, fields: { [_ruleDataUtils.ALERT_REASON]: reason, [_ruleDataUtils.ALERT_ACTION_GROUP]: actionGroup, [_ruleDataUtils.ALERT_EVALUATION_VALUES]: evaluationValues, ...(0, _utils.flattenAdditionalContext)(additionalContext) } }); const { alertOnNoData, alertOnGroupDisappear: _alertOnGroupDisappear } = params; // For backwards-compatibility, interpret undefined alertOnGroupDisappear as true const alertOnGroupDisappear = _alertOnGroupDisappear !== false; const compositeSize = config.thresholdRule.groupByPageSize; const filterQueryIsSame = (0, _lodash.isEqual)(state.filterQuery, params.filterQuery); const groupByIsSame = (0, _lodash.isEqual)(state.groupBy, params.groupBy); const previousMissingGroups = alertOnGroupDisappear && filterQueryIsSame && groupByIsSame && state.missingGroups ? state.missingGroups : []; const initialSearchSource = await searchSourceClient.create(params.searchConfiguration); const dataView = initialSearchSource.getField('index').getIndexPattern(); const timeFieldName = (_initialSearchSource$ = initialSearchSource.getField('index')) === null || _initialSearchSource$ === void 0 ? void 0 : _initialSearchSource$.timeFieldName; if (!dataView) { throw new Error('No matched data view'); } else if (!timeFieldName) { throw new Error('No timestamp field is specified'); } const alertResults = await (0, _evaluate_rule.evaluateRule)(services.scopedClusterClient.asCurrentUser, params, dataView, timeFieldName, compositeSize, alertOnGroupDisappear, logger, state.lastRunTimestamp, { end: startedAt.valueOf() }, (0, _convert_strings_to_missing_groups_record.convertStringsToMissingGroupsRecord)(previousMissingGroups)); const resultGroupSet = new Set(); for (const resultSet of alertResults) { for (const group of Object.keys(resultSet)) { resultGroupSet.add(group); } } const groupByKeysObjectMapping = (0, _utils.getGroupByObject)(params.groupBy, resultGroupSet); const groups = [...resultGroupSet]; const nextMissingGroups = new Set(); const hasGroups = !(0, _lodash.isEqual)(groups, [_utils.UNGROUPED_FACTORY_KEY]); let scheduledActionsCount = 0; // The key of `groups` is the alert instance ID. for (const group of groups) { // AND logic; all criteria must be across the threshold const shouldAlertFire = alertResults.every(result => { var _result$group; return (_result$group = result[group]) === null || _result$group === void 0 ? void 0 : _result$group.shouldFire; }); // 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 = alertResults.some(result => { var _result$group2; return (_result$group2 = result[group]) === null || _result$group2 === void 0 ? void 0 : _result$group2.isNoData; }); if (isNoData && group !== _utils.UNGROUPED_FACTORY_KEY) { nextMissingGroups.add({ key: group, bucketKey: alertResults[0][group].bucketKey }); } const nextState = isNoData ? _types.AlertStates.NO_DATA : shouldAlertFire ? _types.AlertStates.ALERT : _types.AlertStates.OK; let reason; if (nextState === _types.AlertStates.ALERT) { reason = alertResults.map(result => (0, _messages.buildFiredAlertReason)({ ...formatAlertResult(result[group]), group })).join('\n'); } /* NO DATA STATE HANDLING * * - `alertOnNoData` does not indicate IF the alert's next state is No Data, but whether or not the user WANTS TO BE ALERTED * if the state were No Data. * - `alertOnGroupDisappear`, on the other hand, determines whether or not it's possible to return a No Data state * when a group disappears. * * This means we need to handle the possibility that `alertOnNoData` is false, but `alertOnGroupDisappear` is true * * nextState === NO_DATA would be true on both { '*': No Data } or, e.g. { 'a': No Data, 'b': OK, 'c': OK }, but if the user * has for some reason disabled `alertOnNoData` and left `alertOnGroupDisappear` enabled, they would only care about the latter * possibility. In this case, use hasGroups to determine whether to alert on a potential No Data state * * If `alertOnNoData` is true but `alertOnGroupDisappear` is false, we don't need to worry about the {a, b, c} possibility. * At this point in the function, a false `alertOnGroupDisappear` would already have prevented group 'a' from being evaluated at all. */ if (alertOnNoData || alertOnGroupDisappear && hasGroups) { // In the previous line we've determined if the user is interested in No Data states, so only now do we actually // check to see if a No Data state has occurred if (nextState === _types.AlertStates.NO_DATA) { reason = alertResults.filter(result => { var _result$group3; return (_result$group3 = result[group]) === null || _result$group3 === void 0 ? void 0 : _result$group3.isNoData; }).map(result => (0, _messages.buildNoDataAlertReason)({ ...result[group], group })).join('\n'); } } if (reason) { var _alertResults$0$group, _additionalContext$ta, _getAlertStartedDate; const timestamp = startedAt.toISOString(); const actionGroupId = nextState === _types.AlertStates.OK ? _common.RecoveredActionGroup.id : nextState === _types.AlertStates.NO_DATA ? NO_DATA_ACTIONS_ID : FIRED_ACTIONS_ID; const additionalContext = (0, _utils.hasAdditionalContext)(params.groupBy, _utils.validGroupByForContext) ? alertResults && alertResults.length > 0 ? (_alertResults$0$group = alertResults[0][group].context) !== null && _alertResults$0$group !== void 0 ? _alertResults$0$group : {} : {} : {}; additionalContext.tags = Array.from(new Set([...((_additionalContext$ta = additionalContext.tags) !== null && _additionalContext$ta !== void 0 ? _additionalContext$ta : []), ...options.rule.tags])); const evaluationValues = alertResults.reduce((acc, result) => { if (result[group]) { acc.push(result[group].currentValue); } return acc; }, []); const alert = alertFactory(`${group}`, reason, actionGroupId, additionalContext, evaluationValues); const alertUuid = getAlertUuid(group); const indexedStartedAt = (_getAlertStartedDate = getAlertStartedDate(group)) !== null && _getAlertStartedDate !== void 0 ? _getAlertStartedDate : startedAt.toISOString(); scheduledActionsCount++; alert.scheduleActions(actionGroupId, { alertDetailsUrl: await (0, _common2.getAlertUrl)(alertUuid, spaceId, indexedStartedAt, alertsLocator, basePath.publicBaseUrl), groupings: groupByKeysObjectMapping[group], reason, timestamp, value: alertResults.map((result, index) => { const evaluation = result[group]; if (!evaluation && criteria[index].aggType === 'count') { return 0; } else if (!evaluation) { return null; } return formatAlertResult(evaluation).currentValue; }), ...additionalContext }); } } const { getRecoveredAlerts } = services.alertFactory.done(); const recoveredAlerts = getRecoveredAlerts(); const groupByKeysObjectForRecovered = (0, _utils.getGroupByObject)(params.groupBy, new Set(recoveredAlerts.map(recoveredAlert => recoveredAlert.getId()))); for (const alert of recoveredAlerts) { var _getAlertStartedDate2; const recoveredAlertId = alert.getId(); const alertUuid = getAlertUuid(recoveredAlertId); const timestamp = startedAt.toISOString(); const indexedStartedAt = (_getAlertStartedDate2 = getAlertStartedDate(recoveredAlertId)) !== null && _getAlertStartedDate2 !== void 0 ? _getAlertStartedDate2 : timestamp; const alertHits = alertUuid ? await getAlertByAlertUuid(alertUuid) : undefined; const additionalContext = (0, _utils.getContextForRecoveredAlerts)(alertHits); alert.setContext({ alertDetailsUrl: await (0, _common2.getAlertUrl)(alertUuid, spaceId, indexedStartedAt, alertsLocator, basePath.publicBaseUrl), groupings: groupByKeysObjectForRecovered[recoveredAlertId], timestamp: startedAt.toISOString(), ...additionalContext }); } const stopTime = Date.now(); thresholdLogger.debug(`Scheduled ${scheduledActionsCount} actions in ${stopTime - startTime}ms`); return { state: { lastRunTimestamp: startedAt.valueOf(), missingGroups: [...nextMissingGroups], groupBy: params.groupBy, filterQuery: params.filterQuery } }; }; exports.createMetricThresholdExecutor = createMetricThresholdExecutor; const FIRED_ACTIONS = { id: 'threshold.fired', name: _i18n.i18n.translate('xpack.observability.threshold.rule.alerting.threshold.fired', { defaultMessage: 'Alert' }) }; exports.FIRED_ACTIONS = FIRED_ACTIONS; const NO_DATA_ACTIONS = { id: 'threshold.nodata', name: _i18n.i18n.translate('xpack.observability.threshold.rule.alerting.threshold.nodata', { defaultMessage: 'No Data' }) }; exports.NO_DATA_ACTIONS = NO_DATA_ACTIONS; const formatAlertResult = alertResult => { const { metric, currentValue, threshold, comparator } = alertResult; const noDataValue = _i18n.i18n.translate('xpack.observability.threshold.rule.alerting.threshold.noDataFormattedValue', { defaultMessage: '[NO DATA]' }); if (metric.endsWith('.pct')) { const formatter = (0, _formatters.createFormatter)('percent'); return { ...alertResult, currentValue: currentValue !== null && currentValue !== undefined ? formatter(currentValue) : noDataValue, threshold: Array.isArray(threshold) ? threshold.map(v => formatter(v)) : formatter(threshold), comparator }; } const formatter = (0, _formatters.createFormatter)('highPrecision'); return { ...alertResult, currentValue: currentValue !== null && currentValue !== undefined ? formatter(currentValue) : noDataValue, threshold: Array.isArray(threshold) ? threshold.map(v => formatter(v)) : formatter(threshold), comparator }; };