mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[RAM] Add the Logs tab to Rules and Connectors UI (#138852)
* Add initial logs tab, permissions checks still broken * Filter global event logs by permissions * Finalize table in logs list tab * Create new function without wildcard id * Fix error flyout * Fix rule_id test * Add test for getGlobalExecutionLogWithAuth * Fix typechecks * Fix jest types * Fix types and tests * Fix aggregation test * Fix aggregation test * Fix mocks in jest test * Fix localstorage test * Add rule.id to OutcomeAndMessage agg * Move authfilter to query, move ruleid agg to outcomeandmessage * Memoize rule name onclick * Add new route for global execution logs * Remove * from getruleexecutionlog route * Fix types and jest * Fix types * Fix jest * Fix nits * Add cluster client adapter unti tests * Add unit test for event log client * Update get execution log aggregation test * Remove sort options and hide option from Rule column * Add functional test for global execution logs Co-authored-by: Faisal Kanout <faisal.kanout@elastic.co>
This commit is contained in:
parent
12466d8b17
commit
a1f2c483fd
41 changed files with 2305 additions and 420 deletions
|
@ -48,6 +48,7 @@ export interface IExecutionLog {
|
|||
es_search_duration_ms: number;
|
||||
schedule_delay_ms: number;
|
||||
timed_out: boolean;
|
||||
rule_id: string;
|
||||
}
|
||||
|
||||
export interface IExecutionErrors {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { fromKueryExpression } from '@kbn/es-query';
|
||||
import {
|
||||
getNumExecutions,
|
||||
getExecutionLogAggregation,
|
||||
|
@ -271,7 +272,421 @@ describe('getExecutionLogAggregation', () => {
|
|||
top_hits: {
|
||||
size: 1,
|
||||
_source: {
|
||||
includes: ['event.outcome', 'message', 'error.message', 'kibana.version'],
|
||||
includes: [
|
||||
'event.outcome',
|
||||
'message',
|
||||
'error.message',
|
||||
'kibana.version',
|
||||
'rule.id',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
timeoutMessage: {
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
{ match: { 'event.action': 'execute-timeout' } },
|
||||
{ match: { 'event.provider': 'alerting' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should correctly generate aggregation with a defined filter in the form of a string', () => {
|
||||
expect(
|
||||
getExecutionLogAggregation({
|
||||
page: 2,
|
||||
perPage: 10,
|
||||
sort: [{ timestamp: { order: 'asc' } }, { execution_duration: { order: 'desc' } }],
|
||||
filter: 'test:test',
|
||||
})
|
||||
).toEqual({
|
||||
excludeExecuteStart: {
|
||||
filter: {
|
||||
bool: {
|
||||
must_not: [
|
||||
{
|
||||
term: {
|
||||
'event.action': 'execute-start',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
executionUuidCardinality: {
|
||||
aggs: {
|
||||
executionUuidCardinality: {
|
||||
cardinality: { field: 'kibana.alert.rule.execution.uuid' },
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
bool: {
|
||||
filter: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
test: 'test',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
must: [
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: {
|
||||
'event.action': 'execute',
|
||||
},
|
||||
},
|
||||
{
|
||||
match: {
|
||||
'event.provider': 'alerting',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
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',
|
||||
},
|
||||
},
|
||||
actionExecution: {
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
{ match: { 'event.action': 'execute' } },
|
||||
{ match: { 'event.provider': 'actions' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: { actionOutcomes: { terms: { field: 'event.outcome', size: 2 } } },
|
||||
},
|
||||
minExecutionUuidBucket: {
|
||||
bucket_selector: {
|
||||
buckets_path: {
|
||||
count: 'ruleExecution._count',
|
||||
},
|
||||
script: {
|
||||
source: 'params.count > 0',
|
||||
},
|
||||
},
|
||||
},
|
||||
ruleExecution: {
|
||||
filter: {
|
||||
bool: {
|
||||
filter: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
test: 'test',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
must: [
|
||||
{
|
||||
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',
|
||||
},
|
||||
},
|
||||
numGeneratedActions: {
|
||||
max: {
|
||||
field: 'kibana.alert.rule.execution.metrics.number_of_generated_actions',
|
||||
},
|
||||
},
|
||||
numActiveAlerts: {
|
||||
max: {
|
||||
field: 'kibana.alert.rule.execution.metrics.alert_counts.active',
|
||||
},
|
||||
},
|
||||
numRecoveredAlerts: {
|
||||
max: {
|
||||
field: 'kibana.alert.rule.execution.metrics.alert_counts.recovered',
|
||||
},
|
||||
},
|
||||
numNewAlerts: {
|
||||
max: {
|
||||
field: 'kibana.alert.rule.execution.metrics.alert_counts.new',
|
||||
},
|
||||
},
|
||||
executionDuration: { max: { field: 'event.duration' } },
|
||||
outcomeAndMessage: {
|
||||
top_hits: {
|
||||
size: 1,
|
||||
_source: {
|
||||
includes: [
|
||||
'event.outcome',
|
||||
'message',
|
||||
'error.message',
|
||||
'kibana.version',
|
||||
'rule.id',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
timeoutMessage: {
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
{ match: { 'event.action': 'execute-timeout' } },
|
||||
{ match: { 'event.provider': 'alerting' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should correctly generate aggregation with a defined filter in the form of a KueryNode', () => {
|
||||
expect(
|
||||
getExecutionLogAggregation({
|
||||
page: 2,
|
||||
perPage: 10,
|
||||
sort: [{ timestamp: { order: 'asc' } }, { execution_duration: { order: 'desc' } }],
|
||||
filter: fromKueryExpression('test:test'),
|
||||
})
|
||||
).toEqual({
|
||||
excludeExecuteStart: {
|
||||
filter: {
|
||||
bool: {
|
||||
must_not: [
|
||||
{
|
||||
term: {
|
||||
'event.action': 'execute-start',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
executionUuidCardinality: {
|
||||
aggs: {
|
||||
executionUuidCardinality: {
|
||||
cardinality: { field: 'kibana.alert.rule.execution.uuid' },
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
bool: {
|
||||
filter: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
test: 'test',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
must: [
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: {
|
||||
'event.action': 'execute',
|
||||
},
|
||||
},
|
||||
{
|
||||
match: {
|
||||
'event.provider': 'alerting',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
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',
|
||||
},
|
||||
},
|
||||
actionExecution: {
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
{ match: { 'event.action': 'execute' } },
|
||||
{ match: { 'event.provider': 'actions' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: { actionOutcomes: { terms: { field: 'event.outcome', size: 2 } } },
|
||||
},
|
||||
minExecutionUuidBucket: {
|
||||
bucket_selector: {
|
||||
buckets_path: {
|
||||
count: 'ruleExecution._count',
|
||||
},
|
||||
script: {
|
||||
source: 'params.count > 0',
|
||||
},
|
||||
},
|
||||
},
|
||||
ruleExecution: {
|
||||
filter: {
|
||||
bool: {
|
||||
filter: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
test: 'test',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
must: [
|
||||
{
|
||||
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',
|
||||
},
|
||||
},
|
||||
numGeneratedActions: {
|
||||
max: {
|
||||
field: 'kibana.alert.rule.execution.metrics.number_of_generated_actions',
|
||||
},
|
||||
},
|
||||
numActiveAlerts: {
|
||||
max: {
|
||||
field: 'kibana.alert.rule.execution.metrics.alert_counts.active',
|
||||
},
|
||||
},
|
||||
numRecoveredAlerts: {
|
||||
max: {
|
||||
field: 'kibana.alert.rule.execution.metrics.alert_counts.recovered',
|
||||
},
|
||||
},
|
||||
numNewAlerts: {
|
||||
max: {
|
||||
field: 'kibana.alert.rule.execution.metrics.alert_counts.new',
|
||||
},
|
||||
},
|
||||
executionDuration: { max: { field: 'event.duration' } },
|
||||
outcomeAndMessage: {
|
||||
top_hits: {
|
||||
size: 1,
|
||||
_source: {
|
||||
includes: [
|
||||
'event.outcome',
|
||||
'message',
|
||||
'error.message',
|
||||
'kibana.version',
|
||||
'rule.id',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -361,6 +776,7 @@ describe('formatExecutionLogResult', () => {
|
|||
_id: 'S4wIZX8B8TGQpG7XQZns',
|
||||
_score: 1.0,
|
||||
_source: {
|
||||
rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef' },
|
||||
event: {
|
||||
outcome: 'success',
|
||||
},
|
||||
|
@ -444,6 +860,8 @@ describe('formatExecutionLogResult', () => {
|
|||
_id: 'a4wIZX8B8TGQpG7Xwpnz',
|
||||
_score: 1.0,
|
||||
_source: {
|
||||
rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef' },
|
||||
|
||||
event: {
|
||||
outcome: 'success',
|
||||
},
|
||||
|
@ -521,6 +939,7 @@ describe('formatExecutionLogResult', () => {
|
|||
es_search_duration_ms: 0,
|
||||
timed_out: false,
|
||||
schedule_delay_ms: 3074,
|
||||
rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef',
|
||||
},
|
||||
{
|
||||
id: '41b2755e-765a-4044-9745-b03875d5e79a',
|
||||
|
@ -541,6 +960,7 @@ describe('formatExecutionLogResult', () => {
|
|||
es_search_duration_ms: 0,
|
||||
timed_out: false,
|
||||
schedule_delay_ms: 3126,
|
||||
rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -595,6 +1015,7 @@ describe('formatExecutionLogResult', () => {
|
|||
_id: 'S4wIZX8B8TGQpG7XQZns',
|
||||
_score: 1.0,
|
||||
_source: {
|
||||
rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef' },
|
||||
event: {
|
||||
outcome: 'failure',
|
||||
},
|
||||
|
@ -681,6 +1102,7 @@ describe('formatExecutionLogResult', () => {
|
|||
_id: 'a4wIZX8B8TGQpG7Xwpnz',
|
||||
_score: 1.0,
|
||||
_source: {
|
||||
rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef' },
|
||||
event: {
|
||||
outcome: 'success',
|
||||
},
|
||||
|
@ -758,6 +1180,7 @@ describe('formatExecutionLogResult', () => {
|
|||
es_search_duration_ms: 0,
|
||||
timed_out: false,
|
||||
schedule_delay_ms: 3074,
|
||||
rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef',
|
||||
},
|
||||
{
|
||||
id: '41b2755e-765a-4044-9745-b03875d5e79a',
|
||||
|
@ -778,6 +1201,7 @@ describe('formatExecutionLogResult', () => {
|
|||
es_search_duration_ms: 0,
|
||||
timed_out: false,
|
||||
schedule_delay_ms: 3126,
|
||||
rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -832,6 +1256,7 @@ describe('formatExecutionLogResult', () => {
|
|||
_id: 'dJkWa38B1ylB1EvsAckB',
|
||||
_score: 1.0,
|
||||
_source: {
|
||||
rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef' },
|
||||
event: {
|
||||
outcome: 'success',
|
||||
},
|
||||
|
@ -910,6 +1335,7 @@ describe('formatExecutionLogResult', () => {
|
|||
_id: 'a4wIZX8B8TGQpG7Xwpnz',
|
||||
_score: 1.0,
|
||||
_source: {
|
||||
rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef' },
|
||||
event: {
|
||||
outcome: 'success',
|
||||
},
|
||||
|
@ -987,6 +1413,7 @@ describe('formatExecutionLogResult', () => {
|
|||
es_search_duration_ms: 0,
|
||||
timed_out: true,
|
||||
schedule_delay_ms: 3074,
|
||||
rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef',
|
||||
},
|
||||
{
|
||||
id: '41b2755e-765a-4044-9745-b03875d5e79a',
|
||||
|
@ -1007,6 +1434,7 @@ describe('formatExecutionLogResult', () => {
|
|||
es_search_duration_ms: 0,
|
||||
timed_out: false,
|
||||
schedule_delay_ms: 3126,
|
||||
rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -1061,6 +1489,7 @@ describe('formatExecutionLogResult', () => {
|
|||
_id: '7xKcb38BcntAq5ycFwiu',
|
||||
_score: 1.0,
|
||||
_source: {
|
||||
rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef' },
|
||||
event: {
|
||||
outcome: 'success',
|
||||
},
|
||||
|
@ -1144,6 +1573,7 @@ describe('formatExecutionLogResult', () => {
|
|||
_id: 'zRKbb38BcntAq5ycOwgk',
|
||||
_score: 1.0,
|
||||
_source: {
|
||||
rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef' },
|
||||
event: {
|
||||
outcome: 'success',
|
||||
},
|
||||
|
@ -1221,6 +1651,7 @@ describe('formatExecutionLogResult', () => {
|
|||
es_search_duration_ms: 0,
|
||||
timed_out: false,
|
||||
schedule_delay_ms: 3126,
|
||||
rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef',
|
||||
},
|
||||
{
|
||||
id: '61bb867b-661a-471f-bf92-23471afa10b3',
|
||||
|
@ -1241,6 +1672,7 @@ describe('formatExecutionLogResult', () => {
|
|||
es_search_duration_ms: 0,
|
||||
timed_out: false,
|
||||
schedule_delay_ms: 3133,
|
||||
rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { KueryNode } from '@kbn/core-saved-objects-api-server';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import Boom from '@hapi/boom';
|
||||
import { flatMap, get } from 'lodash';
|
||||
|
@ -15,6 +16,7 @@ import { IExecutionLog, IExecutionLogResult } from '../../common';
|
|||
|
||||
const DEFAULT_MAX_BUCKETS_LIMIT = 1000; // do not retrieve more than this number of executions
|
||||
|
||||
const RULE_ID_FIELD = 'rule.id';
|
||||
const PROVIDER_FIELD = 'event.provider';
|
||||
const START_FIELD = 'event.start';
|
||||
const ACTION_FIELD = 'event.action';
|
||||
|
@ -80,7 +82,7 @@ interface ExcludeExecuteStartAggResult extends estypes.AggregationsAggregateBase
|
|||
};
|
||||
}
|
||||
export interface IExecutionLogAggOptions {
|
||||
filter?: string;
|
||||
filter?: string | KueryNode;
|
||||
page: number;
|
||||
perPage: number;
|
||||
sort: estypes.Sort;
|
||||
|
@ -129,7 +131,8 @@ export function getExecutionLogAggregation({
|
|||
|
||||
let dslFilterQuery: estypes.QueryDslBoolQuery['filter'];
|
||||
try {
|
||||
dslFilterQuery = filter ? toElasticsearchQuery(fromKueryExpression(filter)) : undefined;
|
||||
const filterKueryNode = typeof filter === 'string' ? fromKueryExpression(filter) : filter;
|
||||
dslFilterQuery = filter ? toElasticsearchQuery(filterKueryNode) : undefined;
|
||||
} catch (err) {
|
||||
throw Boom.badRequest(`Invalid kuery syntax for filter ${filter}`);
|
||||
}
|
||||
|
@ -256,7 +259,13 @@ export function getExecutionLogAggregation({
|
|||
top_hits: {
|
||||
size: 1,
|
||||
_source: {
|
||||
includes: [OUTCOME_FIELD, MESSAGE_FIELD, ERROR_MESSAGE_FIELD, VERSION_FIELD],
|
||||
includes: [
|
||||
OUTCOME_FIELD,
|
||||
MESSAGE_FIELD,
|
||||
ERROR_MESSAGE_FIELD,
|
||||
VERSION_FIELD,
|
||||
RULE_ID_FIELD,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -325,6 +334,8 @@ function formatExecutionLogAggBucket(bucket: IExecutionUuidAggBucket): IExecutio
|
|||
? `${outcomeAndMessage?.message ?? ''} - ${outcomeAndMessage?.error?.message ?? ''}`
|
||||
: outcomeAndMessage?.message ?? '';
|
||||
const version = outcomeAndMessage ? outcomeAndMessage?.kibana?.version ?? '' : '';
|
||||
|
||||
const ruleId = outcomeAndMessage ? outcomeAndMessage?.rule?.id ?? '' : '';
|
||||
return {
|
||||
id: bucket?.key ?? '',
|
||||
timestamp: bucket?.ruleExecution?.executeStartTime.value_as_string ?? '',
|
||||
|
@ -343,6 +354,7 @@ function formatExecutionLogAggBucket(bucket: IExecutionUuidAggBucket): IExecutio
|
|||
es_search_duration_ms: bucket?.ruleExecution?.esSearchDuration?.value ?? 0,
|
||||
schedule_delay_ms: scheduleDelayUs / Millis2Nanos,
|
||||
timed_out: timedOut,
|
||||
rule_id: ruleId,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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 { getGlobalExecutionLogRoute } from './get_global_execution_logs';
|
||||
import { httpServiceMock } from '@kbn/core/server/mocks';
|
||||
import { licenseStateMock } from '../lib/license_state.mock';
|
||||
import { mockHandlerArguments } from './_mock_handler_arguments';
|
||||
import { rulesClientMock } from '../rules_client.mock';
|
||||
import { IExecutionLogResult } from '../../common';
|
||||
|
||||
const rulesClient = rulesClientMock.create();
|
||||
jest.mock('../lib/license_api_access', () => ({
|
||||
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'",
|
||||
version: '8.2.0',
|
||||
num_active_alerts: 5,
|
||||
num_new_alerts: 5,
|
||||
num_recovered_alerts: 0,
|
||||
num_triggered_actions: 5,
|
||||
num_generated_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,
|
||||
rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef',
|
||||
},
|
||||
{
|
||||
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'",
|
||||
version: '8.2.0',
|
||||
num_active_alerts: 5,
|
||||
num_new_alerts: 5,
|
||||
num_recovered_alerts: 5,
|
||||
num_triggered_actions: 5,
|
||||
num_generated_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,
|
||||
rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it('gets global execution logs', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
getGlobalExecutionLogRoute(router, licenseState);
|
||||
|
||||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/_global_execution_logs"`);
|
||||
|
||||
rulesClient.getGlobalExecutionLogWithAuth.mockResolvedValue(mockedExecutionLog);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ rulesClient },
|
||||
{
|
||||
query: {
|
||||
date_start: dateString,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
sort: [{ timestamp: { order: 'desc' } }],
|
||||
},
|
||||
},
|
||||
['ok']
|
||||
);
|
||||
|
||||
await handler(context, req, res);
|
||||
|
||||
expect(rulesClient.getGlobalExecutionLogWithAuth).toHaveBeenCalledTimes(1);
|
||||
expect(rulesClient.getGlobalExecutionLogWithAuth.mock.calls[0]).toEqual([
|
||||
{
|
||||
dateStart: dateString,
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
sort: [{ timestamp: { order: 'desc' } }],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(res.ok).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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 '@kbn/core/server';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { ILicenseState } from '../lib';
|
||||
import { GetGlobalExecutionLogParams } from '../rules_client';
|
||||
import { RewriteRequestCase, verifyAccessAndContext } from './lib';
|
||||
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types';
|
||||
|
||||
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 }) }),
|
||||
schema.object({ num_generated_actions: schema.object({ order: sortOrderSchema }) }),
|
||||
schema.object({ num_active_alerts: schema.object({ order: sortOrderSchema }) }),
|
||||
schema.object({ num_recovered_alerts: schema.object({ order: sortOrderSchema }) }),
|
||||
schema.object({ num_new_alerts: 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<GetGlobalExecutionLogParams> = ({
|
||||
date_start: dateStart,
|
||||
date_end: dateEnd,
|
||||
per_page: perPage,
|
||||
...rest
|
||||
}) => ({
|
||||
...rest,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
perPage,
|
||||
});
|
||||
|
||||
export const getGlobalExecutionLogRoute = (
|
||||
router: IRouter<AlertingRequestHandlerContext>,
|
||||
licenseState: ILicenseState
|
||||
) => {
|
||||
router.get(
|
||||
{
|
||||
path: `${INTERNAL_BASE_ALERTING_API_PATH}/_global_execution_logs`,
|
||||
validate: {
|
||||
query: querySchema,
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(
|
||||
verifyAccessAndContext(licenseState, async function (context, req, res) {
|
||||
const rulesClient = (await context.alerting).getRulesClient();
|
||||
return res.ok({
|
||||
body: await rulesClient.getGlobalExecutionLogWithAuth(rewriteReq(req.query)),
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
|
@ -46,6 +46,7 @@ describe('getRuleExecutionLogRoute', () => {
|
|||
es_search_duration_ms: 0,
|
||||
timed_out: false,
|
||||
schedule_delay_ms: 3126,
|
||||
rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef',
|
||||
},
|
||||
{
|
||||
id: '41b2755e-765a-4044-9745-b03875d5e79a',
|
||||
|
@ -66,6 +67,7 @@ describe('getRuleExecutionLogRoute', () => {
|
|||
es_search_duration_ms: 0,
|
||||
timed_out: false,
|
||||
schedule_delay_ms: 3008,
|
||||
rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -21,6 +21,7 @@ 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 { getGlobalExecutionLogRoute } from './get_global_execution_logs';
|
||||
import { getActionErrorLogRoute } from './get_action_error_log';
|
||||
import { getRuleStateRoute } from './get_rule_state';
|
||||
import { healthRoute } from './health';
|
||||
|
@ -59,6 +60,7 @@ export function defineRoutes(opts: RouteOptions) {
|
|||
findInternalRulesRoute(router, licenseState, usageCounter);
|
||||
getRuleAlertSummaryRoute(router, licenseState);
|
||||
getRuleExecutionLogRoute(router, licenseState);
|
||||
getGlobalExecutionLogRoute(router, licenseState);
|
||||
getActionErrorLogRoute(router, licenseState);
|
||||
getRuleStateRoute(router, licenseState);
|
||||
healthRoute(router, licenseState, encryptedSavedObjects);
|
||||
|
|
|
@ -30,6 +30,7 @@ const createRulesClientMock = () => {
|
|||
listAlertTypes: jest.fn(),
|
||||
getAlertSummary: jest.fn(),
|
||||
getExecutionLogForRule: jest.fn(),
|
||||
getGlobalExecutionLogWithAuth: jest.fn(),
|
||||
getActionErrorLog: jest.fn(),
|
||||
getSpaceId: jest.fn(),
|
||||
bulkEdit: jest.fn(),
|
||||
|
|
|
@ -25,6 +25,7 @@ export enum RuleAuditAction {
|
|||
AGGREGATE = 'rule_aggregate',
|
||||
BULK_EDIT = 'rule_bulk_edit',
|
||||
GET_EXECUTION_LOG = 'rule_get_execution_log',
|
||||
GET_GLOBAL_EXECUTION_LOG = 'rule_get_global_execution_log',
|
||||
GET_ACTION_ERROR_LOG = 'rule_get_action_error_log',
|
||||
SNOOZE = 'rule_snooze',
|
||||
UNSNOOZE = 'rule_unsnooze',
|
||||
|
@ -53,6 +54,11 @@ const eventVerbs: Record<RuleAuditAction, VerbsTuple> = {
|
|||
'accessing execution log for',
|
||||
'accessed execution log for',
|
||||
],
|
||||
rule_get_global_execution_log: [
|
||||
'access execution log',
|
||||
'accessing execution log',
|
||||
'accessed execution log',
|
||||
],
|
||||
rule_get_action_error_log: [
|
||||
'access action error log for',
|
||||
'accessing action error log for',
|
||||
|
@ -79,6 +85,7 @@ const eventTypes: Record<RuleAuditAction, EcsEventType> = {
|
|||
rule_alert_unmute: 'change',
|
||||
rule_aggregate: 'access',
|
||||
rule_get_execution_log: 'access',
|
||||
rule_get_global_execution_log: 'access',
|
||||
rule_get_action_error_log: 'access',
|
||||
rule_snooze: 'change',
|
||||
rule_unsnooze: 'change',
|
||||
|
|
|
@ -365,6 +365,15 @@ export interface GetExecutionLogByIdParams {
|
|||
sort: estypes.Sort;
|
||||
}
|
||||
|
||||
export interface GetGlobalExecutionLogParams {
|
||||
dateStart: string;
|
||||
dateEnd?: string;
|
||||
filter?: string;
|
||||
page: number;
|
||||
perPage: number;
|
||||
sort: estypes.Sort;
|
||||
}
|
||||
|
||||
export interface GetActionErrorLogByIdParams {
|
||||
id: string;
|
||||
dateStart: string;
|
||||
|
@ -875,6 +884,100 @@ export class RulesClient {
|
|||
}
|
||||
}
|
||||
|
||||
public async getGlobalExecutionLogWithAuth({
|
||||
dateStart,
|
||||
dateEnd,
|
||||
filter,
|
||||
page,
|
||||
perPage,
|
||||
sort,
|
||||
}: GetGlobalExecutionLogParams): Promise<IExecutionLogResult> {
|
||||
this.logger.debug(`getGlobalExecutionLogWithAuth(): getting global execution log`);
|
||||
|
||||
let authorizationTuple;
|
||||
try {
|
||||
authorizationTuple = await this.authorization.getFindAuthorizationFilter(
|
||||
AlertingAuthorizationEntity.Alert,
|
||||
{
|
||||
type: AlertingAuthorizationFilterType.KQL,
|
||||
fieldNames: {
|
||||
ruleTypeId: 'kibana.alert.rule.rule_type_id',
|
||||
consumer: 'kibana.alert.rule.consumer',
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
this.auditLogger?.log(
|
||||
ruleAuditEvent({
|
||||
action: RuleAuditAction.GET_GLOBAL_EXECUTION_LOG,
|
||||
error,
|
||||
})
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.auditLogger?.log(
|
||||
ruleAuditEvent({
|
||||
action: RuleAuditAction.GET_GLOBAL_EXECUTION_LOG,
|
||||
})
|
||||
);
|
||||
|
||||
const dateNow = new Date();
|
||||
const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow);
|
||||
const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow);
|
||||
|
||||
const eventLogClient = await this.getEventLogClient();
|
||||
|
||||
try {
|
||||
const aggResult = await eventLogClient.aggregateEventsWithAuthFilter(
|
||||
'alert',
|
||||
authorizationTuple.filter as KueryNode,
|
||||
{
|
||||
start: parsedDateStart.toISOString(),
|
||||
end: parsedDateEnd.toISOString(),
|
||||
aggs: getExecutionLogAggregation({
|
||||
filter,
|
||||
page,
|
||||
perPage,
|
||||
sort,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const formattedResult = formatExecutionLogResult(aggResult);
|
||||
const ruleIds = [...new Set(formattedResult.data.map((l) => l.rule_id))].filter(
|
||||
Boolean
|
||||
) as string[];
|
||||
const ruleNameIdEntries = await Promise.all(
|
||||
ruleIds.map(async (id) => {
|
||||
try {
|
||||
const result = await this.get({ id });
|
||||
return [id, result.name];
|
||||
} catch (e) {
|
||||
return [id, id];
|
||||
}
|
||||
})
|
||||
);
|
||||
const ruleNameIdMap: Record<string, string> = ruleNameIdEntries.reduce(
|
||||
(result, [key, val]) => ({ ...result, [key]: val }),
|
||||
{}
|
||||
);
|
||||
|
||||
return {
|
||||
...formattedResult,
|
||||
data: formattedResult.data.map((entry) => ({
|
||||
...entry,
|
||||
rule_name: ruleNameIdMap[entry.rule_id!],
|
||||
})),
|
||||
};
|
||||
} catch (err) {
|
||||
this.logger.debug(
|
||||
`rulesClient.getGlobalExecutionLogWithAuth(): error searching global event log: ${err.message}`
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
public async getActionErrorLog({
|
||||
id,
|
||||
dateStart,
|
||||
|
|
|
@ -21,6 +21,7 @@ import { RawRule } from '../../types';
|
|||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import { getBeforeSetup, mockedDateString, setGlobalDate } from './lib';
|
||||
import { getExecutionLogAggregation } from '../../lib/get_execution_log_aggregation';
|
||||
import { fromKueryExpression } from '@kbn/es-query';
|
||||
|
||||
const taskManager = taskManagerMock.createStart();
|
||||
const ruleTypeRegistry = ruleTypeRegistryMock.create();
|
||||
|
@ -142,6 +143,9 @@ const aggregateResults = {
|
|||
_id: 'S4wIZX8B8TGQpG7XQZns',
|
||||
_score: 1.0,
|
||||
_source: {
|
||||
rule: {
|
||||
id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef',
|
||||
},
|
||||
event: {
|
||||
outcome: 'success',
|
||||
},
|
||||
|
@ -171,6 +175,25 @@ const aggregateResults = {
|
|||
value: 1.646667512617e12,
|
||||
value_as_string: '2022-03-07T15:38:32.617Z',
|
||||
},
|
||||
ruleId: {
|
||||
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: {
|
||||
rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
actionExecution: {
|
||||
meta: {},
|
||||
|
@ -225,6 +248,7 @@ const aggregateResults = {
|
|||
_id: 'a4wIZX8B8TGQpG7Xwpnz',
|
||||
_score: 1.0,
|
||||
_source: {
|
||||
rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef' },
|
||||
event: {
|
||||
outcome: 'success',
|
||||
},
|
||||
|
@ -254,6 +278,25 @@ const aggregateResults = {
|
|||
value: 1.646667545604e12,
|
||||
value_as_string: '2022-03-07T15:39:05.604Z',
|
||||
},
|
||||
ruleId: {
|
||||
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: {
|
||||
rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
actionExecution: {
|
||||
meta: {},
|
||||
|
@ -333,6 +376,7 @@ describe('getExecutionLogForRule()', () => {
|
|||
es_search_duration_ms: 0,
|
||||
timed_out: false,
|
||||
schedule_delay_ms: 3126,
|
||||
rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef',
|
||||
},
|
||||
{
|
||||
id: '41b2755e-765a-4044-9745-b03875d5e79a',
|
||||
|
@ -353,6 +397,7 @@ describe('getExecutionLogForRule()', () => {
|
|||
es_search_duration_ms: 0,
|
||||
timed_out: false,
|
||||
schedule_delay_ms: 3345,
|
||||
rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -623,3 +668,72 @@ describe('getExecutionLogForRule()', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGlobalExecutionLogWithAuth()', () => {
|
||||
let rulesClient: RulesClient;
|
||||
|
||||
beforeEach(() => {
|
||||
rulesClient = new RulesClient(rulesClientParams);
|
||||
});
|
||||
|
||||
test('runs as expected with some event log aggregation data', async () => {
|
||||
const ruleSO = getRuleSavedObject({});
|
||||
authorization.getFindAuthorizationFilter.mockResolvedValue({
|
||||
filter: fromKueryExpression('*'),
|
||||
ensureRuleTypeIsAuthorized() {},
|
||||
});
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(ruleSO);
|
||||
eventLogClient.aggregateEventsWithAuthFilter.mockResolvedValueOnce(aggregateResults);
|
||||
|
||||
const result = await rulesClient.getGlobalExecutionLogWithAuth(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'",
|
||||
version: '8.2.0',
|
||||
num_active_alerts: 5,
|
||||
num_new_alerts: 5,
|
||||
num_recovered_alerts: 0,
|
||||
num_triggered_actions: 5,
|
||||
num_generated_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,
|
||||
rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef',
|
||||
rule_name: 'rule-name',
|
||||
},
|
||||
{
|
||||
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'",
|
||||
version: '8.2.0',
|
||||
num_active_alerts: 5,
|
||||
num_new_alerts: 5,
|
||||
num_recovered_alerts: 5,
|
||||
num_triggered_actions: 5,
|
||||
num_generated_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,
|
||||
rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef',
|
||||
rule_name: 'rule-name',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,6 +25,7 @@ const createClusterClientMock = () => {
|
|||
setIndexAliasToHidden: jest.fn(),
|
||||
queryEventsBySavedObjects: jest.fn(),
|
||||
aggregateEventsBySavedObjects: jest.fn(),
|
||||
aggregateEventsWithAuthFilter: jest.fn(),
|
||||
shutdown: jest.fn(),
|
||||
};
|
||||
return mock;
|
||||
|
|
|
@ -13,11 +13,14 @@ import {
|
|||
getQueryBody,
|
||||
FindEventsOptionsBySavedObjectFilter,
|
||||
AggregateEventsOptionsBySavedObjectFilter,
|
||||
AggregateEventsWithAuthFilter,
|
||||
getQueryBodyWithAuthFilter,
|
||||
} from './cluster_client_adapter';
|
||||
import { AggregateOptionsType, queryOptionsSchema } from '../event_log_client';
|
||||
import { delay } from '../lib/delay';
|
||||
import { pick, times } from 'lodash';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/types';
|
||||
import { fromKueryExpression } from '@kbn/es-query';
|
||||
|
||||
type MockedLogger = ReturnType<typeof loggingSystemMock['createLogger']>;
|
||||
|
||||
|
@ -728,6 +731,104 @@ describe('aggregateEventsBySavedObject', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('aggregateEventsWithAuthFilter', () => {
|
||||
const DEFAULT_OPTIONS = {
|
||||
...queryOptionsSchema.validate({}),
|
||||
aggs: {
|
||||
genericAgg: {
|
||||
term: {
|
||||
field: 'event.action',
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test('should call cluster with correct options', async () => {
|
||||
clusterClient.search.mockResponse({
|
||||
aggregations: {
|
||||
genericAgg: {
|
||||
buckets: [
|
||||
{
|
||||
key: 'execute',
|
||||
doc_count: 10,
|
||||
},
|
||||
{
|
||||
key: 'execute-start',
|
||||
doc_count: 10,
|
||||
},
|
||||
{
|
||||
key: 'new-instance',
|
||||
doc_count: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
hits: {
|
||||
hits: [],
|
||||
total: { relation: 'eq', value: 0 },
|
||||
},
|
||||
took: 0,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
failed: 0,
|
||||
successful: 0,
|
||||
total: 0,
|
||||
skipped: 0,
|
||||
},
|
||||
});
|
||||
const options: AggregateEventsWithAuthFilter = {
|
||||
index: 'index-name',
|
||||
namespace: 'namespace',
|
||||
type: 'saved-object-type',
|
||||
aggregateOptions: DEFAULT_OPTIONS as AggregateOptionsType,
|
||||
authFilter: fromKueryExpression('test:test'),
|
||||
};
|
||||
const result = await clusterClientAdapter.aggregateEventsWithAuthFilter(options);
|
||||
|
||||
const [query] = clusterClient.search.mock.calls[0];
|
||||
expect(query).toEqual({
|
||||
index: 'index-name',
|
||||
body: {
|
||||
size: 0,
|
||||
query: getQueryBodyWithAuthFilter(
|
||||
logger,
|
||||
options,
|
||||
pick(options.aggregateOptions, ['start', 'end', 'filter'])
|
||||
),
|
||||
aggs: {
|
||||
genericAgg: {
|
||||
term: {
|
||||
field: 'event.action',
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result).toEqual({
|
||||
aggregations: {
|
||||
genericAgg: {
|
||||
buckets: [
|
||||
{
|
||||
key: 'execute',
|
||||
doc_count: 10,
|
||||
},
|
||||
{
|
||||
key: 'execute-start',
|
||||
doc_count: 10,
|
||||
},
|
||||
{
|
||||
key: 'new-instance',
|
||||
doc_count: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getQueryBody', () => {
|
||||
const options = {
|
||||
index: 'index-name',
|
||||
|
@ -1411,6 +1512,431 @@ describe('getQueryBody', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getQueryBodyWithAuthFilter', () => {
|
||||
const options = {
|
||||
index: 'index-name',
|
||||
namespace: undefined,
|
||||
type: 'saved-object-type',
|
||||
authFilter: fromKueryExpression('test:test'),
|
||||
};
|
||||
test('should correctly build query with namespace filter when namespace is undefined', () => {
|
||||
expect(
|
||||
getQueryBodyWithAuthFilter(logger, options as AggregateEventsWithAuthFilter, {})
|
||||
).toEqual({
|
||||
bool: {
|
||||
filter: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
test: 'test',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
must: [
|
||||
{
|
||||
nested: {
|
||||
path: 'kibana.saved_objects',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.rel': {
|
||||
value: 'primary',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.type': {
|
||||
value: 'saved-object-type',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
exists: {
|
||||
field: 'kibana.saved_objects.namespace',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should correctly build query with namespace filter when namespace is specified', () => {
|
||||
expect(
|
||||
getQueryBodyWithAuthFilter(
|
||||
logger,
|
||||
{ ...options, namespace: 'namespace' } as AggregateEventsWithAuthFilter,
|
||||
{}
|
||||
)
|
||||
).toEqual({
|
||||
bool: {
|
||||
filter: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
test: 'test',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
must: [
|
||||
{
|
||||
nested: {
|
||||
path: 'kibana.saved_objects',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.rel': {
|
||||
value: 'primary',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.type': {
|
||||
value: 'saved-object-type',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.namespace': {
|
||||
value: 'namespace',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should correctly build query when filter is specified', () => {
|
||||
expect(
|
||||
getQueryBodyWithAuthFilter(logger, options as AggregateEventsWithAuthFilter, {
|
||||
filter: 'event.provider: alerting AND event.action:execute',
|
||||
})
|
||||
).toEqual({
|
||||
bool: {
|
||||
filter: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
'event.provider': 'alerting',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
'event.action': 'execute',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
test: 'test',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
must: [
|
||||
{
|
||||
nested: {
|
||||
path: 'kibana.saved_objects',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.rel': {
|
||||
value: 'primary',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.type': {
|
||||
value: 'saved-object-type',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
exists: {
|
||||
field: 'kibana.saved_objects.namespace',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should correctly build query when start is specified', () => {
|
||||
expect(
|
||||
getQueryBodyWithAuthFilter(logger, options as AggregateEventsWithAuthFilter, {
|
||||
start: '2020-07-08T00:52:28.350Z',
|
||||
})
|
||||
).toEqual({
|
||||
bool: {
|
||||
filter: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
test: 'test',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
must: [
|
||||
{
|
||||
nested: {
|
||||
path: 'kibana.saved_objects',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.rel': {
|
||||
value: 'primary',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.type': {
|
||||
value: 'saved-object-type',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
exists: {
|
||||
field: 'kibana.saved_objects.namespace',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: '2020-07-08T00:52:28.350Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should correctly build query when end is specified', () => {
|
||||
expect(
|
||||
getQueryBodyWithAuthFilter(logger, options as AggregateEventsWithAuthFilter, {
|
||||
end: '2020-07-10T00:52:28.350Z',
|
||||
})
|
||||
).toEqual({
|
||||
bool: {
|
||||
filter: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
test: 'test',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
must: [
|
||||
{
|
||||
nested: {
|
||||
path: 'kibana.saved_objects',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.rel': {
|
||||
value: 'primary',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.type': {
|
||||
value: 'saved-object-type',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
exists: {
|
||||
field: 'kibana.saved_objects.namespace',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
lte: '2020-07-10T00:52:28.350Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should correctly build query when start and end are specified', () => {
|
||||
expect(
|
||||
getQueryBodyWithAuthFilter(logger, options as AggregateEventsWithAuthFilter, {
|
||||
start: '2020-07-08T00:52:28.350Z',
|
||||
end: '2020-07-10T00:52:28.350Z',
|
||||
})
|
||||
).toEqual({
|
||||
bool: {
|
||||
filter: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
test: 'test',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
must: [
|
||||
{
|
||||
nested: {
|
||||
path: 'kibana.saved_objects',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.rel': {
|
||||
value: 'primary',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.type': {
|
||||
value: 'saved-object-type',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
exists: {
|
||||
field: 'kibana.saved_objects.namespace',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: '2020-07-08T00:52:28.350Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
lte: '2020-07-10T00:52:28.350Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
type RetryableFunction = () => boolean;
|
||||
|
||||
const RETRY_UNTIL_DEFAULT_COUNT = 20;
|
||||
|
|
|
@ -12,7 +12,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types';
|
|||
import { Logger, ElasticsearchClient } from '@kbn/core/server';
|
||||
import util from 'util';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
|
||||
import { fromKueryExpression, toElasticsearchQuery, KueryNode, nodeBuilder } from '@kbn/es-query';
|
||||
import { IEvent, IValidatedEvent, SAVED_OBJECT_REL_PRIMARY } from '../types';
|
||||
import { AggregateOptionsType, FindOptionsType, QueryOptionsType } from '../event_log_client';
|
||||
import { ParsedIndexAlias } from './init';
|
||||
|
@ -50,6 +50,14 @@ interface QueryOptionsEventsBySavedObjectFilter {
|
|||
legacyIds?: string[];
|
||||
}
|
||||
|
||||
export interface AggregateEventsWithAuthFilter {
|
||||
index: string;
|
||||
namespace: string | undefined;
|
||||
type: string;
|
||||
authFilter: KueryNode;
|
||||
aggregateOptions: AggregateOptionsType;
|
||||
}
|
||||
|
||||
export type FindEventsOptionsBySavedObjectFilter = QueryOptionsEventsBySavedObjectFilter & {
|
||||
findOptions: FindOptionsType;
|
||||
};
|
||||
|
@ -415,6 +423,126 @@ export class ClusterClientAdapter<TDoc extends { body: AliasAny; index: string }
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async aggregateEventsWithAuthFilter(
|
||||
queryOptions: AggregateEventsWithAuthFilter
|
||||
): Promise<AggregateEventsBySavedObjectResult> {
|
||||
const { index, type, aggregateOptions } = queryOptions;
|
||||
const { aggs } = aggregateOptions;
|
||||
|
||||
const esClient = await this.elasticsearchClientPromise;
|
||||
|
||||
const query = getQueryBodyWithAuthFilter(
|
||||
this.logger,
|
||||
queryOptions,
|
||||
pick(queryOptions.aggregateOptions, ['start', 'end', 'filter'])
|
||||
);
|
||||
|
||||
const body: estypes.SearchRequest['body'] = {
|
||||
size: 0,
|
||||
query,
|
||||
aggs,
|
||||
};
|
||||
|
||||
try {
|
||||
const { aggregations } = await esClient.search<IValidatedEvent>({
|
||||
index,
|
||||
body,
|
||||
});
|
||||
return {
|
||||
aggregations,
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`querying for Event Log by for type "${type}" and auth filter failed with: ${err.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getQueryBodyWithAuthFilter(
|
||||
logger: Logger,
|
||||
opts: AggregateEventsWithAuthFilter,
|
||||
queryOptions: QueryOptionsType
|
||||
) {
|
||||
const { namespace, type, authFilter } = opts;
|
||||
const { start, end, filter } = queryOptions ?? {};
|
||||
|
||||
const namespaceQuery = getNamespaceQuery(namespace);
|
||||
let dslFilterQuery: estypes.QueryDslBoolQuery['filter'];
|
||||
try {
|
||||
const filterKueryNode = filter ? fromKueryExpression(filter) : null;
|
||||
const queryFilter = filterKueryNode
|
||||
? nodeBuilder.and([filterKueryNode, authFilter as KueryNode])
|
||||
: authFilter;
|
||||
dslFilterQuery = queryFilter ? toElasticsearchQuery(queryFilter) : undefined;
|
||||
} catch (err) {
|
||||
logger.debug(
|
||||
`esContext: Invalid kuery syntax for the filter (${filter}) error: ${JSON.stringify({
|
||||
message: err.message,
|
||||
statusCode: err.statusCode,
|
||||
})}`
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
|
||||
const savedObjectsQueryMust: estypes.QueryDslQueryContainer[] = [
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.rel': {
|
||||
value: SAVED_OBJECT_REL_PRIMARY,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.type': {
|
||||
value: type,
|
||||
},
|
||||
},
|
||||
},
|
||||
// @ts-expect-error undefined is not assignable as QueryDslTermQuery value
|
||||
namespaceQuery,
|
||||
];
|
||||
|
||||
const musts: estypes.QueryDslQueryContainer[] = [
|
||||
{
|
||||
nested: {
|
||||
path: 'kibana.saved_objects',
|
||||
query: {
|
||||
bool: {
|
||||
must: reject(savedObjectsQueryMust, isUndefined),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (start) {
|
||||
musts.push({
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: start,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (end) {
|
||||
musts.push({
|
||||
range: {
|
||||
'@timestamp': {
|
||||
lte: end,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
bool: {
|
||||
...(dslFilterQuery ? { filter: dslFilterQuery } : {}),
|
||||
must: reject(musts, isUndefined),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getNamespaceQuery(namespace?: string) {
|
||||
|
@ -446,9 +574,15 @@ export function getQueryBody(
|
|||
const { start, end, filter } = queryOptions ?? {};
|
||||
|
||||
const namespaceQuery = getNamespaceQuery(namespace);
|
||||
let filterKueryNode;
|
||||
try {
|
||||
filterKueryNode = JSON.parse(filter ?? '');
|
||||
} catch (e) {
|
||||
filterKueryNode = filter ? fromKueryExpression(filter) : null;
|
||||
}
|
||||
let dslFilterQuery: estypes.QueryDslBoolQuery['filter'];
|
||||
try {
|
||||
dslFilterQuery = filter ? toElasticsearchQuery(fromKueryExpression(filter)) : undefined;
|
||||
dslFilterQuery = filterKueryNode ? toElasticsearchQuery(filterKueryNode) : undefined;
|
||||
} catch (err) {
|
||||
logger.debug(
|
||||
`esContext: Invalid kuery syntax for the filter (${filter}) error: ${JSON.stringify({
|
||||
|
@ -492,6 +626,7 @@ export function getQueryBody(
|
|||
];
|
||||
|
||||
const shouldQuery = [];
|
||||
|
||||
shouldQuery.push({
|
||||
bool: {
|
||||
must: [
|
||||
|
|
|
@ -11,6 +11,7 @@ const createEventLogClientMock = () => {
|
|||
const mock: jest.Mocked<IEventLogClient> = {
|
||||
findEventsBySavedObjectIds: jest.fn(),
|
||||
aggregateEventsBySavedObjectIds: jest.fn(),
|
||||
aggregateEventsWithAuthFilter: jest.fn(),
|
||||
};
|
||||
return mock;
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ import { contextMock } from './es/context.mock';
|
|||
import { merge } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { IClusterClientAdapter } from './es/cluster_client_adapter';
|
||||
import { fromKueryExpression } from '@kbn/es-query';
|
||||
|
||||
const expectedSavedObject = {
|
||||
id: 'saved-object-id',
|
||||
|
@ -240,6 +241,38 @@ describe('EventLogStart', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
describe('aggregateEventsWithAuthFilter', () => {
|
||||
const testAuthFilter = fromKueryExpression('test:test');
|
||||
test('throws when no aggregation is defined in options', async () => {
|
||||
savedObjectGetter.mockResolvedValueOnce(expectedSavedObject);
|
||||
await expect(
|
||||
eventLogClient.aggregateEventsWithAuthFilter('saved-object-type', testAuthFilter)
|
||||
).rejects.toMatchInlineSnapshot(`[Error: No aggregation defined!]`);
|
||||
});
|
||||
test('calls aggregateEventsWithAuthFilter with given aggregation', async () => {
|
||||
savedObjectGetter.mockResolvedValueOnce(expectedSavedObject);
|
||||
await eventLogClient.aggregateEventsWithAuthFilter('saved-object-type', testAuthFilter, {
|
||||
aggs: { myAgg: {} },
|
||||
});
|
||||
expect(esContext.esAdapter.aggregateEventsWithAuthFilter).toHaveBeenCalledWith({
|
||||
index: esContext.esNames.indexPattern,
|
||||
namespace: undefined,
|
||||
type: 'saved-object-type',
|
||||
authFilter: testAuthFilter,
|
||||
aggregateOptions: {
|
||||
aggs: { myAgg: {} },
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
sort: [
|
||||
{
|
||||
sort_field: '@timestamp',
|
||||
sort_order: 'asc',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function fakeEvent(overrides = {}) {
|
||||
|
|
|
@ -12,6 +12,7 @@ import { IClusterClient, KibanaRequest } from '@kbn/core/server';
|
|||
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { SpacesServiceStart } from '@kbn/spaces-plugin/server';
|
||||
|
||||
import { KueryNode } from '@kbn/core-saved-objects-api-server';
|
||||
import { EsContext } from './es';
|
||||
import { IEventLogClient } from './types';
|
||||
import { QueryEventsBySavedObjectResult } from './es/cluster_client_adapter';
|
||||
|
@ -138,6 +139,32 @@ export class EventLogClient implements IEventLogClient {
|
|||
});
|
||||
}
|
||||
|
||||
public async aggregateEventsWithAuthFilter(
|
||||
type: string,
|
||||
authFilter: KueryNode,
|
||||
options?: AggregateOptionsType
|
||||
) {
|
||||
if (!authFilter) {
|
||||
throw new Error('No authorization filter defined!');
|
||||
}
|
||||
|
||||
const aggs = options?.aggs;
|
||||
if (!aggs) {
|
||||
throw new Error('No aggregation defined!');
|
||||
}
|
||||
|
||||
// validate other query options separately from
|
||||
const aggregateOptions = queryOptionsSchema.validate(omit(options, 'aggs') ?? {});
|
||||
|
||||
return await this.esContext.esAdapter.aggregateEventsWithAuthFilter({
|
||||
index: this.esContext.esNames.indexPattern,
|
||||
namespace: await this.getNamespace(),
|
||||
type,
|
||||
authFilter,
|
||||
aggregateOptions: { ...aggregateOptions, aggs } as AggregateOptionsType,
|
||||
});
|
||||
}
|
||||
|
||||
private async getNamespace() {
|
||||
const space = await this.spacesService?.getActiveSpace(this.request);
|
||||
return space && this.spacesService?.spaceIdToNamespace(space.id);
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import type { IRouter, KibanaRequest, CustomRequestHandlerContext } from '@kbn/core/server';
|
||||
import { KueryNode } from '@kbn/es-query';
|
||||
|
||||
export type { IEvent, IValidatedEvent } from '../generated/schemas';
|
||||
export { EventSchema, ECS_VERSION } from '../generated/schemas';
|
||||
|
@ -62,6 +63,11 @@ export interface IEventLogClient {
|
|||
options?: Partial<AggregateOptionsType>,
|
||||
legacyIds?: string[]
|
||||
): Promise<AggregateEventsBySavedObjectResult>;
|
||||
aggregateEventsWithAuthFilter(
|
||||
type: string,
|
||||
authFilter: KueryNode,
|
||||
options?: Partial<AggregateOptionsType>
|
||||
): Promise<AggregateEventsBySavedObjectResult>;
|
||||
}
|
||||
|
||||
export interface IEventLogger {
|
||||
|
|
|
@ -185,7 +185,7 @@ export function RuleDetailsPage() {
|
|||
}),
|
||||
'data-test-subj': 'eventLogListTab',
|
||||
content: getRuleEventLogList<'default'>({
|
||||
rule,
|
||||
ruleId: rule?.id,
|
||||
ruleType,
|
||||
} as RuleEventLogListProps),
|
||||
},
|
||||
|
|
|
@ -73,7 +73,7 @@ export const renderApp = (deps: TriggersAndActionsUiServices) => {
|
|||
|
||||
export const App = ({ deps }: { deps: TriggersAndActionsUiServices }) => {
|
||||
const { dataViews, uiSettings, theme$ } = deps;
|
||||
const sections: Section[] = ['rules', 'connectors', 'alerts'];
|
||||
const sections: Section[] = ['rules', 'connectors', 'logs', 'alerts'];
|
||||
const isDarkMode = useObservable<boolean>(uiSettings.get$('theme:darkMode'));
|
||||
|
||||
const sectionsRegex = sections.join('|');
|
||||
|
|
|
@ -13,11 +13,12 @@ export {
|
|||
} from '@kbn/alerting-plugin/common';
|
||||
export { BASE_ACTION_API_PATH, INTERNAL_BASE_ACTION_API_PATH } from '@kbn/actions-plugin/common';
|
||||
|
||||
export type Section = 'connectors' | 'rules' | 'alerts';
|
||||
export type Section = 'connectors' | 'rules' | 'alerts' | 'logs';
|
||||
|
||||
export const routeToHome = `/`;
|
||||
export const routeToConnectors = `/connectors`;
|
||||
export const routeToRules = `/rules`;
|
||||
export const routeToLogs = `/logs`;
|
||||
export const routeToRuleDetails = `/rule/:ruleId`;
|
||||
export const routeToInternalAlerts = `/alerts`;
|
||||
export const legacyRouteToRules = `/alerts`;
|
||||
|
@ -41,6 +42,8 @@ export const DEFAULT_SEARCH_PAGE_SIZE: number = 10;
|
|||
export const DEFAULT_RULE_INTERVAL = '1m';
|
||||
|
||||
export const RULE_EXECUTION_LOG_COLUMN_IDS = [
|
||||
'rule_id',
|
||||
'rule_name',
|
||||
'id',
|
||||
'timestamp',
|
||||
'execution_duration',
|
||||
|
@ -73,6 +76,7 @@ export const RULE_EXECUTION_LOG_ALERT_COUNT_COLUMNS = [
|
|||
];
|
||||
|
||||
export const LOCKED_COLUMNS = [
|
||||
'rule_name',
|
||||
'timestamp',
|
||||
'execution_duration',
|
||||
'status',
|
||||
|
@ -81,4 +85,5 @@ export const LOCKED_COLUMNS = [
|
|||
'num_errored_actions',
|
||||
];
|
||||
|
||||
export const RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS = [...LOCKED_COLUMNS];
|
||||
export const RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS = [...LOCKED_COLUMNS.slice(1)];
|
||||
export const GLOBAL_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS = ['rule_name', ...LOCKED_COLUMNS];
|
||||
|
|
|
@ -70,7 +70,7 @@ describe('home', () => {
|
|||
let home = mountWithIntl(<TriggersActionsUIHome {...props} />);
|
||||
|
||||
// Just rules/connectors
|
||||
expect(home.find('.euiTab__content').length).toBe(2);
|
||||
expect(home.find('.euiTab__content').length).toBe(3);
|
||||
|
||||
(getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => {
|
||||
if (feature === 'internalAlertsTable') {
|
||||
|
@ -81,6 +81,6 @@ describe('home', () => {
|
|||
|
||||
home = mountWithIntl(<TriggersActionsUIHome {...props} />);
|
||||
// alerts now too!
|
||||
expect(home.find('.euiTab__content').length).toBe(3);
|
||||
expect(home.find('.euiTab__content').length).toBe(4);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,7 +11,13 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { EuiSpacer, EuiButtonEmpty, EuiPageHeader } from '@elastic/eui';
|
||||
|
||||
import { getIsExperimentalFeatureEnabled } from '../common/get_experimental_features';
|
||||
import { Section, routeToConnectors, routeToRules, routeToInternalAlerts } from './constants';
|
||||
import {
|
||||
Section,
|
||||
routeToConnectors,
|
||||
routeToRules,
|
||||
routeToInternalAlerts,
|
||||
routeToLogs,
|
||||
} from './constants';
|
||||
import { getAlertingSectionBreadcrumb } from './lib/breadcrumb';
|
||||
import { getCurrentDocTitle } from './lib/doc_title';
|
||||
import { hasShowActionsCapability } from './lib/capabilities';
|
||||
|
@ -25,6 +31,7 @@ const ActionsConnectorsList = lazy(
|
|||
() => import('./sections/actions_connectors_list/components/actions_connectors_list')
|
||||
);
|
||||
const RulesList = lazy(() => import('./sections/rules_list/components/rules_list'));
|
||||
const LogsList = lazy(() => import('./sections/logs_list/components/logs_list'));
|
||||
const AlertsPage = lazy(() => import('./sections/alerts_table/alerts_page'));
|
||||
|
||||
export interface MatchParams {
|
||||
|
@ -71,6 +78,11 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
|
|||
});
|
||||
}
|
||||
|
||||
tabs.push({
|
||||
id: 'logs',
|
||||
name: <FormattedMessage id="xpack.triggersActionsUI.home.logsTabTitle" defaultMessage="Logs" />,
|
||||
});
|
||||
|
||||
if (isInternalAlertsTableEnabled) {
|
||||
tabs.push({
|
||||
id: 'alerts',
|
||||
|
@ -138,6 +150,11 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
|
|||
<HealthContextProvider>
|
||||
<HealthCheck waitForCheck={true}>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={routeToLogs}
|
||||
component={suspendedComponentWithProps(LogsList, 'xl')}
|
||||
/>
|
||||
{canShowActions && (
|
||||
<Route
|
||||
exact
|
||||
|
|
|
@ -20,8 +20,14 @@ export { loadRuleTypes } from './rule_types';
|
|||
export type { LoadRulesProps } from './rules_helpers';
|
||||
export { loadRules } from './rules';
|
||||
export { loadRuleState } from './state';
|
||||
export type { LoadExecutionLogAggregationsProps } from './load_execution_log_aggregations';
|
||||
export { loadExecutionLogAggregations } from './load_execution_log_aggregations';
|
||||
export type {
|
||||
LoadExecutionLogAggregationsProps,
|
||||
LoadGlobalExecutionLogAggregationsProps,
|
||||
} from './load_execution_log_aggregations';
|
||||
export {
|
||||
loadExecutionLogAggregations,
|
||||
loadGlobalExecutionLogAggregations,
|
||||
} from './load_execution_log_aggregations';
|
||||
export type { LoadActionErrorLogProps } from './load_action_error_log';
|
||||
export { loadActionErrorLog } from './load_action_error_log';
|
||||
export { unmuteAlertInstance } from './unmute_alert';
|
||||
|
|
|
@ -74,6 +74,8 @@ export interface LoadExecutionLogAggregationsProps {
|
|||
sort?: SortField[];
|
||||
}
|
||||
|
||||
export type LoadGlobalExecutionLogAggregationsProps = Omit<LoadExecutionLogAggregationsProps, 'id'>;
|
||||
|
||||
export const loadExecutionLogAggregations = async ({
|
||||
id,
|
||||
http,
|
||||
|
@ -106,3 +108,35 @@ export const loadExecutionLogAggregations = async ({
|
|||
|
||||
return rewriteBodyRes(result);
|
||||
};
|
||||
|
||||
export const loadGlobalExecutionLogAggregations = async ({
|
||||
http,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
outcomeFilter,
|
||||
message,
|
||||
perPage = 10,
|
||||
page = 0,
|
||||
sort = [],
|
||||
}: LoadGlobalExecutionLogAggregationsProps & { http: HttpSetup }) => {
|
||||
const sortField: any[] = sort;
|
||||
const filter = getFilter({ outcomeFilter, message });
|
||||
|
||||
const result = await http.get<AsApiContract<IExecutionLogResult>>(
|
||||
`${INTERNAL_BASE_ALERTING_API_PATH}/_global_execution_logs`,
|
||||
{
|
||||
query: {
|
||||
date_start: dateStart,
|
||||
date_end: dateEnd,
|
||||
filter: filter.length ? filter.join(' and ') : undefined,
|
||||
per_page: perPage,
|
||||
// Need to add the + 1 for pages because APIs are 1 indexed,
|
||||
// whereas data grid sorts are 0 indexed.
|
||||
page: page + 1,
|
||||
sort: sortField.length ? JSON.stringify(sortField) : undefined,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return rewriteBodyRes(result);
|
||||
};
|
||||
|
|
|
@ -36,7 +36,9 @@ import {
|
|||
alertingFrameworkHealth,
|
||||
resolveRule,
|
||||
loadExecutionLogAggregations,
|
||||
loadGlobalExecutionLogAggregations,
|
||||
LoadExecutionLogAggregationsProps,
|
||||
LoadGlobalExecutionLogAggregationsProps,
|
||||
loadActionErrorLog,
|
||||
LoadActionErrorLogProps,
|
||||
snoozeRule,
|
||||
|
@ -70,6 +72,9 @@ export interface ComponentOpts {
|
|||
loadExecutionLogAggregations: (
|
||||
props: LoadExecutionLogAggregationsProps
|
||||
) => Promise<IExecutionLogResult>;
|
||||
loadGlobalExecutionLogAggregations: (
|
||||
props: LoadGlobalExecutionLogAggregationsProps
|
||||
) => Promise<IExecutionLogResult>;
|
||||
loadActionErrorLog: (props: LoadActionErrorLogProps) => Promise<IExecutionErrorsResult>;
|
||||
getHealth: () => Promise<AlertingFrameworkHealth>;
|
||||
resolveRule: (id: Rule['id']) => Promise<ResolvedRule>;
|
||||
|
@ -151,6 +156,14 @@ export function withBulkRuleOperations<T>(
|
|||
http,
|
||||
})
|
||||
}
|
||||
loadGlobalExecutionLogAggregations={async (
|
||||
loadProps: LoadGlobalExecutionLogAggregationsProps
|
||||
) =>
|
||||
loadGlobalExecutionLogAggregations({
|
||||
...loadProps,
|
||||
http,
|
||||
})
|
||||
}
|
||||
loadActionErrorLog={async (loadProps: LoadActionErrorLogProps) =>
|
||||
loadActionErrorLog({
|
||||
...loadProps,
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { suspendedComponentWithProps } from '../../../lib/suspended_component_with_props';
|
||||
import { RuleEventLogListTableWithApi } from '../../rule_details/components/rule_event_log_list_table';
|
||||
|
||||
const GLOBAL_EVENT_LOG_LIST_STORAGE_KEY =
|
||||
'xpack.triggersActionsUI.globalEventLogList.initialColumns';
|
||||
|
||||
export const LogsList = () => {
|
||||
return suspendedComponentWithProps(
|
||||
RuleEventLogListTableWithApi,
|
||||
'xl'
|
||||
)({
|
||||
ruleId: '*',
|
||||
refreshToken: 0,
|
||||
initialPageSize: 50,
|
||||
hasRuleNames: true,
|
||||
localStorageKey: GLOBAL_EVENT_LOG_LIST_STORAGE_KEY,
|
||||
});
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default LogsList;
|
|
@ -31,7 +31,7 @@ import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experime
|
|||
import { suspendedComponentWithProps } from '../../../lib/suspended_component_with_props';
|
||||
import RuleStatusPanelWithApi from './rule_status_panel';
|
||||
|
||||
const RuleEventLogListWithApi = lazy(() => import('./rule_event_log_list'));
|
||||
const RuleEventLogList = lazy(() => import('./rule_event_log_list'));
|
||||
const RuleAlertList = lazy(() => import('./rule_alert_list'));
|
||||
const RuleDefinition = lazy(() => import('./rule_definition'));
|
||||
|
||||
|
@ -104,11 +104,11 @@ export function RuleComponent({
|
|||
}),
|
||||
'data-test-subj': 'eventLogListTab',
|
||||
content: suspendedComponentWithProps<RuleEventLogListProps<'stackManagement'>>(
|
||||
RuleEventLogListWithApi,
|
||||
RuleEventLogList,
|
||||
'xl'
|
||||
)({
|
||||
fetchRuleSummary: false,
|
||||
rule,
|
||||
ruleId: rule.id,
|
||||
ruleType,
|
||||
ruleSummary,
|
||||
numberOfExecutions,
|
||||
|
|
|
@ -11,7 +11,6 @@ import { act } from 'react-dom/test-utils';
|
|||
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
|
||||
import { RuleActionErrorLogFlyout } from './rule_action_error_log_flyout';
|
||||
import { loadActionErrorLog } from '../../../lib/rule_api/load_action_error_log';
|
||||
import { Rule } from '../../../../types';
|
||||
|
||||
jest.mock('../../../lib/rule_api/load_action_error_log', () => ({
|
||||
loadActionErrorLog: jest.fn(),
|
||||
|
@ -43,33 +42,9 @@ const mockErrorLogResponse = {
|
|||
],
|
||||
};
|
||||
|
||||
const mockRule: Rule = {
|
||||
id: uuid.v4(),
|
||||
enabled: true,
|
||||
name: `rule-${uuid.v4()}`,
|
||||
tags: [],
|
||||
ruleTypeId: '.noop',
|
||||
consumer: 'consumer',
|
||||
schedule: { interval: '1m' },
|
||||
actions: [],
|
||||
params: {},
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
apiKeyOwner: null,
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
status: 'unknown',
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
},
|
||||
};
|
||||
|
||||
const mockExecution: any = {
|
||||
id: uuid.v4(),
|
||||
rule_id: uuid.v4(),
|
||||
timestamp: '2022-03-20T07:40:44-07:00',
|
||||
duration: 5000000,
|
||||
status: 'success',
|
||||
|
@ -98,7 +73,7 @@ describe('rule_action_error_log_flyout', () => {
|
|||
|
||||
it('renders correctly', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleActionErrorLogFlyout rule={mockRule} runLog={mockExecution} onClose={mockClose} />
|
||||
<RuleActionErrorLogFlyout runLog={mockExecution} onClose={mockClose} />
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
|
@ -115,7 +90,7 @@ describe('rule_action_error_log_flyout', () => {
|
|||
|
||||
it('can close the flyout', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleActionErrorLogFlyout rule={mockRule} runLog={mockExecution} onClose={mockClose} />
|
||||
<RuleActionErrorLogFlyout runLog={mockExecution} onClose={mockClose} />
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
|
@ -130,7 +105,7 @@ describe('rule_action_error_log_flyout', () => {
|
|||
|
||||
it('switches between push and overlay flyout depending on the size of the screen', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleActionErrorLogFlyout rule={mockRule} runLog={mockExecution} onClose={mockClose} />
|
||||
<RuleActionErrorLogFlyout runLog={mockExecution} onClose={mockClose} />
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
|
|
|
@ -21,23 +21,21 @@ import {
|
|||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { IExecutionLog } from '@kbn/alerting-plugin/common';
|
||||
import { Rule } from '../../../../types';
|
||||
import { RuleErrorLogWithApi } from './rule_error_log';
|
||||
import { RuleActionErrorBadge } from './rule_action_error_badge';
|
||||
|
||||
export interface RuleActionErrorLogFlyoutProps {
|
||||
rule: Rule;
|
||||
runLog: IExecutionLog;
|
||||
refreshToken?: number;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const RuleActionErrorLogFlyout = (props: RuleActionErrorLogFlyoutProps) => {
|
||||
const { rule, runLog, refreshToken, onClose } = props;
|
||||
const { runLog, refreshToken, onClose } = props;
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const { id, message, num_errored_actions: totalErrors } = runLog;
|
||||
const { id, rule_id: ruleId, message, num_errored_actions: totalErrors } = runLog;
|
||||
|
||||
const isFlyoutPush = useIsWithinBreakpoints(['xl']);
|
||||
|
||||
|
@ -84,7 +82,7 @@ export const RuleActionErrorLogFlyout = (props: RuleActionErrorLogFlyoutProps) =
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
<RuleErrorLogWithApi rule={rule} runId={id} refreshToken={refreshToken} />
|
||||
<RuleErrorLogWithApi ruleId={ruleId} runId={id} refreshToken={refreshToken} />
|
||||
<EuiSpacer />
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
|
|
|
@ -141,7 +141,7 @@ describe('rule_error_log', () => {
|
|||
it('renders correctly', async () => {
|
||||
const nowMock = jest.spyOn(Date, 'now').mockReturnValue(0);
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleErrorLog rule={mockRule} loadActionErrorLog={loadActionErrorLogMock} />
|
||||
<RuleErrorLog ruleId={mockRule.id} loadActionErrorLog={loadActionErrorLogMock} />
|
||||
);
|
||||
|
||||
// No data initially
|
||||
|
@ -184,7 +184,7 @@ describe('rule_error_log', () => {
|
|||
const nowMock = jest.spyOn(Date, 'now').mockReturnValue(0);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleErrorLog rule={mockRule} loadActionErrorLog={loadActionErrorLogMock} />
|
||||
<RuleErrorLog ruleId={mockRule.id} loadActionErrorLog={loadActionErrorLogMock} />
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
|
@ -233,7 +233,7 @@ describe('rule_error_log', () => {
|
|||
});
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleErrorLog rule={mockRule} loadActionErrorLog={loadActionErrorLogMock} />
|
||||
<RuleErrorLog ruleId={mockRule.id} loadActionErrorLog={loadActionErrorLogMock} />
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
|
@ -280,7 +280,7 @@ describe('rule_error_log', () => {
|
|||
const nowMock = jest.spyOn(Date, 'now').mockReturnValue(0);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleErrorLog rule={mockRule} loadActionErrorLog={loadActionErrorLogMock} />
|
||||
<RuleErrorLog ruleId={mockRule.id} loadActionErrorLog={loadActionErrorLogMock} />
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
|
@ -328,7 +328,7 @@ describe('rule_error_log', () => {
|
|||
|
||||
it('does not show the refine search prompt normally', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleErrorLog rule={mockRule} loadActionErrorLog={loadActionErrorLogMock} />
|
||||
<RuleErrorLog ruleId={mockRule.id} loadActionErrorLog={loadActionErrorLogMock} />
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
|
@ -346,7 +346,7 @@ describe('rule_error_log', () => {
|
|||
});
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleErrorLog rule={mockRule} loadActionErrorLog={loadActionErrorLogMock} />
|
||||
<RuleErrorLog ruleId={mockRule.id} loadActionErrorLog={loadActionErrorLogMock} />
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
|
|
|
@ -25,7 +25,6 @@ import { IExecutionErrors } from '@kbn/alerting-plugin/common';
|
|||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
|
||||
import { RefineSearchPrompt } from '../refine_search_prompt';
|
||||
import { Rule } from '../../../../types';
|
||||
import {
|
||||
ComponentOpts as RuleApis,
|
||||
withBulkRuleOperations,
|
||||
|
@ -61,14 +60,14 @@ const updateButtonProps = {
|
|||
const MAX_RESULTS = 1000;
|
||||
|
||||
export type RuleErrorLogProps = {
|
||||
rule: Rule;
|
||||
ruleId: string;
|
||||
runId?: string;
|
||||
refreshToken?: number;
|
||||
requestRefresh?: () => Promise<void>;
|
||||
} & Pick<RuleApis, 'loadActionErrorLog'>;
|
||||
|
||||
export const RuleErrorLog = (props: RuleErrorLogProps) => {
|
||||
const { rule, runId, loadActionErrorLog, refreshToken } = props;
|
||||
const { ruleId, runId, loadActionErrorLog, refreshToken } = props;
|
||||
|
||||
const { uiSettings, notifications } = useKibana().services;
|
||||
|
||||
|
@ -131,7 +130,7 @@ export const RuleErrorLog = (props: RuleErrorLogProps) => {
|
|||
setIsLoading(true);
|
||||
try {
|
||||
const result = await loadActionErrorLog({
|
||||
id: rule.id,
|
||||
id: ruleId,
|
||||
runId,
|
||||
message: searchText,
|
||||
dateStart: getParsedDate(dateStart),
|
||||
|
|
|
@ -59,6 +59,7 @@ export interface RuleEventLogDataGrid {
|
|||
dateFormat: string;
|
||||
pageSizeOptions?: number[];
|
||||
selectedRunLog?: IExecutionLog;
|
||||
showRuleNameAndIdColumns?: boolean;
|
||||
onChangeItemsPerPage: (pageSize: number) => void;
|
||||
onChangePage: (pageIndex: number) => void;
|
||||
onFilterChange: (filter: string[]) => void;
|
||||
|
@ -160,6 +161,7 @@ export const RuleEventLogDataGrid = (props: RuleEventLogDataGrid) => {
|
|||
dateFormat,
|
||||
visibleColumns,
|
||||
selectedRunLog,
|
||||
showRuleNameAndIdColumns = false,
|
||||
setVisibleColumns,
|
||||
setSortingColumns,
|
||||
onChangeItemsPerPage,
|
||||
|
@ -180,6 +182,39 @@ export const RuleEventLogDataGrid = (props: RuleEventLogDataGrid) => {
|
|||
|
||||
const columns: EuiDataGridColumn[] = useMemo(
|
||||
() => [
|
||||
...(showRuleNameAndIdColumns
|
||||
? [
|
||||
{
|
||||
id: 'rule_id',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.ruleId',
|
||||
{
|
||||
defaultMessage: 'Rule Id',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('rule_id'),
|
||||
actions: {
|
||||
showSortAsc: false,
|
||||
showSortDesc: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'rule_name',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.ruleName',
|
||||
{
|
||||
defaultMessage: 'Rule',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('rule_name'),
|
||||
actions: {
|
||||
showSortAsc: false,
|
||||
showSortDesc: false,
|
||||
showHide: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'id',
|
||||
displayAsText: i18n.translate(
|
||||
|
@ -394,7 +429,7 @@ export const RuleEventLogDataGrid = (props: RuleEventLogDataGrid) => {
|
|||
isSortable: getIsColumnSortable('timed_out'),
|
||||
},
|
||||
],
|
||||
[getPaginatedRowIndex, onFlyoutOpen, onFilterChange, logs]
|
||||
[getPaginatedRowIndex, onFlyoutOpen, onFilterChange, showRuleNameAndIdColumns, logs]
|
||||
);
|
||||
|
||||
const columnVisibilityProps = useMemo(
|
||||
|
@ -524,6 +559,7 @@ export const RuleEventLogDataGrid = (props: RuleEventLogDataGrid) => {
|
|||
const value = logs[pagedRowIndex]?.[columnId as keyof IExecutionLog] as string;
|
||||
const actionErrors = logs[pagedRowIndex]?.num_errored_actions || (0 as number);
|
||||
const version = logs?.[pagedRowIndex]?.version;
|
||||
const ruleId = runLog?.rule_id;
|
||||
|
||||
if (columnId === 'num_errored_actions' && runLog) {
|
||||
return (
|
||||
|
@ -555,6 +591,7 @@ export const RuleEventLogDataGrid = (props: RuleEventLogDataGrid) => {
|
|||
value={value}
|
||||
version={version}
|
||||
dateFormat={dateFormat}
|
||||
ruleId={ruleId}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -15,7 +15,10 @@ import { EuiSuperDatePicker, EuiDataGrid } from '@elastic/eui';
|
|||
import { RuleEventLogListStatusFilter } from './rule_event_log_list_status_filter';
|
||||
import { RuleEventLogList } from './rule_event_log_list';
|
||||
import { RefineSearchPrompt } from '../refine_search_prompt';
|
||||
import { RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS } from '../../../constants';
|
||||
import {
|
||||
RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS,
|
||||
GLOBAL_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS,
|
||||
} from '../../../constants';
|
||||
import { mockRule, mockRuleType, mockRuleSummary } from './test_helpers';
|
||||
import { RuleType } from '../../../../types';
|
||||
import { loadActionErrorLog } from '../../../lib/rule_api/load_action_error_log';
|
||||
|
@ -161,7 +164,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
it('renders correctly', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleEventLogList
|
||||
rule={ruleMock}
|
||||
ruleId={ruleMock.id}
|
||||
ruleType={ruleType}
|
||||
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
|
||||
numberOfExecutions={60}
|
||||
|
@ -207,7 +210,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
it('can sort by single and/or multiple column(s)', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleEventLogList
|
||||
rule={ruleMock}
|
||||
ruleId={ruleMock.id}
|
||||
ruleType={ruleType}
|
||||
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
|
||||
numberOfExecutions={60}
|
||||
|
@ -312,7 +315,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
it('can filter by execution log outcome status', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleEventLogList
|
||||
rule={ruleMock}
|
||||
ruleId={ruleMock.id}
|
||||
ruleType={ruleType}
|
||||
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
|
||||
numberOfExecutions={60}
|
||||
|
@ -375,7 +378,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleEventLogList
|
||||
rule={ruleMock}
|
||||
ruleId={ruleMock.id}
|
||||
ruleType={ruleType}
|
||||
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
|
||||
numberOfExecutions={60}
|
||||
|
@ -435,7 +438,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleEventLogList
|
||||
rule={ruleMock}
|
||||
ruleId={ruleMock.id}
|
||||
ruleType={ruleType}
|
||||
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
|
||||
numberOfExecutions={60}
|
||||
|
@ -492,7 +495,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
it('can save display columns to localStorage', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleEventLogList
|
||||
rule={ruleMock}
|
||||
ruleId={ruleMock.id}
|
||||
ruleType={ruleType}
|
||||
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
|
||||
numberOfExecutions={60}
|
||||
|
@ -510,7 +513,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
JSON.parse(
|
||||
localStorage.getItem('xpack.triggersActionsUI.ruleEventLogList.initialColumns') ?? 'null'
|
||||
)
|
||||
).toEqual(RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS);
|
||||
).toEqual(GLOBAL_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS);
|
||||
|
||||
wrapper.find('[data-test-subj="dataGridColumnSelectorButton"] button').simulate('click');
|
||||
|
||||
|
@ -535,7 +538,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
it('does not show the refine search prompt normally', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleEventLogList
|
||||
rule={ruleMock}
|
||||
ruleId={ruleMock.id}
|
||||
ruleType={ruleType}
|
||||
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
|
||||
numberOfExecutions={60}
|
||||
|
@ -560,7 +563,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleEventLogList
|
||||
rule={ruleMock}
|
||||
ruleId={ruleMock.id}
|
||||
ruleType={ruleType}
|
||||
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
|
||||
numberOfExecutions={60}
|
||||
|
@ -610,7 +613,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleEventLogList
|
||||
rule={ruleMock}
|
||||
ruleId={ruleMock.id}
|
||||
ruleType={ruleType}
|
||||
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
|
||||
numberOfExecutions={60}
|
||||
|
@ -637,7 +640,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleEventLogList
|
||||
rule={ruleMock}
|
||||
ruleId={ruleMock.id}
|
||||
ruleType={ruleType}
|
||||
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
|
||||
numberOfExecutions={60}
|
||||
|
@ -664,7 +667,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleEventLogList
|
||||
rule={ruleMock}
|
||||
ruleId={ruleMock.id}
|
||||
ruleType={ruleType}
|
||||
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
|
||||
numberOfExecutions={60}
|
||||
|
@ -732,7 +735,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleEventLogList
|
||||
rule={ruleMock}
|
||||
ruleId={ruleMock.id}
|
||||
ruleType={ruleType}
|
||||
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
|
||||
numberOfExecutions={60}
|
||||
|
@ -765,7 +768,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<RuleEventLogList
|
||||
fetchRuleSummary={false}
|
||||
rule={ruleMock}
|
||||
ruleId={ruleMock.id}
|
||||
ruleType={ruleType}
|
||||
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
|
||||
numberOfExecutions={60}
|
||||
|
@ -804,7 +807,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<RuleEventLogList
|
||||
fetchRuleSummary={false}
|
||||
rule={ruleMock}
|
||||
ruleId={ruleMock.id}
|
||||
ruleType={ruleTypeCustom}
|
||||
ruleSummary={ruleSummary}
|
||||
numberOfExecutions={60}
|
||||
|
@ -842,7 +845,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<RuleEventLogList
|
||||
fetchRuleSummary={false}
|
||||
rule={ruleMock}
|
||||
ruleId={ruleMock.id}
|
||||
ruleType={ruleTypeCustom}
|
||||
ruleSummary={ruleSummary}
|
||||
numberOfExecutions={60}
|
||||
|
|
|
@ -5,84 +5,29 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState, useMemo, useRef } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import datemath from '@kbn/datemath';
|
||||
import {
|
||||
EuiFieldSearch,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiProgress,
|
||||
EuiSpacer,
|
||||
EuiDataGridSorting,
|
||||
Pagination,
|
||||
EuiSuperDatePicker,
|
||||
OnTimeChangeProps,
|
||||
} from '@elastic/eui';
|
||||
import { IExecutionLog } from '@kbn/alerting-plugin/common';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS, LOCKED_COLUMNS } from '../../../constants';
|
||||
import { RuleEventLogListStatusFilter } from './rule_event_log_list_status_filter';
|
||||
import { RuleEventLogDataGrid } from './rule_event_log_data_grid';
|
||||
import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner';
|
||||
import React from 'react';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { RuleExecutionSummaryAndChartWithApi } from './rule_execution_summary_and_chart';
|
||||
import { RuleActionErrorLogFlyout } from './rule_action_error_log_flyout';
|
||||
|
||||
import { RefineSearchPrompt } from '../refine_search_prompt';
|
||||
import { LoadExecutionLogAggregationsProps } from '../../../lib/rule_api';
|
||||
import { Rule, RuleSummary, RuleType } from '../../../../types';
|
||||
import {
|
||||
ComponentOpts as RuleApis,
|
||||
withBulkRuleOperations,
|
||||
} from '../../common/components/with_bulk_rule_api_operations';
|
||||
|
||||
const getParsedDate = (date: string) => {
|
||||
if (date.includes('now')) {
|
||||
return datemath.parse(date)?.format() || date;
|
||||
}
|
||||
return date;
|
||||
};
|
||||
|
||||
const API_FAILED_MESSAGE = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.apiError',
|
||||
{
|
||||
defaultMessage: 'Failed to fetch execution history',
|
||||
}
|
||||
);
|
||||
|
||||
const SEARCH_PLACEHOLDER = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.searchPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Search event log message',
|
||||
}
|
||||
);
|
||||
import { RuleSummary, RuleType } from '../../../../types';
|
||||
import { ComponentOpts as RuleApis } from '../../common/components/with_bulk_rule_api_operations';
|
||||
import { RuleEventLogListTableWithApi } from './rule_event_log_list_table';
|
||||
|
||||
const RULE_EVENT_LOG_LIST_STORAGE_KEY = 'xpack.triggersActionsUI.ruleEventLogList.initialColumns';
|
||||
|
||||
const getDefaultColumns = (columns: string[]) => {
|
||||
const columnsWithoutLockedColumn = columns.filter((column) => !LOCKED_COLUMNS.includes(column));
|
||||
return [...LOCKED_COLUMNS, ...columnsWithoutLockedColumn];
|
||||
};
|
||||
|
||||
const updateButtonProps = {
|
||||
iconOnly: true,
|
||||
fill: false,
|
||||
};
|
||||
|
||||
const MAX_RESULTS = 1000;
|
||||
|
||||
const ruleEventListContainerStyle = { minHeight: 400 };
|
||||
|
||||
export type RuleEventLogListOptions = 'stackManagement' | 'default';
|
||||
|
||||
export interface RuleEventLogListCommonProps {
|
||||
rule: Rule;
|
||||
ruleId: string;
|
||||
ruleType: RuleType;
|
||||
localStorageKey?: string;
|
||||
refreshToken?: number;
|
||||
requestRefresh?: () => Promise<void>;
|
||||
loadExecutionLogAggregations?: RuleApis['loadExecutionLogAggregations'];
|
||||
fetchRuleSummary?: boolean;
|
||||
hideChart?: boolean;
|
||||
}
|
||||
|
||||
export interface RuleEventLogListStackManagementProps {
|
||||
|
@ -103,7 +48,7 @@ export const RuleEventLogList = <T extends RuleEventLogListOptions>(
|
|||
props: RuleEventLogListProps<T>
|
||||
) => {
|
||||
const {
|
||||
rule,
|
||||
ruleId,
|
||||
ruleType,
|
||||
localStorageKey = RULE_EVENT_LOG_LIST_STORAGE_KEY,
|
||||
refreshToken,
|
||||
|
@ -118,228 +63,11 @@ export const RuleEventLogList = <T extends RuleEventLogListOptions>(
|
|||
onChangeDuration,
|
||||
isLoadingRuleSummary = false,
|
||||
} = props as RuleEventLogListStackManagementProps;
|
||||
|
||||
const { uiSettings, notifications } = useKibana().services;
|
||||
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [search, setSearch] = useState<string>('');
|
||||
const [isFlyoutOpen, setIsFlyoutOpen] = useState<boolean>(false);
|
||||
const [selectedRunLog, setSelectedRunLog] = useState<IExecutionLog | undefined>();
|
||||
|
||||
// Data grid states
|
||||
const [logs, setLogs] = useState<IExecutionLog[]>();
|
||||
const [visibleColumns, setVisibleColumns] = useState<string[]>(() => {
|
||||
return getDefaultColumns(
|
||||
JSON.parse(localStorage.getItem(localStorageKey) ?? 'null') ||
|
||||
RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS
|
||||
);
|
||||
});
|
||||
const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]);
|
||||
const [filter, setFilter] = useState<string[]>([]);
|
||||
const [actualTotalItemCount, setActualTotalItemCount] = useState<number>(0);
|
||||
const [pagination, setPagination] = useState<Pagination>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
totalItemCount: 0,
|
||||
});
|
||||
|
||||
// Date related states
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [dateStart, setDateStart] = useState<string>('now-24h');
|
||||
const [dateEnd, setDateEnd] = useState<string>('now');
|
||||
const [dateFormat] = useState(() => uiSettings?.get('dateFormat'));
|
||||
const [commonlyUsedRanges] = useState(() => {
|
||||
return (
|
||||
uiSettings
|
||||
?.get('timepicker:quickRanges')
|
||||
?.map(({ from, to, display }: { from: string; to: string; display: string }) => ({
|
||||
start: from,
|
||||
end: to,
|
||||
label: display,
|
||||
})) || []
|
||||
);
|
||||
});
|
||||
|
||||
const isInitialized = useRef(false);
|
||||
|
||||
const isOnLastPage = useMemo(() => {
|
||||
const { pageIndex, pageSize } = pagination;
|
||||
return (pageIndex + 1) * pageSize >= MAX_RESULTS;
|
||||
}, [pagination]);
|
||||
|
||||
// Formats the sort columns to be consumed by the API endpoint
|
||||
const formattedSort = useMemo(() => {
|
||||
return sortingColumns.map(({ id: sortId, direction }) => ({
|
||||
[sortId]: {
|
||||
order: direction,
|
||||
},
|
||||
}));
|
||||
}, [sortingColumns]);
|
||||
|
||||
const loadEventLogs = async () => {
|
||||
if (!loadExecutionLogAggregations) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await loadExecutionLogAggregations({
|
||||
id: rule.id,
|
||||
sort: formattedSort as LoadExecutionLogAggregationsProps['sort'],
|
||||
outcomeFilter: filter,
|
||||
message: searchText,
|
||||
dateStart: getParsedDate(dateStart),
|
||||
dateEnd: getParsedDate(dateEnd),
|
||||
page: pagination.pageIndex,
|
||||
perPage: pagination.pageSize,
|
||||
});
|
||||
setLogs(result.data);
|
||||
setPagination({
|
||||
...pagination,
|
||||
totalItemCount: Math.min(result.total, MAX_RESULTS),
|
||||
});
|
||||
setActualTotalItemCount(result.total);
|
||||
} catch (e) {
|
||||
notifications.toasts.addDanger({
|
||||
title: API_FAILED_MESSAGE,
|
||||
text: e.body.message,
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const onChangeItemsPerPage = useCallback(
|
||||
(pageSize: number) => {
|
||||
setPagination((prevPagination) => ({
|
||||
...prevPagination,
|
||||
pageIndex: 0,
|
||||
pageSize,
|
||||
}));
|
||||
},
|
||||
[setPagination]
|
||||
);
|
||||
|
||||
const onChangePage = useCallback(
|
||||
(pageIndex: number) => {
|
||||
setPagination((prevPagination) => ({
|
||||
...prevPagination,
|
||||
pageIndex,
|
||||
}));
|
||||
},
|
||||
[setPagination]
|
||||
);
|
||||
|
||||
const onTimeChange = useCallback(
|
||||
({ start, end, isInvalid }: OnTimeChangeProps) => {
|
||||
if (isInvalid) {
|
||||
return;
|
||||
}
|
||||
setDateStart(start);
|
||||
setDateEnd(end);
|
||||
},
|
||||
[setDateStart, setDateEnd]
|
||||
);
|
||||
|
||||
const onRefresh = () => {
|
||||
loadEventLogs();
|
||||
};
|
||||
|
||||
const onFilterChange = useCallback(
|
||||
(newFilter: string[]) => {
|
||||
setPagination((prevPagination) => ({
|
||||
...prevPagination,
|
||||
pageIndex: 0,
|
||||
}));
|
||||
setFilter(newFilter);
|
||||
},
|
||||
[setPagination, setFilter]
|
||||
);
|
||||
|
||||
const onFlyoutOpen = useCallback((runLog: IExecutionLog) => {
|
||||
setIsFlyoutOpen(true);
|
||||
setSelectedRunLog(runLog);
|
||||
}, []);
|
||||
|
||||
const onFlyoutClose = useCallback(() => {
|
||||
setIsFlyoutOpen(false);
|
||||
setSelectedRunLog(undefined);
|
||||
}, []);
|
||||
|
||||
const onSearchChange = useCallback(
|
||||
(e) => {
|
||||
if (e.target.value === '') {
|
||||
setSearchText('');
|
||||
}
|
||||
setSearch(e.target.value);
|
||||
},
|
||||
[setSearchText, setSearch]
|
||||
);
|
||||
|
||||
const onKeyUp = useCallback(
|
||||
(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setSearchText(search);
|
||||
}
|
||||
},
|
||||
[search, setSearchText]
|
||||
);
|
||||
|
||||
const renderList = () => {
|
||||
if (!logs) {
|
||||
return <CenterJustifiedSpinner />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{isLoading && (
|
||||
<EuiProgress size="xs" color="accent" data-test-subj="ruleEventLogListProgressBar" />
|
||||
)}
|
||||
<RuleEventLogDataGrid
|
||||
logs={logs}
|
||||
pagination={pagination}
|
||||
sortingColumns={sortingColumns}
|
||||
visibleColumns={visibleColumns}
|
||||
dateFormat={dateFormat}
|
||||
selectedRunLog={selectedRunLog}
|
||||
onChangeItemsPerPage={onChangeItemsPerPage}
|
||||
onChangePage={onChangePage}
|
||||
onFlyoutOpen={onFlyoutOpen}
|
||||
onFilterChange={setFilter}
|
||||
setVisibleColumns={setVisibleColumns}
|
||||
setSortingColumns={setSortingColumns}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadEventLogs();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
sortingColumns,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
filter,
|
||||
pagination.pageIndex,
|
||||
pagination.pageSize,
|
||||
searchText,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized.current) {
|
||||
loadEventLogs();
|
||||
}
|
||||
isInitialized.current = true;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [refreshToken]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(localStorageKey, JSON.stringify(visibleColumns));
|
||||
}, [localStorageKey, visibleColumns]);
|
||||
|
||||
return (
|
||||
<div style={ruleEventListContainerStyle} data-test-subj="ruleEventLogListContainer">
|
||||
<EuiSpacer />
|
||||
<RuleExecutionSummaryAndChartWithApi
|
||||
rule={rule}
|
||||
ruleId={ruleId}
|
||||
ruleType={ruleType}
|
||||
ruleSummary={ruleSummary}
|
||||
numberOfExecutions={numberOfExecutions}
|
||||
|
@ -349,57 +77,15 @@ export const RuleEventLogList = <T extends RuleEventLogListOptions>(
|
|||
requestRefresh={requestRefresh}
|
||||
fetchRuleSummary={fetchRuleSummary}
|
||||
/>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFieldSearch
|
||||
fullWidth
|
||||
isClearable
|
||||
value={search}
|
||||
onChange={onSearchChange}
|
||||
onKeyUp={onKeyUp}
|
||||
placeholder={SEARCH_PLACEHOLDER}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<RuleEventLogListStatusFilter selectedOptions={filter} onChange={onFilterChange} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSuperDatePicker
|
||||
data-test-subj="ruleEventLogListDatePicker"
|
||||
width="auto"
|
||||
isLoading={isLoading}
|
||||
start={dateStart}
|
||||
end={dateEnd}
|
||||
onTimeChange={onTimeChange}
|
||||
onRefresh={onRefresh}
|
||||
dateFormat={dateFormat}
|
||||
commonlyUsedRanges={commonlyUsedRanges}
|
||||
updateButtonProps={updateButtonProps}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
{renderList()}
|
||||
{isOnLastPage && (
|
||||
<RefineSearchPrompt
|
||||
documentSize={actualTotalItemCount}
|
||||
visibleDocumentSize={MAX_RESULTS}
|
||||
backToTopAnchor="rule_event_log_list"
|
||||
/>
|
||||
)}
|
||||
{isFlyoutOpen && selectedRunLog && (
|
||||
<RuleActionErrorLogFlyout
|
||||
rule={rule}
|
||||
runLog={selectedRunLog}
|
||||
refreshToken={refreshToken}
|
||||
onClose={onFlyoutClose}
|
||||
/>
|
||||
)}
|
||||
<RuleEventLogListTableWithApi
|
||||
localStorageKey={localStorageKey}
|
||||
ruleId={ruleId}
|
||||
refreshToken={refreshToken}
|
||||
overrideLoadExecutionLogAggregations={loadExecutionLogAggregations}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const RuleEventLogListWithApi = withBulkRuleOperations(RuleEventLogList);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { RuleEventLogListWithApi as default };
|
||||
export { RuleEventLogList as default };
|
||||
|
|
|
@ -5,9 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import moment from 'moment';
|
||||
import type { EcsEventOutcome } from '@kbn/core/server';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { routeToRuleDetails } from '../../../constants';
|
||||
import { formatRuleAlertCount } from '../../../../common/lib/format_rule_alert_count';
|
||||
import { RuleEventLogListStatus } from './rule_event_log_list_status';
|
||||
import { RuleDurationFormat } from '../../rules_list/components/rule_duration_format';
|
||||
|
@ -26,10 +29,17 @@ interface RuleEventLogListCellRendererProps {
|
|||
version?: string;
|
||||
value?: string;
|
||||
dateFormat?: string;
|
||||
ruleId?: string;
|
||||
}
|
||||
|
||||
export const RuleEventLogListCellRenderer = (props: RuleEventLogListCellRendererProps) => {
|
||||
const { columnId, value, version, dateFormat = DEFAULT_DATE_FORMAT } = props;
|
||||
const { columnId, value, version, dateFormat = DEFAULT_DATE_FORMAT, ruleId } = props;
|
||||
const history = useHistory();
|
||||
|
||||
const onClickRuleName = useCallback(
|
||||
() => ruleId && history.push(routeToRuleDetails.replace(':ruleId', ruleId)),
|
||||
[ruleId, history]
|
||||
);
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
return null;
|
||||
|
@ -43,6 +53,10 @@ export const RuleEventLogListCellRenderer = (props: RuleEventLogListCellRenderer
|
|||
return <>{moment(value).format(dateFormat)}</>;
|
||||
}
|
||||
|
||||
if (columnId === 'rule_name' && ruleId) {
|
||||
return <EuiLink onClick={onClickRuleName}>{value}</EuiLink>;
|
||||
}
|
||||
|
||||
if (RULE_EXECUTION_LOG_ALERT_COUNT_COLUMNS.includes(columnId)) {
|
||||
return <>{formatRuleAlertCount(value, version)}</>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,396 @@
|
|||
/*
|
||||
* 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 React, { useCallback, useEffect, useState, useMemo, useRef } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import datemath from '@kbn/datemath';
|
||||
import {
|
||||
EuiFieldSearch,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiProgress,
|
||||
EuiSpacer,
|
||||
EuiDataGridSorting,
|
||||
Pagination,
|
||||
EuiSuperDatePicker,
|
||||
OnTimeChangeProps,
|
||||
} from '@elastic/eui';
|
||||
import { IExecutionLog } from '@kbn/alerting-plugin/common';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import {
|
||||
RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS,
|
||||
GLOBAL_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS,
|
||||
LOCKED_COLUMNS,
|
||||
} from '../../../constants';
|
||||
import { RuleEventLogListStatusFilter } from './rule_event_log_list_status_filter';
|
||||
import { RuleEventLogDataGrid } from './rule_event_log_data_grid';
|
||||
import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner';
|
||||
import { RuleActionErrorLogFlyout } from './rule_action_error_log_flyout';
|
||||
|
||||
import { RefineSearchPrompt } from '../refine_search_prompt';
|
||||
import { LoadExecutionLogAggregationsProps } from '../../../lib/rule_api';
|
||||
import {
|
||||
ComponentOpts as RuleApis,
|
||||
withBulkRuleOperations,
|
||||
} from '../../common/components/with_bulk_rule_api_operations';
|
||||
|
||||
const getParsedDate = (date: string) => {
|
||||
if (date.includes('now')) {
|
||||
return datemath.parse(date)?.format() || date;
|
||||
}
|
||||
return date;
|
||||
};
|
||||
|
||||
const API_FAILED_MESSAGE = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.apiError',
|
||||
{
|
||||
defaultMessage: 'Failed to fetch execution history',
|
||||
}
|
||||
);
|
||||
|
||||
const SEARCH_PLACEHOLDER = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.searchPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Search event log message',
|
||||
}
|
||||
);
|
||||
|
||||
const RULE_EVENT_LOG_LIST_STORAGE_KEY = 'xpack.triggersActionsUI.ruleEventLogList.initialColumns';
|
||||
|
||||
const getDefaultColumns = (columns: string[]) => {
|
||||
const columnsWithoutLockedColumn = columns.filter((column) => !LOCKED_COLUMNS.includes(column));
|
||||
return [...LOCKED_COLUMNS, ...columnsWithoutLockedColumn];
|
||||
};
|
||||
|
||||
const updateButtonProps = {
|
||||
iconOnly: true,
|
||||
fill: false,
|
||||
};
|
||||
|
||||
const MAX_RESULTS = 1000;
|
||||
|
||||
export type RuleEventLogListOptions = 'stackManagement' | 'default';
|
||||
|
||||
export type RuleEventLogListCommonProps = {
|
||||
ruleId: string;
|
||||
localStorageKey?: string;
|
||||
refreshToken?: number;
|
||||
initialPageSize?: number;
|
||||
// Duplicating these properties is extremely silly but it's the only way to get Jest to cooperate with the way this component is structured
|
||||
overrideLoadExecutionLogAggregations?: RuleApis['loadExecutionLogAggregations'];
|
||||
overrideLoadGlobalExecutionLogAggregations?: RuleApis['loadGlobalExecutionLogAggregations'];
|
||||
hasRuleNames?: boolean;
|
||||
} & Pick<RuleApis, 'loadExecutionLogAggregations' | 'loadGlobalExecutionLogAggregations'>;
|
||||
|
||||
export type RuleEventLogListTableProps<T extends RuleEventLogListOptions = 'default'> =
|
||||
T extends 'default'
|
||||
? RuleEventLogListCommonProps
|
||||
: T extends 'stackManagement'
|
||||
? RuleEventLogListCommonProps
|
||||
: never;
|
||||
|
||||
export const RuleEventLogListTable = <T extends RuleEventLogListOptions>(
|
||||
props: RuleEventLogListTableProps<T>
|
||||
) => {
|
||||
const {
|
||||
ruleId,
|
||||
localStorageKey = RULE_EVENT_LOG_LIST_STORAGE_KEY,
|
||||
refreshToken,
|
||||
loadGlobalExecutionLogAggregations,
|
||||
loadExecutionLogAggregations,
|
||||
overrideLoadGlobalExecutionLogAggregations,
|
||||
overrideLoadExecutionLogAggregations,
|
||||
initialPageSize = 10,
|
||||
hasRuleNames = false,
|
||||
} = props;
|
||||
|
||||
const { uiSettings, notifications } = useKibana().services;
|
||||
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [search, setSearch] = useState<string>('');
|
||||
const [isFlyoutOpen, setIsFlyoutOpen] = useState<boolean>(false);
|
||||
const [selectedRunLog, setSelectedRunLog] = useState<IExecutionLog | undefined>();
|
||||
|
||||
// Data grid states
|
||||
const [logs, setLogs] = useState<IExecutionLog[]>();
|
||||
const [visibleColumns, setVisibleColumns] = useState<string[]>(() => {
|
||||
return getDefaultColumns(
|
||||
JSON.parse(localStorage.getItem(localStorageKey) ?? 'null') || hasRuleNames
|
||||
? GLOBAL_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS
|
||||
: RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS
|
||||
);
|
||||
});
|
||||
const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]);
|
||||
const [filter, setFilter] = useState<string[]>([]);
|
||||
const [actualTotalItemCount, setActualTotalItemCount] = useState<number>(0);
|
||||
const [pagination, setPagination] = useState<Pagination>({
|
||||
pageIndex: 0,
|
||||
pageSize: initialPageSize,
|
||||
totalItemCount: 0,
|
||||
});
|
||||
|
||||
// Date related states
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [dateStart, setDateStart] = useState<string>('now-24h');
|
||||
const [dateEnd, setDateEnd] = useState<string>('now');
|
||||
const [dateFormat] = useState(() => uiSettings?.get('dateFormat'));
|
||||
const [commonlyUsedRanges] = useState(() => {
|
||||
return (
|
||||
uiSettings
|
||||
?.get('timepicker:quickRanges')
|
||||
?.map(({ from, to, display }: { from: string; to: string; display: string }) => ({
|
||||
start: from,
|
||||
end: to,
|
||||
label: display,
|
||||
})) || []
|
||||
);
|
||||
});
|
||||
|
||||
const isInitialized = useRef(false);
|
||||
|
||||
const isOnLastPage = useMemo(() => {
|
||||
const { pageIndex, pageSize } = pagination;
|
||||
return (pageIndex + 1) * pageSize >= MAX_RESULTS;
|
||||
}, [pagination]);
|
||||
|
||||
// Formats the sort columns to be consumed by the API endpoint
|
||||
const formattedSort = useMemo(() => {
|
||||
return sortingColumns.map(({ id: sortId, direction }) => ({
|
||||
[sortId]: {
|
||||
order: direction,
|
||||
},
|
||||
}));
|
||||
}, [sortingColumns]);
|
||||
|
||||
const loadLogsFn = useMemo(() => {
|
||||
if (ruleId === '*') {
|
||||
return overrideLoadGlobalExecutionLogAggregations ?? loadGlobalExecutionLogAggregations;
|
||||
}
|
||||
return overrideLoadExecutionLogAggregations ?? loadExecutionLogAggregations;
|
||||
}, [
|
||||
ruleId,
|
||||
overrideLoadExecutionLogAggregations,
|
||||
overrideLoadGlobalExecutionLogAggregations,
|
||||
loadExecutionLogAggregations,
|
||||
loadGlobalExecutionLogAggregations,
|
||||
]);
|
||||
|
||||
const loadEventLogs = async () => {
|
||||
if (!loadLogsFn) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await loadLogsFn({
|
||||
id: ruleId,
|
||||
sort: formattedSort as LoadExecutionLogAggregationsProps['sort'],
|
||||
outcomeFilter: filter,
|
||||
message: searchText,
|
||||
dateStart: getParsedDate(dateStart),
|
||||
dateEnd: getParsedDate(dateEnd),
|
||||
page: pagination.pageIndex,
|
||||
perPage: pagination.pageSize,
|
||||
});
|
||||
setLogs(result.data);
|
||||
setPagination({
|
||||
...pagination,
|
||||
totalItemCount: Math.min(result.total, MAX_RESULTS),
|
||||
});
|
||||
setActualTotalItemCount(result.total);
|
||||
} catch (e) {
|
||||
notifications.toasts.addDanger({
|
||||
title: API_FAILED_MESSAGE,
|
||||
text: e.body?.message ?? e,
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const onChangeItemsPerPage = useCallback(
|
||||
(pageSize: number) => {
|
||||
setPagination((prevPagination) => ({
|
||||
...prevPagination,
|
||||
pageIndex: 0,
|
||||
pageSize,
|
||||
}));
|
||||
},
|
||||
[setPagination]
|
||||
);
|
||||
|
||||
const onChangePage = useCallback(
|
||||
(pageIndex: number) => {
|
||||
setPagination((prevPagination) => ({
|
||||
...prevPagination,
|
||||
pageIndex,
|
||||
}));
|
||||
},
|
||||
[setPagination]
|
||||
);
|
||||
|
||||
const onTimeChange = useCallback(
|
||||
({ start, end, isInvalid }: OnTimeChangeProps) => {
|
||||
if (isInvalid) {
|
||||
return;
|
||||
}
|
||||
setDateStart(start);
|
||||
setDateEnd(end);
|
||||
},
|
||||
[setDateStart, setDateEnd]
|
||||
);
|
||||
|
||||
const onRefresh = () => {
|
||||
loadEventLogs();
|
||||
};
|
||||
|
||||
const onFilterChange = useCallback(
|
||||
(newFilter: string[]) => {
|
||||
setPagination((prevPagination) => ({
|
||||
...prevPagination,
|
||||
pageIndex: 0,
|
||||
}));
|
||||
setFilter(newFilter);
|
||||
},
|
||||
[setPagination, setFilter]
|
||||
);
|
||||
|
||||
const onFlyoutOpen = useCallback((runLog: IExecutionLog) => {
|
||||
setIsFlyoutOpen(true);
|
||||
setSelectedRunLog(runLog);
|
||||
}, []);
|
||||
|
||||
const onFlyoutClose = useCallback(() => {
|
||||
setIsFlyoutOpen(false);
|
||||
setSelectedRunLog(undefined);
|
||||
}, []);
|
||||
|
||||
const onSearchChange = useCallback(
|
||||
(e) => {
|
||||
if (e.target.value === '') {
|
||||
setSearchText('');
|
||||
}
|
||||
setSearch(e.target.value);
|
||||
},
|
||||
[setSearchText, setSearch]
|
||||
);
|
||||
|
||||
const onKeyUp = useCallback(
|
||||
(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setSearchText(search);
|
||||
}
|
||||
},
|
||||
[search, setSearchText]
|
||||
);
|
||||
|
||||
const renderList = () => {
|
||||
if (!logs) {
|
||||
return <CenterJustifiedSpinner />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{isLoading && (
|
||||
<EuiProgress size="xs" color="accent" data-test-subj="ruleEventLogListProgressBar" />
|
||||
)}
|
||||
<RuleEventLogDataGrid
|
||||
logs={logs}
|
||||
pagination={pagination}
|
||||
sortingColumns={sortingColumns}
|
||||
visibleColumns={visibleColumns}
|
||||
dateFormat={dateFormat}
|
||||
selectedRunLog={selectedRunLog}
|
||||
showRuleNameAndIdColumns={hasRuleNames}
|
||||
onChangeItemsPerPage={onChangeItemsPerPage}
|
||||
onChangePage={onChangePage}
|
||||
onFlyoutOpen={onFlyoutOpen}
|
||||
onFilterChange={setFilter}
|
||||
setVisibleColumns={setVisibleColumns}
|
||||
setSortingColumns={setSortingColumns}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadEventLogs();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
sortingColumns,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
filter,
|
||||
pagination.pageIndex,
|
||||
pagination.pageSize,
|
||||
searchText,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized.current) {
|
||||
loadEventLogs();
|
||||
}
|
||||
isInitialized.current = true;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [refreshToken]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(localStorageKey, JSON.stringify(visibleColumns));
|
||||
}, [localStorageKey, visibleColumns]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFieldSearch
|
||||
fullWidth
|
||||
isClearable
|
||||
value={search}
|
||||
onChange={onSearchChange}
|
||||
onKeyUp={onKeyUp}
|
||||
placeholder={SEARCH_PLACEHOLDER}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<RuleEventLogListStatusFilter selectedOptions={filter} onChange={onFilterChange} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSuperDatePicker
|
||||
data-test-subj="ruleEventLogListDatePicker"
|
||||
width="auto"
|
||||
isLoading={isLoading}
|
||||
start={dateStart}
|
||||
end={dateEnd}
|
||||
onTimeChange={onTimeChange}
|
||||
onRefresh={onRefresh}
|
||||
dateFormat={dateFormat}
|
||||
commonlyUsedRanges={commonlyUsedRanges}
|
||||
updateButtonProps={updateButtonProps}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
{renderList()}
|
||||
{isOnLastPage && (
|
||||
<RefineSearchPrompt
|
||||
documentSize={actualTotalItemCount}
|
||||
visibleDocumentSize={MAX_RESULTS}
|
||||
backToTopAnchor="rule_event_log_list"
|
||||
/>
|
||||
)}
|
||||
{isFlyoutOpen && selectedRunLog && (
|
||||
<RuleActionErrorLogFlyout
|
||||
runLog={selectedRunLog}
|
||||
refreshToken={refreshToken}
|
||||
onClose={onFlyoutClose}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const RuleEventLogListTableWithApi = withBulkRuleOperations(RuleEventLogListTable);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { RuleEventLogListTableWithApi as default };
|
|
@ -44,7 +44,7 @@ describe('rule_execution_summary_and_chart', () => {
|
|||
it('becomes a stateless component when "fetchRuleSummary" is false', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleExecutionSummaryAndChart
|
||||
rule={ruleMock}
|
||||
ruleId={ruleMock.id}
|
||||
ruleType={ruleType}
|
||||
ruleSummary={mockRuleSummary()}
|
||||
numberOfExecutions={60}
|
||||
|
@ -100,7 +100,7 @@ describe('rule_execution_summary_and_chart', () => {
|
|||
it('becomes a container component when "fetchRuleSummary" is true', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleExecutionSummaryAndChart
|
||||
rule={ruleMock}
|
||||
ruleId={ruleMock.id}
|
||||
ruleType={ruleType}
|
||||
fetchRuleSummary={true}
|
||||
loadRuleSummary={loadRuleSummaryMock}
|
||||
|
@ -154,7 +154,7 @@ describe('rule_execution_summary_and_chart', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleExecutionSummaryAndChart
|
||||
rule={ruleMock}
|
||||
ruleId={ruleMock.id}
|
||||
ruleType={ruleType}
|
||||
fetchRuleSummary={true}
|
||||
loadRuleSummary={loadRuleSummaryMock}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React, { useMemo, useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiPanel, EuiStat, EuiFlexItem, EuiFlexGroup, EuiIconTip } from '@elastic/eui';
|
||||
import { Rule, RuleSummary, RuleType } from '../../../../types';
|
||||
import { RuleSummary, RuleType } from '../../../../types';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner';
|
||||
import { ExecutionDurationChart } from '../../common/components/execution_duration_chart';
|
||||
|
@ -24,7 +24,7 @@ import {
|
|||
export const DEFAULT_NUMBER_OF_EXECUTIONS = 60;
|
||||
|
||||
type RuleExecutionSummaryAndChartProps = {
|
||||
rule: Rule;
|
||||
ruleId: string;
|
||||
ruleType: RuleType;
|
||||
ruleSummary?: RuleSummary;
|
||||
numberOfExecutions?: number;
|
||||
|
@ -37,7 +37,7 @@ type RuleExecutionSummaryAndChartProps = {
|
|||
|
||||
export const RuleExecutionSummaryAndChart = (props: RuleExecutionSummaryAndChartProps) => {
|
||||
const {
|
||||
rule,
|
||||
ruleId,
|
||||
ruleType,
|
||||
ruleSummary,
|
||||
refreshToken,
|
||||
|
@ -103,7 +103,7 @@ export const RuleExecutionSummaryAndChart = (props: RuleExecutionSummaryAndChart
|
|||
}
|
||||
setInternalIsLoadingRuleSummary(true);
|
||||
try {
|
||||
const loadedSummary = await loadRuleSummary(rule.id, computedNumberOfExecutions);
|
||||
const loadedSummary = await loadRuleSummary(ruleId, computedNumberOfExecutions);
|
||||
setInternalRuleSummary(loadedSummary);
|
||||
} catch (e) {
|
||||
toasts.addDanger({
|
||||
|
@ -124,7 +124,7 @@ export const RuleExecutionSummaryAndChart = (props: RuleExecutionSummaryAndChart
|
|||
useEffect(() => {
|
||||
getRuleSummary();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [rule, computedNumberOfExecutions]);
|
||||
}, [ruleId, computedNumberOfExecutions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized.current) {
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 { UserAtSpaceScenarios } from '../../../scenarios';
|
||||
import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function globalExecutionLogTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
const retry = getService('retry');
|
||||
|
||||
describe('globalExecutionLog', () => {
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
|
||||
after(() => objectRemover.removeAll());
|
||||
|
||||
it('should return logs only from the current space', async () => {
|
||||
const startTime = new Date().toISOString();
|
||||
|
||||
const spaceId = UserAtSpaceScenarios[1].space.id;
|
||||
const user = UserAtSpaceScenarios[1].user;
|
||||
const response = await supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
rule_type_id: 'test.noop',
|
||||
schedule: { interval: '1s' },
|
||||
throttle: null,
|
||||
})
|
||||
);
|
||||
|
||||
expect(response.status).to.eql(200);
|
||||
const alertId = response.body.id;
|
||||
objectRemover.add(spaceId, alertId, 'rule', 'alerting');
|
||||
|
||||
const spaceId2 = UserAtSpaceScenarios[4].space.id;
|
||||
const response2 = await supertest
|
||||
.post(`${getUrlPrefix(spaceId2)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
rule_type_id: 'test.noop',
|
||||
schedule: { interval: '1s' },
|
||||
throttle: null,
|
||||
})
|
||||
);
|
||||
|
||||
expect(response2.status).to.eql(200);
|
||||
const alertId2 = response2.body.id;
|
||||
objectRemover.add(spaceId2, alertId2, 'rule', 'alerting');
|
||||
|
||||
const logs = await retry.try(async () => {
|
||||
// there can be a successful execute before the error one
|
||||
const logResponse = await supertestWithoutAuth
|
||||
.get(
|
||||
`${getUrlPrefix(
|
||||
spaceId
|
||||
)}/internal/alerting/_global_execution_logs?date_start=${startTime}&date_end=9999-12-31T23:59:59Z&per_page=50&page=1`
|
||||
)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password);
|
||||
expect(logResponse.statusCode).to.be(200);
|
||||
|
||||
return logResponse.body.data;
|
||||
});
|
||||
|
||||
// Filter out any excess logs from rules not created by this test
|
||||
const sanitizedLogs = logs.filter((l: any) => [alertId, alertId2].includes(l.rule_id));
|
||||
const allLogsSpace0 = sanitizedLogs.every((l: any) => l.rule_id === alertId);
|
||||
expect(allLogsSpace0).to.be(true);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -44,6 +44,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC
|
|||
loadTestFile(require.resolve('./health'));
|
||||
loadTestFile(require.resolve('./excluded'));
|
||||
loadTestFile(require.resolve('./snooze'));
|
||||
loadTestFile(require.resolve('./global_execution_log'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue