diff --git a/x-pack/plugins/alerting/common/execution_log_types.ts b/x-pack/plugins/alerting/common/execution_log_types.ts index 223b45cb98923..2d5e34df8f766 100644 --- a/x-pack/plugins/alerting/common/execution_log_types.ts +++ b/x-pack/plugins/alerting/common/execution_log_types.ts @@ -61,6 +61,7 @@ export interface IExecutionLog { schedule_delay_ms: number; timed_out: boolean; rule_id: string; + space_ids: string[]; rule_name: string; } diff --git a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts index ee05e3cda32f6..6a57baaacef27 100644 --- a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts +++ b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts @@ -280,6 +280,7 @@ describe('getExecutionLogAggregation', () => { 'error.message', 'kibana.version', 'rule.id', + 'kibana.space_ids', 'rule.name', 'kibana.alerting.outcome', ], @@ -486,6 +487,7 @@ describe('getExecutionLogAggregation', () => { 'error.message', 'kibana.version', 'rule.id', + 'kibana.space_ids', 'rule.name', 'kibana.alerting.outcome', ], @@ -692,6 +694,7 @@ describe('getExecutionLogAggregation', () => { 'error.message', 'kibana.version', 'rule.id', + 'kibana.space_ids', 'rule.name', 'kibana.alerting.outcome', ], @@ -954,6 +957,7 @@ describe('formatExecutionLogResult', () => { schedule_delay_ms: 3074, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', + space_ids: [], }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -976,6 +980,7 @@ describe('formatExecutionLogResult', () => { schedule_delay_ms: 3126, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', + space_ids: [], }, ], }); @@ -1203,6 +1208,7 @@ describe('formatExecutionLogResult', () => { schedule_delay_ms: 3074, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', + space_ids: [], }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -1225,6 +1231,7 @@ describe('formatExecutionLogResult', () => { schedule_delay_ms: 3126, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', + space_ids: [], }, ], }); @@ -1444,6 +1451,7 @@ describe('formatExecutionLogResult', () => { schedule_delay_ms: 3074, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', + space_ids: [], }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -1466,6 +1474,7 @@ describe('formatExecutionLogResult', () => { schedule_delay_ms: 3126, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', + space_ids: [], }, ], }); @@ -1690,6 +1699,7 @@ describe('formatExecutionLogResult', () => { schedule_delay_ms: 3126, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', + space_ids: [], }, { id: '61bb867b-661a-471f-bf92-23471afa10b3', @@ -1712,6 +1722,7 @@ describe('formatExecutionLogResult', () => { schedule_delay_ms: 3133, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', + space_ids: [], }, ], }); diff --git a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts index c5a2c18ad679b..b65499de20d45 100644 --- a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts +++ b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts @@ -18,6 +18,7 @@ const DEFAULT_MAX_BUCKETS_LIMIT = 1000; // do not retrieve more than this number const DEFAULT_MAX_KPI_BUCKETS_LIMIT = 10000; const RULE_ID_FIELD = 'rule.id'; +const SPACE_ID_FIELD = 'kibana.space_ids'; const RULE_NAME_FIELD = 'rule.name'; const PROVIDER_FIELD = 'event.provider'; const START_FIELD = 'event.start'; @@ -410,6 +411,7 @@ export function getExecutionLogAggregation({ ERROR_MESSAGE_FIELD, VERSION_FIELD, RULE_ID_FIELD, + SPACE_ID_FIELD, RULE_NAME_FIELD, ALERTING_OUTCOME_FIELD, ], @@ -494,8 +496,9 @@ function formatExecutionLogAggBucket(bucket: IExecutionUuidAggBucket): IExecutio status === 'failure' ? `${outcomeMessage} - ${outcomeErrorMessage}` : outcomeMessage; const version = outcomeAndMessage.kibana?.version ?? ''; - const ruleId = outcomeAndMessage.rule?.id ?? ''; - const ruleName = outcomeAndMessage.rule?.name ?? ''; + const ruleId = outcomeAndMessage ? outcomeAndMessage?.rule?.id ?? '' : ''; + const spaceIds = outcomeAndMessage ? outcomeAndMessage?.kibana?.space_ids ?? [] : []; + const ruleName = outcomeAndMessage ? outcomeAndMessage.rule?.name ?? '' : ''; return { id: bucket?.key ?? '', timestamp: bucket?.ruleExecution?.executeStartTime.value_as_string ?? '', @@ -515,6 +518,7 @@ function formatExecutionLogAggBucket(bucket: IExecutionUuidAggBucket): IExecutio schedule_delay_ms: scheduleDelayUs / Millis2Nanos, timed_out: timedOut, rule_id: ruleId, + space_ids: spaceIds, rule_name: ruleName, }; } diff --git a/x-pack/plugins/alerting/server/routes/get_action_error_log.ts b/x-pack/plugins/alerting/server/routes/get_action_error_log.ts index c833b65e34bb0..7e8028cad7f16 100644 --- a/x-pack/plugins/alerting/server/routes/get_action_error_log.ts +++ b/x-pack/plugins/alerting/server/routes/get_action_error_log.ts @@ -34,15 +34,19 @@ const querySchema = schema.object({ per_page: schema.number({ defaultValue: 10, min: 1 }), page: schema.number({ defaultValue: 1, min: 1 }), sort: sortFieldsSchema, + namespace: schema.maybe(schema.string()), + with_auth: schema.maybe(schema.boolean()), }); const rewriteReq: RewriteRequestCase = ({ date_start: dateStart, date_end: dateEnd, per_page: perPage, + namespace, ...rest }) => ({ ...rest, + namespace, dateStart, dateEnd, perPage, @@ -64,8 +68,13 @@ export const getActionErrorLogRoute = ( verifyAccessAndContext(licenseState, async function (context, req, res) { const rulesClient = (await context.alerting).getRulesClient(); const { id } = req.params; + const withAuth = req.query.with_auth; + const rewrittenReq = rewriteReq({ id, ...req.query }); + const getter = ( + withAuth ? rulesClient.getActionErrorLogWithAuth : rulesClient.getActionErrorLog + ).bind(rulesClient); return res.ok({ - body: await rulesClient.getActionErrorLog(rewriteReq({ id, ...req.query })), + body: await getter(rewrittenReq), }); }) ) diff --git a/x-pack/plugins/alerting/server/routes/get_global_execution_kpi.ts b/x-pack/plugins/alerting/server/routes/get_global_execution_kpi.ts index 29937cc3d8c98..2aec9d998a9e6 100644 --- a/x-pack/plugins/alerting/server/routes/get_global_execution_kpi.ts +++ b/x-pack/plugins/alerting/server/routes/get_global_execution_kpi.ts @@ -7,7 +7,7 @@ import { IRouter } from '@kbn/core/server'; import { schema } from '@kbn/config-schema'; import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types'; -import { RewriteRequestCase, verifyAccessAndContext } from './lib'; +import { RewriteRequestCase, verifyAccessAndContext, rewriteNamespaces } from './lib'; import { GetGlobalExecutionKPIParams } from '../rules_client'; import { ILicenseState } from '../lib'; @@ -15,14 +15,17 @@ const querySchema = schema.object({ date_start: schema.string(), date_end: schema.maybe(schema.string()), filter: schema.maybe(schema.string()), + namespaces: schema.maybe(schema.arrayOf(schema.string())), }); const rewriteReq: RewriteRequestCase = ({ date_start: dateStart, date_end: dateEnd, + namespaces, ...rest }) => ({ ...rest, + namespaces: rewriteNamespaces(namespaces), dateStart, dateEnd, }); diff --git a/x-pack/plugins/alerting/server/routes/get_global_execution_logs.test.ts b/x-pack/plugins/alerting/server/routes/get_global_execution_logs.test.ts index 43b08ed0787e2..3ee2b0d1816ba 100644 --- a/x-pack/plugins/alerting/server/routes/get_global_execution_logs.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_global_execution_logs.test.ts @@ -47,6 +47,7 @@ describe('getRuleExecutionLogRoute', () => { schedule_delay_ms: 3126, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule-name', + space_ids: ['namespace'], }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -69,6 +70,7 @@ describe('getRuleExecutionLogRoute', () => { schedule_delay_ms: 3008, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule-name', + space_ids: ['namespace'], }, ], }; diff --git a/x-pack/plugins/alerting/server/routes/get_global_execution_logs.ts b/x-pack/plugins/alerting/server/routes/get_global_execution_logs.ts index 4695e5e7bdf89..e08ec1ac5bcb8 100644 --- a/x-pack/plugins/alerting/server/routes/get_global_execution_logs.ts +++ b/x-pack/plugins/alerting/server/routes/get_global_execution_logs.ts @@ -9,7 +9,7 @@ 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 { RewriteRequestCase, verifyAccessAndContext, rewriteNamespaces } from './lib'; import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types'; const sortOrderSchema = schema.oneOf([schema.literal('asc'), schema.literal('desc')]); @@ -38,15 +38,18 @@ const querySchema = schema.object({ per_page: schema.number({ defaultValue: 10, min: 1 }), page: schema.number({ defaultValue: 1, min: 1 }), sort: sortFieldsSchema, + namespaces: schema.maybe(schema.arrayOf(schema.string())), }); const rewriteReq: RewriteRequestCase = ({ date_start: dateStart, date_end: dateEnd, per_page: perPage, + namespaces, ...rest }) => ({ ...rest, + namespaces: rewriteNamespaces(namespaces), dateStart, dateEnd, perPage, diff --git a/x-pack/plugins/alerting/server/routes/get_rule_execution_log.test.ts b/x-pack/plugins/alerting/server/routes/get_rule_execution_log.test.ts index 048da6cbabeb3..eb22a6429809a 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule_execution_log.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule_execution_log.test.ts @@ -48,6 +48,7 @@ describe('getRuleExecutionLogRoute', () => { schedule_delay_ms: 3126, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', + space_ids: ['namespace'], }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -70,6 +71,7 @@ describe('getRuleExecutionLogRoute', () => { schedule_delay_ms: 3008, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', + space_ids: ['namespace'], }, ], }; diff --git a/x-pack/plugins/alerting/server/routes/lib/index.ts b/x-pack/plugins/alerting/server/routes/lib/index.ts index e772f091bb059..90d903ada6eed 100644 --- a/x-pack/plugins/alerting/server/routes/lib/index.ts +++ b/x-pack/plugins/alerting/server/routes/lib/index.ts @@ -19,3 +19,4 @@ export type { export { verifyAccessAndContext } from './verify_access_and_context'; export { countUsageOfPredefinedIds } from './count_usage_of_predefined_ids'; export { rewriteRule } from './rewrite_rule'; +export { rewriteNamespaces } from './rewrite_namespaces'; diff --git a/x-pack/plugins/alerting/server/routes/lib/rewrite_namespaces.ts b/x-pack/plugins/alerting/server/routes/lib/rewrite_namespaces.ts new file mode 100644 index 0000000000000..5339b41526efe --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/lib/rewrite_namespaces.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +export const rewriteNamespaces = (namespaces?: Array) => + namespaces + ? namespaces.map((id: string | undefined) => (id === 'default' ? undefined : id)) + : undefined; diff --git a/x-pack/plugins/alerting/server/rules_client.mock.ts b/x-pack/plugins/alerting/server/rules_client.mock.ts index aa29e64d2f460..46a6c36bdea2a 100644 --- a/x-pack/plugins/alerting/server/rules_client.mock.ts +++ b/x-pack/plugins/alerting/server/rules_client.mock.ts @@ -34,6 +34,7 @@ const createRulesClientMock = () => { getGlobalExecutionKpiWithAuth: jest.fn(), getGlobalExecutionLogWithAuth: jest.fn(), getActionErrorLog: jest.fn(), + getActionErrorLogWithAuth: jest.fn(), getSpaceId: jest.fn(), bulkEdit: jest.fn(), bulkDeleteRules: jest.fn(), diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 777cf340b53e7..bd4f9deb36b5d 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -419,6 +419,7 @@ export interface GetGlobalExecutionKPIParams { dateStart: string; dateEnd?: string; filter?: string; + namespaces?: Array; } export interface GetGlobalExecutionLogParams { @@ -428,6 +429,7 @@ export interface GetGlobalExecutionLogParams { page: number; perPage: number; sort: estypes.Sort; + namespaces?: Array; } export interface GetActionErrorLogByIdParams { @@ -438,6 +440,7 @@ export interface GetActionErrorLogByIdParams { page: number; perPage: number; sort: estypes.Sort; + namespace?: string; } interface ScheduleTaskOptions { @@ -458,6 +461,9 @@ const MAX_RULES_NUMBER_FOR_BULK_OPERATION = 10000; const API_KEY_GENERATE_CONCURRENCY = 50; const RULE_TYPE_CHECKS_CONCURRENCY = 50; +const actionErrorLogDefaultFilter = + 'event.provider:actions AND ((event.action:execute AND (event.outcome:failure OR kibana.alerting.status:warning)) OR (event.action:execute-timeout))'; + const alertingAuthorizationFilterOpts: AlertingAuthorizationFilterOpts = { type: AlertingAuthorizationFilterType.KQL, fieldNames: { ruleTypeId: 'alert.attributes.alertTypeId', consumer: 'alert.attributes.consumer' }, @@ -951,6 +957,7 @@ export class RulesClient { page, perPage, sort, + namespaces, }: GetGlobalExecutionLogParams): Promise { this.logger.debug(`getGlobalExecutionLogWithAuth(): getting global execution log`); @@ -1001,7 +1008,8 @@ export class RulesClient { perPage, sort, }), - } + }, + namespaces ); return formatExecutionLogResult(aggResult); @@ -1050,9 +1058,6 @@ export class RulesClient { }) ); - const defaultFilter = - 'event.provider:actions AND ((event.action:execute AND (event.outcome:failure OR kibana.alerting.status:warning)) OR (event.action:execute-timeout))'; - // default duration of instance summary is 60 * rule interval const dateNow = new Date(); const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow); @@ -1069,7 +1074,9 @@ export class RulesClient { end: parsedDateEnd.toISOString(), page, per_page: perPage, - filter: filter ? `(${defaultFilter}) AND (${filter})` : defaultFilter, + filter: filter + ? `(${actionErrorLogDefaultFilter}) AND (${filter})` + : actionErrorLogDefaultFilter, sort: convertEsSortToEventLogSort(sort), }, rule.legacyId !== null ? [rule.legacyId] : undefined @@ -1083,10 +1090,85 @@ export class RulesClient { } } + public async getActionErrorLogWithAuth({ + id, + dateStart, + dateEnd, + filter, + page, + perPage, + sort, + namespace, + }: GetActionErrorLogByIdParams): Promise { + this.logger.debug(`getActionErrorLogWithAuth(): getting action error logs for rule ${id}`); + + 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_ACTION_ERROR_LOG, + error, + }) + ); + throw error; + } + + this.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET_ACTION_ERROR_LOG, + savedObject: { type: 'alert', id }, + }) + ); + + // default duration of instance summary is 60 * rule interval + const dateNow = new Date(); + const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow); + const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow); + + const eventLogClient = await this.getEventLogClient(); + + try { + const errorResult = await eventLogClient.findEventsWithAuthFilter( + 'alert', + [id], + authorizationTuple.filter as KueryNode, + namespace, + { + start: parsedDateStart.toISOString(), + end: parsedDateEnd.toISOString(), + page, + per_page: perPage, + filter: filter + ? `(${actionErrorLogDefaultFilter}) AND (${filter})` + : actionErrorLogDefaultFilter, + sort: convertEsSortToEventLogSort(sort), + } + ); + return formatExecutionErrorsResult(errorResult); + } catch (err) { + this.logger.debug( + `rulesClient.getActionErrorLog(): error searching event log for rule ${id}: ${err.message}` + ); + throw err; + } + } + public async getGlobalExecutionKpiWithAuth({ dateStart, dateEnd, filter, + namespaces, }: GetGlobalExecutionKPIParams) { this.logger.debug(`getGlobalExecutionLogWithAuth(): getting global execution log`); @@ -1132,7 +1214,8 @@ export class RulesClient { start: parsedDateStart.toISOString(), end: parsedDateEnd.toISOString(), aggs: getExecutionKPIAggregation(filter), - } + }, + namespaces ); return formatExecutionKPIResult(aggResult); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_action_error_log.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_action_error_log.test.ts index 3a18634b4f5db..6b635abe5d7f0 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get_action_error_log.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get_action_error_log.test.ts @@ -8,6 +8,7 @@ import { RulesClient, ConstructorOptions, GetActionErrorLogByIdParams } from '../rules_client'; import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; +import { fromKueryExpression } from '@kbn/es-query'; import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; @@ -574,3 +575,63 @@ describe('getActionErrorLog()', () => { }); }); }); + +describe('getActionErrorLogWithAuth()', () => { + let rulesClient: RulesClient; + + beforeEach(() => { + rulesClient = new RulesClient(rulesClientParams); + }); + + test('returns the expected return values when called', async () => { + const ruleSO = getRuleSavedObject({}); + authorization.getFindAuthorizationFilter.mockResolvedValue({ + filter: fromKueryExpression('*'), + ensureRuleTypeIsAuthorized() {}, + }); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(ruleSO); + eventLogClient.findEventsWithAuthFilter.mockResolvedValueOnce(findResults); + + const result = await rulesClient.getActionErrorLogWithAuth(getActionErrorLogParams()); + expect(result).toEqual({ + totalErrors: 5, + errors: [ + { + id: '08d9b0f5-0b41-47c9-951f-a666b5788ddc', + timestamp: '2022-03-23T17:37:07.106Z', + type: 'actions', + message: + 'action execution failure: .server-log:9e67b8b0-9e2c-11ec-bd64-774ed95c43ef: s - an error occurred while running the action executor: something funky with the server log', + }, + { + id: '08d9b0f5-0b41-47c9-951f-a666b5788ddc', + timestamp: '2022-03-23T17:37:07.102Z', + type: 'actions', + message: + 'action execution failure: .server-log:9e67b8b0-9e2c-11ec-bd64-774ed95c43ef: s - an error occurred while running the action executor: something funky with the server log', + }, + { + id: '08d9b0f5-0b41-47c9-951f-a666b5788ddc', + timestamp: '2022-03-23T17:37:07.098Z', + type: 'actions', + message: + 'action execution failure: .server-log:9e67b8b0-9e2c-11ec-bd64-774ed95c43ef: s - an error occurred while running the action executor: something funky with the server log', + }, + { + id: '08d9b0f5-0b41-47c9-951f-a666b5788ddc', + timestamp: '2022-03-23T17:37:07.096Z', + type: 'actions', + message: + 'action execution failure: .server-log:9e67b8b0-9e2c-11ec-bd64-774ed95c43ef: s - an error occurred while running the action executor: something funky with the server log', + }, + { + id: '08d9b0f5-0b41-47c9-951f-a666b5788ddc', + timestamp: '2022-03-23T17:37:07.086Z', + type: 'actions', + message: + 'action execution failure: .server-log:9e67b8b0-9e2c-11ec-bd64-774ed95c43ef: s - an error occurred while running the action executor: something funky with the server log', + }, + ], + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts index 83b85f4879cff..ffc40cd705abd 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts @@ -385,6 +385,7 @@ describe('getExecutionLogForRule()', () => { schedule_delay_ms: 3126, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule-name', + space_ids: [], }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -407,6 +408,7 @@ describe('getExecutionLogForRule()', () => { schedule_delay_ms: 3345, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule-name', + space_ids: [], }, ], }); @@ -719,6 +721,7 @@ describe('getGlobalExecutionLogWithAuth()', () => { schedule_delay_ms: 3126, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule-name', + space_ids: [], }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -741,6 +744,7 @@ describe('getGlobalExecutionLogWithAuth()', () => { schedule_delay_ms: 3345, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule-name', + space_ids: [], }, ], }); diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.mock.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.mock.ts index 3cb1b8d12c0b1..adf1ecf0f7881 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.mock.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.mock.ts @@ -24,6 +24,7 @@ const createClusterClientMock = () => { getExistingIndexAliases: jest.fn(), setIndexAliasToHidden: jest.fn(), queryEventsBySavedObjects: jest.fn(), + queryEventsWithAuthFilter: jest.fn(), aggregateEventsBySavedObjects: jest.fn(), aggregateEventsWithAuthFilter: jest.fn(), shutdown: jest.fn(), diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts index ea3e98e599ab5..a7a9e8bd0867a 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts @@ -779,7 +779,7 @@ describe('aggregateEventsWithAuthFilter', () => { }); const options: AggregateEventsWithAuthFilter = { index: 'index-name', - namespace: 'namespace', + namespaces: ['namespace'], type: 'saved-object-type', aggregateOptions: DEFAULT_OPTIONS as AggregateOptionsType, authFilter: fromKueryExpression('test:test'), @@ -1515,7 +1515,7 @@ describe('getQueryBody', () => { describe('getQueryBodyWithAuthFilter', () => { const options = { index: 'index-name', - namespace: undefined, + namespaces: undefined, type: 'saved-object-type', authFilter: fromKueryExpression('test:test'), }; @@ -1559,11 +1559,17 @@ describe('getQueryBodyWithAuthFilter', () => { }, { bool: { - must_not: { - exists: { - field: 'kibana.saved_objects.namespace', + should: [ + { + bool: { + must_not: { + exists: { + field: 'kibana.saved_objects.namespace', + }, + }, + }, }, - }, + ], }, }, ], @@ -1580,7 +1586,7 @@ describe('getQueryBodyWithAuthFilter', () => { expect( getQueryBodyWithAuthFilter( logger, - { ...options, namespace: 'namespace' } as AggregateEventsWithAuthFilter, + { ...options, namespaces: ['namespace'] } as AggregateEventsWithAuthFilter, {} ) ).toEqual({ @@ -1619,10 +1625,16 @@ describe('getQueryBodyWithAuthFilter', () => { }, }, { - term: { - 'kibana.saved_objects.namespace': { - value: 'namespace', - }, + bool: { + should: [ + { + term: { + 'kibana.saved_objects.namespace': { + value: 'namespace', + }, + }, + }, + ], }, }, ], @@ -1713,11 +1725,17 @@ describe('getQueryBodyWithAuthFilter', () => { }, { bool: { - must_not: { - exists: { - field: 'kibana.saved_objects.namespace', + should: [ + { + bool: { + must_not: { + exists: { + field: 'kibana.saved_objects.namespace', + }, + }, + }, }, - }, + ], }, }, ], @@ -1772,11 +1790,17 @@ describe('getQueryBodyWithAuthFilter', () => { }, { bool: { - must_not: { - exists: { - field: 'kibana.saved_objects.namespace', + should: [ + { + bool: { + must_not: { + exists: { + field: 'kibana.saved_objects.namespace', + }, + }, + }, }, - }, + ], }, }, ], @@ -1838,11 +1862,17 @@ describe('getQueryBodyWithAuthFilter', () => { }, { bool: { - must_not: { - exists: { - field: 'kibana.saved_objects.namespace', + should: [ + { + bool: { + must_not: { + exists: { + field: 'kibana.saved_objects.namespace', + }, + }, + }, }, - }, + ], }, }, ], @@ -1905,11 +1935,17 @@ describe('getQueryBodyWithAuthFilter', () => { }, { bool: { - must_not: { - exists: { - field: 'kibana.saved_objects.namespace', + should: [ + { + bool: { + must_not: { + exists: { + field: 'kibana.saved_objects.namespace', + }, + }, + }, }, - }, + ], }, }, ], diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts index e807899d6290b..0d38895dbd800 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts @@ -50,14 +50,26 @@ interface QueryOptionsEventsBySavedObjectFilter { legacyIds?: string[]; } -export interface AggregateEventsWithAuthFilter { +interface QueryOptionsEventsWithAuthFilter { index: string; namespace: string | undefined; type: string; + ids: string[]; + authFilter: KueryNode; +} + +export interface AggregateEventsWithAuthFilter { + index: string; + namespaces?: Array; + type: string; authFilter: KueryNode; aggregateOptions: AggregateOptionsType; } +export type FindEventsOptionsWithAuthFilter = QueryOptionsEventsWithAuthFilter & { + findOptions: FindOptionsType; +}; + export type FindEventsOptionsBySavedObjectFilter = QueryOptionsEventsBySavedObjectFilter & { findOptions: FindOptionsType; }; @@ -70,6 +82,12 @@ export interface AggregateEventsBySavedObjectResult { aggregations: Record | undefined; } +type GetQueryBodyWithAuthFilterOpts = + | (FindEventsOptionsWithAuthFilter & { + namespaces: AggregateEventsWithAuthFilter['namespaces']; + }) + | AggregateEventsWithAuthFilter; + // eslint-disable-next-line @typescript-eslint/no-explicit-any type AliasAny = any; @@ -389,6 +407,50 @@ export class ClusterClientAdapter { + const { index, type, ids, findOptions } = queryOptions; + const { page, per_page: perPage, sort } = findOptions; + + const esClient = await this.elasticsearchClientPromise; + + const query = getQueryBodyWithAuthFilter( + this.logger, + { ...queryOptions, namespaces: [queryOptions.namespace] }, + pick(queryOptions.findOptions, ['start', 'end', 'filter']) + ); + + const body: estypes.SearchRequest['body'] = { + size: perPage, + from: (page - 1) * perPage, + query, + ...(sort + ? { sort: sort.map((s) => ({ [s.sort_field]: { order: s.sort_order } })) as estypes.Sort } + : {}), + }; + + try { + const { + hits: { hits, total }, + } = await esClient.search({ + index, + track_total_hits: true, + body, + }); + return { + page, + per_page: perPage, + total: isNumber(total) ? total : total!.value, + data: hits.map((hit) => hit._source), + }; + } catch (err) { + throw new Error( + `querying for Event Log by for type "${type}" and ids "${ids}" failed with: ${err.message}` + ); + } + } + public async aggregateEventsBySavedObjects( queryOptions: AggregateEventsOptionsBySavedObjectFilter ): Promise { @@ -462,13 +524,15 @@ export class ClusterClientAdapter + getNamespaceQuery(namespace) + ); let dslFilterQuery: estypes.QueryDslBoolQuery['filter']; try { const filterKueryNode = filter ? fromKueryExpression(filter) : null; @@ -501,8 +565,12 @@ export function getQueryBodyWithAuthFilter( }, }, }, - // @ts-expect-error undefined is not assignable as QueryDslTermQuery value - namespaceQuery, + { + bool: { + // @ts-expect-error undefined is not assignable as QueryDslTermQuery value + should: namespaceQuery, + }, + }, ]; const musts: estypes.QueryDslQueryContainer[] = [ diff --git a/x-pack/plugins/event_log/server/event_log_client.mock.ts b/x-pack/plugins/event_log/server/event_log_client.mock.ts index 0e11ded65be65..a44a319626ded 100644 --- a/x-pack/plugins/event_log/server/event_log_client.mock.ts +++ b/x-pack/plugins/event_log/server/event_log_client.mock.ts @@ -10,6 +10,7 @@ import { IEventLogClient } from './types'; const createEventLogClientMock = () => { const mock: jest.Mocked = { findEventsBySavedObjectIds: jest.fn(), + findEventsWithAuthFilter: jest.fn(), aggregateEventsBySavedObjectIds: jest.fn(), aggregateEventsWithAuthFilter: jest.fn(), }; diff --git a/x-pack/plugins/event_log/server/event_log_client.test.ts b/x-pack/plugins/event_log/server/event_log_client.test.ts index 5e4fd08819fb3..b91ef3ef43836 100644 --- a/x-pack/plugins/event_log/server/event_log_client.test.ts +++ b/x-pack/plugins/event_log/server/event_log_client.test.ts @@ -256,7 +256,7 @@ describe('EventLogStart', () => { }); expect(esContext.esAdapter.aggregateEventsWithAuthFilter).toHaveBeenCalledWith({ index: esContext.esNames.indexPattern, - namespace: undefined, + namespaces: [undefined], type: 'saved-object-type', authFilter: testAuthFilter, aggregateOptions: { diff --git a/x-pack/plugins/event_log/server/event_log_client.ts b/x-pack/plugins/event_log/server/event_log_client.ts index 85a798d0fb8bb..9a35868d248e5 100644 --- a/x-pack/plugins/event_log/server/event_log_client.ts +++ b/x-pack/plugins/event_log/server/event_log_client.ts @@ -112,6 +112,31 @@ export class EventLogClient implements IEventLogClient { }); } + public async findEventsWithAuthFilter( + type: string, + ids: string[], + authFilter: KueryNode, + namespace: string | undefined, + options?: Partial + ): Promise { + if (!authFilter) { + throw new Error('No authorization filter defined!'); + } + + const findOptions = queryOptionsSchema.validate(options ?? {}); + + return await this.esContext.esAdapter.queryEventsWithAuthFilter({ + index: this.esContext.esNames.indexPattern, + namespace: namespace + ? this.spacesService?.spaceIdToNamespace(namespace) + : await this.getNamespace(), + type, + ids, + findOptions, + authFilter, + }); + } + public async aggregateEventsBySavedObjectIds( type: string, ids: string[], @@ -142,7 +167,8 @@ export class EventLogClient implements IEventLogClient { public async aggregateEventsWithAuthFilter( type: string, authFilter: KueryNode, - options?: AggregateOptionsType + options?: AggregateOptionsType, + namespaces?: Array ) { if (!authFilter) { throw new Error('No authorization filter defined!'); @@ -158,7 +184,7 @@ export class EventLogClient implements IEventLogClient { return await this.esContext.esAdapter.aggregateEventsWithAuthFilter({ index: this.esContext.esNames.indexPattern, - namespace: await this.getNamespace(), + namespaces: namespaces ?? [await this.getNamespace()], type, authFilter, aggregateOptions: { ...aggregateOptions, aggs } as AggregateOptionsType, diff --git a/x-pack/plugins/event_log/server/types.ts b/x-pack/plugins/event_log/server/types.ts index d610a8bff9c2a..07a7e7ed2f7e2 100644 --- a/x-pack/plugins/event_log/server/types.ts +++ b/x-pack/plugins/event_log/server/types.ts @@ -57,6 +57,13 @@ export interface IEventLogClient { options?: Partial, legacyIds?: string[] ): Promise; + findEventsWithAuthFilter( + type: string, + ids: string[], + authFilter: KueryNode, + namespace: string | undefined, + options?: Partial + ): Promise; aggregateEventsBySavedObjectIds( type: string, ids: string[], @@ -66,7 +73,8 @@ export interface IEventLogClient { aggregateEventsWithAuthFilter( type: string, authFilter: KueryNode, - options?: Partial + options?: Partial, + namespaces?: Array ): Promise; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts index 19d3b038c6350..a8b15215632c1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts @@ -44,6 +44,7 @@ export const DEFAULT_RULE_INTERVAL = '1m'; export const RULE_EXECUTION_LOG_COLUMN_IDS = [ 'rule_id', 'rule_name', + 'space_ids', 'id', 'timestamp', 'execution_duration', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.test.ts index 56de4f5c4c890..de273fbf394e5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.test.ts @@ -118,9 +118,11 @@ describe('loadActionErrorLog', () => { "date_end": "2022-03-23T16:17:53.482Z", "date_start": "2022-03-23T16:17:53.482Z", "filter": "(message: \\"test\\" OR error.message: \\"test\\") and kibana.alert.rule.execution.uuid: 123", + "namespace": undefined, "page": 1, "per_page": 10, "sort": "[{\\"@timestamp\\":{\\"order\\":\\"asc\\"}}]", + "with_auth": false, }, }, ] diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.ts index 10f2879085cd0..7bfef44335a4c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.ts @@ -28,6 +28,8 @@ export interface LoadActionErrorLogProps { perPage?: number; page?: number; sort?: SortField[]; + namespace?: string; + withAuth?: boolean; } const SORT_MAP: Record = { @@ -60,6 +62,8 @@ export const loadActionErrorLog = ({ perPage = 10, page = 0, sort, + namespace, + withAuth = false, }: LoadActionErrorLogProps & { http: HttpSetup }) => { const renamedSort = getRenamedSort(sort); const filter = getFilter({ runId, message }); @@ -76,6 +80,8 @@ export const loadActionErrorLog = ({ // whereas data grid sorts are 0 indexed. page: page + 1, sort: renamedSort.length ? JSON.stringify(renamedSort) : undefined, + namespace, + with_auth: withAuth, }, } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_execution_log_aggregations.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_execution_log_aggregations.ts index bf5e529499b42..671a1edce467d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_execution_log_aggregations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_execution_log_aggregations.ts @@ -59,7 +59,10 @@ export interface LoadExecutionLogAggregationsProps { sort?: SortField[]; } -export type LoadGlobalExecutionLogAggregationsProps = Omit; +export type LoadGlobalExecutionLogAggregationsProps = Omit< + LoadExecutionLogAggregationsProps, + 'id' +> & { namespaces?: Array }; export const loadExecutionLogAggregations = async ({ id, @@ -103,6 +106,7 @@ export const loadGlobalExecutionLogAggregations = async ({ perPage = 10, page = 0, sort = [], + namespaces, }: LoadGlobalExecutionLogAggregationsProps & { http: HttpSetup }) => { const sortField: any[] = sort; const filter = getFilter({ outcomeFilter, message }); @@ -119,6 +123,7 @@ export const loadGlobalExecutionLogAggregations = async ({ // whereas data grid sorts are 0 indexed. page: page + 1, sort: sortField.length ? JSON.stringify(sortField) : undefined, + namespaces: namespaces ? JSON.stringify(namespaces) : undefined, }, } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_global_execution_kpi_aggregations.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_global_execution_kpi_aggregations.ts index 332e14ad4383f..7052257d1fc87 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_global_execution_kpi_aggregations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_global_execution_kpi_aggregations.ts @@ -16,6 +16,7 @@ export interface LoadGlobalExecutionKPIAggregationsProps { message?: string; dateStart: string; dateEnd?: string; + namespaces?: Array; } export const loadGlobalExecutionKPIAggregations = ({ @@ -25,6 +26,7 @@ export const loadGlobalExecutionKPIAggregations = ({ message, dateStart, dateEnd, + namespaces, }: LoadGlobalExecutionKPIAggregationsProps & { http: HttpSetup }) => { const filter = getFilter({ outcomeFilter, message }); @@ -33,6 +35,7 @@ export const loadGlobalExecutionKPIAggregations = ({ filter: filter.length ? filter.join(' and ') : undefined, date_start: dateStart, date_end: dateEnd, + namespaces: namespaces ? JSON.stringify(namespaces) : namespaces, }, }); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/logs_list/components/logs_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/logs_list/components/logs_list.tsx index 79e617ee05a49..404457af8fd01 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/logs_list/components/logs_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/logs_list/components/logs_list.tsx @@ -20,6 +20,7 @@ export const LogsList = () => { refreshToken: 0, initialPageSize: 50, hasRuleNames: true, + hasAllSpaceSwitch: true, localStorageKey: GLOBAL_EVENT_LOG_LIST_STORAGE_KEY, }); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_action_error_log_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_action_error_log_flyout.tsx index 8c46e3574560c..aa914e2818c03 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_action_error_log_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_action_error_log_flyout.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiTitle, @@ -28,17 +28,29 @@ export interface RuleActionErrorLogFlyoutProps { runLog: IExecutionLog; refreshToken?: number; onClose: () => void; + activeSpaceId?: string; } export const RuleActionErrorLogFlyout = (props: RuleActionErrorLogFlyoutProps) => { - const { runLog, refreshToken, onClose } = props; + const { runLog, refreshToken, onClose, activeSpaceId } = props; const { euiTheme } = useEuiTheme(); - const { id, rule_id: ruleId, message, num_errored_actions: totalErrors } = runLog; + const { + id, + rule_id: ruleId, + message, + num_errored_actions: totalErrors, + space_ids: spaceIds = [], + } = runLog; const isFlyoutPush = useIsWithinBreakpoints(['xl']); + const logFromDifferentSpace = useMemo( + () => Boolean(activeSpaceId && !spaceIds?.includes(activeSpaceId)), + [activeSpaceId, spaceIds] + ); + return ( - + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_error_log.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_error_log.tsx index 7c14b17f8d12b..e07dd0ce5f6ed 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_error_log.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_error_log.tsx @@ -63,11 +63,13 @@ export type RuleErrorLogProps = { ruleId: string; runId?: string; refreshToken?: number; + spaceId?: string; + logFromDifferentSpace?: boolean; requestRefresh?: () => Promise; } & Pick; export const RuleErrorLog = (props: RuleErrorLogProps) => { - const { ruleId, runId, loadActionErrorLog, refreshToken } = props; + const { ruleId, runId, loadActionErrorLog, refreshToken, spaceId, logFromDifferentSpace } = props; const { uiSettings, notifications } = useKibana().services; @@ -138,6 +140,8 @@ export const RuleErrorLog = (props: RuleErrorLogProps) => { page: pagination.pageIndex, perPage: pagination.pageSize, sort: formattedSort, + namespace: spaceId, + withAuth: logFromDifferentSpace, }); setLogs(result.errors); setPagination({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_data_grid.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_data_grid.tsx index 0f6dcc13b1667..20f3612f3a41b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_data_grid.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_data_grid.tsx @@ -60,6 +60,7 @@ export interface RuleEventLogDataGrid { pageSizeOptions?: number[]; selectedRunLog?: IExecutionLog; showRuleNameAndIdColumns?: boolean; + showSpaceColumns?: boolean; onChangeItemsPerPage: (pageSize: number) => void; onChangePage: (pageIndex: number) => void; onFilterChange: (filter: string[]) => void; @@ -162,6 +163,7 @@ export const RuleEventLogDataGrid = (props: RuleEventLogDataGrid) => { visibleColumns, selectedRunLog, showRuleNameAndIdColumns = false, + showSpaceColumns = false, setVisibleColumns, setSortingColumns, onChangeItemsPerPage, @@ -215,6 +217,25 @@ export const RuleEventLogDataGrid = (props: RuleEventLogDataGrid) => { }, ] : []), + ...(showSpaceColumns + ? [ + { + id: 'space_ids', + displayAsText: i18n.translate( + 'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.spaceIds', + { + defaultMessage: 'Space', + } + ), + isSortable: getIsColumnSortable('space_ids'), + actions: { + showSortAsc: false, + showSortDesc: false, + showHide: false, + }, + }, + ] + : []), { id: 'id', displayAsText: i18n.translate( @@ -429,16 +450,22 @@ export const RuleEventLogDataGrid = (props: RuleEventLogDataGrid) => { isSortable: getIsColumnSortable('timed_out'), }, ], - [getPaginatedRowIndex, onFlyoutOpen, onFilterChange, showRuleNameAndIdColumns, logs] + [ + getPaginatedRowIndex, + onFlyoutOpen, + onFilterChange, + showRuleNameAndIdColumns, + showSpaceColumns, + logs, + ] ); - const columnVisibilityProps = useMemo( - () => ({ + const columnVisibilityProps = useMemo(() => { + return { visibleColumns, setVisibleColumns, - }), - [visibleColumns, setVisibleColumns] - ); + }; + }, [visibleColumns, setVisibleColumns]); const sortingProps = useMemo( () => ({ @@ -560,6 +587,7 @@ export const RuleEventLogDataGrid = (props: RuleEventLogDataGrid) => { const actionErrors = logs[pagedRowIndex]?.num_errored_actions || (0 as number); const version = logs?.[pagedRowIndex]?.version; const ruleId = runLog?.rule_id; + const spaceIds = runLog?.space_ids; if (columnId === 'num_errored_actions' && runLog) { return ( @@ -592,6 +620,7 @@ export const RuleEventLogDataGrid = (props: RuleEventLogDataGrid) => { version={version} dateFormat={dateFormat} ruleId={ruleId} + spaceIds={spaceIds} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_cell_renderer.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_cell_renderer.test.tsx index de9cd783c1ff6..08362962890e5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_cell_renderer.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_cell_renderer.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import moment from 'moment'; -import { EuiIcon } from '@elastic/eui'; +import { EuiIcon, EuiLink } from '@elastic/eui'; import { shallow, mount } from 'enzyme'; import { RuleEventLogListCellRenderer, @@ -16,7 +16,53 @@ import { import { RuleEventLogListStatus } from './rule_event_log_list_status'; import { RuleDurationFormat } from '../../rules_list/components/rule_duration_format'; +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + location: { + pathname: '/logs', + }, + }), +})); + +jest.mock('../../../../common/lib/kibana', () => ({ + useSpacesData: () => ({ + spacesMap: new Map([ + ['space1', { id: 'space1' }], + ['space2', { id: 'space2' }], + ]), + activeSpaceId: 'space1', + }), + useKibana: () => ({ + services: { + http: { + basePath: { + get: () => '/basePath', + }, + }, + }, + }), +})); + describe('rule_event_log_list_cell_renderer', () => { + const savedLocation = window.location; + beforeAll(() => { + // @ts-ignore Mocking window.location + delete window.location; + // @ts-ignore + window.location = Object.assign( + new URL('https://localhost/app/management/insightsAndAlerting/triggersActions/logs'), + { + ancestorOrigins: '', + assign: jest.fn(), + reload: jest.fn(), + replace: jest.fn(), + } + ); + }); + afterAll(() => { + window.location = savedLocation; + }); + it('renders primitive values correctly', () => { const wrapper = mount(); @@ -67,4 +113,31 @@ describe('rule_event_log_list_cell_renderer', () => { expect(wrapper.find(RuleEventLogListStatus).text()).toEqual('newOutcome'); expect(wrapper.find(EuiIcon).props().color).toEqual('gray'); }); + + it('links to rules on the correct space', () => { + const wrapper1 = shallow( + + ); + // @ts-ignore data-href is not a native EuiLink prop + expect(wrapper1.find(EuiLink).props()['data-href']).toEqual('/rule/1'); + const wrapper2 = shallow( + + ); + // @ts-ignore data-href is not a native EuiLink prop + expect(wrapper2.find(EuiLink).props()['data-href']).toEqual( + '/basePath/s/space2/app/management/insightsAndAlerting/triggersActions/rule/1' + ); + + window.location = savedLocation; + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_cell_renderer.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_cell_renderer.tsx index 0f6e0477642b3..bcca56ad0027e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_cell_renderer.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_cell_renderer.tsx @@ -5,13 +5,14 @@ * 2.0. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import moment from 'moment'; import { EuiLink } from '@elastic/eui'; import { RuleAlertingOutcome } from '@kbn/alerting-plugin/common'; import { useHistory } from 'react-router-dom'; import { routeToRuleDetails } from '../../../constants'; import { formatRuleAlertCount } from '../../../../common/lib/format_rule_alert_count'; +import { useKibana, useSpacesData } from '../../../../common/lib/kibana'; import { RuleEventLogListStatus } from './rule_event_log_list_status'; import { RuleDurationFormat } from '../../rules_list/components/rule_duration_format'; import { @@ -27,20 +28,58 @@ export type ColumnId = typeof RULE_EXECUTION_LOG_COLUMN_IDS[number]; interface RuleEventLogListCellRendererProps { columnId: ColumnId; version?: string; - value?: string; + value?: string | string[]; dateFormat?: string; ruleId?: string; + spaceIds?: string[]; } export const RuleEventLogListCellRenderer = (props: RuleEventLogListCellRendererProps) => { - const { columnId, value, version, dateFormat = DEFAULT_DATE_FORMAT, ruleId } = props; + const { columnId, value, version, dateFormat = DEFAULT_DATE_FORMAT, ruleId, spaceIds } = props; + const spacesData = useSpacesData(); + const { http } = useKibana().services; + const history = useHistory(); - const onClickRuleName = useCallback( - () => ruleId && history.push(routeToRuleDetails.replace(':ruleId', ruleId)), - [ruleId, history] + const activeSpace = useMemo( + () => spacesData?.spacesMap.get(spacesData?.activeSpaceId), + [spacesData] + ); + + const ruleOnDifferentSpace = useMemo( + () => activeSpace && !spaceIds?.includes(activeSpace.id), + [activeSpace, spaceIds] ); + const ruleNamePathname = useMemo(() => { + if (!ruleId) return ''; + const ruleRoute = routeToRuleDetails.replace(':ruleId', ruleId); + if (ruleOnDifferentSpace) { + const [linkedSpaceId] = spaceIds ?? []; + const basePath = http.basePath.get(); + const spacePath = linkedSpaceId !== 'default' ? `/s/${linkedSpaceId}` : ''; + const historyPathname = history.location.pathname; + const newPathname = `${basePath.replace( + `/s/${activeSpace!.id}`, + '' + )}${spacePath}${window.location.pathname + .replace(basePath, '') + .replace(historyPathname, ruleRoute)}`; + return newPathname; + } + return ruleRoute; + }, [ruleId, ruleOnDifferentSpace, history, activeSpace, http, spaceIds]); + + const onClickRuleName = useCallback(() => { + if (!ruleId) return; + if (ruleOnDifferentSpace) { + const newUrl = window.location.href.replace(window.location.pathname, ruleNamePathname); + window.open(newUrl, '_blank'); + return; + } + history.push(ruleNamePathname); + }, [ruleNamePathname, history, ruleOnDifferentSpace, ruleId]); + if (typeof value === 'undefined') { return null; } @@ -54,15 +93,24 @@ export const RuleEventLogListCellRenderer = (props: RuleEventLogListCellRenderer } if (columnId === 'rule_name' && ruleId) { - return {value}; + return ( + + {value} + + ); + } + + if (columnId === 'space_ids') { + if (activeSpace && value.includes(activeSpace.id)) return <>{activeSpace.name}; + if (spacesData) return <>{spacesData.spacesMap.get(value[0])?.name ?? value[0]}; } if (RULE_EXECUTION_LOG_ALERT_COUNT_COLUMNS.includes(columnId)) { - return <>{formatRuleAlertCount(value, version)}; + return <>{formatRuleAlertCount(value as string, version)}; } if (RULE_EXECUTION_LOG_DURATION_COLUMNS.includes(columnId)) { - return ; + return ; } return <>{value}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.tsx index 970390359f0d7..0696f857261ec 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.tsx @@ -84,6 +84,7 @@ export type RuleEventLogListKPIProps = { outcomeFilter?: string[]; message?: string; refreshToken?: number; + namespaces?: Array; } & Pick; export const RuleEventLogListKPI = (props: RuleEventLogListKPIProps) => { @@ -94,6 +95,7 @@ export const RuleEventLogListKPI = (props: RuleEventLogListKPIProps) => { outcomeFilter, message, refreshToken, + namespaces, loadExecutionKPIAggregations, loadGlobalExecutionKPIAggregations, } = props; @@ -122,6 +124,7 @@ export const RuleEventLogListKPI = (props: RuleEventLogListKPIProps) => { dateEnd: getParsedDate(dateEnd), outcomeFilter, message, + ...(namespaces ? { namespaces } : {}), }); setKpi(newKpi); } catch (e) { @@ -136,7 +139,7 @@ export const RuleEventLogListKPI = (props: RuleEventLogListKPIProps) => { useEffect(() => { loadKPIs(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ruleId, dateStart, dateEnd, outcomeFilter, message]); + }, [ruleId, dateStart, dateEnd, outcomeFilter, message, namespaces]); useEffect(() => { if (isInitialized.current) { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx index 58cd6447ca737..1ea613e20055f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx @@ -18,9 +18,11 @@ import { Pagination, EuiSuperDatePicker, OnTimeChangeProps, + EuiSwitch, } from '@elastic/eui'; import { IExecutionLog } from '@kbn/alerting-plugin/common'; -import { useKibana } from '../../../../common/lib/kibana'; +import { SpacesContextProps } from '@kbn/spaces-plugin/public'; +import { useKibana, useSpacesData } from '../../../../common/lib/kibana'; import { RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS, GLOBAL_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS, @@ -38,6 +40,8 @@ import { withBulkRuleOperations, } from '../../common/components/with_bulk_rule_api_operations'; +const getEmptyFunctionComponent: React.FC = ({ children }) => <>{children}; + const getParsedDate = (date: string) => { if (date.includes('now')) { return datemath.parse(date)?.format() || date; @@ -66,6 +70,13 @@ const getDefaultColumns = (columns: string[]) => { return [...LOCKED_COLUMNS, ...columnsWithoutLockedColumn]; }; +const ALL_SPACES_LABEL = i18n.translate( + 'xpack.triggersActionsUI.ruleEventLogList.showAllSpacesToggle', + { + defaultMessage: 'Show rules from all spaces', + } +); + const updateButtonProps = { iconOnly: true, fill: false, @@ -84,6 +95,7 @@ export type RuleEventLogListCommonProps = { overrideLoadExecutionLogAggregations?: RuleApis['loadExecutionLogAggregations']; overrideLoadGlobalExecutionLogAggregations?: RuleApis['loadGlobalExecutionLogAggregations']; hasRuleNames?: boolean; + hasAllSpaceSwitch?: boolean; } & Pick; export type RuleEventLogListTableProps = @@ -106,6 +118,7 @@ export const RuleEventLogListTable = ( overrideLoadExecutionLogAggregations, initialPageSize = 10, hasRuleNames = false, + hasAllSpaceSwitch = false, } = props; const { uiSettings, notifications } = useKibana().services; @@ -117,6 +130,7 @@ export const RuleEventLogListTable = ( const [internalRefreshToken, setInternalRefreshToken] = useState( refreshToken ); + const [showFromAllSpaces, setShowFromAllSpaces] = useState(false); // Data grid states const [logs, setLogs] = useState(); @@ -153,6 +167,24 @@ export const RuleEventLogListTable = ( ); }); + const spacesData = useSpacesData(); + const accessibleSpaceIds = useMemo( + () => (spacesData ? [...spacesData.spacesMap.values()].map((e) => e.id) : []), + [spacesData] + ); + const areMultipleSpacesAccessible = useMemo( + () => accessibleSpaceIds.length > 1, + [accessibleSpaceIds] + ); + const namespaces = useMemo( + () => (showFromAllSpaces && spacesData ? accessibleSpaceIds : undefined), + [showFromAllSpaces, spacesData, accessibleSpaceIds] + ); + const activeSpace = useMemo( + () => spacesData?.spacesMap.get(spacesData?.activeSpaceId), + [spacesData] + ); + const isInitialized = useRef(false); const isOnLastPage = useMemo(() => { @@ -197,6 +229,7 @@ export const RuleEventLogListTable = ( dateEnd: getParsedDate(dateEnd), page: pagination.pageIndex, perPage: pagination.pageSize, + namespaces, }); setLogs(result.data); setPagination({ @@ -290,6 +323,20 @@ export const RuleEventLogListTable = ( [search, setSearchText] ); + const onShowAllSpacesChange = useCallback(() => { + setShowFromAllSpaces((prev) => !prev); + const nextShowFromAllSpaces = !showFromAllSpaces; + + if (nextShowFromAllSpaces && !visibleColumns.includes('space_ids')) { + const ruleNameIndex = visibleColumns.findIndex((c) => c === 'rule_name'); + const newVisibleColumns = [...visibleColumns]; + newVisibleColumns.splice(ruleNameIndex + 1, 0, 'space_ids'); + setVisibleColumns(newVisibleColumns); + } else if (!nextShowFromAllSpaces && visibleColumns.includes('space_ids')) { + setVisibleColumns(visibleColumns.filter((c) => c !== 'space_ids')); + } + }, [setShowFromAllSpaces, showFromAllSpaces, visibleColumns]); + const renderList = () => { if (!logs) { return ; @@ -307,6 +354,7 @@ export const RuleEventLogListTable = ( dateFormat={dateFormat} selectedRunLog={selectedRunLog} showRuleNameAndIdColumns={hasRuleNames} + showSpaceColumns={showFromAllSpaces} onChangeItemsPerPage={onChangeItemsPerPage} onChangePage={onChangePage} onFlyoutOpen={onFlyoutOpen} @@ -329,6 +377,7 @@ export const RuleEventLogListTable = ( pagination.pageIndex, pagination.pageSize, searchText, + showFromAllSpaces, ]); useEffect(() => { @@ -350,7 +399,7 @@ export const RuleEventLogListTable = ( return ( - + ( updateButtonProps={updateButtonProps} /> + {hasAllSpaceSwitch && areMultipleSpacesAccessible && ( + + + + )} @@ -389,6 +447,7 @@ export const RuleEventLogListTable = ( outcomeFilter={filter} message={searchText} refreshToken={internalRefreshToken} + namespaces={namespaces} /> @@ -407,13 +466,29 @@ export const RuleEventLogListTable = ( runLog={selectedRunLog} refreshToken={refreshToken} onClose={onFlyoutClose} + activeSpaceId={activeSpace?.id} /> )} ); }; -export const RuleEventLogListTableWithApi = withBulkRuleOperations(RuleEventLogListTable); +const RuleEventLogListTableWithSpaces: React.FC = (props) => { + const { spaces } = useKibana().services; + + // eslint-disable-next-line react-hooks/exhaustive-deps + const SpacesContextWrapper = useCallback( + spaces ? spaces.ui.components.getSpacesContextProvider : getEmptyFunctionComponent, + [spaces] + ); + return ( + + + + ); +}; + +export const RuleEventLogListTableWithApi = withBulkRuleOperations(RuleEventLogListTableWithSpaces); // eslint-disable-next-line import/no-default-export export { RuleEventLogListTableWithApi as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/__mocks__/index.ts index 6772eacc2aaed..e7c8215fd4625 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/__mocks__/index.ts @@ -31,3 +31,4 @@ export const useCurrentUser = jest.fn(); export const withKibana = jest.fn(createWithKibanaMock()); export const KibanaContextProvider = jest.fn(createKibanaContextProviderMock()); export const useGetUserSavedObjectPermissions = jest.fn(); +export const useSpacesData = jest.fn(); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/index.ts index 3970993a0c732..de8f3b63d1c5c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/index.ts @@ -6,3 +6,4 @@ */ export * from './kibana_react'; +export * from './use_spaces_data'; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/use_spaces_data.tsx b/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/use_spaces_data.tsx new file mode 100644 index 0000000000000..54f2baafa21c3 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/use_spaces_data.tsx @@ -0,0 +1,24 @@ +/* + * 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 { useState, useEffect } from 'react'; +import { SpacesData } from '@kbn/spaces-plugin/public'; +import { useKibana } from './kibana_react'; + +export const useSpacesData = () => { + const { spaces } = useKibana().services; + const [spacesData, setSpacesData] = useState(undefined); + const spacesService = spaces?.ui.useSpaces(); + + useEffect(() => { + (async () => { + const result = await spacesService?.spacesDataPromise; + setSpacesData(result); + })(); + }, [spaces, spacesService, setSpacesData]); + return spacesData; +};