[Response Ops] API to retrieve execution log entries from event log. (#127339)

* wip

* wip

* Reverting changes not related to event log aggregation

* Reverting changes not related to event log aggregation

* Updating event log client find to take array of sort options

* Updating tests and adding basic aggregation function

* Adding tests

* Fixing functional test

* Fixing functional test

* Revert "Reverting changes not related to event log aggregation"

This reverts commit 939340e252.

* Revert "Reverting changes not related to event log aggregation"

This reverts commit 40a93a4b3c.

* Getting aggregation and parsing aggregation results

* Cleanup

* Changing api to internal

* Fixing types

* PR feedback

* omg types

* types and optional accessors

* Adding fn to calculate num executions based on date range

* Fleshing out rules client function and tests

* http api

* Cleanup

* Adding schedule delay

* Limit to 1000 logs

* Fixing security tests

* Fixing unit tests

* Validating numExecutions

* Changing sort input format

* Adding more sort fields

* Fixing unit tests

* Adding functional tests

* Adding sort to terms aggregation

* Fixing functional test

* Adding audit event for rule GET

* Adding audit event for rule execution log GET

* PR feedback

* Adding gap policy and using static num buckets

* Fixing checks

* Fixing checks

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ying Mao 2022-03-18 18:38:42 -04:00 committed by GitHub
parent 484d23ce48
commit 5435cf922e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 2603 additions and 1 deletions

View file

@ -213,6 +213,10 @@ Refer to the corresponding {es} logs for potential write errors.
| `success` | User has accessed a rule.
| `failure` | User is not authorized to access a rule.
.2+| `rule_get_execution_log`
| `success` | User has accessed execution log for a rule.
| `failure` | User is not authorized to access execution log for a rule.
.2+| `rule_find`
| `success` | User has accessed a rule as part of a search operation.
| `failure` | User is not authorized to search for rules.

View file

@ -643,6 +643,7 @@ When a user is granted the `read` role in the Alerting Framework, they will be a
- `get`
- `getRuleState`
- `getAlertSummary`
- `getExecutionLog`
- `find`
When a user is granted the `all` role in the Alerting Framework, they will be able to execute all of the `read` privileged api calls, but in addition they'll be granted the following calls:

View file

@ -30,6 +30,7 @@ export enum ReadOperations {
Get = 'get',
GetRuleState = 'getRuleState',
GetAlertSummary = 'getAlertSummary',
GetExecutionLog = 'getExecutionLog',
Find = 'find',
}

View file

@ -0,0 +1,887 @@
/*
* 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 {
getNumExecutions,
getExecutionLogAggregation,
formatExecutionLogResult,
formatSortForBucketSort,
formatSortForTermSort,
} from './get_execution_log_aggregation';
describe('formatSortForBucketSort', () => {
test('should correctly format array of sort combinations for bucket sorting', () => {
expect(
formatSortForBucketSort([
{ timestamp: { order: 'desc' } },
{ execution_duration: { order: 'asc' } },
])
).toEqual([
{ 'ruleExecution>executeStartTime': { order: 'desc' } },
{ 'ruleExecution>executionDuration': { order: 'asc' } },
]);
});
});
describe('formatSortForTermSort', () => {
test('should correctly format array of sort combinations for bucket sorting', () => {
expect(
formatSortForTermSort([
{ timestamp: { order: 'desc' } },
{ execution_duration: { order: 'asc' } },
])
).toEqual([
{ 'ruleExecution>executeStartTime': 'desc' },
{ 'ruleExecution>executionDuration': 'asc' },
]);
});
});
describe('getNumExecutions', () => {
test('should calculate the expected number of executions in a given date range with a given schedule interval', () => {
expect(
getNumExecutions(
new Date('2020-12-01T00:00:00.000Z'),
new Date('2020-12-02T00:00:00.000Z'),
'1h'
)
).toEqual(24);
});
test('should return 0 if dateEnd is less that dateStart', () => {
expect(
getNumExecutions(
new Date('2020-12-02T00:00:00.000Z'),
new Date('2020-12-01T00:00:00.000Z'),
'1h'
)
).toEqual(0);
});
test('should cap numExecutions at default max buckets limit', () => {
expect(
getNumExecutions(
new Date('2020-12-01T00:00:00.000Z'),
new Date('2020-12-02T00:00:00.000Z'),
'1s'
)
).toEqual(1000);
});
});
describe('getExecutionLogAggregation', () => {
test('should throw error when given bad sort field', () => {
expect(() => {
getExecutionLogAggregation({
page: 1,
perPage: 10,
sort: [{ notsortable: { order: 'asc' } }],
});
}).toThrowErrorMatchingInlineSnapshot(
`"Invalid sort field \\"notsortable\\" - must be one of [timestamp,execution_duration,total_search_duration,es_search_duration,schedule_delay,num_triggered_actions]"`
);
});
test('should throw error when given one bad sort field', () => {
expect(() => {
getExecutionLogAggregation({
page: 1,
perPage: 10,
sort: [{ notsortable: { order: 'asc' } }, { timestamp: { order: 'asc' } }],
});
}).toThrowErrorMatchingInlineSnapshot(
`"Invalid sort field \\"notsortable\\" - must be one of [timestamp,execution_duration,total_search_duration,es_search_duration,schedule_delay,num_triggered_actions]"`
);
});
test('should throw error when given bad page field', () => {
expect(() => {
getExecutionLogAggregation({
page: 0,
perPage: 10,
sort: [{ timestamp: { order: 'asc' } }],
});
}).toThrowErrorMatchingInlineSnapshot(`"Invalid page field \\"0\\" - must be greater than 0"`);
});
test('should throw error when given bad perPage field', () => {
expect(() => {
getExecutionLogAggregation({
page: 1,
perPage: 0,
sort: [{ timestamp: { order: 'asc' } }],
});
}).toThrowErrorMatchingInlineSnapshot(
`"Invalid perPage field \\"0\\" - must be greater than 0"`
);
});
test('should correctly generate aggregation', () => {
expect(
getExecutionLogAggregation({
page: 2,
perPage: 10,
sort: [{ timestamp: { order: 'asc' } }, { execution_duration: { order: 'desc' } }],
})
).toEqual({
executionUuidCardinality: { cardinality: { field: 'kibana.alert.rule.execution.uuid' } },
executionUuid: {
terms: {
field: 'kibana.alert.rule.execution.uuid',
size: 1000,
order: [
{ 'ruleExecution>executeStartTime': 'asc' },
{ 'ruleExecution>executionDuration': 'desc' },
],
},
aggs: {
executionUuidSorted: {
bucket_sort: {
sort: [
{ 'ruleExecution>executeStartTime': { order: 'asc' } },
{ 'ruleExecution>executionDuration': { order: 'desc' } },
],
from: 10,
size: 10,
gap_policy: 'insert_zeros',
},
},
alertCounts: {
filters: {
filters: {
newAlerts: { match: { 'event.action': 'new-instance' } },
activeAlerts: { match: { 'event.action': 'active-instance' } },
recoveredAlerts: { match: { 'event.action': 'recovered-instance' } },
},
},
},
actionExecution: {
filter: {
bool: {
must: [
{ match: { 'event.action': 'execute' } },
{ match: { 'event.provider': 'actions' } },
],
},
},
aggs: { actionOutcomes: { terms: { field: 'event.outcome', size: 2 } } },
},
ruleExecution: {
filter: {
bool: {
must: [
{ match: { 'event.action': 'execute' } },
{ match: { 'event.provider': 'alerting' } },
],
},
},
aggs: {
executeStartTime: { min: { field: 'event.start' } },
scheduleDelay: {
max: {
field: 'kibana.task.schedule_delay',
},
},
totalSearchDuration: {
max: { field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms' },
},
esSearchDuration: {
max: { field: 'kibana.alert.rule.execution.metrics.es_search_duration_ms' },
},
numTriggeredActions: {
max: { field: 'kibana.alert.rule.execution.metrics.number_of_triggered_actions' },
},
executionDuration: { max: { field: 'event.duration' } },
outcomeAndMessage: {
top_hits: { size: 1, _source: { includes: ['event.outcome', 'message'] } },
},
},
},
timeoutMessage: {
filter: {
bool: {
must: [
{ match: { 'event.action': 'execute-timeout' } },
{ match: { 'event.provider': 'alerting' } },
],
},
},
},
},
},
});
});
});
describe('formatExecutionLogResult', () => {
test('should return empty results if aggregations are undefined', () => {
expect(formatExecutionLogResult({ aggregations: undefined })).toEqual({
total: 0,
data: [],
});
});
test('should format results correctly', () => {
const results = {
aggregations: {
executionUuid: {
meta: {},
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
{
key: '6705da7d-2635-499d-a6a8-1aee1ae1eac9',
doc_count: 27,
timeoutMessage: {
meta: {},
doc_count: 0,
},
alertCounts: {
meta: {},
buckets: {
activeAlerts: {
doc_count: 5,
},
newAlerts: {
doc_count: 5,
},
recoveredAlerts: {
doc_count: 0,
},
},
},
ruleExecution: {
meta: {},
doc_count: 1,
numTriggeredActions: {
value: 5.0,
},
outcomeAndMessage: {
hits: {
total: {
value: 1,
relation: 'eq',
},
max_score: 1.0,
hits: [
{
_index: '.kibana-event-log-8.2.0-000001',
_id: 'S4wIZX8B8TGQpG7XQZns',
_score: 1.0,
_source: {
event: {
outcome: 'success',
},
message:
"rule executed: example.always-firing:a348a740-9e2c-11ec-bd64-774ed95c43ef: 'test rule'",
},
},
],
},
},
scheduleDelay: {
value: 3.074e9,
},
totalSearchDuration: {
value: 0.0,
},
esSearchDuration: {
value: 0.0,
},
executionDuration: {
value: 1.056e9,
},
executeStartTime: {
value: 1.646667512617e12,
value_as_string: '2022-03-07T15:38:32.617Z',
},
},
actionExecution: {
meta: {},
doc_count: 5,
actionOutcomes: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
{
key: 'success',
doc_count: 5,
},
],
},
},
},
{
key: '41b2755e-765a-4044-9745-b03875d5e79a',
doc_count: 32,
timeoutMessage: {
meta: {},
doc_count: 0,
},
alertCounts: {
meta: {},
buckets: {
activeAlerts: {
doc_count: 5,
},
newAlerts: {
doc_count: 5,
},
recoveredAlerts: {
doc_count: 5,
},
},
},
ruleExecution: {
meta: {},
doc_count: 1,
numTriggeredActions: {
value: 5.0,
},
outcomeAndMessage: {
hits: {
total: {
value: 1,
relation: 'eq',
},
max_score: 1.0,
hits: [
{
_index: '.kibana-event-log-8.2.0-000001',
_id: 'a4wIZX8B8TGQpG7Xwpnz',
_score: 1.0,
_source: {
event: {
outcome: 'success',
},
message:
"rule executed: example.always-firing:a348a740-9e2c-11ec-bd64-774ed95c43ef: 'test rule'",
},
},
],
},
},
scheduleDelay: {
value: 3.126e9,
},
totalSearchDuration: {
value: 0.0,
},
esSearchDuration: {
value: 0.0,
},
executionDuration: {
value: 1.165e9,
},
executeStartTime: {
value: 1.646667545604e12,
value_as_string: '2022-03-07T15:39:05.604Z',
},
},
actionExecution: {
meta: {},
doc_count: 5,
actionOutcomes: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
{
key: 'success',
doc_count: 5,
},
],
},
},
},
],
},
executionUuidCardinality: {
value: 374,
},
},
};
expect(formatExecutionLogResult(results)).toEqual({
total: 374,
data: [
{
id: '6705da7d-2635-499d-a6a8-1aee1ae1eac9',
timestamp: '2022-03-07T15:38:32.617Z',
duration_ms: 1056,
status: 'success',
message:
"rule executed: example.always-firing:a348a740-9e2c-11ec-bd64-774ed95c43ef: 'test rule'",
num_active_alerts: 5,
num_new_alerts: 5,
num_recovered_alerts: 0,
num_triggered_actions: 5,
num_succeeded_actions: 5,
num_errored_actions: 0,
total_search_duration_ms: 0,
es_search_duration_ms: 0,
timed_out: false,
schedule_delay_ms: 3074,
},
{
id: '41b2755e-765a-4044-9745-b03875d5e79a',
timestamp: '2022-03-07T15:39:05.604Z',
duration_ms: 1165,
status: 'success',
message:
"rule executed: example.always-firing:a348a740-9e2c-11ec-bd64-774ed95c43ef: 'test rule'",
num_active_alerts: 5,
num_new_alerts: 5,
num_recovered_alerts: 5,
num_triggered_actions: 5,
num_succeeded_actions: 5,
num_errored_actions: 0,
total_search_duration_ms: 0,
es_search_duration_ms: 0,
timed_out: false,
schedule_delay_ms: 3126,
},
],
});
});
test('should format results correctly when execution timeouts occur', () => {
const results = {
aggregations: {
executionUuid: {
meta: {},
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
{
key: '09b5aeab-d50d-43b2-88e7-f1a20f682b3f',
doc_count: 3,
timeoutMessage: {
meta: {},
doc_count: 1,
},
alertCounts: {
meta: {},
buckets: {
activeAlerts: {
doc_count: 0,
},
newAlerts: {
doc_count: 0,
},
recoveredAlerts: {
doc_count: 0,
},
},
},
ruleExecution: {
meta: {},
doc_count: 1,
numTriggeredActions: {
value: 0.0,
},
outcomeAndMessage: {
hits: {
total: {
value: 1,
relation: 'eq',
},
max_score: 1.0,
hits: [
{
_index: '.kibana-event-log-8.2.0-000001',
_id: 'dJkWa38B1ylB1EvsAckB',
_score: 1.0,
_source: {
event: {
outcome: 'success',
},
message:
"rule executed: example.always-firing:a348a740-9e2c-11ec-bd64-774ed95c43ef: 'test rule'",
},
},
],
},
},
scheduleDelay: {
value: 3.074e9,
},
totalSearchDuration: {
value: 0.0,
},
esSearchDuration: {
value: 0.0,
},
executionDuration: {
value: 1.0279e10,
},
executeStartTime: {
value: 1.646769067607e12,
value_as_string: '2022-03-08T19:51:07.607Z',
},
},
actionExecution: {
meta: {},
doc_count: 0,
actionOutcomes: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [],
},
},
},
{
key: '41b2755e-765a-4044-9745-b03875d5e79a',
doc_count: 32,
timeoutMessage: {
meta: {},
doc_count: 0,
},
alertCounts: {
meta: {},
buckets: {
activeAlerts: {
doc_count: 5,
},
newAlerts: {
doc_count: 5,
},
recoveredAlerts: {
doc_count: 5,
},
},
},
ruleExecution: {
meta: {},
doc_count: 1,
numTriggeredActions: {
value: 5.0,
},
outcomeAndMessage: {
hits: {
total: {
value: 1,
relation: 'eq',
},
max_score: 1.0,
hits: [
{
_index: '.kibana-event-log-8.2.0-000001',
_id: 'a4wIZX8B8TGQpG7Xwpnz',
_score: 1.0,
_source: {
event: {
outcome: 'success',
},
message:
"rule executed: example.always-firing:a348a740-9e2c-11ec-bd64-774ed95c43ef: 'test rule'",
},
},
],
},
},
scheduleDelay: {
value: 3.126e9,
},
totalSearchDuration: {
value: 0.0,
},
esSearchDuration: {
value: 0.0,
},
executionDuration: {
value: 1.165e9,
},
executeStartTime: {
value: 1.646667545604e12,
value_as_string: '2022-03-07T15:39:05.604Z',
},
},
actionExecution: {
meta: {},
doc_count: 5,
actionOutcomes: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
{
key: 'success',
doc_count: 5,
},
],
},
},
},
],
},
executionUuidCardinality: {
value: 374,
},
},
};
expect(formatExecutionLogResult(results)).toEqual({
total: 374,
data: [
{
id: '09b5aeab-d50d-43b2-88e7-f1a20f682b3f',
timestamp: '2022-03-08T19:51:07.607Z',
duration_ms: 10279,
status: 'success',
message:
"rule executed: example.always-firing:a348a740-9e2c-11ec-bd64-774ed95c43ef: 'test rule'",
num_active_alerts: 0,
num_new_alerts: 0,
num_recovered_alerts: 0,
num_triggered_actions: 0,
num_succeeded_actions: 0,
num_errored_actions: 0,
total_search_duration_ms: 0,
es_search_duration_ms: 0,
timed_out: true,
schedule_delay_ms: 3074,
},
{
id: '41b2755e-765a-4044-9745-b03875d5e79a',
timestamp: '2022-03-07T15:39:05.604Z',
duration_ms: 1165,
status: 'success',
message:
"rule executed: example.always-firing:a348a740-9e2c-11ec-bd64-774ed95c43ef: 'test rule'",
num_active_alerts: 5,
num_new_alerts: 5,
num_recovered_alerts: 5,
num_triggered_actions: 5,
num_succeeded_actions: 5,
num_errored_actions: 0,
total_search_duration_ms: 0,
es_search_duration_ms: 0,
timed_out: false,
schedule_delay_ms: 3126,
},
],
});
});
test('should format results correctly when action errors occur', () => {
const results = {
aggregations: {
executionUuid: {
meta: {},
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
{
key: 'ecf7ac4c-1c15-4a1d-818a-cacbf57f6158',
doc_count: 32,
timeoutMessage: {
meta: {},
doc_count: 0,
},
alertCounts: {
meta: {},
buckets: {
activeAlerts: {
doc_count: 5,
},
newAlerts: {
doc_count: 5,
},
recoveredAlerts: {
doc_count: 5,
},
},
},
ruleExecution: {
meta: {},
doc_count: 1,
numTriggeredActions: {
value: 5.0,
},
outcomeAndMessage: {
hits: {
total: {
value: 1,
relation: 'eq',
},
max_score: 1.0,
hits: [
{
_index: '.kibana-event-log-8.2.0-000001',
_id: '7xKcb38BcntAq5ycFwiu',
_score: 1.0,
_source: {
event: {
outcome: 'success',
},
message:
"rule executed: example.always-firing:a348a740-9e2c-11ec-bd64-774ed95c43ef: 'test rule'",
},
},
],
},
},
scheduleDelay: {
value: 3.126e9,
},
totalSearchDuration: {
value: 0.0,
},
esSearchDuration: {
value: 0.0,
},
executionDuration: {
value: 1.374e9,
},
executeStartTime: {
value: 1.646844973039e12,
value_as_string: '2022-03-09T16:56:13.039Z',
},
},
actionExecution: {
meta: {},
doc_count: 5,
actionOutcomes: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
{
key: 'failure',
doc_count: 5,
},
],
},
},
},
{
key: '61bb867b-661a-471f-bf92-23471afa10b3',
doc_count: 32,
timeoutMessage: {
meta: {},
doc_count: 0,
},
alertCounts: {
meta: {},
buckets: {
activeAlerts: {
doc_count: 5,
},
newAlerts: {
doc_count: 5,
},
recoveredAlerts: {
doc_count: 5,
},
},
},
ruleExecution: {
meta: {},
doc_count: 1,
numTriggeredActions: {
value: 5.0,
},
outcomeAndMessage: {
hits: {
total: {
value: 1,
relation: 'eq',
},
max_score: 1.0,
hits: [
{
_index: '.kibana-event-log-8.2.0-000001',
_id: 'zRKbb38BcntAq5ycOwgk',
_score: 1.0,
_source: {
event: {
outcome: 'success',
},
message:
"rule executed: example.always-firing:a348a740-9e2c-11ec-bd64-774ed95c43ef: 'test rule'",
},
},
],
},
},
scheduleDelay: {
value: 3.133e9,
},
totalSearchDuration: {
value: 0.0,
},
esSearchDuration: {
value: 0.0,
},
executionDuration: {
value: 4.18e8,
},
executeStartTime: {
value: 1.646844917518e12,
value_as_string: '2022-03-09T16:55:17.518Z',
},
},
actionExecution: {
meta: {},
doc_count: 5,
actionOutcomes: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
{
key: 'success',
doc_count: 5,
},
],
},
},
},
],
},
executionUuidCardinality: {
value: 417,
},
},
};
expect(formatExecutionLogResult(results)).toEqual({
total: 417,
data: [
{
id: 'ecf7ac4c-1c15-4a1d-818a-cacbf57f6158',
timestamp: '2022-03-09T16:56:13.039Z',
duration_ms: 1374,
status: 'success',
message:
"rule executed: example.always-firing:a348a740-9e2c-11ec-bd64-774ed95c43ef: 'test rule'",
num_active_alerts: 5,
num_new_alerts: 5,
num_recovered_alerts: 5,
num_triggered_actions: 5,
num_succeeded_actions: 0,
num_errored_actions: 5,
total_search_duration_ms: 0,
es_search_duration_ms: 0,
timed_out: false,
schedule_delay_ms: 3126,
},
{
id: '61bb867b-661a-471f-bf92-23471afa10b3',
timestamp: '2022-03-09T16:55:17.518Z',
duration_ms: 418,
status: 'success',
message:
"rule executed: example.always-firing:a348a740-9e2c-11ec-bd64-774ed95c43ef: 'test rule'",
num_active_alerts: 5,
num_new_alerts: 5,
num_recovered_alerts: 5,
num_triggered_actions: 5,
num_succeeded_actions: 5,
num_errored_actions: 0,
total_search_duration_ms: 0,
es_search_duration_ms: 0,
timed_out: false,
schedule_delay_ms: 3133,
},
],
});
});
});

View file

@ -0,0 +1,325 @@
/*
* 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 type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import Boom from '@hapi/boom';
import { flatMap, get } from 'lodash';
import { parseDuration } from '.';
import { AggregateEventsBySavedObjectResult } from '../../../event_log/server';
const DEFAULT_MAX_BUCKETS_LIMIT = 1000; // do not retrieve more than this number of executions
const PROVIDER_FIELD = 'event.provider';
const START_FIELD = 'event.start';
const ACTION_FIELD = 'event.action';
const OUTCOME_FIELD = 'event.outcome';
const DURATION_FIELD = 'event.duration';
const MESSAGE_FIELD = 'message';
const SCHEDULE_DELAY_FIELD = 'kibana.task.schedule_delay';
const ES_SEARCH_DURATION_FIELD = 'kibana.alert.rule.execution.metrics.es_search_duration_ms';
const TOTAL_SEARCH_DURATION_FIELD = 'kibana.alert.rule.execution.metrics.total_search_duration_ms';
const NUMBER_OF_TRIGGERED_ACTIONS_FIELD =
'kibana.alert.rule.execution.metrics.number_of_triggered_actions';
const EXECUTION_UUID_FIELD = 'kibana.alert.rule.execution.uuid';
const Millis2Nanos = 1000 * 1000;
export interface IExecutionLog {
id: string;
timestamp: string;
duration_ms: number;
status: string;
message: string;
num_active_alerts: number;
num_new_alerts: number;
num_recovered_alerts: number;
num_triggered_actions: number;
num_succeeded_actions: number;
num_errored_actions: number;
total_search_duration_ms: number;
es_search_duration_ms: number;
schedule_delay_ms: number;
timed_out: boolean;
}
export interface IExecutionLogResult {
total: number;
data: IExecutionLog[];
}
interface IAlertCounts extends estypes.AggregationsMultiBucketAggregateBase {
buckets: {
activeAlerts: estypes.AggregationsSingleBucketAggregateBase;
newAlerts: estypes.AggregationsSingleBucketAggregateBase;
recoveredAlerts: estypes.AggregationsSingleBucketAggregateBase;
};
}
interface IActionExecution
extends estypes.AggregationsTermsAggregateBase<{ key: string; doc_count: number }> {
buckets: Array<{ key: string; doc_count: number }>;
}
interface IExecutionUuidAggBucket extends estypes.AggregationsStringTermsBucketKeys {
timeoutMessage: estypes.AggregationsMultiBucketBase;
ruleExecution: {
executeStartTime: estypes.AggregationsMinAggregate;
executionDuration: estypes.AggregationsMaxAggregate;
scheduleDelay: estypes.AggregationsMaxAggregate;
esSearchDuration: estypes.AggregationsMaxAggregate;
totalSearchDuration: estypes.AggregationsMaxAggregate;
numTriggeredActions: estypes.AggregationsMaxAggregate;
outcomeAndMessage: estypes.AggregationsTopHitsAggregate;
};
alertCounts: IAlertCounts;
actionExecution: {
actionOutcomes: IActionExecution;
};
}
interface ExecutionUuidAggResult<TBucket = IExecutionUuidAggBucket>
extends estypes.AggregationsAggregateBase {
buckets: TBucket[];
}
export interface IExecutionLogAggOptions {
page: number;
perPage: number;
sort: estypes.Sort;
}
const ExecutionLogSortFields: Record<string, string> = {
timestamp: 'ruleExecution>executeStartTime',
execution_duration: 'ruleExecution>executionDuration',
total_search_duration: 'ruleExecution>totalSearchDuration',
es_search_duration: 'ruleExecution>esSearchDuration',
schedule_delay: 'ruleExecution>scheduleDelay',
num_triggered_actions: 'ruleExecution>numTriggeredActions',
};
export function getExecutionLogAggregation({ page, perPage, sort }: IExecutionLogAggOptions) {
// Check if valid sort fields
const sortFields = flatMap(sort as estypes.SortCombinations[], (s) => Object.keys(s));
for (const field of sortFields) {
if (!Object.keys(ExecutionLogSortFields).includes(field)) {
throw Boom.badRequest(
`Invalid sort field "${field}" - must be one of [${Object.keys(ExecutionLogSortFields).join(
','
)}]`
);
}
}
// Check if valid page value
if (page <= 0) {
throw Boom.badRequest(`Invalid page field "${page}" - must be greater than 0`);
}
// Check if valid page value
if (perPage <= 0) {
throw Boom.badRequest(`Invalid perPage field "${perPage}" - must be greater than 0`);
}
return {
// Get total number of executions
executionUuidCardinality: {
cardinality: {
field: EXECUTION_UUID_FIELD,
},
},
executionUuid: {
// Bucket by execution UUID
terms: {
field: EXECUTION_UUID_FIELD,
size: DEFAULT_MAX_BUCKETS_LIMIT,
order: formatSortForTermSort(sort),
},
aggs: {
// Bucket sort to allow paging through executions
executionUuidSorted: {
bucket_sort: {
sort: formatSortForBucketSort(sort),
from: (page - 1) * perPage,
size: perPage,
gap_policy: 'insert_zeros' as estypes.AggregationsGapPolicy,
},
},
// Get counts for types of alerts and whether there was an execution timeout
alertCounts: {
filters: {
filters: {
newAlerts: { match: { [ACTION_FIELD]: 'new-instance' } },
activeAlerts: { match: { [ACTION_FIELD]: 'active-instance' } },
recoveredAlerts: { match: { [ACTION_FIELD]: 'recovered-instance' } },
},
},
},
// Filter by action execute doc and get information from this event
actionExecution: {
filter: getProviderAndActionFilter('actions', 'execute'),
aggs: {
actionOutcomes: {
terms: {
field: OUTCOME_FIELD,
size: 2,
},
},
},
},
// Filter by rule execute doc and get information from this event
ruleExecution: {
filter: getProviderAndActionFilter('alerting', 'execute'),
aggs: {
executeStartTime: {
min: {
field: START_FIELD,
},
},
scheduleDelay: {
max: {
field: SCHEDULE_DELAY_FIELD,
},
},
totalSearchDuration: {
max: {
field: TOTAL_SEARCH_DURATION_FIELD,
},
},
esSearchDuration: {
max: {
field: ES_SEARCH_DURATION_FIELD,
},
},
numTriggeredActions: {
max: {
field: NUMBER_OF_TRIGGERED_ACTIONS_FIELD,
},
},
executionDuration: {
max: {
field: DURATION_FIELD,
},
},
outcomeAndMessage: {
top_hits: {
size: 1,
_source: {
includes: [OUTCOME_FIELD, MESSAGE_FIELD],
},
},
},
},
},
// If there was a timeout, this filter will return non-zero doc count
timeoutMessage: {
filter: getProviderAndActionFilter('alerting', 'execute-timeout'),
},
},
},
};
}
function getProviderAndActionFilter(provider: string, action: string) {
return {
bool: {
must: [
{
match: {
[ACTION_FIELD]: action,
},
},
{
match: {
[PROVIDER_FIELD]: provider,
},
},
],
},
};
}
function formatExecutionLogAggBucket(bucket: IExecutionUuidAggBucket): IExecutionLog {
const durationUs = bucket?.ruleExecution?.executionDuration?.value
? bucket.ruleExecution.executionDuration.value
: 0;
const scheduleDelayUs = bucket?.ruleExecution?.scheduleDelay?.value
? bucket.ruleExecution.scheduleDelay.value
: 0;
const timedOut = (bucket?.timeoutMessage?.doc_count ?? 0) > 0;
const actionExecutionOutcomes = bucket?.actionExecution?.actionOutcomes?.buckets ?? [];
const actionExecutionSuccess =
actionExecutionOutcomes.find((subBucket) => subBucket?.key === 'success')?.doc_count ?? 0;
const actionExecutionError =
actionExecutionOutcomes.find((subBucket) => subBucket?.key === 'failure')?.doc_count ?? 0;
return {
id: bucket?.key ?? '',
timestamp: bucket?.ruleExecution?.executeStartTime.value_as_string ?? '',
duration_ms: durationUs / Millis2Nanos,
status: bucket?.ruleExecution?.outcomeAndMessage?.hits?.hits[0]?._source?.event?.outcome,
message: bucket?.ruleExecution?.outcomeAndMessage?.hits?.hits[0]?._source?.message,
num_active_alerts: bucket?.alertCounts?.buckets?.activeAlerts?.doc_count ?? 0,
num_new_alerts: bucket?.alertCounts?.buckets?.newAlerts?.doc_count ?? 0,
num_recovered_alerts: bucket?.alertCounts?.buckets?.recoveredAlerts?.doc_count ?? 0,
num_triggered_actions: bucket?.ruleExecution?.numTriggeredActions?.value ?? 0,
num_succeeded_actions: actionExecutionSuccess,
num_errored_actions: actionExecutionError,
total_search_duration_ms: bucket?.ruleExecution?.totalSearchDuration?.value ?? 0,
es_search_duration_ms: bucket?.ruleExecution?.esSearchDuration?.value ?? 0,
schedule_delay_ms: scheduleDelayUs / Millis2Nanos,
timed_out: timedOut,
};
}
export function formatExecutionLogResult(
results: AggregateEventsBySavedObjectResult
): IExecutionLogResult {
const { aggregations } = results;
if (!aggregations) {
return {
total: 0,
data: [],
};
}
const total = (aggregations.executionUuidCardinality as estypes.AggregationsCardinalityAggregate)
.value;
const buckets = (aggregations.executionUuid as ExecutionUuidAggResult).buckets;
return {
total,
data: buckets.map((bucket: IExecutionUuidAggBucket) => formatExecutionLogAggBucket(bucket)),
};
}
export function getNumExecutions(dateStart: Date, dateEnd: Date, ruleSchedule: string) {
const durationInMillis = dateEnd.getTime() - dateStart.getTime();
const scheduleMillis = parseDuration(ruleSchedule);
const numExecutions = Math.ceil(durationInMillis / scheduleMillis);
return Math.min(numExecutions < 0 ? 0 : numExecutions, DEFAULT_MAX_BUCKETS_LIMIT);
}
export function formatSortForBucketSort(sort: estypes.Sort) {
return (sort as estypes.SortCombinations[]).map((s) =>
Object.keys(s).reduce(
(acc, curr) => ({ ...acc, [ExecutionLogSortFields[curr]]: get(s, curr) }),
{}
)
);
}
export function formatSortForTermSort(sort: estypes.Sort) {
return (sort as estypes.SortCombinations[]).map((s) =>
Object.keys(s).reduce(
(acc, curr) => ({ ...acc, [ExecutionLogSortFields[curr]]: get(s, `${curr}.order`) }),
{}
)
);
}

View file

@ -0,0 +1,140 @@
/*
* 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 { getRuleExecutionLogRoute } from './get_rule_execution_log';
import { httpServiceMock } from 'src/core/server/mocks';
import { licenseStateMock } from '../lib/license_state.mock';
import { mockHandlerArguments } from './_mock_handler_arguments';
import { SavedObjectsErrorHelpers } from 'src/core/server';
import { rulesClientMock } from '../rules_client.mock';
import { IExecutionLogResult } from '../lib/get_execution_log_aggregation';
const rulesClient = rulesClientMock.create();
jest.mock('../lib/license_api_access.ts', () => ({
verifyApiAccess: jest.fn(),
}));
beforeEach(() => {
jest.resetAllMocks();
});
describe('getRuleExecutionLogRoute', () => {
const dateString = new Date().toISOString();
const mockedExecutionLog: IExecutionLogResult = {
total: 374,
data: [
{
id: '6705da7d-2635-499d-a6a8-1aee1ae1eac9',
timestamp: '2022-03-07T15:38:32.617Z',
duration_ms: 1056,
status: 'success',
message:
"rule executed: example.always-firing:a348a740-9e2c-11ec-bd64-774ed95c43ef: 'test rule'",
num_active_alerts: 5,
num_new_alerts: 5,
num_recovered_alerts: 0,
num_triggered_actions: 5,
num_succeeded_actions: 5,
num_errored_actions: 0,
total_search_duration_ms: 0,
es_search_duration_ms: 0,
timed_out: false,
schedule_delay_ms: 3126,
},
{
id: '41b2755e-765a-4044-9745-b03875d5e79a',
timestamp: '2022-03-07T15:39:05.604Z',
duration_ms: 1165,
status: 'success',
message:
"rule executed: example.always-firing:a348a740-9e2c-11ec-bd64-774ed95c43ef: 'test rule'",
num_active_alerts: 5,
num_new_alerts: 5,
num_recovered_alerts: 5,
num_triggered_actions: 5,
num_succeeded_actions: 5,
num_errored_actions: 0,
total_search_duration_ms: 0,
es_search_duration_ms: 0,
timed_out: false,
schedule_delay_ms: 3008,
},
],
};
it('gets rule execution log', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();
getRuleExecutionLogRoute(router, licenseState);
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rule/{id}/_execution_log"`);
rulesClient.getExecutionLogForRule.mockResolvedValue(mockedExecutionLog);
const [context, req, res] = mockHandlerArguments(
{ rulesClient },
{
params: {
id: '1',
},
query: {
date_start: dateString,
per_page: 10,
page: 1,
sort: [{ timestamp: { order: 'desc' } }],
},
},
['ok']
);
await handler(context, req, res);
expect(rulesClient.getExecutionLogForRule).toHaveBeenCalledTimes(1);
expect(rulesClient.getExecutionLogForRule.mock.calls[0]).toEqual([
{
dateStart: dateString,
id: '1',
page: 1,
perPage: 10,
sort: [{ timestamp: { order: 'desc' } }],
},
]);
expect(res.ok).toHaveBeenCalled();
});
it('returns NOT-FOUND when rule is not found', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();
getRuleExecutionLogRoute(router, licenseState);
const [, handler] = router.get.mock.calls[0];
rulesClient.getExecutionLogForRule = jest
.fn()
.mockRejectedValueOnce(SavedObjectsErrorHelpers.createGenericNotFoundError('alert', '1'));
const [context, req, res] = mockHandlerArguments(
{ rulesClient },
{
params: {
id: '1',
},
query: {},
},
['notFound']
);
expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(
`[Error: Saved object [alert/1] not found]`
);
});
});

View file

@ -0,0 +1,77 @@
/*
* 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 { IRouter } from 'kibana/server';
import { schema } from '@kbn/config-schema';
import { ILicenseState } from '../lib';
import { GetExecutionLogByIdParams } from '../rules_client';
import { RewriteRequestCase, verifyAccessAndContext } from './lib';
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types';
const paramSchema = schema.object({
id: schema.string(),
});
const sortOrderSchema = schema.oneOf([schema.literal('asc'), schema.literal('desc')]);
const sortFieldSchema = schema.oneOf([
schema.object({ timestamp: schema.object({ order: sortOrderSchema }) }),
schema.object({ execution_duration: schema.object({ order: sortOrderSchema }) }),
schema.object({ total_search_duration: schema.object({ order: sortOrderSchema }) }),
schema.object({ es_search_duration: schema.object({ order: sortOrderSchema }) }),
schema.object({ schedule_delay: schema.object({ order: sortOrderSchema }) }),
schema.object({ num_triggered_actions: schema.object({ order: sortOrderSchema }) }),
]);
const sortFieldsSchema = schema.arrayOf(sortFieldSchema, {
defaultValue: [{ timestamp: { order: 'desc' } }],
});
const querySchema = schema.object({
date_start: schema.string(),
date_end: schema.maybe(schema.string()),
filter: schema.maybe(schema.string()),
per_page: schema.number({ defaultValue: 10, min: 1 }),
page: schema.number({ defaultValue: 1, min: 1 }),
sort: sortFieldsSchema,
});
const rewriteReq: RewriteRequestCase<GetExecutionLogByIdParams> = ({
date_start: dateStart,
date_end: dateEnd,
per_page: perPage,
...rest
}) => ({
...rest,
dateStart,
dateEnd,
perPage,
});
export const getRuleExecutionLogRoute = (
router: IRouter<AlertingRequestHandlerContext>,
licenseState: ILicenseState
) => {
router.get(
{
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rule/{id}/_execution_log`,
validate: {
params: paramSchema,
query: querySchema,
},
},
router.handleLegacyErrors(
verifyAccessAndContext(licenseState, async function (context, req, res) {
const rulesClient = context.alerting.getRulesClient();
const { id } = req.params;
return res.ok({
body: await rulesClient.getExecutionLogForRule(rewriteReq({ id, ...req.query })),
});
})
)
);
};

View file

@ -20,6 +20,7 @@ import { disableRuleRoute } from './disable_rule';
import { enableRuleRoute } from './enable_rule';
import { findRulesRoute, findInternalRulesRoute } from './find_rules';
import { getRuleAlertSummaryRoute } from './get_rule_alert_summary';
import { getRuleExecutionLogRoute } from './get_rule_execution_log';
import { getRuleStateRoute } from './get_rule_state';
import { healthRoute } from './health';
import { resolveRuleRoute } from './resolve_rule';
@ -54,6 +55,7 @@ export function defineRoutes(opts: RouteOptions) {
findRulesRoute(router, licenseState, usageCounter);
findInternalRulesRoute(router, licenseState, usageCounter);
getRuleAlertSummaryRoute(router, licenseState);
getRuleExecutionLogRoute(router, licenseState);
getRuleStateRoute(router, licenseState);
healthRoute(router, licenseState, encryptedSavedObjects);
ruleTypesRoute(router, licenseState);

View file

@ -30,6 +30,7 @@ const createRulesClientMock = () => {
unmuteInstance: jest.fn(),
listAlertTypes: jest.fn(),
getAlertSummary: jest.fn(),
getExecutionLogForRule: jest.fn(),
getSpaceId: jest.fn(),
snooze: jest.fn(),
};

View file

@ -23,6 +23,7 @@ export enum RuleAuditAction {
MUTE_ALERT = 'rule_alert_mute',
UNMUTE_ALERT = 'rule_alert_unmute',
AGGREGATE = 'rule_aggregate',
GET_EXECUTION_LOG = 'rule_get_execution_log',
SNOOZE = 'rule_snooze',
}
@ -43,6 +44,11 @@ const eventVerbs: Record<RuleAuditAction, VerbsTuple> = {
rule_alert_mute: ['mute alert of', 'muting alert of', 'muted alert of'],
rule_alert_unmute: ['unmute alert of', 'unmuting alert of', 'unmuted alert of'],
rule_aggregate: ['access', 'accessing', 'accessed'],
rule_get_execution_log: [
'access execution log for',
'accessing execution log for',
'accessed execution log for',
],
rule_snooze: ['snooze', 'snoozing', 'snoozed'],
};
@ -61,6 +67,7 @@ const eventTypes: Record<RuleAuditAction, EcsEventType> = {
rule_alert_mute: 'change',
rule_alert_unmute: 'change',
rule_aggregate: 'access',
rule_get_execution_log: 'access',
rule_snooze: 'change',
};

View file

@ -84,6 +84,11 @@ import {
getModifiedSearch,
modifyFilterKueryNode,
} from './lib/mapped_params_utils';
import {
formatExecutionLogResult,
getExecutionLogAggregation,
IExecutionLogResult,
} from '../lib/get_execution_log_aggregation';
import { validateSnoozeDate } from '../lib/validate_snooze_date';
import { RuleMutedError } from '../lib/errors/rule_muted';
@ -235,6 +240,16 @@ export interface GetAlertSummaryParams {
numberOfExecutions?: number;
}
export interface GetExecutionLogByIdParams {
id: string;
dateStart: string;
dateEnd?: string;
filter?: string;
page: number;
perPage: number;
sort: estypes.Sort;
}
// NOTE: Changing this prefix will require a migration to update the prefix in all existing `rule` saved objects
const extractedSavedObjectParamReferenceNamePrefix = 'param:';
@ -639,6 +654,70 @@ export class RulesClient {
});
}
public async getExecutionLogForRule({
id,
dateStart,
dateEnd,
filter,
page,
perPage,
sort,
}: GetExecutionLogByIdParams): Promise<IExecutionLogResult> {
this.logger.debug(`getExecutionLogForRule(): getting execution log for rule ${id}`);
const rule = (await this.get({ id, includeLegacyId: true })) as SanitizedRuleWithLegacyId;
try {
// Make sure user has access to this rule
await this.authorization.ensureAuthorized({
ruleTypeId: rule.alertTypeId,
consumer: rule.consumer,
operation: ReadOperations.GetExecutionLog,
entity: AlertingAuthorizationEntity.Rule,
});
} catch (error) {
this.auditLogger?.log(
ruleAuditEvent({
action: RuleAuditAction.GET_EXECUTION_LOG,
savedObject: { type: 'alert', id },
error,
})
);
throw error;
}
this.auditLogger?.log(
ruleAuditEvent({
action: RuleAuditAction.GET_EXECUTION_LOG,
savedObject: { type: 'alert', id },
})
);
// default duration of instance summary is 60 * rule interval
const dateNow = new Date();
const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow);
const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow);
const eventLogClient = await this.getEventLogClient();
const results = await eventLogClient.aggregateEventsBySavedObjectIds(
'alert',
[id],
{
start: parsedDateStart.toISOString(),
end: parsedDateEnd.toISOString(),
filter,
aggs: getExecutionLogAggregation({
page,
perPage,
sort,
}),
},
rule.legacyId !== null ? [rule.legacyId] : undefined
);
return formatExecutionLogResult(results);
}
public async find<Params extends RuleTypeParams = never>({
options: { fields, ...options } = {},
excludeFromPublicApi = false,

View file

@ -0,0 +1,613 @@
/*
* 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 type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks';
import { taskManagerMock } from '../../../../task_manager/server/mocks';
import { ruleTypeRegistryMock } from '../../rule_type_registry.mock';
import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock';
import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks';
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertingAuthorization } from '../../authorization/alerting_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { eventLogClientMock } from '../../../../event_log/server/mocks';
import { SavedObject } from 'kibana/server';
import { RawRule } from '../../types';
import { auditLoggerMock } from '../../../../security/server/audit/mocks';
import { getBeforeSetup, mockedDateString, setGlobalDate } from './lib';
import { getExecutionLogAggregation } from '../../lib/get_execution_log_aggregation';
const taskManager = taskManagerMock.createStart();
const ruleTypeRegistry = ruleTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const eventLogClient = eventLogClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
taskManager,
ruleTypeRegistry,
unsecuredSavedObjectsClient,
authorization: authorization as unknown as AlertingAuthorization,
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
minimumScheduleInterval: '1m',
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
auditLogger,
};
beforeEach(() => {
getBeforeSetup(rulesClientParams, taskManager, ruleTypeRegistry, eventLogClient);
(auditLogger.log as jest.Mock).mockClear();
});
setGlobalDate();
const RuleIntervalSeconds = 1;
const BaseRuleSavedObject: SavedObject<RawRule> = {
id: '1',
type: 'alert',
attributes: {
enabled: true,
name: 'rule-name',
tags: ['tag-1', 'tag-2'],
alertTypeId: '123',
consumer: 'rule-consumer',
legacyId: null,
schedule: { interval: `${RuleIntervalSeconds}s` },
actions: [],
params: {},
createdBy: null,
updatedBy: null,
createdAt: mockedDateString,
updatedAt: mockedDateString,
apiKey: null,
apiKeyOwner: null,
throttle: null,
notifyWhen: null,
muteAll: false,
mutedInstanceIds: [],
executionStatus: {
status: 'unknown',
lastExecutionDate: '2020-08-20T19:23:38Z',
error: null,
warning: null,
},
},
references: [],
};
const aggregateResults = {
aggregations: {
executionUuid: {
meta: {},
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
{
key: '6705da7d-2635-499d-a6a8-1aee1ae1eac9',
doc_count: 27,
timeoutMessage: {
meta: {},
doc_count: 0,
},
alertCounts: {
meta: {},
buckets: {
activeAlerts: {
doc_count: 5,
},
newAlerts: {
doc_count: 5,
},
recoveredAlerts: {
doc_count: 0,
},
},
},
ruleExecution: {
meta: {},
doc_count: 1,
numTriggeredActions: {
value: 5.0,
},
outcomeAndMessage: {
hits: {
total: {
value: 1,
relation: 'eq',
},
max_score: 1.0,
hits: [
{
_index: '.kibana-event-log-8.2.0-000001',
_id: 'S4wIZX8B8TGQpG7XQZns',
_score: 1.0,
_source: {
event: {
outcome: 'success',
},
message:
"rule executed: example.always-firing:a348a740-9e2c-11ec-bd64-774ed95c43ef: 'test rule'",
},
},
],
},
},
scheduleDelay: {
value: 3.126e9,
},
totalSearchDuration: {
value: 0.0,
},
esSearchDuration: {
value: 0.0,
},
executionDuration: {
value: 1.056e9,
},
executeStartTime: {
value: 1.646667512617e12,
value_as_string: '2022-03-07T15:38:32.617Z',
},
},
actionExecution: {
meta: {},
doc_count: 5,
actionOutcomes: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
{
key: 'success',
doc_count: 5,
},
],
},
},
},
{
key: '41b2755e-765a-4044-9745-b03875d5e79a',
doc_count: 32,
timeoutMessage: {
meta: {},
doc_count: 0,
},
alertCounts: {
meta: {},
buckets: {
activeAlerts: {
doc_count: 5,
},
newAlerts: {
doc_count: 5,
},
recoveredAlerts: {
doc_count: 5,
},
},
},
ruleExecution: {
meta: {},
doc_count: 1,
numTriggeredActions: {
value: 5.0,
},
outcomeAndMessage: {
hits: {
total: {
value: 1,
relation: 'eq',
},
max_score: 1.0,
hits: [
{
_index: '.kibana-event-log-8.2.0-000001',
_id: 'a4wIZX8B8TGQpG7Xwpnz',
_score: 1.0,
_source: {
event: {
outcome: 'success',
},
message:
"rule executed: example.always-firing:a348a740-9e2c-11ec-bd64-774ed95c43ef: 'test rule'",
},
},
],
},
},
scheduleDelay: {
value: 3.345e9,
},
totalSearchDuration: {
value: 0.0,
},
esSearchDuration: {
value: 0.0,
},
executionDuration: {
value: 1.165e9,
},
executeStartTime: {
value: 1.646667545604e12,
value_as_string: '2022-03-07T15:39:05.604Z',
},
},
actionExecution: {
meta: {},
doc_count: 5,
actionOutcomes: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
{
key: 'success',
doc_count: 5,
},
],
},
},
},
],
},
executionUuidCardinality: {
value: 374,
},
},
};
function getRuleSavedObject(attributes: Partial<RawRule> = {}): SavedObject<RawRule> {
return {
...BaseRuleSavedObject,
attributes: { ...BaseRuleSavedObject.attributes, ...attributes },
};
}
function getExecutionLogByIdParams(overwrites = {}) {
return {
id: '1',
dateStart: new Date(Date.now() - 3600000).toISOString(),
page: 1,
perPage: 10,
sort: [{ timestamp: { order: 'desc' } }] as estypes.Sort,
...overwrites,
};
}
describe('getExecutionLogForRule()', () => {
let rulesClient: RulesClient;
beforeEach(() => {
rulesClient = new RulesClient(rulesClientParams);
});
test('runs as expected with some event log aggregation data', async () => {
const ruleSO = getRuleSavedObject({});
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(ruleSO);
eventLogClient.aggregateEventsBySavedObjectIds.mockResolvedValueOnce(aggregateResults);
const result = await rulesClient.getExecutionLogForRule(getExecutionLogByIdParams());
expect(result).toEqual({
total: 374,
data: [
{
id: '6705da7d-2635-499d-a6a8-1aee1ae1eac9',
timestamp: '2022-03-07T15:38:32.617Z',
duration_ms: 1056,
status: 'success',
message:
"rule executed: example.always-firing:a348a740-9e2c-11ec-bd64-774ed95c43ef: 'test rule'",
num_active_alerts: 5,
num_new_alerts: 5,
num_recovered_alerts: 0,
num_triggered_actions: 5,
num_succeeded_actions: 5,
num_errored_actions: 0,
total_search_duration_ms: 0,
es_search_duration_ms: 0,
timed_out: false,
schedule_delay_ms: 3126,
},
{
id: '41b2755e-765a-4044-9745-b03875d5e79a',
timestamp: '2022-03-07T15:39:05.604Z',
duration_ms: 1165,
status: 'success',
message:
"rule executed: example.always-firing:a348a740-9e2c-11ec-bd64-774ed95c43ef: 'test rule'",
num_active_alerts: 5,
num_new_alerts: 5,
num_recovered_alerts: 5,
num_triggered_actions: 5,
num_succeeded_actions: 5,
num_errored_actions: 0,
total_search_duration_ms: 0,
es_search_duration_ms: 0,
timed_out: false,
schedule_delay_ms: 3345,
},
],
});
});
// Further tests don't check the result of `getExecutionLogForRule()`, as the result
// is just the result from the `formatExecutionLogResult()`, which itself
// has a complete set of tests.
test('calls saved objects and event log client with default params', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getRuleSavedObject());
eventLogClient.aggregateEventsBySavedObjectIds.mockResolvedValueOnce(aggregateResults);
await rulesClient.getExecutionLogForRule(getExecutionLogByIdParams());
expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
expect(eventLogClient.aggregateEventsBySavedObjectIds).toHaveBeenCalledTimes(1);
expect(eventLogClient.aggregateEventsBySavedObjectIds.mock.calls[0]).toEqual([
'alert',
['1'],
{
aggs: getExecutionLogAggregation({
page: 1,
perPage: 10,
sort: [{ timestamp: { order: 'desc' } }],
}),
end: mockedDateString,
start: '2019-02-12T20:01:22.479Z',
},
undefined,
]);
});
test('calls event log client with legacy ids param', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(
getRuleSavedObject({ legacyId: '99999' })
);
eventLogClient.aggregateEventsBySavedObjectIds.mockResolvedValueOnce(aggregateResults);
await rulesClient.getExecutionLogForRule(getExecutionLogByIdParams());
expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
expect(eventLogClient.aggregateEventsBySavedObjectIds).toHaveBeenCalledTimes(1);
expect(eventLogClient.aggregateEventsBySavedObjectIds.mock.calls[0]).toEqual([
'alert',
['1'],
{
aggs: getExecutionLogAggregation({
page: 1,
perPage: 10,
sort: [{ timestamp: { order: 'desc' } }],
}),
end: mockedDateString,
start: '2019-02-12T20:01:22.479Z',
},
['99999'],
]);
});
test('calls event log client with end date if specified', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getRuleSavedObject());
eventLogClient.aggregateEventsBySavedObjectIds.mockResolvedValueOnce(aggregateResults);
await rulesClient.getExecutionLogForRule(
getExecutionLogByIdParams({ dateEnd: new Date(Date.now() - 2700000).toISOString() })
);
expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
expect(eventLogClient.aggregateEventsBySavedObjectIds).toHaveBeenCalledTimes(1);
expect(eventLogClient.aggregateEventsBySavedObjectIds.mock.calls[0]).toEqual([
'alert',
['1'],
{
aggs: getExecutionLogAggregation({
page: 1,
perPage: 10,
sort: [{ timestamp: { order: 'desc' } }],
}),
end: '2019-02-12T20:16:22.479Z',
start: '2019-02-12T20:01:22.479Z',
},
undefined,
]);
});
test('calls event log client with filter if specified', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getRuleSavedObject());
eventLogClient.aggregateEventsBySavedObjectIds.mockResolvedValueOnce(aggregateResults);
await rulesClient.getExecutionLogForRule(
getExecutionLogByIdParams({ filter: 'event.outcome: success' })
);
expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
expect(eventLogClient.aggregateEventsBySavedObjectIds).toHaveBeenCalledTimes(1);
expect(eventLogClient.aggregateEventsBySavedObjectIds.mock.calls[0]).toEqual([
'alert',
['1'],
{
aggs: getExecutionLogAggregation({
page: 1,
perPage: 10,
sort: [{ timestamp: { order: 'desc' } }],
}),
filter: 'event.outcome: success',
end: mockedDateString,
start: '2019-02-12T20:01:22.479Z',
},
undefined,
]);
});
test('invalid start date throws an error', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getRuleSavedObject());
eventLogClient.aggregateEventsBySavedObjectIds.mockResolvedValueOnce(aggregateResults);
const dateStart = 'ain"t no way this will get parsed as a date';
expect(
rulesClient.getExecutionLogForRule(getExecutionLogByIdParams({ dateStart }))
).rejects.toMatchInlineSnapshot(
`[Error: Invalid date for parameter dateStart: "ain"t no way this will get parsed as a date"]`
);
});
test('invalid end date throws an error', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getRuleSavedObject());
eventLogClient.aggregateEventsBySavedObjectIds.mockResolvedValueOnce(aggregateResults);
const dateEnd = 'ain"t no way this will get parsed as a date';
expect(
rulesClient.getExecutionLogForRule(getExecutionLogByIdParams({ dateEnd }))
).rejects.toMatchInlineSnapshot(
`[Error: Invalid date for parameter dateEnd: "ain"t no way this will get parsed as a date"]`
);
});
test('invalid page value throws an error', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getRuleSavedObject());
eventLogClient.aggregateEventsBySavedObjectIds.mockResolvedValueOnce(aggregateResults);
expect(
rulesClient.getExecutionLogForRule(getExecutionLogByIdParams({ page: -3 }))
).rejects.toMatchInlineSnapshot(`[Error: Invalid page field "-3" - must be greater than 0]`);
});
test('invalid perPage value throws an error', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getRuleSavedObject());
eventLogClient.aggregateEventsBySavedObjectIds.mockResolvedValueOnce(aggregateResults);
expect(
rulesClient.getExecutionLogForRule(getExecutionLogByIdParams({ perPage: -3 }))
).rejects.toMatchInlineSnapshot(`[Error: Invalid perPage field "-3" - must be greater than 0]`);
});
test('invalid sort value throws an error', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getRuleSavedObject());
eventLogClient.aggregateEventsBySavedObjectIds.mockResolvedValueOnce(aggregateResults);
expect(
rulesClient.getExecutionLogForRule(
getExecutionLogByIdParams({ sort: [{ foo: { order: 'desc' } }] })
)
).rejects.toMatchInlineSnapshot(
`[Error: Invalid sort field "foo" - must be one of [timestamp,execution_duration,total_search_duration,es_search_duration,schedule_delay,num_triggered_actions]]`
);
});
test('throws error when saved object get throws an error', async () => {
unsecuredSavedObjectsClient.get.mockRejectedValueOnce(new Error('OMG!'));
eventLogClient.aggregateEventsBySavedObjectIds.mockResolvedValueOnce(aggregateResults);
expect(
rulesClient.getExecutionLogForRule(getExecutionLogByIdParams())
).rejects.toMatchInlineSnapshot(`[Error: OMG!]`);
});
test('throws error when eventLog.aggregateEventsBySavedObjectIds throws an error', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getRuleSavedObject());
eventLogClient.aggregateEventsBySavedObjectIds.mockRejectedValueOnce(new Error('OMG 2!'));
expect(
rulesClient.getExecutionLogForRule(getExecutionLogByIdParams())
).rejects.toMatchInlineSnapshot(`[Error: OMG 2!]`);
});
describe('authorization', () => {
beforeEach(() => {
const ruleSO = getRuleSavedObject({});
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(ruleSO);
});
test('ensures user is authorised to get this type of alert under the consumer', async () => {
eventLogClient.aggregateEventsBySavedObjectIds.mockResolvedValueOnce(aggregateResults);
await rulesClient.getExecutionLogForRule(getExecutionLogByIdParams());
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
entity: 'rule',
consumer: 'rule-consumer',
operation: 'get',
ruleTypeId: '123',
});
});
test('throws when user is not authorised to get this type of alert', async () => {
authorization.ensureAuthorized.mockRejectedValueOnce(
new Error(`Unauthorized to get a "myType" alert for "myApp"`)
);
await expect(
rulesClient.getExecutionLogForRule(getExecutionLogByIdParams())
).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to get a "myType" alert for "myApp"]`);
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
entity: 'rule',
consumer: 'rule-consumer',
operation: 'get',
ruleTypeId: '123',
});
});
});
describe('auditLogger', () => {
beforeEach(() => {
const ruleSO = getRuleSavedObject({});
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(ruleSO);
});
test('logs audit event when getting a rule execution log', async () => {
eventLogClient.aggregateEventsBySavedObjectIds.mockResolvedValueOnce(aggregateResults);
await rulesClient.getExecutionLogForRule(getExecutionLogByIdParams());
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
action: 'rule_get_execution_log',
outcome: 'success',
}),
kibana: { saved_object: { id: '1', type: 'alert' } },
})
);
});
test('logs audit event when not authorised to get a rule', async () => {
// first call occurs during rule SO get
authorization.ensureAuthorized.mockResolvedValueOnce();
authorization.ensureAuthorized.mockRejectedValueOnce(new Error('Unauthorized'));
await expect(
rulesClient.getExecutionLogForRule(getExecutionLogByIdParams())
).rejects.toMatchInlineSnapshot(`[Error: Unauthorized]`);
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
action: 'rule_get_execution_log',
outcome: 'failure',
}),
kibana: {
saved_object: {
id: '1',
type: 'alert',
},
},
error: {
code: 'Error',
message: 'Unauthorized',
},
})
);
});
});
});

View file

@ -87,6 +87,7 @@ describe(`feature_privilege_builder`, () => {
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/get",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleState",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/getExecutionLog",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/find",
]
`);
@ -169,6 +170,7 @@ describe(`feature_privilege_builder`, () => {
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/get",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleState",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/getExecutionLog",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/find",
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/get",
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/find",
@ -211,6 +213,7 @@ describe(`feature_privilege_builder`, () => {
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/get",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleState",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/getExecutionLog",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/find",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/create",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/delete",
@ -304,6 +307,7 @@ describe(`feature_privilege_builder`, () => {
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/get",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleState",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/getExecutionLog",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/find",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/create",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/delete",
@ -357,6 +361,7 @@ describe(`feature_privilege_builder`, () => {
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/get",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleState",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/getExecutionLog",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/find",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/create",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/delete",
@ -371,6 +376,7 @@ describe(`feature_privilege_builder`, () => {
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/get",
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getRuleState",
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getAlertSummary",
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getExecutionLog",
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/find",
]
`);
@ -456,6 +462,7 @@ describe(`feature_privilege_builder`, () => {
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/get",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleState",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/getExecutionLog",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/find",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/create",
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/delete",
@ -470,6 +477,7 @@ describe(`feature_privilege_builder`, () => {
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/get",
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getRuleState",
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getAlertSummary",
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getExecutionLog",
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/find",
"alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/get",
"alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/find",

View file

@ -16,7 +16,7 @@ enum AlertingEntity {
}
const readOperations: Record<AlertingEntity, string[]> = {
rule: ['get', 'getRuleState', 'getAlertSummary', 'find'],
rule: ['get', 'getRuleState', 'getAlertSummary', 'getExecutionLog', 'find'],
alert: ['get', 'find'],
};

View file

@ -0,0 +1,456 @@
/*
* 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 expect from '@kbn/expect';
import { Spaces } from '../../scenarios';
import {
getUrlPrefix,
ObjectRemover,
getTestRuleData,
getEventLog,
ESTestIndexTool,
} from '../../../common/lib';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function createGetExecutionLogTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const retry = getService('retry');
const es = getService('es');
const esTestIndexTool = new ESTestIndexTool(es, retry);
const dateStart = new Date(Date.now() - 600000).toISOString();
describe('getExecutionLog', () => {
const objectRemover = new ObjectRemover(supertest);
beforeEach(async () => {
await esTestIndexTool.destroy();
await esTestIndexTool.setup();
});
afterEach(() => objectRemover.removeAll());
it(`handles non-existent rule`, async () => {
await supertest
.get(
`${getUrlPrefix(
Spaces.space1.id
)}/internal/alerting/rule/1/_execution_log?date_start=${dateStart}`
)
.expect(404, {
statusCode: 404,
error: 'Not Found',
message: 'Saved object [alert/1] not found',
});
});
it('gets execution log for rule with executions', async () => {
const { body: createdRule } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '15s' } }))
.expect(200);
objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting');
await waitForEvents(createdRule.id, 'alerting', new Map([['execute', { gte: 2 }]]));
const response = await supertest.get(
`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${
createdRule.id
}/_execution_log?date_start=${dateStart}`
);
expect(response.status).to.eql(200);
expect(response.body.total).to.eql(2);
const execLogs = response.body.data;
expect(execLogs.length).to.eql(2);
let previousTimestamp: string | null = null;
for (const log of execLogs) {
if (previousTimestamp) {
// default sort is `desc` by timestamp
expect(Date.parse(log.timestamp)).to.be.lessThan(Date.parse(previousTimestamp));
}
previousTimestamp = log.timstamp;
expect(Date.parse(log.timestamp)).to.be.greaterThan(Date.parse(dateStart));
expect(Date.parse(log.timestamp)).to.be.lessThan(Date.parse(new Date().toISOString()));
expect(log.duration_ms).to.be.greaterThan(0);
expect(log.schedule_delay_ms).to.be.greaterThan(0);
expect(log.status).to.equal('success');
expect(log.timed_out).to.equal(false);
// no-op rule doesn't generate alerts
expect(log.num_active_alerts).to.equal(0);
expect(log.num_new_alerts).to.equal(0);
expect(log.num_recovered_alerts).to.equal(0);
expect(log.num_triggered_actions).to.equal(0);
expect(log.num_succeeded_actions).to.equal(0);
expect(log.num_errored_actions).to.equal(0);
// no-op rule doesn't query ES
expect(log.total_search_duration_ms).to.equal(0);
expect(log.es_search_duration_ms).to.equal(0);
}
});
it('gets execution log for rule with no executions', async () => {
const { body: createdRule } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '15s' } }))
.expect(200);
objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting');
const response = await supertest.get(
`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${
createdRule.id
}/_execution_log?date_start=${dateStart}`
);
expect(response.status).to.eql(200);
expect(response.body.total).to.eql(0);
expect(response.body.data).to.eql([]);
});
it('gets execution log for rule that performs ES searches', async () => {
const { body: createdRule } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
rule_type_id: 'test.multipleSearches',
params: {
numSearches: 2,
delay: `2s`,
},
})
)
.expect(200);
objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting');
await waitForEvents(createdRule.id, 'alerting', new Map([['execute', { gte: 1 }]]));
const response = await supertest.get(
`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${
createdRule.id
}/_execution_log?date_start=${dateStart}`
);
expect(response.status).to.eql(200);
expect(response.body.total).to.eql(1);
const execLogs = response.body.data;
expect(execLogs.length).to.eql(1);
for (const log of execLogs) {
expect(log.duration_ms).to.be.greaterThan(0);
expect(log.schedule_delay_ms).to.be.greaterThan(0);
expect(log.status).to.equal('success');
expect(log.timed_out).to.equal(false);
// no-op rule doesn't generate alerts
expect(log.num_active_alerts).to.equal(0);
expect(log.num_new_alerts).to.equal(0);
expect(log.num_recovered_alerts).to.equal(0);
expect(log.num_triggered_actions).to.equal(0);
expect(log.num_succeeded_actions).to.equal(0);
expect(log.num_errored_actions).to.equal(0);
// rule executes 2 searches with delay of 2 seconds each
// setting compare threshold lower to avoid flakiness
expect(log.total_search_duration_ms).to.be.greaterThan(2000);
expect(log.es_search_duration_ms).to.be.greaterThan(2000);
}
});
it('gets execution log for rule that errors', async () => {
const { body: createdRule } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
rule_type_id: 'test.throw',
})
)
.expect(200);
objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting');
await waitForEvents(createdRule.id, 'alerting', new Map([['execute', { gte: 1 }]]));
const response = await supertest.get(
`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${
createdRule.id
}/_execution_log?date_start=${dateStart}`
);
expect(response.status).to.eql(200);
expect(response.body.total).to.eql(1);
const execLogs = response.body.data;
expect(execLogs.length).to.eql(1);
for (const log of execLogs) {
expect(log.status).to.equal('failure');
expect(log.timed_out).to.equal(false);
}
});
it('gets execution log for rule that times out', async () => {
const { body: createdRule } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
rule_type_id: 'test.patternLongRunning',
params: {
pattern: [true, true, true, true],
},
})
)
.expect(200);
objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting');
await waitForEvents(createdRule.id, 'alerting', new Map([['execute', { gte: 1 }]]));
const response = await supertest.get(
`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${
createdRule.id
}/_execution_log?date_start=${dateStart}`
);
expect(response.status).to.eql(200);
expect(response.body.total).to.eql(1);
const execLogs = response.body.data;
expect(execLogs.length).to.eql(1);
for (const log of execLogs) {
expect(log.status).to.equal('success');
expect(log.timed_out).to.equal(true);
}
});
it('gets execution log for rule that triggers actions', async () => {
const { body: createdConnector } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.send({
name: 'noop connector',
connector_type_id: 'test.noop',
config: {},
secrets: {},
})
.expect(200);
objectRemover.add(Spaces.space1.id, createdConnector.id, 'action', 'actions');
const { body: createdRule } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
rule_type_id: 'test.cumulative-firing',
actions: [
{
id: createdConnector.id,
group: 'default',
params: {},
},
],
})
)
.expect(200);
objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting');
await waitForEvents(createdRule.id, 'alerting', new Map([['execute', { gte: 1 }]]));
await waitForEvents(createdRule.id, 'actions', new Map([['execute', { gte: 1 }]]));
const response = await supertest.get(
`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${
createdRule.id
}/_execution_log?date_start=${dateStart}`
);
expect(response.status).to.eql(200);
expect(response.body.total).to.eql(1);
const execLogs = response.body.data;
expect(execLogs.length).to.eql(1);
for (const log of execLogs) {
expect(log.status).to.equal('success');
expect(log.num_active_alerts).to.equal(1);
expect(log.num_new_alerts).to.equal(1);
expect(log.num_recovered_alerts).to.equal(0);
expect(log.num_triggered_actions).to.equal(1);
expect(log.num_succeeded_actions).to.equal(1);
expect(log.num_errored_actions).to.equal(0);
}
});
it('gets execution log for rule that has failed actions', async () => {
const { body: createdConnector } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.send({
name: 'connector that throws',
connector_type_id: 'test.throw',
config: {},
secrets: {},
})
.expect(200);
objectRemover.add(Spaces.space1.id, createdConnector.id, 'action', 'actions');
const { body: createdRule } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
rule_type_id: 'test.cumulative-firing',
actions: [
{
id: createdConnector.id,
group: 'default',
params: {},
},
],
})
)
.expect(200);
objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting');
await waitForEvents(createdRule.id, 'alerting', new Map([['execute', { gte: 1 }]]));
await waitForEvents(createdRule.id, 'actions', new Map([['execute', { gte: 1 }]]));
const response = await supertest.get(
`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${
createdRule.id
}/_execution_log?date_start=${dateStart}`
);
expect(response.status).to.eql(200);
expect(response.body.total).to.eql(1);
const execLogs = response.body.data;
expect(execLogs.length).to.eql(1);
for (const log of execLogs) {
expect(log.status).to.equal('success');
expect(log.num_active_alerts).to.equal(1);
expect(log.num_new_alerts).to.equal(1);
expect(log.num_recovered_alerts).to.equal(0);
expect(log.num_triggered_actions).to.equal(1);
expect(log.num_succeeded_actions).to.equal(0);
expect(log.num_errored_actions).to.equal(1);
}
});
it('handles date_end if specified', async () => {
const { body: createdRule } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '10s' } }))
.expect(200);
objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting');
await waitForEvents(createdRule.id, 'alerting', new Map([['execute', { gte: 2 }]]));
// set the date end to date start - should filter out all execution logs
const earlierDateStart = new Date(new Date(dateStart).getTime() - 900000).toISOString();
const response = await supertest.get(
`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${
createdRule.id
}/_execution_log?date_start=${earlierDateStart}&date_end=${dateStart}`
);
expect(response.status).to.eql(200);
expect(response.body.total).to.eql(0);
expect(response.body.data.length).to.eql(0);
});
it('handles sort query parameter', async () => {
const { body: createdRule } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '5s' } }))
.expect(200);
objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting');
await waitForEvents(createdRule.id, 'alerting', new Map([['execute', { gte: 3 }]]));
const response = await supertest.get(
`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${
createdRule.id
}/_execution_log?date_start=${dateStart}&sort=[{"timestamp":{"order":"asc"}}]`
);
expect(response.status).to.eql(200);
expect(response.body.total).to.eql(3);
const execLogs = response.body.data;
expect(execLogs.length).to.eql(3);
let previousTimestamp: string | null = null;
for (const log of execLogs) {
if (previousTimestamp) {
// sorting by `asc` timestamp
expect(Date.parse(log.timestamp)).to.be.greaterThan(Date.parse(previousTimestamp));
}
previousTimestamp = log.timstamp;
}
});
it(`handles invalid date_start`, async () => {
const { body: createdRule } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '10s' } }))
.expect(200);
objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting');
await waitForEvents(createdRule.id, 'alerting', new Map([['execute', { gte: 2 }]]));
await supertest
.get(
`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${
createdRule.id
}/_execution_log?date_start=X0X0-08-08T08:08:08.008Z`
)
.expect(400, {
statusCode: 400,
error: 'Bad Request',
message: 'Invalid date for parameter dateStart: "X0X0-08-08T08:08:08.008Z"',
});
});
});
async function waitForEvents(
id: string,
provider: string,
actions: Map<
string,
{
gte: number;
}
>
) {
await retry.try(async () => {
return await getEventLog({
getService,
spaceId: Spaces.space1.id,
type: 'alert',
id,
provider,
actions,
});
});
}
}

View file

@ -23,6 +23,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC
loadTestFile(require.resolve('./get'));
loadTestFile(require.resolve('./get_alert_state'));
loadTestFile(require.resolve('./get_alert_summary'));
loadTestFile(require.resolve('./get_execution_log'));
loadTestFile(require.resolve('./rule_types'));
loadTestFile(require.resolve('./event_log'));
loadTestFile(require.resolve('./execution_status'));