mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Response Ops][Alerting] Aliasing context and state variables for detection rules in alert summary mode (#154864)
Toward enabling per-action-alerts for detection rules https://github.com/elastic/kibana/pull/154977 ## Summary This PR provides the necessary changes for detection rules to fully onboard onto alerting framework alert summaries. * Aliases detection rule context and state variables for summary action variables. This provides backwards compatibility with the `context.alerts`, `context.results_link` and `state.signals_count` action variables that are currently used by detection rules. * Calculates time bounds for summary alerts that can be passed back to the view in app URL generator. This allows rule types to generate view in app URLs limited to the timeframe that will match the summary alerts time range * For throttled summary alerts, the time range is generated as `now - throttle duration` * For per execution summary alerts, we use the `previousStartedAt` time from the task state if available and the schedule duration if not available. This is because some rules write out alerts with `@timestamp: task.startedAt` so just using `now - schedule duration` may not capture those alerts due to task manager schedule delays. --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
fce3664397
commit
fc3496902a
9 changed files with 221 additions and 5 deletions
|
@ -21,6 +21,7 @@ import {
|
|||
RuleTypeParams,
|
||||
RuleTypeState,
|
||||
SanitizedRule,
|
||||
GetViewInAppRelativeUrlFnOpts,
|
||||
} from '../types';
|
||||
import { RuleRunMetricsStore } from '../lib/rule_run_metrics_store';
|
||||
import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_event_logger.mock';
|
||||
|
@ -80,6 +81,7 @@ const rule = {
|
|||
contextVal: 'My other {{context.value}} goes here',
|
||||
stateVal: 'My other {{state.value}} goes here',
|
||||
},
|
||||
schedule: { interval: '1m' },
|
||||
notifyWhen: 'onActiveAlert',
|
||||
actions: [
|
||||
{
|
||||
|
@ -116,6 +118,7 @@ const defaultExecutionParams = {
|
|||
ruleLabel: 'rule-label',
|
||||
request: {} as KibanaRequest,
|
||||
alertingEventLogger,
|
||||
previousStartedAt: null,
|
||||
taskInstance: {
|
||||
params: { spaceId: 'test1', alertId: '1' },
|
||||
} as unknown as ConcreteTaskInstance,
|
||||
|
@ -1447,6 +1450,25 @@ describe('Execution Handler', () => {
|
|||
],
|
||||
} as unknown as SanitizedRule<RuleTypeParams>;
|
||||
|
||||
const summaryRuleWithUrl = {
|
||||
...rule,
|
||||
actions: [
|
||||
{
|
||||
id: '1',
|
||||
group: null,
|
||||
actionTypeId: 'test',
|
||||
frequency: {
|
||||
summary: true,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
throttle: null,
|
||||
},
|
||||
params: {
|
||||
val: 'rule url: {{rule.url}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as SanitizedRule<RuleTypeParams>;
|
||||
|
||||
it('populates the rule.url in the action params when the base url and rule id are specified', async () => {
|
||||
const execParams = {
|
||||
...defaultExecutionParams,
|
||||
|
@ -1474,6 +1496,55 @@ describe('Execution Handler', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
it('populates the rule.url with start and stop time when available', async () => {
|
||||
clock.reset();
|
||||
clock.tick(90000);
|
||||
getSummarizedAlertsMock.mockResolvedValue({
|
||||
new: {
|
||||
count: 2,
|
||||
data: [
|
||||
mockAAD,
|
||||
{
|
||||
...mockAAD,
|
||||
'@timestamp': '2022-12-07T15:45:41.4672Z',
|
||||
alert: { instance: { id: 'all' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
ongoing: { count: 0, data: [] },
|
||||
recovered: { count: 0, data: [] },
|
||||
});
|
||||
const execParams = {
|
||||
...defaultExecutionParams,
|
||||
ruleType: {
|
||||
...ruleType,
|
||||
getViewInAppRelativeUrl: (opts: GetViewInAppRelativeUrlFnOpts<RuleTypeParams>) =>
|
||||
`/app/test/rule/${opts.rule.id}?start=${opts.start ?? 0}&end=${opts.end ?? 0}`,
|
||||
},
|
||||
rule: summaryRuleWithUrl,
|
||||
taskRunnerContext: {
|
||||
...defaultExecutionParams.taskRunnerContext,
|
||||
kibanaBaseUrl: 'http://localhost:12345',
|
||||
},
|
||||
};
|
||||
|
||||
const executionHandler = new ExecutionHandler(generateExecutionParams(execParams));
|
||||
await executionHandler.run(generateAlert({ id: 1 }));
|
||||
|
||||
expect(injectActionParamsMock.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"actionParams": Object {
|
||||
"val": "rule url: http://localhost:12345/s/test1/app/test/rule/1?start=30000&end=90000",
|
||||
},
|
||||
"actionTypeId": "test",
|
||||
"ruleId": "1",
|
||||
"spaceId": "test1",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('populates the rule.url without the space specifier when the spaceId is the string "default"', async () => {
|
||||
const execParams = {
|
||||
...defaultExecutionParams,
|
||||
|
|
|
@ -40,6 +40,7 @@ import {
|
|||
import {
|
||||
generateActionHash,
|
||||
getSummaryActionsFromTaskState,
|
||||
getSummaryActionTimeBounds,
|
||||
isActionOnInterval,
|
||||
isSummaryAction,
|
||||
isSummaryActionOnInterval,
|
||||
|
@ -91,6 +92,7 @@ export class ExecutionHandler<
|
|||
private actionsClient: PublicMethodsOf<ActionsClient>;
|
||||
private ruleTypeActionGroups?: Map<ActionGroupIds | RecoveryActionGroupId, string>;
|
||||
private mutedAlertIdsSet: Set<string> = new Set();
|
||||
private previousStartedAt: Date | null;
|
||||
|
||||
constructor({
|
||||
rule,
|
||||
|
@ -104,6 +106,7 @@ export class ExecutionHandler<
|
|||
ruleConsumer,
|
||||
executionId,
|
||||
ruleLabel,
|
||||
previousStartedAt,
|
||||
actionsClient,
|
||||
}: ExecutionHandlerOptions<
|
||||
Params,
|
||||
|
@ -130,6 +133,7 @@ export class ExecutionHandler<
|
|||
this.ruleTypeActionGroups = new Map(
|
||||
ruleType.actionGroups.map((actionGroup) => [actionGroup.id, actionGroup.name])
|
||||
);
|
||||
this.previousStartedAt = previousStartedAt;
|
||||
this.mutedAlertIdsSet = new Set(rule.mutedInstanceIds);
|
||||
}
|
||||
|
||||
|
@ -205,6 +209,11 @@ export class ExecutionHandler<
|
|||
ruleRunMetricsStore.incrementNumberOfTriggeredActionsByConnectorType(actionTypeId);
|
||||
|
||||
if (isSummaryAction(action) && summarizedAlerts) {
|
||||
const { start, end } = getSummaryActionTimeBounds(
|
||||
action,
|
||||
this.rule.schedule,
|
||||
this.previousStartedAt
|
||||
);
|
||||
const actionToRun = {
|
||||
...action,
|
||||
params: injectActionParams({
|
||||
|
@ -221,7 +230,7 @@ export class ExecutionHandler<
|
|||
actionsPlugin,
|
||||
actionTypeId,
|
||||
kibanaBaseUrl: this.taskRunnerContext.kibanaBaseUrl,
|
||||
ruleUrl: this.buildRuleUrl(spaceId),
|
||||
ruleUrl: this.buildRuleUrl(spaceId, start, end),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
@ -419,13 +428,13 @@ export class ExecutionHandler<
|
|||
return alert.getScheduledActionOptions()?.actionGroup || this.ruleType.recoveryActionGroup.id;
|
||||
}
|
||||
|
||||
private buildRuleUrl(spaceId: string): string | undefined {
|
||||
private buildRuleUrl(spaceId: string, start?: number, end?: number): string | undefined {
|
||||
if (!this.taskRunnerContext.kibanaBaseUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relativePath = this.ruleType.getViewInAppRelativeUrl
|
||||
? this.ruleType.getViewInAppRelativeUrl({ rule: this.rule })
|
||||
? this.ruleType.getViewInAppRelativeUrl({ rule: this.rule, start, end })
|
||||
: `${triggersActionsRoute}${getRuleDetailsRoute(this.rule.id)}`;
|
||||
|
||||
try {
|
||||
|
|
|
@ -14,8 +14,12 @@ import {
|
|||
isSummaryAction,
|
||||
isSummaryActionOnInterval,
|
||||
isSummaryActionThrottled,
|
||||
getSummaryActionTimeBounds,
|
||||
} from './rule_action_helper';
|
||||
|
||||
const now = '2021-05-13T12:33:37.000Z';
|
||||
Date.now = jest.fn().mockReturnValue(new Date(now));
|
||||
|
||||
const mockOldAction: RuleAction = {
|
||||
id: '1',
|
||||
group: 'default',
|
||||
|
@ -309,4 +313,60 @@ describe('rule_action_helper', () => {
|
|||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSummaryActionTimeBounds', () => {
|
||||
test('returns undefined start and end action is not summary action', () => {
|
||||
expect(getSummaryActionTimeBounds(mockAction, { interval: '1m' }, null)).toEqual({
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('returns start and end for summary action with throttle', () => {
|
||||
const { start, end } = getSummaryActionTimeBounds(
|
||||
mockSummaryAction,
|
||||
{ interval: '1m' },
|
||||
null
|
||||
);
|
||||
expect(end).toEqual(1620909217000);
|
||||
expect(end).toEqual(new Date(now).valueOf());
|
||||
expect(start).toEqual(1620822817000);
|
||||
// start is end - throttle interval (1d)
|
||||
expect(start).toEqual(new Date('2021-05-12T12:33:37.000Z').valueOf());
|
||||
});
|
||||
|
||||
test('returns start and end for summary action without throttle with previousStartedAt', () => {
|
||||
const { start, end } = getSummaryActionTimeBounds(
|
||||
{
|
||||
...mockSummaryAction,
|
||||
frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null },
|
||||
},
|
||||
{ interval: '1m' },
|
||||
new Date('2021-05-13T12:31:57.000Z')
|
||||
);
|
||||
|
||||
expect(end).toEqual(1620909217000);
|
||||
expect(end).toEqual(new Date(now).valueOf());
|
||||
expect(start).toEqual(1620909117000);
|
||||
// start is previous started at time
|
||||
expect(start).toEqual(new Date('2021-05-13T12:31:57.000Z').valueOf());
|
||||
});
|
||||
|
||||
test('returns start and end for summary action without throttle without previousStartedAt', () => {
|
||||
const { start, end } = getSummaryActionTimeBounds(
|
||||
{
|
||||
...mockSummaryAction,
|
||||
frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null },
|
||||
},
|
||||
{ interval: '1m' },
|
||||
null
|
||||
);
|
||||
|
||||
expect(end).toEqual(1620909217000);
|
||||
expect(end).toEqual(new Date(now).valueOf());
|
||||
expect(start).toEqual(1620909157000);
|
||||
// start is end - schedule interval (1m)
|
||||
expect(start).toEqual(new Date('2021-05-13T12:32:37.000Z').valueOf());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { Logger } from '@kbn/logging';
|
||||
import {
|
||||
IntervalSchedule,
|
||||
parseDuration,
|
||||
RuleAction,
|
||||
RuleNotifyWhenTypeValues,
|
||||
|
@ -99,3 +100,32 @@ export const getSummaryActionsFromTaskState = ({
|
|||
}
|
||||
}, {});
|
||||
};
|
||||
|
||||
export const getSummaryActionTimeBounds = (
|
||||
action: RuleAction,
|
||||
ruleSchedule: IntervalSchedule,
|
||||
previousStartedAt: Date | null
|
||||
): { start?: number; end?: number } => {
|
||||
if (!isSummaryAction(action)) {
|
||||
return { start: undefined, end: undefined };
|
||||
}
|
||||
let startDate: Date;
|
||||
const now = Date.now();
|
||||
|
||||
if (isActionOnInterval(action)) {
|
||||
// If action is throttled, set time bounds using throttle interval
|
||||
const throttleMills = parseDuration(action.frequency!.throttle!);
|
||||
startDate = new Date(now - throttleMills);
|
||||
} else {
|
||||
// If action is not throttled, set time bounds to previousStartedAt - now
|
||||
// If previousStartedAt is null, use the rule schedule interval
|
||||
if (previousStartedAt) {
|
||||
startDate = previousStartedAt;
|
||||
} else {
|
||||
const scheduleMillis = parseDuration(ruleSchedule.interval);
|
||||
startDate = new Date(now - scheduleMillis);
|
||||
}
|
||||
}
|
||||
|
||||
return { start: startDate.valueOf(), end: now.valueOf() };
|
||||
};
|
||||
|
|
|
@ -458,6 +458,7 @@ export class TaskRunner<
|
|||
ruleConsumer: this.ruleConsumer!,
|
||||
executionId: this.executionId,
|
||||
ruleLabel,
|
||||
previousStartedAt: previousStartedAt ? new Date(previousStartedAt) : null,
|
||||
alertingEventLogger: this.alertingEventLogger,
|
||||
actionsClient: await this.context.actionsPlugin.getActionsClientWithRequest(fakeRequest),
|
||||
});
|
||||
|
|
|
@ -626,13 +626,13 @@ describe('transformSummaryActionParams', () => {
|
|||
new: { count: 1, data: [mockAAD] },
|
||||
ongoing: { count: 0, data: [] },
|
||||
recovered: { count: 0, data: [] },
|
||||
all: { count: 0, data: [] },
|
||||
all: { count: 1, data: [mockAAD] },
|
||||
},
|
||||
rule: {
|
||||
id: '1',
|
||||
name: 'test-rule',
|
||||
tags: ['test-tag'],
|
||||
params: {},
|
||||
params: { foo: 'bar', fooBar: true },
|
||||
} as SanitizedRule,
|
||||
ruleTypeId: 'rule-type-id',
|
||||
actionId: 'action-id',
|
||||
|
@ -657,6 +657,33 @@ describe('transformSummaryActionParams', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
test('renders aliased context values', () => {
|
||||
const actionParams = {
|
||||
message:
|
||||
'Value "{{context.alerts}}", "{{context.results_link}}" and "{{context.rule}}" exist',
|
||||
};
|
||||
|
||||
const result = transformSummaryActionParams({ ...params, actionParams });
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"message": "Value \\"{\\"@timestamp\\":\\"2022-12-07T15:38:43.472Z\\",\\"event\\":{\\"kind\\":\\"signal\\",\\"action\\":\\"active\\"},\\"kibana\\":{\\"version\\":\\"8.7.0\\",\\"space_ids\\":[\\"default\\"],\\"alert\\":{\\"instance\\":{\\"id\\":\\"*\\"},\\"uuid\\":\\"2d3e8fe5-3e8b-4361-916e-9eaab0bf2084\\",\\"status\\":\\"active\\",\\"workflow_status\\":\\"open\\",\\"reason\\":\\"system.cpu is 90% in the last 1 min for all hosts. Alert when > 50%.\\",\\"time_range\\":{\\"gte\\":\\"2022-01-01T12:00:00.000Z\\"},\\"start\\":\\"2022-12-07T15:23:13.488Z\\",\\"duration\\":{\\"us\\":100000},\\"flapping\\":false,\\"rule\\":{\\"category\\":\\"Metric threshold\\",\\"consumer\\":\\"alerts\\",\\"execution\\":{\\"uuid\\":\\"c35db7cc-5bf7-46ea-b43f-b251613a5b72\\"},\\"name\\":\\"test-rule\\",\\"producer\\":\\"infrastructure\\",\\"rule_type_id\\":\\"metrics.alert.threshold\\",\\"uuid\\":\\"0de91960-7643-11ed-b719-bb9db8582cb6\\",\\"tags\\":[]}}}}\\", \\"http://ruleurl\\" and \\"{\\"foo\\":\\"bar\\",\\"foo_bar\\":true,\\"name\\":\\"test-rule\\",\\"id\\":\\"1\\"}\\" exist",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('renders aliased state values', () => {
|
||||
const actionParams = {
|
||||
message: 'Value "{{state.signals_count}}" exists',
|
||||
};
|
||||
|
||||
const result = transformSummaryActionParams({ ...params, actionParams });
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"message": "Value \\"1\\" exists",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('renders alerts values', () => {
|
||||
const actionParams = {
|
||||
message:
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { PluginStartContract as ActionsPluginStartContract } from '@kbn/actions-plugin/server';
|
||||
import { mapKeys, snakeCase } from 'lodash/fp';
|
||||
import {
|
||||
RuleActionParams,
|
||||
AlertInstanceState,
|
||||
|
@ -143,6 +144,19 @@ export function transformSummaryActionParams({
|
|||
const variables = {
|
||||
kibanaBaseUrl,
|
||||
date: new Date().toISOString(),
|
||||
// For backwards compatibility with security solutions rules
|
||||
context: {
|
||||
alerts: alerts.all.data ?? [],
|
||||
results_link: ruleUrl,
|
||||
rule: mapKeys(snakeCase, {
|
||||
...rule.params,
|
||||
name: rule.name,
|
||||
id: rule.id,
|
||||
}),
|
||||
},
|
||||
state: {
|
||||
signals_count: alerts.all.count ?? 0,
|
||||
},
|
||||
rule: {
|
||||
params: rule.params,
|
||||
id: rule.id,
|
||||
|
|
|
@ -85,6 +85,7 @@ export interface ExecutionHandlerOptions<
|
|||
ruleConsumer: string;
|
||||
executionId: string;
|
||||
ruleLabel: string;
|
||||
previousStartedAt: Date | null;
|
||||
actionsClient: PublicMethodsOf<ActionsClient>;
|
||||
}
|
||||
|
||||
|
|
|
@ -168,6 +168,9 @@ export interface CombinedSummarizedAlerts extends SummarizedAlerts {
|
|||
export type GetSummarizedAlertsFn = (opts: GetSummarizedAlertsFnOpts) => Promise<SummarizedAlerts>;
|
||||
export interface GetViewInAppRelativeUrlFnOpts<Params extends RuleTypeParams> {
|
||||
rule: Omit<SanitizedRule<Params>, 'viewInAppRelativeUrl'>;
|
||||
// Optional time bounds
|
||||
start?: number;
|
||||
end?: number;
|
||||
}
|
||||
export type GetViewInAppRelativeUrlFn<Params extends RuleTypeParams> = (
|
||||
opts: GetViewInAppRelativeUrlFnOpts<Params>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue