Skip to content

Commit

Permalink
[ResponseOps] Create Rule Result Service (elastic#147278)
Browse files Browse the repository at this point in the history
Relates to elastic#135127

## Summary

Creates `RuleResultService`, which can be used to set and get values for
the rules last execution: `errors`, `warnings` and `outcomeMessages`.

### Checklist

Delete any items that are not applicable to this PR.

- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
2 people authored and crespocarlos committed Dec 23, 2022
1 parent 3d8e442 commit a63648a
Show file tree
Hide file tree
Showing 13 changed files with 382 additions and 45 deletions.
2 changes: 1 addition & 1 deletion x-pack/plugins/alerting/common/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export interface RuleAggregations {
export interface RuleLastRun {
outcome: RuleLastRunOutcomes;
warning?: RuleExecutionStatusErrorReasons | RuleExecutionStatusWarningReasons | null;
outcomeMsg?: string | null;
outcomeMsg?: string[] | null;
alertsCount: {
active?: number | null;
new?: number | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe('common_transformations', () => {
},
last_run: {
outcome: RuleLastRunOutcomeValues[2],
outcome_msg: 'this is just a test',
outcome_msg: ['this is just a test'],
warning: RuleExecutionStatusErrorReasons.Unknown,
alerts_count: {
new: 1,
Expand Down Expand Up @@ -134,7 +134,9 @@ describe('common_transformations', () => {
"recovered": 3,
},
"outcome": "failed",
"outcomeMsg": "this is just a test",
"outcomeMsg": Array [
"this is just a test",
],
"warning": "unknown",
},
"monitoring": Object {
Expand Down Expand Up @@ -253,7 +255,7 @@ describe('common_transformations', () => {
},
last_run: {
outcome: 'failed',
outcome_msg: 'this is just a test',
outcome_msg: ['this is just a test'],
warning: RuleExecutionStatusErrorReasons.Unknown,
alerts_count: {
new: 1,
Expand Down Expand Up @@ -295,7 +297,9 @@ describe('common_transformations', () => {
"recovered": 3,
},
"outcome": "failed",
"outcomeMsg": "this is just a test",
"outcomeMsg": Array [
"this is just a test",
],
"warning": "unknown",
},
"monitoring": Object {
Expand Down
171 changes: 149 additions & 22 deletions x-pack/plugins/alerting/server/lib/last_run_status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@
import { lastRunFromState } from './last_run_status';
import { ActionsCompletion } from '../../common';
import { RuleRunMetrics } from './rule_run_metrics_store';
const getMetrics = (): RuleRunMetrics => {
import { RuleResultServiceResults, RuleResultService } from '../monitoring/rule_result_service';

const getMetrics = ({
hasReachedAlertLimit = false,
triggeredActionsStatus = ActionsCompletion.COMPLETE,
}): RuleRunMetrics => {
return {
triggeredActionsStatus: ActionsCompletion.COMPLETE,
triggeredActionsStatus,
esSearchDurationMs: 3,
numSearches: 1,
numberOfActiveAlerts: 10,
Expand All @@ -19,16 +24,33 @@ const getMetrics = (): RuleRunMetrics => {
numberOfRecoveredAlerts: 11,
numberOfTriggeredActions: 5,
totalSearchDurationMs: 2,
hasReachedAlertLimit: false,
hasReachedAlertLimit,
};
};

const getRuleResultService = ({
errors = [],
warnings = [],
outcomeMessage = '',
}: Partial<RuleResultServiceResults>) => {
const ruleResultService = new RuleResultService();
const { addLastRunError, addLastRunWarning, setLastRunOutcomeMessage } =
ruleResultService.getLastRunSetters();
errors.forEach((error) => addLastRunError(error));
warnings.forEach((warning) => addLastRunWarning(warning));
setLastRunOutcomeMessage(outcomeMessage);
return ruleResultService;
};

describe('lastRunFromState', () => {
it('successfuly outcome', () => {
const result = lastRunFromState({ metrics: getMetrics() });
it('returns successful outcome if no errors or warnings reported', () => {
const result = lastRunFromState(
{ metrics: getMetrics({}) },
getRuleResultService({ outcomeMessage: 'Rule executed succesfully' })
);

expect(result.lastRun.outcome).toEqual('succeeded');
expect(result.lastRun.outcomeMsg).toEqual(null);
expect(result.lastRun.outcomeMsg).toEqual(['Rule executed succesfully']);
expect(result.lastRun.warning).toEqual(null);

expect(result.lastRun.alertsCount).toEqual({
Expand All @@ -39,18 +61,39 @@ describe('lastRunFromState', () => {
});
});

it('limited reached outcome', () => {
const result = lastRunFromState({
metrics: {
...getMetrics(),
hasReachedAlertLimit: true,
},
});
it('returns a warning outcome if rules last execution reported one', () => {
const result = lastRunFromState(
{ metrics: getMetrics({}) },
getRuleResultService({
warnings: ['MOCK_WARNING'],
outcomeMessage: 'Rule execution reported a warning',
})
);

expect(result.lastRun.outcome).toEqual('warning');
expect(result.lastRun.outcomeMsg).toEqual(
'Rule reported more than the maximum number of alerts in a single run. Alerts may be missed and recovery notifications may be delayed'
expect(result.lastRun.outcomeMsg).toEqual(['Rule execution reported a warning']);
expect(result.lastRun.warning).toEqual(null);

expect(result.lastRun.alertsCount).toEqual({
active: 10,
new: 12,
recovered: 11,
ignored: 0,
});
});

it('returns warning if rule has reached alert limit and alert circuit breaker opens', () => {
const result = lastRunFromState(
{
metrics: getMetrics({ hasReachedAlertLimit: true }),
},
getRuleResultService({})
);

expect(result.lastRun.outcome).toEqual('warning');
expect(result.lastRun.outcomeMsg).toEqual([
'Rule reported more than the maximum number of alerts in a single run. Alerts may be missed and recovery notifications may be delayed',
]);
expect(result.lastRun.warning).toEqual('maxAlerts');

expect(result.lastRun.alertsCount).toEqual({
Expand All @@ -61,18 +104,76 @@ describe('lastRunFromState', () => {
});
});

it('partial triggered actions status outcome', () => {
const result = lastRunFromState({
metrics: {
...getMetrics(),
triggeredActionsStatus: ActionsCompletion.PARTIAL,
it('returns warning if rules actions completition is partial and action circuit breaker opens', () => {
const result = lastRunFromState(
{
metrics: getMetrics({ triggeredActionsStatus: ActionsCompletion.PARTIAL }),
},
getRuleResultService({})
);

expect(result.lastRun.outcome).toEqual('warning');
expect(result.lastRun.outcomeMsg).toEqual([
'The maximum number of actions for this rule type was reached; excess actions were not triggered.',
]);
expect(result.lastRun.warning).toEqual('maxExecutableActions');

expect(result.lastRun.alertsCount).toEqual({
active: 10,
new: 12,
recovered: 11,
ignored: 0,
});
});

it('overwrites rule execution warning if rule has reached alert limit; outcome messages are merged', () => {
const ruleExecutionOutcomeMessage = 'Rule execution reported a warning';
const frameworkOutcomeMessage =
'Rule reported more than the maximum number of alerts in a single run. Alerts may be missed and recovery notifications may be delayed';
const result = lastRunFromState(
{
metrics: getMetrics({ hasReachedAlertLimit: true }),
},
getRuleResultService({
warnings: ['MOCK_WARNING'],
outcomeMessage: 'Rule execution reported a warning',
})
);

expect(result.lastRun.outcome).toEqual('warning');
expect(result.lastRun.outcomeMsg).toEqual(
'The maximum number of actions for this rule type was reached; excess actions were not triggered.'
expect(result.lastRun.outcomeMsg).toEqual([
frameworkOutcomeMessage,
ruleExecutionOutcomeMessage,
]);
expect(result.lastRun.warning).toEqual('maxAlerts');

expect(result.lastRun.alertsCount).toEqual({
active: 10,
new: 12,
recovered: 11,
ignored: 0,
});
});

it('overwrites rule execution warning if rule has reached action limit; outcome messages are merged', () => {
const ruleExecutionOutcomeMessage = 'Rule execution reported a warning';
const frameworkOutcomeMessage =
'The maximum number of actions for this rule type was reached; excess actions were not triggered.';
const result = lastRunFromState(
{
metrics: getMetrics({ triggeredActionsStatus: ActionsCompletion.PARTIAL }),
},
getRuleResultService({
warnings: ['MOCK_WARNING'],
outcomeMessage: 'Rule execution reported a warning',
})
);

expect(result.lastRun.outcome).toEqual('warning');
expect(result.lastRun.outcomeMsg).toEqual([
frameworkOutcomeMessage,
ruleExecutionOutcomeMessage,
]);
expect(result.lastRun.warning).toEqual('maxExecutableActions');

expect(result.lastRun.alertsCount).toEqual({
Expand All @@ -82,4 +183,30 @@ describe('lastRunFromState', () => {
ignored: 0,
});
});

it('overwrites warning outcome to error if rule execution reports an error', () => {
const result = lastRunFromState(
{
metrics: getMetrics({ hasReachedAlertLimit: true }),
},
getRuleResultService({
errors: ['MOCK_ERROR'],
outcomeMessage: 'Rule execution reported an error',
})
);

expect(result.lastRun.outcome).toEqual('failed');
expect(result.lastRun.outcomeMsg).toEqual([
'Rule reported more than the maximum number of alerts in a single run. Alerts may be missed and recovery notifications may be delayed',
'Rule execution reported an error',
]);
expect(result.lastRun.warning).toEqual('maxAlerts');

expect(result.lastRun.alertsCount).toEqual({
active: 10,
new: 12,
recovered: 11,
ignored: 0,
});
});
});
43 changes: 32 additions & 11 deletions x-pack/plugins/alerting/server/lib/last_run_status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,64 @@
import { RuleTaskStateAndMetrics } from '../task_runner/types';
import { getReasonFromError } from './error_with_reason';
import { getEsErrorMessage } from './errors';
import { ActionsCompletion } from '../../common';
import { ActionsCompletion, RuleLastRunOutcomes } from '../../common';
import {
RuleLastRunOutcomeValues,
RuleLastRunOutcomes,
RuleExecutionStatusWarningReasons,
RawRuleLastRun,
RuleLastRun,
} from '../types';
import { translations } from '../constants/translations';
import { RuleRunMetrics } from './rule_run_metrics_store';
import { RuleResultService } from '../monitoring/rule_result_service';

export interface ILastRun {
lastRun: RuleLastRun;
metrics: RuleRunMetrics | null;
}

export const lastRunFromState = (stateWithMetrics: RuleTaskStateAndMetrics): ILastRun => {
const { metrics } = stateWithMetrics;
export const lastRunFromState = (
stateWithMetrics: RuleTaskStateAndMetrics,
ruleResultService: RuleResultService
): ILastRun => {
let outcome: RuleLastRunOutcomes = RuleLastRunOutcomeValues[0];
// Check for warning states
let warning = null;
let outcomeMsg = null;
let warning: RuleLastRun['warning'] = null;
const outcomeMsg: string[] = [];

const { errors, warnings, outcomeMessage } = ruleResultService.getLastRunResults();
const { metrics } = stateWithMetrics;

if (warnings.length > 0) {
outcome = RuleLastRunOutcomeValues[1];
}

// We only have a single warning field so prioritizing the alert circuit breaker over the actions circuit breaker
if (metrics.hasReachedAlertLimit) {
outcome = RuleLastRunOutcomeValues[1];
warning = RuleExecutionStatusWarningReasons.MAX_ALERTS;
outcomeMsg = translations.taskRunner.warning.maxAlerts;
outcomeMsg.push(translations.taskRunner.warning.maxAlerts);
} else if (metrics.triggeredActionsStatus === ActionsCompletion.PARTIAL) {
outcome = RuleLastRunOutcomeValues[1];
warning = RuleExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS;
outcomeMsg = translations.taskRunner.warning.maxExecutableActions;
outcomeMsg.push(translations.taskRunner.warning.maxExecutableActions);
}

// Overwrite outcome to be error if last run reported any errors
if (errors.length > 0) {
outcome = RuleLastRunOutcomeValues[2];
}

// Optionally push outcome message reported by
// rule execution to the Framework's outcome message array
if (outcomeMessage) {
outcomeMsg.push(outcomeMessage);
}

return {
lastRun: {
outcome,
outcomeMsg: outcomeMsg || null,
outcomeMsg: outcomeMsg.length > 0 ? outcomeMsg : null,
warning: warning || null,
alertsCount: {
active: metrics.numberOfActiveAlerts,
Expand All @@ -59,11 +79,12 @@ export const lastRunFromState = (stateWithMetrics: RuleTaskStateAndMetrics): ILa
};

export const lastRunFromError = (error: Error): ILastRun => {
const esErrorMessage = getEsErrorMessage(error);
return {
lastRun: {
outcome: RuleLastRunOutcomeValues[2],
warning: getReasonFromError(error),
outcomeMsg: getEsErrorMessage(error),
outcomeMsg: esErrorMessage ? [esErrorMessage] : null,
alertsCount: {},
},
metrics: null,
Expand All @@ -82,6 +103,6 @@ export const lastRunToRaw = (lastRun: ILastRun['lastRun']): RawRuleLastRun => {
ignored: alertsCount.ignored || 0,
},
warning: warning ?? null,
outcomeMsg: outcomeMsg ?? null,
outcomeMsg: outcomeMsg && !Array.isArray(outcomeMsg) ? [outcomeMsg] : outcomeMsg,
};
};
Loading

0 comments on commit a63648a

Please sign in to comment.