diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/messages.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/messages.ts index 4878574e39d16..4add0ee9af5d3 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/messages.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/messages.ts @@ -24,7 +24,6 @@ export const stateToAlertMessage = { [AlertStates.ERROR]: i18n.translate('xpack.infra.metrics.alerting.threshold.errorState', { defaultMessage: 'ERROR', }), - // TODO: Implement recovered message state [AlertStates.OK]: i18n.translate('xpack.infra.metrics.alerting.threshold.okState', { defaultMessage: 'OK [Recovered]', }), @@ -62,6 +61,33 @@ const comparatorToI18n = (comparator: Comparator, threshold: number[], currentVa } }; +const recoveredComparatorToI18n = ( + comparator: Comparator, + threshold: number[], + currentValue: number +) => { + const belowText = i18n.translate('xpack.infra.metrics.alerting.threshold.belowRecovery', { + defaultMessage: 'below', + }); + const aboveText = i18n.translate('xpack.infra.metrics.alerting.threshold.aboveRecovery', { + defaultMessage: 'above', + }); + switch (comparator) { + case Comparator.BETWEEN: + return currentValue < threshold[0] ? belowText : aboveText; + case Comparator.OUTSIDE_RANGE: + return i18n.translate('xpack.infra.metrics.alerting.threshold.betweenRecovery', { + defaultMessage: 'between', + }); + case Comparator.GT: + case Comparator.GT_OR_EQ: + return belowText; + case Comparator.LT: + case Comparator.LT_OR_EQ: + return aboveText; + } +}; + const thresholdToI18n = ([a, b]: number[]) => { if (typeof b === 'undefined') return a; return i18n.translate('xpack.infra.metrics.alerting.threshold.thresholdRange', { @@ -87,6 +113,23 @@ export const buildFiredAlertReason: (alertResult: { }, }); +export const buildRecoveredAlertReason: (alertResult: { + metric: string; + comparator: Comparator; + threshold: number[]; + currentValue: number; +}) => string = ({ metric, comparator, threshold, currentValue }) => + i18n.translate('xpack.infra.metrics.alerting.threshold.recoveredAlertReason', { + defaultMessage: + '{metric} is now {comparator} a threshold of {threshold} (current value is {currentValue})', + values: { + metric, + comparator: recoveredComparatorToI18n(comparator, threshold, currentValue), + threshold: thresholdToI18n(threshold), + currentValue, + }, + }); + export const buildNoDataAlertReason: (alertResult: { metric: string; timeSize: number; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index ed5efc1473953..19efc88e216ca 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -20,6 +20,8 @@ interface AlertTestInstance { state: any; } +let persistAlertInstances = false; // eslint-disable-line + describe('The metric threshold alert type', () => { describe('querying the entire infrastructure', () => { const instanceID = 'test-*'; @@ -313,6 +315,50 @@ describe('The metric threshold alert type', () => { expect(getState(instanceID).alertState).toBe(AlertStates.NO_DATA); }); }); + + // describe('querying a metric that later recovers', () => { + // const instanceID = 'test-*'; + // const execute = (threshold: number[]) => + // executor({ + // services, + // params: { + // criteria: [ + // { + // ...baseCriterion, + // comparator: Comparator.GT, + // threshold, + // }, + // ], + // }, + // }); + // beforeAll(() => (persistAlertInstances = true)); + // afterAll(() => (persistAlertInstances = false)); + + // test('sends a recovery alert as soon as the metric recovers', async () => { + // await execute([0.5]); + // expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + // expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); + // await execute([2]); + // expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + // expect(getState(instanceID).alertState).toBe(AlertStates.OK); + // }); + // test('does not continue to send a recovery alert if the metric is still OK', async () => { + // await execute([2]); + // expect(mostRecentAction(instanceID)).toBe(undefined); + // expect(getState(instanceID).alertState).toBe(AlertStates.OK); + // await execute([2]); + // expect(mostRecentAction(instanceID)).toBe(undefined); + // expect(getState(instanceID).alertState).toBe(AlertStates.OK); + // }); + // test('sends a recovery alert again once the metric alerts and recovers again', async () => { + // await execute([0.5]); + // expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + // expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); + // await execute([2]); + // expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + // expect(getState(instanceID).alertState).toBe(AlertStates.OK); + // }); + // }); }); const createMockStaticConfiguration = (sources: any) => ({ @@ -397,12 +443,19 @@ services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId const alertInstances = new Map(); services.alertInstanceFactory.mockImplementation((instanceID: string) => { - const alertInstance: AlertTestInstance = { + const newAlertInstance: AlertTestInstance = { instance: alertsMock.createAlertInstanceFactory(), actionQueue: [], state: {}, }; + const alertInstance: AlertTestInstance = persistAlertInstances + ? alertInstances.get(instanceID) || newAlertInstance + : newAlertInstance; alertInstances.set(instanceID, alertInstance); + + alertInstance.instance.getState.mockImplementation(() => { + return alertInstance.state; + }); alertInstance.instance.replaceState.mockImplementation((newState: any) => { alertInstance.state = newState; return alertInstance.instance;