From c15da164074a162f71ee770258cfee652243a0c5 Mon Sep 17 00:00:00 2001 From: Maryam Saeidi Date: Thu, 26 Oct 2023 13:22:05 +0200 Subject: [PATCH] Improve reason message of custom threshold rule by adding data view information (#169414) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #162710 ## Summary This PR improves the custom threshold rule reason message by adding the data view indices and adjusting the reason for multiple aggregations. Previously, for multiple aggregations, we repeat some information that is shared between aggregations, such as interval and group information. Also, this PR improves the reason messages for single aggregation based on the selected aggregation and field, similar to what we currently have in the metric threshold rule. |Previous reason message | New reason message| |---|---| |![image](https://github.com/elastic/kibana/assets/12370520/bb7e0048-3590-48f0-adfe-218618c48782)|![image](https://github.com/elastic/kibana/assets/12370520/7a3d9778-f84b-4bbb-a8e0-a99debfe78d1)| ## 🧪 How to test - Create some custom threshold rules and check the reason message - Single condition (different aggregators and comparators) - With a label for the equation - Without a label - Multiple conditions (different aggregators and comparators) - With a label for the equation - Without a label ### Known issue I created an issue for `is not between` comparator and I wasn't able to genarate an alert for it: https://github.com/elastic/kibana/issues/169524 --- .../custom_threshold_executor.test.ts | 24 +- .../custom_threshold_executor.ts | 77 +------ .../custom_threshold/lib/evaluate_rule.ts | 44 +++- .../lib/format_alert_result.ts | 47 ++++ .../lib/rules/custom_threshold/messages.ts | 212 +++++++----------- .../register_custom_threshold_rule_type.ts | 9 +- .../rules/custom_threshold/translations.ts | 164 ++++++++++++++ .../custom_threshold_rule/avg_pct_fired.ts | 4 +- .../custom_threshold_rule/avg_pct_no_data.ts | 2 +- .../custom_threshold_rule/avg_us_fired.ts | 5 +- .../custom_eq_avg_bytes_fired.ts | 2 +- .../documents_count_fired.ts | 2 +- .../custom_threshold_rule/group_by_fired.ts | 4 +- .../custom_threshold_rule/group_by_fired.ts | 4 +- 14 files changed, 362 insertions(+), 238 deletions(-) create mode 100644 x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/format_alert_result.ts create mode 100644 x-pack/plugins/observability/server/lib/rules/custom_threshold/translations.ts diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts index 46a39f397957e..00d875ab1ea73 100644 --- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts @@ -17,10 +17,9 @@ import { LifecycleAlertServices } from '@kbn/rule-registry-plugin/server'; import { ruleRegistryMocks } from '@kbn/rule-registry-plugin/server/mocks'; import { createMetricThresholdExecutor, - FIRED_ACTIONS, MetricThresholdAlertContext, - NO_DATA_ACTIONS, } from './custom_threshold_executor'; +import { FIRED_ACTIONS, NO_DATA_ACTIONS } from './translations'; import { Evaluation } from './lib/evaluate_rule'; import type { LogMeta, Logger } from '@kbn/logging'; import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common'; @@ -237,10 +236,9 @@ describe('The metric threshold alert type', () => { await execute(Comparator.GT, [0.75]); const { action } = mostRecentAction(instanceID); expect(action.group).toBeUndefined(); - expect(action.reason).toContain('is 1'); - expect(action.reason).toContain('Alert when > 0.75'); - expect(action.reason).toContain('test.metric.1'); - expect(action.reason).toContain('in the last 1 min'); + expect(action.reason).toBe( + 'test.metric.1 is 1, above the threshold of 0.75. (duration: 1 min, data view: mockedIndexPattern)' + ); }); }); @@ -997,16 +995,10 @@ describe('The metric threshold alert type', () => { const instanceID = '*'; await execute(Comparator.GT_OR_EQ, [1.0], [3.0]); const { action } = mostRecentAction(instanceID); - const reasons = action.reason.split('\n'); - expect(reasons.length).toBe(2); - expect(reasons[0]).toContain('test.metric.1'); - expect(reasons[1]).toContain('test.metric.2'); - expect(reasons[0]).toContain('is 1'); - expect(reasons[1]).toContain('is 3'); - expect(reasons[0]).toContain('Alert when >= 1'); - expect(reasons[1]).toContain('Alert when >= 3'); - expect(reasons[0]).toContain('in the last 1 min'); - expect(reasons[1]).toContain('in the last 1 min'); + const reasons = action.reason; + expect(reasons).toBe( + 'test.metric.1 is 1, above the threshold of 1; test.metric.2 is 3, above the threshold of 3. (duration: 1 min, data view: mockedIndexPattern)' + ); }); }); describe('querying with the count aggregator', () => { diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts index 972fcaec08892..e910b301e42bf 100644 --- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts @@ -7,7 +7,6 @@ import { isEqual } from 'lodash'; import { TypeOf } from '@kbn/config-schema'; -import { i18n } from '@kbn/i18n'; import { ALERT_ACTION_GROUP, ALERT_EVALUATION_VALUES, @@ -23,11 +22,10 @@ import { import { Alert, RuleTypeState } from '@kbn/alerting-plugin/server'; import { IBasePath, Logger } from '@kbn/core/server'; import { LifecycleRuleExecutor } from '@kbn/rule-registry-plugin/server'; -import { AlertsLocatorParams, getAlertUrl, TimeUnitChar } from '../../../../common'; -import { createFormatter } from '../../../../common/custom_threshold_rule/formatters'; -import { Comparator } from '../../../../common/custom_threshold_rule/types'; +import { AlertsLocatorParams, getAlertUrl } from '../../../../common'; import { ObservabilityConfig } from '../../..'; import { AlertStates, searchConfigurationSchema } from './types'; +import { FIRED_ACTIONS, NO_DATA_ACTIONS } from './translations'; import { buildFiredAlertReason, @@ -45,6 +43,7 @@ import { getFormattedGroupBy, } from './utils'; +import { formatAlertResult } from './lib/format_alert_result'; import { EvaluatedRuleParams, evaluateRule } from './lib/evaluate_rule'; import { MissingGroupsRecord } from './lib/check_missing_group'; import { convertStringsToMissingGroupsRecord } from './lib/convert_strings_to_missing_groups_record'; @@ -64,7 +63,8 @@ export interface MetricThresholdAlertContext extends Record { group?: object; reason?: string; timestamp: string; // ISO string - value?: Array | null; + // String type is for [NO DATA] + value?: Array; } export const FIRED_ACTIONS_ID = 'custom_threshold.fired'; @@ -240,14 +240,7 @@ export const createMetricThresholdExecutor = ({ let reason; if (nextState === AlertStates.ALERT) { - reason = alertResults - .map((result) => - buildFiredAlertReason({ - ...formatAlertResult(result[group]), - group, - }) - ) - .join('\n'); + reason = buildFiredAlertReason(alertResults, group, dataView); } /* NO DATA STATE HANDLING @@ -383,61 +376,3 @@ export const createMetricThresholdExecutor = ({ }, }; }; - -export const FIRED_ACTIONS = { - id: 'custom_threshold.fired', - name: i18n.translate('xpack.observability.customThreshold.rule.alerting.custom_threshold.fired', { - defaultMessage: 'Alert', - }), -}; - -export const NO_DATA_ACTIONS = { - id: 'custom_threshold.nodata', - name: i18n.translate( - 'xpack.observability.customThreshold.rule.alerting.custom_threshold.nodata', - { - defaultMessage: 'No Data', - } - ), -}; - -const formatAlertResult = ( - alertResult: { - metric: string; - currentValue: number | null; - threshold: number[]; - comparator: Comparator; - timeSize: number; - timeUnit: TimeUnitChar; - } & AlertResult -) => { - const { metric, currentValue, threshold, comparator } = alertResult; - const noDataValue = i18n.translate( - 'xpack.observability.customThreshold.rule.alerting.threshold.noDataFormattedValue', - { defaultMessage: '[NO DATA]' } - ); - - if (metric.endsWith('.pct')) { - const formatter = createFormatter('percent'); - return { - ...alertResult, - currentValue: - currentValue !== null && currentValue !== undefined ? formatter(currentValue) : noDataValue, - threshold: Array.isArray(threshold) - ? threshold.map((v: number) => formatter(v)) - : formatter(threshold), - comparator, - }; - } - - const formatter = createFormatter('highPrecision'); - return { - ...alertResult, - currentValue: - currentValue !== null && currentValue !== undefined ? formatter(currentValue) : noDataValue, - threshold: Array.isArray(threshold) - ? threshold.map((v: number) => formatter(v)) - : formatter(threshold), - comparator, - }; -}; diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/evaluate_rule.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/evaluate_rule.ts index 97523fc102c1a..c491e0799438e 100644 --- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/evaluate_rule.ts +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/evaluate_rule.ts @@ -8,14 +8,26 @@ import moment from 'moment'; import { ElasticsearchClient } from '@kbn/core/server'; import type { Logger } from '@kbn/logging'; -import { MetricExpressionParams } from '../../../../../common/custom_threshold_rule/types'; -import { isCustom } from './metric_expression_params'; +import { + Aggregators, + CustomMetricExpressionParams, + MetricExpressionParams, +} from '../../../../../common/custom_threshold_rule/types'; import { AdditionalContext, getIntervalInSeconds } from '../utils'; import { SearchConfigurationType } from '../custom_threshold_executor'; -import { CUSTOM_EQUATION_I18N, DOCUMENT_COUNT_I18N } from '../messages'; +import { + AVERAGE_I18N, + CARDINALITY_I18N, + CUSTOM_EQUATION_I18N, + DOCUMENT_COUNT_I18N, + MAX_I18N, + MIN_I18N, + SUM_I18N, +} from '../translations'; import { createTimerange } from './create_timerange'; import { getData } from './get_data'; import { checkMissingGroups, MissingGroupsRecord } from './check_missing_group'; +import { isCustom } from './metric_expression_params'; export interface EvaluatedRuleParams { criteria: MetricExpressionParams[]; @@ -33,6 +45,26 @@ export type Evaluation = Omit & { context?: AdditionalContext; }; +const getMetric = (criterion: CustomMetricExpressionParams) => { + if (!criterion.label && criterion.metrics.length === 1) { + switch (criterion.metrics[0].aggType) { + case Aggregators.COUNT: + return DOCUMENT_COUNT_I18N; + case Aggregators.AVERAGE: + return AVERAGE_I18N(criterion.metrics[0].field!); + case Aggregators.MAX: + return MAX_I18N(criterion.metrics[0].field!); + case Aggregators.MIN: + return MIN_I18N(criterion.metrics[0].field!); + case Aggregators.CARDINALITY: + return CARDINALITY_I18N(criterion.metrics[0].field!); + case Aggregators.SUM: + return SUM_I18N(criterion.metrics[0].field!); + } + } + return criterion.label || CUSTOM_EQUATION_I18N; +}; + export const evaluateRule = async ( esClient: ElasticsearchClient, params: Params, @@ -104,10 +136,8 @@ export const evaluateRule = async & { + currentValue: string; + threshold: string[]; +}; + +export const formatAlertResult = (evaluationResult: Evaluation): FormattedEvaluation => { + const { metric, currentValue, threshold, comparator } = evaluationResult; + const noDataValue = i18n.translate( + 'xpack.observability.customThreshold.rule.alerting.threshold.noDataFormattedValue', + { defaultMessage: '[NO DATA]' } + ); + + if (metric.endsWith('.pct')) { + const formatter = createFormatter('percent'); + return { + ...evaluationResult, + currentValue: + currentValue !== null && currentValue !== undefined ? formatter(currentValue) : noDataValue, + threshold: Array.isArray(threshold) + ? threshold.map((v: number) => formatter(v)) + : [formatter(threshold)], + comparator, + }; + } + + const formatter = createFormatter('highPrecision'); + return { + ...evaluationResult, + currentValue: + currentValue !== null && currentValue !== undefined ? formatter(currentValue) : noDataValue, + threshold: Array.isArray(threshold) + ? threshold.map((v: number) => formatter(v)) + : [formatter(threshold)], + comparator, + }; +}; diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/messages.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/messages.ts index 80ca06c24e59f..9f39adb4af046 100644 --- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/messages.ts +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/messages.ts @@ -7,50 +7,40 @@ import { i18n } from '@kbn/i18n'; import { Comparator } from '../../../../common/custom_threshold_rule/types'; -import { formatDurationFromTimeUnitChar, TimeUnitChar } from '../../../../common'; +import { formatDurationFromTimeUnitChar } from '../../../../common'; +import { Evaluation } from './lib/evaluate_rule'; +import { formatAlertResult, FormattedEvaluation } from './lib/format_alert_result'; import { UNGROUPED_FACTORY_KEY } from './utils'; -export const DOCUMENT_COUNT_I18N = i18n.translate( - 'xpack.observability.customThreshold.rule.threshold.documentCount', - { - defaultMessage: 'Document count', - } -); +const toNumber = (value: number | string) => + typeof value === 'string' ? parseFloat(value) : value; -export const CUSTOM_EQUATION_I18N = i18n.translate( - 'xpack.observability.customThreshold.rule.threshold.customEquation', +const belowText = i18n.translate('xpack.observability.customThreshold.rule.threshold.below', { + defaultMessage: 'below', +}); +const aboveText = i18n.translate('xpack.observability.customThreshold.rule.threshold.above', { + defaultMessage: 'above', +}); +const betweenText = i18n.translate('xpack.observability.customThreshold.rule.threshold.between', { + defaultMessage: 'between', +}); +const notBetweenText = i18n.translate( + 'xpack.observability.customThreshold.rule.threshold.notBetween', { - defaultMessage: 'Custom equation', + defaultMessage: 'not between', } ); -const toNumber = (value: number | string) => - typeof value === 'string' ? parseFloat(value) : value; - const recoveredComparatorToI18n = ( comparator: Comparator, threshold: number[], currentValue: number ) => { - const belowText = i18n.translate( - 'xpack.observability.customThreshold.rule.threshold.belowRecovery', - { - defaultMessage: 'below', - } - ); - const aboveText = i18n.translate( - 'xpack.observability.customThreshold.rule.threshold.aboveRecovery', - { - defaultMessage: 'above', - } - ); switch (comparator) { case Comparator.BETWEEN: return currentValue < threshold[0] ? belowText : aboveText; case Comparator.OUTSIDE_RANGE: - return i18n.translate('xpack.observability.customThreshold.rule.threshold.betweenRecovery', { - defaultMessage: 'between', - }); + return betweenText; case Comparator.GT: case Comparator.GT_OR_EQ: return belowText; @@ -60,6 +50,21 @@ const recoveredComparatorToI18n = ( } }; +const alertComparatorToI18n = (comparator: Comparator) => { + switch (comparator) { + case Comparator.BETWEEN: + return betweenText; + case Comparator.OUTSIDE_RANGE: + return notBetweenText; + case Comparator.GT: + case Comparator.GT_OR_EQ: + return aboveText; + case Comparator.LT: + case Comparator.LT_OR_EQ: + return belowText; + } +}; + const thresholdToI18n = ([a, b]: Array) => { if (typeof b === 'undefined') return a; return i18n.translate('xpack.observability.customThreshold.rule.threshold.thresholdRange', { @@ -70,25 +75,63 @@ const thresholdToI18n = ([a, b]: Array) => { const formatGroup = (group: string) => (group === UNGROUPED_FACTORY_KEY ? '' : ` for ${group}`); -export const buildFiredAlertReason: (alertResult: { - group: string; - metric: string; - comparator: Comparator; - threshold: Array; - currentValue: number | string; - timeSize: number; - timeUnit: TimeUnitChar; -}) => string = ({ group, metric, comparator, threshold, currentValue, timeSize, timeUnit }) => +export const buildFiredAlertReason: ( + alertResults: Array>, + group: string, + dataView: string +) => string = (alertResults, group, dataView) => { + const aggregationReason = + alertResults + .map((result: any) => buildAggregationReason(formatAlertResult(result[group]))) + .join('; ') + '.'; + const sharedReason = + '(' + + [ + i18n.translate('xpack.observability.customThreshold.rule.reason.forTheLast', { + defaultMessage: 'duration: {duration}', + values: { + duration: formatDurationFromTimeUnitChar( + alertResults[0][group].timeSize, + alertResults[0][group].timeUnit + ), + }, + }), + i18n.translate('xpack.observability.customThreshold.rule.reason.dataView', { + defaultMessage: 'data view: {dataView}', + values: { + dataView, + }, + }), + group !== UNGROUPED_FACTORY_KEY + ? i18n.translate('xpack.observability.customThreshold.rule.reason.group', { + defaultMessage: 'group: {group}', + values: { + group, + }, + }) + : null, + ] + .filter((item) => !!item) + .join(', ') + + ')'; + return aggregationReason + ' ' + sharedReason; +}; + +const buildAggregationReason: (evaluation: FormattedEvaluation) => string = ({ + metric, + comparator, + threshold, + currentValue, + timeSize, + timeUnit, +}) => i18n.translate('xpack.observability.customThreshold.rule.threshold.firedAlertReason', { - defaultMessage: - '{metric} is {currentValue} in the last {duration}{group}. Alert when {comparator} {threshold}.', + defaultMessage: '{metric} is {currentValue}, {comparator} the threshold of {threshold}', values: { - group: formatGroup(group), metric, - comparator, + comparator: alertComparatorToI18n(comparator), threshold: thresholdToI18n(threshold), currentValue, - duration: formatDurationFromTimeUnitChar(timeSize, timeUnit), }, }); @@ -138,88 +181,3 @@ export const buildErrorAlertReason = (metric: string) => metric, }, }); - -export const groupByKeysActionVariableDescription = i18n.translate( - 'xpack.observability.customThreshold.rule.groupByKeysActionVariableDescription', - { - defaultMessage: 'The object containing groups that are reporting data', - } -); - -export const alertDetailUrlActionVariableDescription = i18n.translate( - 'xpack.observability.customThreshold.rule.alertDetailUrlActionVariableDescription', - { - defaultMessage: - 'Link to the alert troubleshooting view for further context and details. This will be an empty string if the server.publicBaseUrl is not configured.', - } -); - -export const reasonActionVariableDescription = i18n.translate( - 'xpack.observability.customThreshold.rule.reasonActionVariableDescription', - { - defaultMessage: 'A concise description of the reason for the alert', - } -); - -export const timestampActionVariableDescription = i18n.translate( - 'xpack.observability.customThreshold.rule.timestampDescription', - { - defaultMessage: 'A timestamp of when the alert was detected.', - } -); - -export const valueActionVariableDescription = i18n.translate( - 'xpack.observability.customThreshold.rule.valueActionVariableDescription', - { - defaultMessage: 'List of the condition values.', - } -); - -export const viewInAppUrlActionVariableDescription = i18n.translate( - 'xpack.observability.customThreshold.rule.viewInAppUrlActionVariableDescription', - { - defaultMessage: 'Link to the alert source', - } -); - -export const cloudActionVariableDescription = i18n.translate( - 'xpack.observability.customThreshold.rule.cloudActionVariableDescription', - { - defaultMessage: 'The cloud object defined by ECS if available in the source.', - } -); - -export const hostActionVariableDescription = i18n.translate( - 'xpack.observability.customThreshold.rule.hostActionVariableDescription', - { - defaultMessage: 'The host object defined by ECS if available in the source.', - } -); - -export const containerActionVariableDescription = i18n.translate( - 'xpack.observability.customThreshold.rule.containerActionVariableDescription', - { - defaultMessage: 'The container object defined by ECS if available in the source.', - } -); - -export const orchestratorActionVariableDescription = i18n.translate( - 'xpack.observability.customThreshold.rule.orchestratorActionVariableDescription', - { - defaultMessage: 'The orchestrator object defined by ECS if available in the source.', - } -); - -export const labelsActionVariableDescription = i18n.translate( - 'xpack.observability.customThreshold.rule.labelsActionVariableDescription', - { - defaultMessage: 'List of labels associated with the entity where this alert triggered.', - } -); - -export const tagsActionVariableDescription = i18n.translate( - 'xpack.observability.customThreshold.rule.tagsActionVariableDescription', - { - defaultMessage: 'List of tags associated with the entity where this alert triggered.', - } -); diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/register_custom_threshold_rule_type.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/register_custom_threshold_rule_type.ts index 60d028c2c165c..035da92fcda4c 100644 --- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/register_custom_threshold_rule_type.ts +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/register_custom_threshold_rule_type.ts @@ -38,13 +38,10 @@ import { tagsActionVariableDescription, timestampActionVariableDescription, valueActionVariableDescription, -} from './messages'; +} from './translations'; import { oneOfLiterals, validateKQLStringFilter } from './utils'; -import { - createMetricThresholdExecutor, - FIRED_ACTIONS, - NO_DATA_ACTIONS, -} from './custom_threshold_executor'; +import { createMetricThresholdExecutor } from './custom_threshold_executor'; +import { FIRED_ACTIONS, NO_DATA_ACTIONS } from './translations'; import { ObservabilityConfig } from '../../..'; import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/custom_threshold_rule/constants'; diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/translations.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/translations.ts new file mode 100644 index 0000000000000..dc5a47b143976 --- /dev/null +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/translations.ts @@ -0,0 +1,164 @@ +/* + * 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'; + +export const FIRED_ACTIONS = { + id: 'custom_threshold.fired', + name: i18n.translate('xpack.observability.customThreshold.rule.alerting.custom_threshold.fired', { + defaultMessage: 'Alert', + }), +}; + +export const NO_DATA_ACTIONS = { + id: 'custom_threshold.nodata', + name: i18n.translate( + 'xpack.observability.customThreshold.rule.alerting.custom_threshold.nodata', + { + defaultMessage: 'No Data', + } + ), +}; + +export const DOCUMENT_COUNT_I18N = i18n.translate( + 'xpack.observability.customThreshold.rule.aggregators.documentCount', + { + defaultMessage: 'Document count', + } +); + +export const AVERAGE_I18N = (metric: string) => + i18n.translate('xpack.observability.customThreshold.rule.aggregators.average', { + defaultMessage: 'Average {metric}', + values: { + metric, + }, + }); + +export const MAX_I18N = (metric: string) => + i18n.translate('xpack.observability.customThreshold.rule.aggregators.max', { + defaultMessage: 'Max {metric}', + values: { + metric, + }, + }); + +export const MIN_I18N = (metric: string) => + i18n.translate('xpack.observability.customThreshold.rule.aggregators.min', { + defaultMessage: 'Min {metric}', + values: { + metric, + }, + }); + +export const CARDINALITY_I18N = (metric: string) => + i18n.translate('xpack.observability.customThreshold.rule.aggregators.cardinality', { + defaultMessage: 'Cardinality of the {metric}', + values: { + metric, + }, + }); + +export const SUM_I18N = (metric: string) => + i18n.translate('xpack.observability.customThreshold.rule.aggregators.sum', { + defaultMessage: 'Sum of the {metric}', + values: { + metric, + }, + }); + +export const CUSTOM_EQUATION_I18N = i18n.translate( + 'xpack.observability.customThreshold.rule.aggregators.customEquation', + { + defaultMessage: 'Custom equation', + } +); + +export const groupByKeysActionVariableDescription = i18n.translate( + 'xpack.observability.customThreshold.rule.groupByKeysActionVariableDescription', + { + defaultMessage: 'The object containing groups that are reporting data', + } +); + +export const alertDetailUrlActionVariableDescription = i18n.translate( + 'xpack.observability.customThreshold.rule.alertDetailUrlActionVariableDescription', + { + defaultMessage: + 'Link to the alert troubleshooting view for further context and details. This will be an empty string if the server.publicBaseUrl is not configured.', + } +); + +export const reasonActionVariableDescription = i18n.translate( + 'xpack.observability.customThreshold.rule.reasonActionVariableDescription', + { + defaultMessage: 'A concise description of the reason for the alert', + } +); + +export const timestampActionVariableDescription = i18n.translate( + 'xpack.observability.customThreshold.rule.timestampDescription', + { + defaultMessage: 'A timestamp of when the alert was detected.', + } +); + +export const valueActionVariableDescription = i18n.translate( + 'xpack.observability.customThreshold.rule.valueActionVariableDescription', + { + defaultMessage: 'List of the condition values.', + } +); + +export const viewInAppUrlActionVariableDescription = i18n.translate( + 'xpack.observability.customThreshold.rule.viewInAppUrlActionVariableDescription', + { + defaultMessage: 'Link to the alert source', + } +); + +export const cloudActionVariableDescription = i18n.translate( + 'xpack.observability.customThreshold.rule.cloudActionVariableDescription', + { + defaultMessage: 'The cloud object defined by ECS if available in the source.', + } +); + +export const hostActionVariableDescription = i18n.translate( + 'xpack.observability.customThreshold.rule.hostActionVariableDescription', + { + defaultMessage: 'The host object defined by ECS if available in the source.', + } +); + +export const containerActionVariableDescription = i18n.translate( + 'xpack.observability.customThreshold.rule.containerActionVariableDescription', + { + defaultMessage: 'The container object defined by ECS if available in the source.', + } +); + +export const orchestratorActionVariableDescription = i18n.translate( + 'xpack.observability.customThreshold.rule.orchestratorActionVariableDescription', + { + defaultMessage: 'The orchestrator object defined by ECS if available in the source.', + } +); + +export const labelsActionVariableDescription = i18n.translate( + 'xpack.observability.customThreshold.rule.labelsActionVariableDescription', + { + defaultMessage: 'List of labels associated with the entity where this alert triggered.', + } +); + +export const tagsActionVariableDescription = i18n.translate( + 'xpack.observability.customThreshold.rule.tagsActionVariableDescription', + { + defaultMessage: 'List of tags associated with the entity where this alert triggered.', + } +); diff --git a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/avg_pct_fired.ts b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/avg_pct_fired.ts index 91b767ca8087a..e47705c6e47aa 100644 --- a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/avg_pct_fired.ts +++ b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/avg_pct_fired.ts @@ -213,9 +213,9 @@ export default function ({ getService }: FtrProviderContext) { `https://localhost:5601/app/observability/alerts?_a=(kuery:%27kibana.alert.uuid:%20%22${alertId}%22%27%2CrangeFrom:%27${rangeFrom}%27%2CrangeTo:now%2Cstatus:all)` ); expect(resp.hits.hits[0]._source?.reason).eql( - 'Custom equation is 2.5 in the last 5 mins. Alert when > 0.5.' + `Average system.cpu.user.pct is 250%, above the threshold of 50%. (duration: 5 mins, data view: ${DATE_VIEW})` ); - expect(resp.hits.hits[0]._source?.value).eql('2.5'); + expect(resp.hits.hits[0]._source?.value).eql('250%'); }); }); }); diff --git a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/avg_pct_no_data.ts b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/avg_pct_no_data.ts index 949a74480382b..6363c7542e261 100644 --- a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/avg_pct_no_data.ts +++ b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/avg_pct_no_data.ts @@ -208,7 +208,7 @@ export default function ({ getService }: FtrProviderContext) { `https://localhost:5601/app/observability/alerts?_a=(kuery:%27kibana.alert.uuid:%20%22${alertId}%22%27%2CrangeFrom:%27${rangeFrom}%27%2CrangeTo:now%2Cstatus:all)` ); expect(resp.hits.hits[0]._source?.reason).eql( - 'Custom equation reported no data in the last 5m' + 'Average system.cpu.user.pct reported no data in the last 5m' ); expect(resp.hits.hits[0]._source?.value).eql('[NO DATA]'); }); diff --git a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/avg_us_fired.ts b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/avg_us_fired.ts index 46b109d48a0df..5c0b1bbef7610 100644 --- a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/avg_us_fired.ts +++ b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/avg_us_fired.ts @@ -40,6 +40,7 @@ export default function ({ getService }: FtrProviderContext) { describe('Custom Threshold rule - AVG - US - FIRED', () => { const CUSTOM_THRESHOLD_RULE_ALERT_INDEX = '.alerts-observability.threshold.alerts-default'; const ALERT_ACTION_INDEX = 'alert-action-threshold'; + const DATE_VIEW = 'traces-apm*,metrics-apm*,logs-apm*'; const DATA_VIEW_ID = 'data-view-id'; let synthtraceEsClient: ApmSynthtraceEsClient; @@ -55,7 +56,7 @@ export default function ({ getService }: FtrProviderContext) { supertest, name: 'test-data-view', id: DATA_VIEW_ID, - title: 'traces-apm*,metrics-apm*,logs-apm*', + title: DATE_VIEW, }); }); @@ -217,7 +218,7 @@ export default function ({ getService }: FtrProviderContext) { `https://localhost:5601/app/observability/alerts?_a=(kuery:%27kibana.alert.uuid:%20%22${alertId}%22%27%2CrangeFrom:%27${rangeFrom}%27%2CrangeTo:now%2Cstatus:all)` ); expect(resp.hits.hits[0]._source?.reason).eql( - 'Custom equation is 10,000,000 in the last 5 mins. Alert when > 7,500,000.' + `Average span.self_time.sum.us is 10,000,000, above the threshold of 7,500,000. (duration: 5 mins, data view: ${DATE_VIEW})` ); expect(resp.hits.hits[0]._source?.value).eql('10,000,000'); }); diff --git a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/custom_eq_avg_bytes_fired.ts b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/custom_eq_avg_bytes_fired.ts index 3ebb2fa0dbc76..cb254eedaf865 100644 --- a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/custom_eq_avg_bytes_fired.ts +++ b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/custom_eq_avg_bytes_fired.ts @@ -224,7 +224,7 @@ export default function ({ getService }: FtrProviderContext) { `https://localhost:5601/app/observability/alerts?_a=(kuery:%27kibana.alert.uuid:%20%22${alertId}%22%27%2CrangeFrom:%27${rangeFrom}%27%2CrangeTo:now%2Cstatus:all)` ); expect(resp.hits.hits[0]._source?.reason).eql( - 'Custom equation is 1 in the last 1 min. Alert when > 0.9.' + `Custom equation is 1, above the threshold of 0.9. (duration: 1 min, data view: ${DATE_VIEW})` ); expect(resp.hits.hits[0]._source?.value).eql('1'); }); diff --git a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/documents_count_fired.ts b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/documents_count_fired.ts index 07efa46cf175f..76ded7c07d6d7 100644 --- a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/documents_count_fired.ts +++ b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/documents_count_fired.ts @@ -212,7 +212,7 @@ export default function ({ getService }: FtrProviderContext) { `https://localhost:5601/app/observability/alerts?_a=(kuery:%27kibana.alert.uuid:%20%22${alertId}%22%27%2CrangeFrom:%27${rangeFrom}%27%2CrangeTo:now%2Cstatus:all)` ); expect(resp.hits.hits[0]._source?.reason).eql( - 'Custom equation is 3 in the last 1 min. Alert when > 2.' + `Document count is 3, above the threshold of 2. (duration: 1 min, data view: ${DATE_VIEW})` ); expect(resp.hits.hits[0]._source?.value).eql('3'); }); diff --git a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/group_by_fired.ts b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/group_by_fired.ts index 1d5ffa15ff97f..3e42f6bc8c19f 100644 --- a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/group_by_fired.ts +++ b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/group_by_fired.ts @@ -240,9 +240,9 @@ export default function ({ getService }: FtrProviderContext) { `https://localhost:5601/app/observability/alerts?_a=(kuery:%27kibana.alert.uuid:%20%22${alertId}%22%27%2CrangeFrom:%27${rangeFrom}%27%2CrangeTo:now%2Cstatus:all)` ); expect(resp.hits.hits[0]._source?.reason).eql( - 'Custom equation is 0.8 in the last 1 min for host-0,container-0. Alert when >= 0.2.' + `Average system.cpu.total.norm.pct is 80%, above the threshold of 20%. (duration: 1 min, data view: ${DATE_VIEW}, group: host-0,container-0)` ); - expect(resp.hits.hits[0]._source?.value).eql('0.8'); + expect(resp.hits.hits[0]._source?.value).eql('80%'); expect(resp.hits.hits[0]._source?.host).eql( '{"name":"host-0","mac":["00-00-5E-00-53-23","00-00-5E-00-53-24"]}' ); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/group_by_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/group_by_fired.ts index 2cab54a072f7f..4b82ab7085a68 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/group_by_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/group_by_fired.ts @@ -232,9 +232,9 @@ export default function ({ getService }: FtrProviderContext) { `${protocol}s://${hostname}:${port}/app/observability/alerts?_a=(kuery:%27kibana.alert.uuid:%20%22${alertId}%22%27%2CrangeFrom:%27${rangeFrom}%27%2CrangeTo:now%2Cstatus:all)` ); expect(resp.hits.hits[0]._source?.reason).eql( - 'Custom equation is 0.8 in the last 1 min for host-0. Alert when >= 0.2.' + `Average system.cpu.total.norm.pct is 80%, above the threshold of 20%. (duration: 1 min, data view: ${DATE_VIEW}, group: host-0)` ); - expect(resp.hits.hits[0]._source?.value).eql('0.8'); + expect(resp.hits.hits[0]._source?.value).eql('80%'); expect(resp.hits.hits[0]._source?.host).eql( '{"name":"host-0","mac":["00-00-5E-00-53-23","00-00-5E-00-53-24"]}' );