mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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 commit939340e252
. * Revert "Reverting changes not related to event log aggregation" This reverts commit40a93a4b3c
. * 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:
parent
484d23ce48
commit
5435cf922e
16 changed files with 2603 additions and 1 deletions
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -30,6 +30,7 @@ export enum ReadOperations {
|
|||
Get = 'get',
|
||||
GetRuleState = 'getRuleState',
|
||||
GetAlertSummary = 'getAlertSummary',
|
||||
GetExecutionLog = 'getExecutionLog',
|
||||
Find = 'find',
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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`) }),
|
||||
{}
|
||||
)
|
||||
);
|
||||
}
|
|
@ -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]`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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 })),
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
|
|
|
@ -30,6 +30,7 @@ const createRulesClientMock = () => {
|
|||
unmuteInstance: jest.fn(),
|
||||
listAlertTypes: jest.fn(),
|
||||
getAlertSummary: jest.fn(),
|
||||
getExecutionLogForRule: jest.fn(),
|
||||
getSpaceId: jest.fn(),
|
||||
snooze: jest.fn(),
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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",
|
||||
|
|
|
@ -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'],
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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'));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue