diff --git a/x-pack/plugins/actions/common/monitoring/types.ts b/x-pack/plugins/actions/common/monitoring/types.ts new file mode 100644 index 000000000000..f735f91221b4 --- /dev/null +++ b/x-pack/plugins/actions/common/monitoring/types.ts @@ -0,0 +1,33 @@ +/* + * 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 { ValidMetricSet } from '@kbn/monitoring-collection-plugin/common/types'; + +export interface RuleMonitoringMetrics { + [key: string]: { + data: Array<{ timestamp: number; value: number }>; + }; +} + +export interface NodeLevelMetricsType { + kibana_alerting_node_action_executions: number; + kibana_alerting_node_action_execution_time: number; + kibana_alerting_node_action_failures: number; + kibana_alerting_node_action_timeouts: number; +} + +export enum NodeLevelMetricsEnum { + kibana_alerting_node_action_executions = 'kibana_alerting_node_action_executions', + kibana_alerting_node_action_execution_time = 'kibana_alerting_node_action_execution_time', + kibana_alerting_node_action_failures = 'kibana_alerting_node_action_failures', + kibana_alerting_node_action_timeouts = 'kibana_alerting_node_action_timeouts', +} + +export interface ClusterLevelMetricsType extends ValidMetricSet { + kibana_alerting_cluster_actions_overdue_count: number; + kibana_alerting_cluster_actions_overdue_delay_p50: number; + kibana_alerting_cluster_actions_overdue_delay_p99: number; +} diff --git a/x-pack/plugins/actions/server/action_type_registry.test.ts b/x-pack/plugins/actions/server/action_type_registry.test.ts index ec88ba5fb12a..4f71f645868d 100644 --- a/x-pack/plugins/actions/server/action_type_registry.test.ts +++ b/x-pack/plugins/actions/server/action_type_registry.test.ts @@ -13,10 +13,8 @@ import { actionsConfigMock } from './actions_config.mock'; import { licenseStateMock } from './lib/license_state.mock'; import { ActionsConfigurationUtilities } from './actions_config'; import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; -import { inMemoryMetricsMock } from './monitoring/in_memory_metrics.mock'; const mockTaskManager = taskManagerMock.createSetup(); -const inMemoryMetrics = inMemoryMetricsMock.create(); let mockedLicenseState: jest.Mocked; let mockedActionsConfig: jest.Mocked; let actionTypeRegistryParams: ActionTypeRegistryOpts; @@ -28,10 +26,7 @@ beforeEach(() => { actionTypeRegistryParams = { licensing: licensingMock.createSetup(), taskManager: mockTaskManager, - taskRunnerFactory: new TaskRunnerFactory( - new ActionExecutor({ isESOCanEncrypt: true }), - inMemoryMetrics - ), + taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor({ isESOCanEncrypt: true })), actionsConfigUtils: mockedActionsConfig, licenseState: mockedLicenseState, preconfiguredActions: [ diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index afee13b8c9bc..7189edd5aa68 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -36,7 +36,6 @@ import { ConnectorTokenClient } from './builtin_action_types/lib/connector_token import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; import { Logger } from '@kbn/core/server'; import { connectorTokenClientMock } from './builtin_action_types/lib/connector_token_client.mock'; -import { inMemoryMetricsMock } from './monitoring/in_memory_metrics.mock'; jest.mock('@kbn/core/server/saved_objects/service/lib/utils', () => ({ SavedObjectsUtils: { @@ -83,7 +82,6 @@ const executor: ExecutorType<{}, {}, {}, void> = async (options) => { }; const connectorTokenClient = connectorTokenClientMock.create(); -const inMemoryMetrics = inMemoryMetricsMock.create(); beforeEach(() => { jest.resetAllMocks(); @@ -91,10 +89,7 @@ beforeEach(() => { actionTypeRegistryParams = { licensing: licensingMock.createSetup(), taskManager: mockTaskManager, - taskRunnerFactory: new TaskRunnerFactory( - new ActionExecutor({ isESOCanEncrypt: true }), - inMemoryMetrics - ), + taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor({ isESOCanEncrypt: true })), actionsConfigUtils: actionsConfigMock.create(), licenseState: mockedLicenseState, preconfiguredActions: [], @@ -503,10 +498,7 @@ describe('create()', () => { const localActionTypeRegistryParams = { licensing: licensingMock.createSetup(), taskManager: mockTaskManager, - taskRunnerFactory: new TaskRunnerFactory( - new ActionExecutor({ isESOCanEncrypt: true }), - inMemoryMetrics - ), + taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor({ isESOCanEncrypt: true })), actionsConfigUtils: localConfigUtils, licenseState: licenseStateMock.create(), preconfiguredActions: [], diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts index 2465e64179a7..344323685541 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts @@ -14,7 +14,6 @@ import { loggingSystemMock } from '@kbn/core/server/mocks'; import { actionsConfigMock } from '../actions_config.mock'; import { licenseStateMock } from '../lib/license_state.mock'; import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; -import { inMemoryMetricsMock } from '../monitoring/in_memory_metrics.mock'; const ACTION_TYPE_IDS = [ '.index', @@ -33,14 +32,10 @@ export function createActionTypeRegistry(): { actionTypeRegistry: ActionTypeRegistry; } { const logger = loggingSystemMock.create().get() as jest.Mocked; - const inMemoryMetrics = inMemoryMetricsMock.create(); const actionTypeRegistry = new ActionTypeRegistry({ taskManager: taskManagerMock.createSetup(), licensing: licensingMock.createSetup(), - taskRunnerFactory: new TaskRunnerFactory( - new ActionExecutor({ isESOCanEncrypt: true }), - inMemoryMetrics - ), + taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor({ isESOCanEncrypt: true })), actionsConfigUtils: actionsConfigMock.create(), licenseState: licenseStateMock.create(), preconfiguredActions: [], diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 093236c939aa..195f79a70e91 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -15,6 +15,7 @@ import { eventLoggerMock } from '@kbn/event-log-plugin/server/mocks'; import { spacesServiceMock } from '@kbn/spaces-plugin/server/spaces_service/spaces_service.mock'; import { ActionType } from '../types'; import { actionsMock, actionsClientMock } from '../mocks'; +import { nodeLevelMetricsMock } from '../monitoring/node_level_metrics.mock'; import { pick } from 'lodash'; const actionExecutor = new ActionExecutor({ isESOCanEncrypt: true }); @@ -34,6 +35,7 @@ const executeParams = { request: {} as KibanaRequest, }; +const nodeLevelMetrics = nodeLevelMetricsMock.create(); const spacesMock = spacesServiceMock.createStartContract(); const loggerMock = loggingSystemMock.create().get(); const getActionsClientWithRequest = jest.fn(); @@ -46,6 +48,7 @@ actionExecutor.initialize({ encryptedSavedObjectsClient, eventLogger, preconfiguredActions: [], + nodeLevelMetrics, }); beforeEach(() => { @@ -734,6 +737,39 @@ test('writes to event log for execute and execute start when consumer and relate }); }); +test('increments monitoring metrics after execution', async () => { + const executorMock = setupActionExecutorMock(); + executorMock.mockResolvedValue({ + actionId: '1', + status: 'ok', + }); + await actionExecutor.execute(executeParams); + + expect(nodeLevelMetrics.execution).toHaveBeenCalledTimes(1); +}); + +test('increments monitoring metrics after a failed execution', async () => { + const executorMock = setupActionExecutorMock(); + executorMock.mockRejectedValue(new Error('this action execution is intended to fail')); + await actionExecutor.execute(executeParams); + expect(nodeLevelMetrics.execution).toHaveBeenCalledTimes(1); + expect(nodeLevelMetrics.failure).toHaveBeenCalledTimes(1); +}); + +test('increments monitoring metrics after a timeout', async () => { + setupActionExecutorMock(); + + await actionExecutor.logCancellation({ + actionId: 'action1', + executionId: '123abc', + consumer: 'test-consumer', + relatedSavedObjects: [], + request: {} as KibanaRequest, + }); + + expect(nodeLevelMetrics.timeout).toHaveBeenCalledTimes(1); +}); + function setupActionExecutorMock() { const actionType: jest.Mocked = { id: 'test', diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index fe77b72f47aa..fd6ff5ebaf67 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -30,6 +30,7 @@ import { ActionsClient } from '../actions_client'; import { ActionExecutionSource } from './action_execution_source'; import { RelatedSavedObjects } from './related_saved_objects'; import { createActionEventLogRecordObject } from './create_action_event_log_record_object'; +import { NodeLevelMetrics } from '../monitoring'; // 1,000,000 nanoseconds in 1 millisecond const Millis2Nanos = 1000 * 1000; @@ -46,6 +47,7 @@ export interface ActionExecutorContext { actionTypeRegistry: ActionTypeRegistryContract; eventLogger: IEventLogger; preconfiguredActions: PreConfiguredAction[]; + nodeLevelMetrics?: NodeLevelMetrics; } export interface TaskInfo { @@ -249,6 +251,12 @@ export class ActionExecutor { event.event = event.event || {}; + this.actionExecutorContext?.nodeLevelMetrics?.execution( + actionId, + actionTypeId, + event.event?.duration ? event.event?.duration / Millis2Nanos : undefined + ); + if (result.status === 'ok') { span?.setOutcome('success'); event.event.outcome = 'success'; @@ -260,6 +268,7 @@ export class ActionExecutor { event.error = event.error || {}; event.error.message = actionErrorToMessage(result); logger.warn(`action execution failure: ${actionLabel}: ${event.error.message}`); + this.actionExecutorContext?.nodeLevelMetrics?.failure(actionId, actionTypeId); } else { span?.setOutcome('failure'); event.event.outcome = 'failure'; @@ -269,6 +278,7 @@ export class ActionExecutor { logger.warn( `action execution failure: ${actionLabel}: returned unexpected result "${result.status}"` ); + this.actionExecutorContext?.nodeLevelMetrics?.failure(actionId, actionTypeId); } eventLogger.logEvent(event); @@ -345,6 +355,7 @@ export class ActionExecutor { }); eventLogger.logEvent(event); + this.actionExecutorContext?.nodeLevelMetrics?.timeout(actionId, this.actionInfo.actionTypeId); } } diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts index 7fd09f2237ea..4402d11202a4 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts @@ -17,15 +17,14 @@ import { savedObjectsClientMock, loggingSystemMock, httpServiceMock } from '@kbn import { eventLoggerMock } from '@kbn/event-log-plugin/server/mocks'; import { ActionTypeDisabledError } from './errors'; import { actionsClientMock } from '../mocks'; -import { inMemoryMetricsMock } from '../monitoring/in_memory_metrics.mock'; -import { IN_MEMORY_METRICS } from '../monitoring'; +import { nodeLevelMetricsMock } from '../monitoring/node_level_metrics.mock'; const spaceIdToNamespace = jest.fn(); const actionTypeRegistry = actionTypeRegistryMock.create(); const mockedEncryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient(); const mockedActionExecutor = actionExecutorMock.create(); const eventLogger = eventLoggerMock.create(); -const inMemoryMetrics = inMemoryMetricsMock.create(); +const mockedNodeLevelMetrics = nodeLevelMetricsMock.create(); let fakeTimer: sinon.SinonFakeTimers; let taskRunnerFactory: TaskRunnerFactory; @@ -49,7 +48,7 @@ beforeAll(() => { }, taskType: 'actions:1', }; - taskRunnerFactory = new TaskRunnerFactory(mockedActionExecutor, inMemoryMetrics); + taskRunnerFactory = new TaskRunnerFactory(mockedActionExecutor); mockedActionExecutor.initialize(actionExecutorInitializerParams); taskRunnerFactory.initialize(taskRunnerFactoryInitializerParams); }); @@ -76,6 +75,7 @@ const taskRunnerFactoryInitializerParams = { encryptedSavedObjectsClient: mockedEncryptedSavedObjectsClient, basePathService: httpServiceMock.createBasePath(), getUnsecuredSavedObjectsClient: jest.fn().mockReturnValue(services.savedObjectsClient), + nodeLevelMetrics: mockedNodeLevelMetrics, }; beforeEach(() => { @@ -87,20 +87,14 @@ beforeEach(() => { }); test(`throws an error if factory isn't initialized`, () => { - const factory = new TaskRunnerFactory( - new ActionExecutor({ isESOCanEncrypt: true }), - inMemoryMetrics - ); + const factory = new TaskRunnerFactory(new ActionExecutor({ isESOCanEncrypt: true })); expect(() => factory.create({ taskInstance: mockedTaskInstance }) ).toThrowErrorMatchingInlineSnapshot(`"TaskRunnerFactory not initialized"`); }); test(`throws an error if factory is already initialized`, () => { - const factory = new TaskRunnerFactory( - new ActionExecutor({ isESOCanEncrypt: true }), - inMemoryMetrics - ); + const factory = new TaskRunnerFactory(new ActionExecutor({ isESOCanEncrypt: true })); factory.initialize(taskRunnerFactoryInitializerParams); expect(() => factory.initialize(taskRunnerFactoryInitializerParams) @@ -627,7 +621,7 @@ test('sanitizes invalid relatedSavedObjects when provided', async () => { }); test(`doesn't use API key when not provided`, async () => { - const factory = new TaskRunnerFactory(mockedActionExecutor, inMemoryMetrics); + const factory = new TaskRunnerFactory(mockedActionExecutor); factory.initialize(taskRunnerFactoryInitializerParams); const taskRunner = factory.create({ taskInstance: mockedTaskInstance }); @@ -850,92 +844,3 @@ test('treats errors as errors if the error is thrown instead of returned', async `Action '2' failed and will retry: undefined` ); }); - -test('increments monitoring metrics after execution', async () => { - const taskRunner = taskRunnerFactory.create({ - taskInstance: mockedTaskInstance, - }); - - mockedActionExecutor.execute.mockResolvedValueOnce({ status: 'ok', actionId: '2' }); - spaceIdToNamespace.mockReturnValueOnce('namespace-test'); - mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '3', - type: 'action_task_params', - attributes: { - actionId: '2', - params: { baz: true }, - executionId: '123abc', - apiKey: Buffer.from('123:abc').toString('base64'), - }, - references: [], - }); - - await taskRunner.run(); - - expect(inMemoryMetrics.increment).toHaveBeenCalledTimes(1); - expect(inMemoryMetrics.increment.mock.calls[0][0]).toBe(IN_MEMORY_METRICS.ACTION_EXECUTIONS); -}); - -test('increments monitoring metrics after a failed execution', async () => { - const taskRunner = taskRunnerFactory.create({ - taskInstance: mockedTaskInstance, - }); - - mockedActionExecutor.execute.mockResolvedValueOnce({ - status: 'error', - actionId: '2', - message: 'Error message', - data: { foo: true }, - retry: false, - }); - - spaceIdToNamespace.mockReturnValueOnce('namespace-test'); - mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '3', - type: 'action_task_params', - attributes: { - actionId: '2', - params: { baz: true }, - executionId: '123abc', - apiKey: Buffer.from('123:abc').toString('base64'), - }, - references: [], - }); - - let err; - try { - await taskRunner.run(); - } catch (e) { - err = e; - } - - expect(err).toBeDefined(); - expect(inMemoryMetrics.increment).toHaveBeenCalledTimes(2); - expect(inMemoryMetrics.increment.mock.calls[0][0]).toBe(IN_MEMORY_METRICS.ACTION_EXECUTIONS); - expect(inMemoryMetrics.increment.mock.calls[1][0]).toBe(IN_MEMORY_METRICS.ACTION_FAILURES); -}); - -test('increments monitoring metrics after a timeout', async () => { - const taskRunner = taskRunnerFactory.create({ - taskInstance: mockedTaskInstance, - }); - - mockedActionExecutor.execute.mockResolvedValueOnce({ status: 'ok', actionId: '2' }); - spaceIdToNamespace.mockReturnValueOnce('namespace-test'); - mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '3', - type: 'action_task_params', - attributes: { - actionId: '2', - params: { baz: true }, - executionId: '123abc', - apiKey: Buffer.from('123:abc').toString('base64'), - }, - references: [], - }); - - await taskRunner.cancel(); - - expect(inMemoryMetrics.increment).toHaveBeenCalledTimes(1); - expect(inMemoryMetrics.increment.mock.calls[0][0]).toBe(IN_MEMORY_METRICS.ACTION_TIMEOUTS); -}); diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index 3c94c243b56a..2e342a7c6227 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -34,7 +34,6 @@ import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../constants/saved_objects import { asSavedObjectExecutionSource } from './action_execution_source'; import { RelatedSavedObjects, validatedRelatedSavedObjects } from './related_saved_objects'; import { injectSavedObjectReferences } from './action_task_params_utils'; -import { InMemoryMetrics, IN_MEMORY_METRICS } from '../monitoring'; export interface TaskRunnerContext { logger: Logger; @@ -49,11 +48,9 @@ export class TaskRunnerFactory { private isInitialized = false; private taskRunnerContext?: TaskRunnerContext; private readonly actionExecutor: ActionExecutorContract; - private readonly inMemoryMetrics: InMemoryMetrics; - constructor(actionExecutor: ActionExecutorContract, inMemoryMetrics: InMemoryMetrics) { + constructor(actionExecutor: ActionExecutorContract) { this.actionExecutor = actionExecutor; - this.inMemoryMetrics = inMemoryMetrics; } public initialize(taskRunnerContext: TaskRunnerContext) { @@ -69,7 +66,7 @@ export class TaskRunnerFactory { throw new Error('TaskRunnerFactory not initialized'); } - const { actionExecutor, inMemoryMetrics } = this; + const { actionExecutor } = this; const { logger, encryptedSavedObjectsClient, @@ -134,14 +131,12 @@ export class TaskRunnerFactory { } } - inMemoryMetrics.increment(IN_MEMORY_METRICS.ACTION_EXECUTIONS); if ( executorResult && executorResult?.status === 'error' && executorResult?.retry !== undefined && isRetryableBasedOnAttempts ) { - inMemoryMetrics.increment(IN_MEMORY_METRICS.ACTION_FAILURES); logger.error( `Action '${actionId}' failed ${ !!executorResult.retry ? willRetryMessage : willNotRetryMessage @@ -155,7 +150,6 @@ export class TaskRunnerFactory { executorResult.retry as boolean | Date ); } else if (executorResult && executorResult?.status === 'error') { - inMemoryMetrics.increment(IN_MEMORY_METRICS.ACTION_FAILURES); logger.error( `Action '${actionId}' failed ${willNotRetryMessage}: ${executorResult.message}` ); @@ -207,8 +201,6 @@ export class TaskRunnerFactory { ...getSourceFromReferences(references), }); - inMemoryMetrics.increment(IN_MEMORY_METRICS.ACTION_TIMEOUTS); - logger.debug( `Cancelling action task for action with id ${actionId} - execution error due to timeout.` ); diff --git a/x-pack/plugins/actions/server/monitoring/register_cluster_collector.test.ts b/x-pack/plugins/actions/server/monitoring/cluster_level_metrics.test.ts similarity index 69% rename from x-pack/plugins/actions/server/monitoring/register_cluster_collector.test.ts rename to x-pack/plugins/actions/server/monitoring/cluster_level_metrics.test.ts index 488e4b5d20e3..6f7ffc62e858 100644 --- a/x-pack/plugins/actions/server/monitoring/register_cluster_collector.test.ts +++ b/x-pack/plugins/actions/server/monitoring/cluster_level_metrics.test.ts @@ -7,15 +7,16 @@ import { coreMock } from '@kbn/core/public/mocks'; import { CoreSetup } from '@kbn/core/server'; import { monitoringCollectionMock } from '@kbn/monitoring-collection-plugin/server/mocks'; -import { Metric } from '@kbn/monitoring-collection-plugin/server'; -import { registerClusterCollector } from './register_cluster_collector'; +import { MetricSet } from '@kbn/monitoring-collection-plugin/server'; +import { registerClusterLevelMetrics } from './cluster_level_metrics'; import { ActionsPluginsStart } from '../plugin'; -import { ClusterActionsMetric } from './types'; +import { ValidMetricSet } from '@kbn/monitoring-collection-plugin/common/types'; +import { ClusterLevelMetricsType } from '../../common/monitoring/types'; jest.useFakeTimers('modern'); jest.setSystemTime(new Date('2020-03-09').getTime()); -describe('registerClusterCollector()', () => { +describe('registerClusterLevelMetrics()', () => { const monitoringCollection = monitoringCollectionMock.createSetup(); const coreSetup = coreMock.createSetup() as unknown as CoreSetup; const taskManagerFetch = jest.fn(); @@ -34,15 +35,15 @@ describe('registerClusterCollector()', () => { }); it('should get overdue actions', async () => { - const metrics: Record> = {}; - monitoringCollection.registerMetric.mockImplementation((metric) => { - metrics[metric.type] = metric; + const metrics: Record> = {}; + monitoringCollection.registerMetricSet.mockImplementation((set) => { + metrics[set.id] = set; }); - registerClusterCollector({ monitoringCollection, core: coreSetup }); + registerClusterLevelMetrics({ monitoringCollection, core: coreSetup }); const metricTypes = Object.keys(metrics); expect(metricTypes.length).toBe(1); - expect(metricTypes[0]).toBe('cluster_actions'); + expect(metricTypes[0]).toBe('kibana_alerting_cluster_actions'); const nowInMs = +new Date(); const docs = [ @@ -55,10 +56,11 @@ describe('registerClusterCollector()', () => { ]; taskManagerFetch.mockImplementation(async () => ({ docs })); - const result = (await metrics.cluster_actions.fetch()) as ClusterActionsMetric; - expect(result.overdue.count).toBe(docs.length); - expect(result.overdue.delay.p50).toBe(1000); - expect(result.overdue.delay.p99).toBe(1000); + const result = + (await metrics.kibana_alerting_cluster_actions.fetch()) as ClusterLevelMetricsType; + expect(result.kibana_alerting_cluster_actions_overdue_count).toBe(docs.length); + expect(result.kibana_alerting_cluster_actions_overdue_delay_p50).toBe(1000); + expect(result.kibana_alerting_cluster_actions_overdue_delay_p99).toBe(1000); expect(taskManagerFetch).toHaveBeenCalledWith({ query: { bool: { @@ -130,15 +132,15 @@ describe('registerClusterCollector()', () => { }); it('should calculate accurate p50 and p99', async () => { - const metrics: Record> = {}; - monitoringCollection.registerMetric.mockImplementation((metric) => { - metrics[metric.type] = metric; + const metrics: Record> = {}; + monitoringCollection.registerMetricSet.mockImplementation((set) => { + metrics[set.id] = set; }); - registerClusterCollector({ monitoringCollection, core: coreSetup }); + registerClusterLevelMetrics({ monitoringCollection, core: coreSetup }); const metricTypes = Object.keys(metrics); expect(metricTypes.length).toBe(1); - expect(metricTypes[0]).toBe('cluster_actions'); + expect(metricTypes[0]).toBe('kibana_alerting_cluster_actions'); const nowInMs = +new Date(); const docs = [ @@ -150,9 +152,10 @@ describe('registerClusterCollector()', () => { ]; taskManagerFetch.mockImplementation(async () => ({ docs })); - const result = (await metrics.cluster_actions.fetch()) as ClusterActionsMetric; - expect(result.overdue.count).toBe(docs.length); - expect(result.overdue.delay.p50).toBe(3000); - expect(result.overdue.delay.p99).toBe(40000); + const result = + (await metrics.kibana_alerting_cluster_actions.fetch()) as ClusterLevelMetricsType; + expect(result.kibana_alerting_cluster_actions_overdue_count).toBe(docs.length); + expect(result.kibana_alerting_cluster_actions_overdue_delay_p50).toBe(3000); + expect(result.kibana_alerting_cluster_actions_overdue_delay_p99).toBe(40000); }); }); diff --git a/x-pack/plugins/actions/server/monitoring/register_cluster_collector.ts b/x-pack/plugins/actions/server/monitoring/cluster_level_metrics.ts similarity index 58% rename from x-pack/plugins/actions/server/monitoring/register_cluster_collector.ts rename to x-pack/plugins/actions/server/monitoring/cluster_level_metrics.ts index 895eb44bfcee..723e75d690fa 100644 --- a/x-pack/plugins/actions/server/monitoring/register_cluster_collector.ts +++ b/x-pack/plugins/actions/server/monitoring/cluster_level_metrics.ts @@ -12,35 +12,20 @@ import { } from '@kbn/task-manager-plugin/server'; import { CoreSetup } from '@kbn/core/server'; import { ActionsPluginsStart } from '../plugin'; -import { ClusterActionsMetric } from './types'; +import { ClusterLevelMetricsType } from '../../common/monitoring/types'; -export function registerClusterCollector({ +export function registerClusterLevelMetrics({ monitoringCollection, core, }: { monitoringCollection: MonitoringCollectionSetup; core: CoreSetup; }) { - monitoringCollection.registerMetric({ - type: 'cluster_actions', - schema: { - overdue: { - count: { - type: 'long', - }, - delay: { - p50: { - type: 'long', - }, - p99: { - type: 'long', - }, - }, - }, - }, + monitoringCollection.registerMetricSet({ + id: `kibana_alerting_cluster_actions`, fetch: async () => { const [_, pluginStart] = await core.getStartServices(); - const nowInMs = +new Date(); + const now = +new Date(); const { docs: overdueTasks } = await pluginStart.taskManager.fetch({ query: { bool: { @@ -63,28 +48,15 @@ export function registerClusterCollector({ }); const overdueTasksDelay = overdueTasks.map( - (overdueTask) => nowInMs - +new Date(overdueTask.runAt || overdueTask.retryAt) + (overdueTask) => now - +new Date(overdueTask.runAt || overdueTask.retryAt) ); - - const metrics: ClusterActionsMetric = { - overdue: { - count: overdueTasks.length, - delay: { - p50: stats.percentile(overdueTasksDelay, 0.5), - p99: stats.percentile(overdueTasksDelay, 0.99), - }, - }, + const p50 = stats.percentile(overdueTasksDelay, 0.5); + const p99 = stats.percentile(overdueTasksDelay, 0.99); + return { + kibana_alerting_cluster_actions_overdue_count: overdueTasks.length, + kibana_alerting_cluster_actions_overdue_delay_p50: isNaN(p50) ? 0 : p50, + kibana_alerting_cluster_actions_overdue_delay_p99: isNaN(p99) ? 0 : p99, }; - - if (isNaN(metrics.overdue.delay.p50)) { - metrics.overdue.delay.p50 = 0; - } - - if (isNaN(metrics.overdue.delay.p99)) { - metrics.overdue.delay.p99 = 0; - } - - return metrics; }, }); } diff --git a/x-pack/plugins/actions/server/monitoring/in_memory_metrics.test.ts b/x-pack/plugins/actions/server/monitoring/in_memory_metrics.test.ts deleted file mode 100644 index 705979f022dc..000000000000 --- a/x-pack/plugins/actions/server/monitoring/in_memory_metrics.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 { InMemoryMetrics, IN_MEMORY_METRICS } from '.'; -import { loggingSystemMock } from '@kbn/core/server/mocks'; - -describe('inMemoryMetrics', () => { - const logger = loggingSystemMock.createLogger(); - const inMemoryMetrics = new InMemoryMetrics(logger); - - beforeEach(() => { - const all = inMemoryMetrics.getAllInMemoryMetrics(); - for (const key of Object.keys(all)) { - all[key as IN_MEMORY_METRICS] = 0; - } - }); - - it('should increment', () => { - inMemoryMetrics.increment(IN_MEMORY_METRICS.ACTION_EXECUTIONS); - expect(inMemoryMetrics.getInMemoryMetric(IN_MEMORY_METRICS.ACTION_EXECUTIONS)).toBe(1); - }); - - it('should set to null if incrementing will set over the max integer', () => { - const all = inMemoryMetrics.getAllInMemoryMetrics(); - all[IN_MEMORY_METRICS.ACTION_EXECUTIONS] = Number.MAX_SAFE_INTEGER; - inMemoryMetrics.increment(IN_MEMORY_METRICS.ACTION_EXECUTIONS); - expect(inMemoryMetrics.getInMemoryMetric(IN_MEMORY_METRICS.ACTION_EXECUTIONS)).toBe(null); - expect(logger.info).toHaveBeenCalledWith( - `Metric ${IN_MEMORY_METRICS.ACTION_EXECUTIONS} has reached the max safe integer value and will no longer be used, skipping increment.` - ); - inMemoryMetrics.increment(IN_MEMORY_METRICS.ACTION_EXECUTIONS); - expect(inMemoryMetrics.getInMemoryMetric(IN_MEMORY_METRICS.ACTION_EXECUTIONS)).toBe(null); - expect(logger.info).toHaveBeenCalledWith( - `Metric ${IN_MEMORY_METRICS.ACTION_EXECUTIONS} is null because the counter ran over the max safe integer value, skipping increment.` - ); - }); -}); diff --git a/x-pack/plugins/actions/server/monitoring/in_memory_metrics.ts b/x-pack/plugins/actions/server/monitoring/in_memory_metrics.ts deleted file mode 100644 index 2d9b9f61407d..000000000000 --- a/x-pack/plugins/actions/server/monitoring/in_memory_metrics.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 { Logger } from '@kbn/logging'; - -export enum IN_MEMORY_METRICS { - ACTION_EXECUTIONS = 'actionExecutions', - ACTION_FAILURES = 'actionFailures', - ACTION_TIMEOUTS = 'actionTimeouts', -} - -export class InMemoryMetrics { - private logger: Logger; - private inMemoryMetrics: Record = { - [IN_MEMORY_METRICS.ACTION_EXECUTIONS]: 0, - [IN_MEMORY_METRICS.ACTION_FAILURES]: 0, - [IN_MEMORY_METRICS.ACTION_TIMEOUTS]: 0, - }; - - constructor(logger: Logger) { - this.logger = logger; - } - - public increment(metric: IN_MEMORY_METRICS) { - if (this.inMemoryMetrics[metric] === null) { - this.logger.info( - `Metric ${metric} is null because the counter ran over the max safe integer value, skipping increment.` - ); - return; - } - - if ((this.inMemoryMetrics[metric] as number) >= Number.MAX_SAFE_INTEGER) { - this.inMemoryMetrics[metric] = null; - this.logger.info( - `Metric ${metric} has reached the max safe integer value and will no longer be used, skipping increment.` - ); - } else { - (this.inMemoryMetrics[metric] as number)++; - } - } - - public getInMemoryMetric(metric: IN_MEMORY_METRICS) { - return this.inMemoryMetrics[metric]; - } - - public getAllInMemoryMetrics() { - return this.inMemoryMetrics; - } -} diff --git a/x-pack/plugins/actions/server/monitoring/index.ts b/x-pack/plugins/actions/server/monitoring/index.ts index f084c1a42032..2a69a7f902ed 100644 --- a/x-pack/plugins/actions/server/monitoring/index.ts +++ b/x-pack/plugins/actions/server/monitoring/index.ts @@ -4,7 +4,5 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -export { registerClusterCollector } from './register_cluster_collector'; -export { registerNodeCollector } from './register_node_collector'; -export * from './types'; -export * from './in_memory_metrics'; +export * from './node_level_metrics'; +export * from './cluster_level_metrics'; diff --git a/x-pack/plugins/actions/server/monitoring/in_memory_metrics.mock.ts b/x-pack/plugins/actions/server/monitoring/node_level_metrics.mock.ts similarity index 60% rename from x-pack/plugins/actions/server/monitoring/in_memory_metrics.mock.ts rename to x-pack/plugins/actions/server/monitoring/node_level_metrics.mock.ts index 4b613753d616..017bc903a2ef 100644 --- a/x-pack/plugins/actions/server/monitoring/in_memory_metrics.mock.ts +++ b/x-pack/plugins/actions/server/monitoring/node_level_metrics.mock.ts @@ -4,17 +4,16 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -function createInMemoryMetricsMock() { +function createNodeLevelMetricsMock() { return jest.fn().mockImplementation(() => { return { - increment: jest.fn(), - getInMemoryMetric: jest.fn(), - getAllInMemoryMetrics: jest.fn(), + execution: jest.fn(), + failure: jest.fn(), + timeout: jest.fn(), }; }); } -export const inMemoryMetricsMock = { - create: createInMemoryMetricsMock(), +export const nodeLevelMetricsMock = { + create: createNodeLevelMetricsMock(), }; diff --git a/x-pack/plugins/actions/server/monitoring/node_level_metrics.test.ts b/x-pack/plugins/actions/server/monitoring/node_level_metrics.test.ts new file mode 100644 index 000000000000..a83fd8beb1b6 --- /dev/null +++ b/x-pack/plugins/actions/server/monitoring/node_level_metrics.test.ts @@ -0,0 +1,81 @@ +/* + * 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 { monitoringCollectionMock } from '@kbn/monitoring-collection-plugin/server/mocks'; +import { NodeLevelMetrics } from './node_level_metrics'; + +describe('NodeLevelMetrics', () => { + const monitoringCollection = monitoringCollectionMock.createStart(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('execution', () => { + it('should register a counter when called', () => { + const metrics = new NodeLevelMetrics(monitoringCollection); + metrics.execution('actionA', 'actionTypeA'); + expect(monitoringCollection.reportCounter).toHaveBeenCalledTimes(1); + expect(monitoringCollection.reportCounter).toHaveBeenCalledWith( + 'kibana_alerting_node_action_executions', + { action_id: 'actionA', action_type_id: 'actionTypeA' } + ); + }); + + it('should report a gauge when provided with an execution time', () => { + const executionTime = 1000; + const metrics = new NodeLevelMetrics(monitoringCollection); + metrics.execution('actionA', 'actionTypeA', executionTime); + expect(monitoringCollection.reportCounter).toHaveBeenCalledTimes(1); + expect(monitoringCollection.reportCounter).toHaveBeenCalledWith( + 'kibana_alerting_node_action_executions', + { action_id: 'actionA', action_type_id: 'actionTypeA' } + ); + expect(monitoringCollection.reportGauge).toHaveBeenCalledTimes(1); + expect(monitoringCollection.reportGauge).toHaveBeenCalledWith( + 'kibana_alerting_node_action_execution_time', + { action_id: 'actionA', action_type_id: 'actionTypeA' }, + executionTime + ); + }); + }); + + describe('failure', () => { + it('should register a counter when called', () => { + const metrics = new NodeLevelMetrics(monitoringCollection); + metrics.failure('actionA', 'actionTypeA'); + expect(monitoringCollection.reportCounter).toHaveBeenCalledTimes(1); + expect(monitoringCollection.reportCounter).toHaveBeenCalledWith( + `kibana_alerting_node_action_failures`, + { action_id: 'actionA', action_type_id: 'actionTypeA' } + ); + }); + }); + + describe('timeout', () => { + it('should register a counter when called', () => { + const metrics = new NodeLevelMetrics(monitoringCollection); + metrics.timeout('actionA', 'actionTypeA'); + expect(monitoringCollection.reportCounter).toHaveBeenCalledTimes(1); + expect(monitoringCollection.reportCounter).toHaveBeenCalledWith( + `kibana_alerting_node_action_timeouts`, + { action_id: 'actionA', action_type_id: 'actionTypeA' } + ); + }); + + it('should report the timeout if provided', () => { + const timeout = '1000'; + const metrics = new NodeLevelMetrics(monitoringCollection); + metrics.timeout('actionA', 'actionTypeA', timeout); + expect(monitoringCollection.reportCounter).toHaveBeenCalledTimes(1); + expect(monitoringCollection.reportCounter).toHaveBeenCalledWith( + `kibana_alerting_node_action_timeouts`, + { action_id: 'actionA', action_type_id: 'actionTypeA', timeout } + ); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/monitoring/node_level_metrics.ts b/x-pack/plugins/actions/server/monitoring/node_level_metrics.ts new file mode 100644 index 000000000000..3e384cf4984e --- /dev/null +++ b/x-pack/plugins/actions/server/monitoring/node_level_metrics.ts @@ -0,0 +1,55 @@ +/* + * 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 { MonitoringCollectionStart } from '@kbn/monitoring-collection-plugin/server'; +import { NodeLevelMetricsEnum } from '../../common/monitoring/types'; + +export class NodeLevelMetrics { + private monitoringCollection: MonitoringCollectionStart; + + constructor(monitoringCollection: MonitoringCollectionStart) { + this.monitoringCollection = monitoringCollection; + } + + public execution(actionId: string, actionTypeId: string, executionTime?: number) { + this.monitoringCollection.reportCounter( + NodeLevelMetricsEnum.kibana_alerting_node_action_executions, + { action_id: actionId, action_type_id: actionTypeId } + ); + if (typeof executionTime === 'number') { + this.monitoringCollection.reportGauge( + NodeLevelMetricsEnum.kibana_alerting_node_action_execution_time, + { action_id: actionId, action_type_id: actionTypeId }, + executionTime + ); + } + } + + public failure(actionId: string, actionTypeId: string) { + this.monitoringCollection.reportCounter( + NodeLevelMetricsEnum.kibana_alerting_node_action_failures, + { + action_id: actionId, + action_type_id: actionTypeId, + } + ); + } + + public timeout(actionId: string, actionTypeId: string, timeout?: string) { + const dimensions: Record = { + action_id: actionId, + action_type_id: actionTypeId, + }; + if (timeout) { + dimensions.timeout = timeout; + } + this.monitoringCollection.reportCounter( + NodeLevelMetricsEnum.kibana_alerting_node_action_timeouts, + dimensions + ); + } +} diff --git a/x-pack/plugins/actions/server/monitoring/register_node_collector.test.ts b/x-pack/plugins/actions/server/monitoring/register_node_collector.test.ts deleted file mode 100644 index 19fce333ecc3..000000000000 --- a/x-pack/plugins/actions/server/monitoring/register_node_collector.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 { monitoringCollectionMock } from '@kbn/monitoring-collection-plugin/server/mocks'; -import { Metric } from '@kbn/monitoring-collection-plugin/server'; -import { registerNodeCollector } from './register_node_collector'; -import { NodeActionsMetric } from './types'; -import { IN_MEMORY_METRICS } from '.'; -import { inMemoryMetricsMock } from './in_memory_metrics.mock'; - -describe('registerNodeCollector()', () => { - const monitoringCollection = monitoringCollectionMock.createSetup(); - const inMemoryMetrics = inMemoryMetricsMock.create(); - - it('should get in memory action metrics', async () => { - const metrics: Record> = {}; - monitoringCollection.registerMetric.mockImplementation((metric) => { - metrics[metric.type] = metric; - }); - registerNodeCollector({ monitoringCollection, inMemoryMetrics }); - - const metricTypes = Object.keys(metrics); - expect(metricTypes.length).toBe(1); - expect(metricTypes[0]).toBe('node_actions'); - - (inMemoryMetrics.getInMemoryMetric as jest.Mock).mockImplementation((metric) => { - switch (metric) { - case IN_MEMORY_METRICS.ACTION_FAILURES: - return 2; - case IN_MEMORY_METRICS.ACTION_EXECUTIONS: - return 10; - case IN_MEMORY_METRICS.ACTION_TIMEOUTS: - return 1; - } - }); - - const result = (await metrics.node_actions.fetch()) as NodeActionsMetric; - expect(result).toStrictEqual({ failures: 2, executions: 10, timeouts: 1 }); - }); -}); diff --git a/x-pack/plugins/actions/server/monitoring/register_node_collector.ts b/x-pack/plugins/actions/server/monitoring/register_node_collector.ts deleted file mode 100644 index f2e0473ed63a..000000000000 --- a/x-pack/plugins/actions/server/monitoring/register_node_collector.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 { MonitoringCollectionSetup } from '@kbn/monitoring-collection-plugin/server'; -import { InMemoryMetrics, IN_MEMORY_METRICS } from '.'; - -export function registerNodeCollector({ - monitoringCollection, - inMemoryMetrics, -}: { - monitoringCollection: MonitoringCollectionSetup; - inMemoryMetrics: InMemoryMetrics; -}) { - monitoringCollection.registerMetric({ - type: 'node_actions', - schema: { - failures: { - type: 'long', - }, - executions: { - type: 'long', - }, - timeouts: { - type: 'long', - }, - }, - fetch: async () => { - return { - failures: inMemoryMetrics.getInMemoryMetric(IN_MEMORY_METRICS.ACTION_FAILURES), - executions: inMemoryMetrics.getInMemoryMetric(IN_MEMORY_METRICS.ACTION_EXECUTIONS), - timeouts: inMemoryMetrics.getInMemoryMetric(IN_MEMORY_METRICS.ACTION_TIMEOUTS), - }; - }, - }); -} diff --git a/x-pack/plugins/actions/server/monitoring/types.ts b/x-pack/plugins/actions/server/monitoring/types.ts deleted file mode 100644 index 6ce104118757..000000000000 --- a/x-pack/plugins/actions/server/monitoring/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 { MetricResult } from '@kbn/monitoring-collection-plugin/server'; - -export type ClusterActionsMetric = MetricResult<{ - overdue: { - count: number; - delay: { - p50: number; - p99: number; - }; - }; -}>; - -export type NodeActionsMetric = MetricResult<{ - failures: number | null; - executions: number | null; - timeouts: number | null; -}>; diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 1fad2a618969..fcac5740a833 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -34,7 +34,10 @@ import { SpacesPluginStart, SpacesPluginSetup } from '@kbn/spaces-plugin/server' import { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server'; import { SecurityPluginSetup } from '@kbn/security-plugin/server'; import { IEventLogger, IEventLogService } from '@kbn/event-log-plugin/server'; -import { MonitoringCollectionSetup } from '@kbn/monitoring-collection-plugin/server'; +import { + MonitoringCollectionSetup, + MonitoringCollectionStart, +} from '@kbn/monitoring-collection-plugin/server'; import { ensureCleanupFailedExecutionsTaskScheduled, registerCleanupFailedExecutionsTaskDefinition, @@ -92,7 +95,7 @@ import { createAlertHistoryIndexTemplate } from './preconfigured_connectors/aler import { ACTIONS_FEATURE_ID, AlertHistoryEsIndexConnectorId } from '../common'; import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER } from './constants/event_log'; import { ConnectorTokenClient } from './builtin_action_types/lib/connector_token_client'; -import { InMemoryMetrics, registerClusterCollector, registerNodeCollector } from './monitoring'; +import { registerClusterLevelMetrics, NodeLevelMetrics } from './monitoring'; import { isConnectorDeprecated, ConnectorWithOptionalDeprecation, @@ -151,6 +154,7 @@ export interface ActionsPluginsStart { taskManager: TaskManagerStartContract; licensing: LicensingPluginStart; spaces?: SpacesPluginStart; + monitoringCollection?: MonitoringCollectionStart; } const includedHiddenTypes = [ @@ -174,7 +178,6 @@ export class ActionsPlugin implements Plugin core.savedObjects.getScopedClient(request); + let nodeLevelMetrics; + if (plugins.monitoringCollection) { + nodeLevelMetrics = new NodeLevelMetrics(plugins.monitoringCollection); + } + actionExecutor!.initialize({ logger, eventLogger: this.eventLogger!, @@ -452,6 +466,7 @@ export class ActionsPlugin implements Plugin { - const inMemoryMetrics = inMemoryMetricsMock.create(); const actionTypeRegistryParams: ActionTypeRegistryOpts = { licensing: licensingMock.createSetup(), taskManager: taskManagerMock.createSetup(), - taskRunnerFactory: new TaskRunnerFactory( - new ActionExecutor({ isESOCanEncrypt: true }), - inMemoryMetrics - ), + taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor({ isESOCanEncrypt: true })), actionsConfigUtils: actionsConfigMock.create(), licenseState: licenseStateMock.create(), preconfiguredActions: [], diff --git a/x-pack/plugins/alerting/common/monitoring/types.ts b/x-pack/plugins/alerting/common/monitoring/types.ts new file mode 100644 index 000000000000..86e8acf29bb8 --- /dev/null +++ b/x-pack/plugins/alerting/common/monitoring/types.ts @@ -0,0 +1,33 @@ +/* + * 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 { ValidMetricSet } from '@kbn/monitoring-collection-plugin/common/types'; + +export interface RuleMonitoringMetrics { + [key: string]: { + data: Array<{ timestamp: number; value: number }>; + }; +} + +export interface NodeLevelMetricsType { + kibana_alerting_node_rule_executions: number; + kibana_alerting_node_rule_execution_time: number; + kibana_alerting_node_rule_failures: number; + kibana_alerting_node_rule_timeouts: number; +} + +export enum NodeLevelMetricsEnum { + kibana_alerting_node_rule_executions = 'kibana_alerting_node_rule_executions', + kibana_alerting_node_rule_execution_time = 'kibana_alerting_node_rule_execution_time', + kibana_alerting_node_rule_failures = 'kibana_alerting_node_rule_failures', + kibana_alerting_node_rule_timeouts = 'kibana_alerting_node_rule_timeouts', +} + +export interface ClusterLevelMetricsType extends ValidMetricSet { + kibana_alerting_cluster_rules_overdue_count: number; + kibana_alerting_cluster_rules_overdue_delay_p50: number; + kibana_alerting_cluster_rules_overdue_delay_p99: number; +} diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index 1ff36f483a21..1b58433d3303 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -43,6 +43,7 @@ export { WriteOperations, AlertingAuthorizationEntity, } from './authorization'; +export type { NodeLevelMetricsType, ClusterLevelMetricsType } from '../common/monitoring/types'; export const plugin = (initContext: PluginInitializerContext) => new AlertingPlugin(initContext); diff --git a/x-pack/plugins/alerting/server/monitoring/register_cluster_collector.test.ts b/x-pack/plugins/alerting/server/monitoring/cluster_level_metrics.test.ts similarity index 69% rename from x-pack/plugins/alerting/server/monitoring/register_cluster_collector.test.ts rename to x-pack/plugins/alerting/server/monitoring/cluster_level_metrics.test.ts index ac90fc283ca7..d5a38fde73e8 100644 --- a/x-pack/plugins/alerting/server/monitoring/register_cluster_collector.test.ts +++ b/x-pack/plugins/alerting/server/monitoring/cluster_level_metrics.test.ts @@ -7,15 +7,16 @@ import { coreMock } from '@kbn/core/public/mocks'; import { CoreSetup } from '@kbn/core/server'; import { monitoringCollectionMock } from '@kbn/monitoring-collection-plugin/server/mocks'; -import { Metric } from '@kbn/monitoring-collection-plugin/server'; -import { registerClusterCollector } from './register_cluster_collector'; +import { MetricSet } from '@kbn/monitoring-collection-plugin/server'; +import { registerClusterLevelMetrics } from './cluster_level_metrics'; import { AlertingPluginsStart } from '../plugin'; -import { ClusterRulesMetric } from './types'; +import { ValidMetricSet } from '@kbn/monitoring-collection-plugin/common/types'; +import { ClusterLevelMetricsType } from '@kbn/actions-plugin/common/monitoring/types'; jest.useFakeTimers('modern'); jest.setSystemTime(new Date('2020-03-09').getTime()); -describe('registerClusterCollector()', () => { +describe('registerClusterLevelMetrics()', () => { const monitoringCollection = monitoringCollectionMock.createSetup(); const coreSetup = coreMock.createSetup() as unknown as CoreSetup; const taskManagerFetch = jest.fn(); @@ -34,15 +35,15 @@ describe('registerClusterCollector()', () => { }); it('should get overdue rules', async () => { - const metrics: Record> = {}; - monitoringCollection.registerMetric.mockImplementation((metric) => { - metrics[metric.type] = metric; + const metrics: Record> = {}; + monitoringCollection.registerMetricSet.mockImplementation((set) => { + metrics[set.id] = set; }); - registerClusterCollector({ monitoringCollection, core: coreSetup }); + registerClusterLevelMetrics({ monitoringCollection, core: coreSetup }); const metricTypes = Object.keys(metrics); expect(metricTypes.length).toBe(1); - expect(metricTypes[0]).toBe('cluster_rules'); + expect(metricTypes[0]).toBe('kibana_alerting_cluster_rules'); const nowInMs = +new Date(); const docs = [ @@ -55,10 +56,10 @@ describe('registerClusterCollector()', () => { ]; taskManagerFetch.mockImplementation(async () => ({ docs })); - const result = (await metrics.cluster_rules.fetch()) as ClusterRulesMetric; - expect(result.overdue.count).toBe(docs.length); - expect(result.overdue.delay.p50).toBe(1000); - expect(result.overdue.delay.p99).toBe(1000); + const result = (await metrics.kibana_alerting_cluster_rules.fetch()) as ClusterLevelMetricsType; + expect(result.kibana_alerting_cluster_rules_overdue_count).toBe(docs.length); + expect(result.kibana_alerting_cluster_rules_overdue_delay_p50).toBe(1000); + expect(result.kibana_alerting_cluster_rules_overdue_delay_p99).toBe(1000); expect(taskManagerFetch).toHaveBeenCalledWith({ query: { bool: { @@ -130,15 +131,15 @@ describe('registerClusterCollector()', () => { }); it('should calculate accurate p50 and p99', async () => { - const metrics: Record> = {}; - monitoringCollection.registerMetric.mockImplementation((metric) => { - metrics[metric.type] = metric; + const metrics: Record> = {}; + monitoringCollection.registerMetricSet.mockImplementation((set) => { + metrics[set.id] = set; }); - registerClusterCollector({ monitoringCollection, core: coreSetup }); + registerClusterLevelMetrics({ monitoringCollection, core: coreSetup }); const metricTypes = Object.keys(metrics); expect(metricTypes.length).toBe(1); - expect(metricTypes[0]).toBe('cluster_rules'); + expect(metricTypes[0]).toBe('kibana_alerting_cluster_rules'); const nowInMs = +new Date(); const docs = [ @@ -150,9 +151,9 @@ describe('registerClusterCollector()', () => { ]; taskManagerFetch.mockImplementation(async () => ({ docs })); - const result = (await metrics.cluster_rules.fetch()) as ClusterRulesMetric; - expect(result.overdue.count).toBe(docs.length); - expect(result.overdue.delay.p50).toBe(3000); - expect(result.overdue.delay.p99).toBe(40000); + const result = (await metrics.kibana_alerting_cluster_rules.fetch()) as ClusterLevelMetricsType; + expect(result.kibana_alerting_cluster_rules_overdue_count).toBe(docs.length); + expect(result.kibana_alerting_cluster_rules_overdue_delay_p50).toBe(3000); + expect(result.kibana_alerting_cluster_rules_overdue_delay_p99).toBe(40000); }); }); diff --git a/x-pack/plugins/alerting/server/monitoring/register_cluster_collector.ts b/x-pack/plugins/alerting/server/monitoring/cluster_level_metrics.ts similarity index 63% rename from x-pack/plugins/alerting/server/monitoring/register_cluster_collector.ts rename to x-pack/plugins/alerting/server/monitoring/cluster_level_metrics.ts index f1764d922069..4604a9d82b69 100644 --- a/x-pack/plugins/alerting/server/monitoring/register_cluster_collector.ts +++ b/x-pack/plugins/alerting/server/monitoring/cluster_level_metrics.ts @@ -12,32 +12,17 @@ import { } from '@kbn/task-manager-plugin/server'; import { CoreSetup } from '@kbn/core/server'; import { AlertingPluginsStart } from '../plugin'; -import { ClusterRulesMetric } from './types'; +import { ClusterLevelMetricsType } from '../../common/monitoring/types'; -export function registerClusterCollector({ +export function registerClusterLevelMetrics({ monitoringCollection, core, }: { monitoringCollection: MonitoringCollectionSetup; core: CoreSetup; }) { - monitoringCollection.registerMetric({ - type: 'cluster_rules', - schema: { - overdue: { - count: { - type: 'long', - }, - delay: { - p50: { - type: 'long', - }, - p99: { - type: 'long', - }, - }, - }, - }, + monitoringCollection.registerMetricSet({ + id: `kibana_alerting_cluster_rules`, fetch: async () => { const [_, pluginStart] = await core.getStartServices(); const now = +new Date(); @@ -65,26 +50,13 @@ export function registerClusterCollector({ const overdueTasksDelay = overdueTasks.map( (overdueTask) => now - +new Date(overdueTask.runAt || overdueTask.retryAt) ); - - const metrics: ClusterRulesMetric = { - overdue: { - count: overdueTasks.length, - delay: { - p50: stats.percentile(overdueTasksDelay, 0.5), - p99: stats.percentile(overdueTasksDelay, 0.99), - }, - }, + const p50 = stats.percentile(overdueTasksDelay, 0.5); + const p99 = stats.percentile(overdueTasksDelay, 0.99); + return { + kibana_alerting_cluster_rules_overdue_count: overdueTasks.length, + kibana_alerting_cluster_rules_overdue_delay_p50: isNaN(p50) ? 0 : p50, + kibana_alerting_cluster_rules_overdue_delay_p99: isNaN(p99) ? 0 : p99, }; - - if (isNaN(metrics.overdue.delay.p50)) { - metrics.overdue.delay.p50 = 0; - } - - if (isNaN(metrics.overdue.delay.p99)) { - metrics.overdue.delay.p99 = 0; - } - - return metrics; }, }); } diff --git a/x-pack/plugins/alerting/server/monitoring/in_memory_metrics.test.ts b/x-pack/plugins/alerting/server/monitoring/in_memory_metrics.test.ts deleted file mode 100644 index 6a139df0aa84..000000000000 --- a/x-pack/plugins/alerting/server/monitoring/in_memory_metrics.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 { InMemoryMetrics, IN_MEMORY_METRICS } from '.'; -import { loggingSystemMock } from '@kbn/core/server/mocks'; - -describe('inMemoryMetrics', () => { - const logger = loggingSystemMock.createLogger(); - const inMemoryMetrics = new InMemoryMetrics(logger); - - beforeEach(() => { - const all = inMemoryMetrics.getAllInMemoryMetrics(); - for (const key of Object.keys(all)) { - all[key as IN_MEMORY_METRICS] = 0; - } - }); - - it('should increment', () => { - inMemoryMetrics.increment(IN_MEMORY_METRICS.RULE_EXECUTIONS); - expect(inMemoryMetrics.getInMemoryMetric(IN_MEMORY_METRICS.RULE_EXECUTIONS)).toBe(1); - }); - - it('should set to null if incrementing will set over the max integer', () => { - const all = inMemoryMetrics.getAllInMemoryMetrics(); - all[IN_MEMORY_METRICS.RULE_EXECUTIONS] = Number.MAX_SAFE_INTEGER; - inMemoryMetrics.increment(IN_MEMORY_METRICS.RULE_EXECUTIONS); - expect(inMemoryMetrics.getInMemoryMetric(IN_MEMORY_METRICS.RULE_EXECUTIONS)).toBe(null); - expect(logger.info).toHaveBeenCalledWith( - `Metric ${IN_MEMORY_METRICS.RULE_EXECUTIONS} has reached the max safe integer value and will no longer be used, skipping increment.` - ); - inMemoryMetrics.increment(IN_MEMORY_METRICS.RULE_EXECUTIONS); - expect(inMemoryMetrics.getInMemoryMetric(IN_MEMORY_METRICS.RULE_EXECUTIONS)).toBe(null); - expect(logger.info).toHaveBeenCalledWith( - `Metric ${IN_MEMORY_METRICS.RULE_EXECUTIONS} is null because the counter ran over the max safe integer value, skipping increment.` - ); - }); -}); diff --git a/x-pack/plugins/alerting/server/monitoring/in_memory_metrics.ts b/x-pack/plugins/alerting/server/monitoring/in_memory_metrics.ts deleted file mode 100644 index a2d0425da142..000000000000 --- a/x-pack/plugins/alerting/server/monitoring/in_memory_metrics.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 { Logger } from '@kbn/logging'; - -export enum IN_MEMORY_METRICS { - RULE_EXECUTIONS = 'ruleExecutions', - RULE_FAILURES = 'ruleFailures', - RULE_TIMEOUTS = 'ruleTimeouts', -} - -export class InMemoryMetrics { - private logger: Logger; - private inMemoryMetrics: Record = { - [IN_MEMORY_METRICS.RULE_EXECUTIONS]: 0, - [IN_MEMORY_METRICS.RULE_FAILURES]: 0, - [IN_MEMORY_METRICS.RULE_TIMEOUTS]: 0, - }; - - constructor(logger: Logger) { - this.logger = logger; - } - - public increment(metric: IN_MEMORY_METRICS) { - if (this.inMemoryMetrics[metric] === null) { - this.logger.info( - `Metric ${metric} is null because the counter ran over the max safe integer value, skipping increment.` - ); - return; - } - - if ((this.inMemoryMetrics[metric] as number) >= Number.MAX_SAFE_INTEGER) { - this.inMemoryMetrics[metric] = null; - this.logger.info( - `Metric ${metric} has reached the max safe integer value and will no longer be used, skipping increment.` - ); - } else { - (this.inMemoryMetrics[metric] as number)++; - } - } - - public getInMemoryMetric(metric: IN_MEMORY_METRICS) { - return this.inMemoryMetrics[metric]; - } - - public getAllInMemoryMetrics() { - return this.inMemoryMetrics; - } -} diff --git a/x-pack/plugins/alerting/server/monitoring/index.ts b/x-pack/plugins/alerting/server/monitoring/index.ts index 5f298456554f..2a69a7f902ed 100644 --- a/x-pack/plugins/alerting/server/monitoring/index.ts +++ b/x-pack/plugins/alerting/server/monitoring/index.ts @@ -4,7 +4,5 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -export { registerNodeCollector } from './register_node_collector'; -export { registerClusterCollector } from './register_cluster_collector'; -export * from './types'; -export * from './in_memory_metrics'; +export * from './node_level_metrics'; +export * from './cluster_level_metrics'; diff --git a/x-pack/plugins/alerting/server/monitoring/in_memory_metrics.mock.ts b/x-pack/plugins/alerting/server/monitoring/node_level_metrics.mock.ts similarity index 60% rename from x-pack/plugins/alerting/server/monitoring/in_memory_metrics.mock.ts rename to x-pack/plugins/alerting/server/monitoring/node_level_metrics.mock.ts index 4b613753d616..017bc903a2ef 100644 --- a/x-pack/plugins/alerting/server/monitoring/in_memory_metrics.mock.ts +++ b/x-pack/plugins/alerting/server/monitoring/node_level_metrics.mock.ts @@ -4,17 +4,16 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -function createInMemoryMetricsMock() { +function createNodeLevelMetricsMock() { return jest.fn().mockImplementation(() => { return { - increment: jest.fn(), - getInMemoryMetric: jest.fn(), - getAllInMemoryMetrics: jest.fn(), + execution: jest.fn(), + failure: jest.fn(), + timeout: jest.fn(), }; }); } -export const inMemoryMetricsMock = { - create: createInMemoryMetricsMock(), +export const nodeLevelMetricsMock = { + create: createNodeLevelMetricsMock(), }; diff --git a/x-pack/plugins/alerting/server/monitoring/node_level_metrics.test.ts b/x-pack/plugins/alerting/server/monitoring/node_level_metrics.test.ts new file mode 100644 index 000000000000..811a836dd703 --- /dev/null +++ b/x-pack/plugins/alerting/server/monitoring/node_level_metrics.test.ts @@ -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 { monitoringCollectionMock } from '@kbn/monitoring-collection-plugin/server/mocks'; +import { RuleExecutionStatusErrorReasons } from '../types'; +import { NodeLevelMetrics } from './node_level_metrics'; + +describe('NodeLevelMetrics', () => { + const monitoringCollection = monitoringCollectionMock.createStart(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('execution', () => { + it('should register a counter when called', () => { + const metrics = new NodeLevelMetrics(monitoringCollection); + metrics.execution('ruleA'); + expect(monitoringCollection.reportCounter).toHaveBeenCalledTimes(1); + expect(monitoringCollection.reportCounter).toHaveBeenCalledWith( + 'kibana_alerting_node_rule_executions', + { rule_id: 'ruleA' } + ); + }); + + it('should report a gauge when provided with an execution time', () => { + const executionTime = 1000; + const metrics = new NodeLevelMetrics(monitoringCollection); + metrics.execution('ruleA', executionTime); + expect(monitoringCollection.reportCounter).toHaveBeenCalledTimes(1); + expect(monitoringCollection.reportCounter).toHaveBeenCalledWith( + 'kibana_alerting_node_rule_executions', + { rule_id: 'ruleA' } + ); + expect(monitoringCollection.reportGauge).toHaveBeenCalledTimes(1); + expect(monitoringCollection.reportGauge).toHaveBeenCalledWith( + 'kibana_alerting_node_rule_execution_time', + { rule_id: 'ruleA' }, + executionTime + ); + }); + }); + + describe('failure', () => { + it('should register a counter when called', () => { + const metrics = new NodeLevelMetrics(monitoringCollection); + metrics.failure('ruleA', RuleExecutionStatusErrorReasons.Read); + expect(monitoringCollection.reportCounter).toHaveBeenCalledTimes(1); + expect(monitoringCollection.reportCounter).toHaveBeenCalledWith( + `kibana_alerting_node_rule_failures`, + { rule_id: 'ruleA', failure_reason: RuleExecutionStatusErrorReasons.Read } + ); + }); + }); + + describe('timeout', () => { + it('should register a counter when called', () => { + const metrics = new NodeLevelMetrics(monitoringCollection); + metrics.timeout('ruleA'); + expect(monitoringCollection.reportCounter).toHaveBeenCalledTimes(1); + expect(monitoringCollection.reportCounter).toHaveBeenCalledWith( + `kibana_alerting_node_rule_timeouts`, + { rule_id: 'ruleA' } + ); + }); + + it('should report the timeout if provided', () => { + const timeout = '1000'; + const metrics = new NodeLevelMetrics(monitoringCollection); + metrics.timeout('ruleA', timeout); + expect(monitoringCollection.reportCounter).toHaveBeenCalledTimes(1); + expect(monitoringCollection.reportCounter).toHaveBeenCalledWith( + `kibana_alerting_node_rule_timeouts`, + { rule_id: 'ruleA', timeout } + ); + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/monitoring/node_level_metrics.ts b/x-pack/plugins/alerting/server/monitoring/node_level_metrics.ts new file mode 100644 index 000000000000..301092585709 --- /dev/null +++ b/x-pack/plugins/alerting/server/monitoring/node_level_metrics.ts @@ -0,0 +1,53 @@ +/* + * 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 { MonitoringCollectionStart } from '@kbn/monitoring-collection-plugin/server'; +import { RuleExecutionStatusErrorReasons } from '../types'; +import { NodeLevelMetricsEnum } from '../../common/monitoring/types'; + +export class NodeLevelMetrics { + private monitoringCollection: MonitoringCollectionStart; + + constructor(monitoringCollection: MonitoringCollectionStart) { + this.monitoringCollection = monitoringCollection; + } + + public execution(ruleId: string, executionTime?: number) { + this.monitoringCollection.reportCounter( + NodeLevelMetricsEnum.kibana_alerting_node_rule_executions, + { rule_id: ruleId } + ); + if (typeof executionTime === 'number') { + this.monitoringCollection.reportGauge( + NodeLevelMetricsEnum.kibana_alerting_node_rule_execution_time, + { rule_id: ruleId }, + executionTime + ); + } + } + + public failure(ruleId: string, reason: RuleExecutionStatusErrorReasons) { + this.monitoringCollection.reportCounter( + NodeLevelMetricsEnum.kibana_alerting_node_rule_failures, + { + rule_id: ruleId, + failure_reason: reason, + } + ); + } + + public timeout(ruleId: string, timeout?: string) { + const dimensions: Record = { rule_id: ruleId }; + if (timeout) { + dimensions.timeout = timeout; + } + this.monitoringCollection.reportCounter( + NodeLevelMetricsEnum.kibana_alerting_node_rule_timeouts, + dimensions + ); + } +} diff --git a/x-pack/plugins/alerting/server/monitoring/register_node_collector.test.ts b/x-pack/plugins/alerting/server/monitoring/register_node_collector.test.ts deleted file mode 100644 index a17cf5b47020..000000000000 --- a/x-pack/plugins/alerting/server/monitoring/register_node_collector.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 { monitoringCollectionMock } from '@kbn/monitoring-collection-plugin/server/mocks'; -import { loggingSystemMock } from '@kbn/core/server/mocks'; -import { Metric } from '@kbn/monitoring-collection-plugin/server'; -import { registerNodeCollector } from './register_node_collector'; -import { NodeRulesMetric } from './types'; -import { InMemoryMetrics, IN_MEMORY_METRICS } from '.'; - -jest.mock('./in_memory_metrics'); - -describe('registerNodeCollector()', () => { - const monitoringCollection = monitoringCollectionMock.createSetup(); - const logger = loggingSystemMock.createLogger(); - const inMemoryMetrics = new InMemoryMetrics(logger); - - afterEach(() => { - (inMemoryMetrics.getInMemoryMetric as jest.Mock).mockClear(); - }); - - it('should get in memory rule metrics', async () => { - const metrics: Record> = {}; - monitoringCollection.registerMetric.mockImplementation((metric) => { - metrics[metric.type] = metric; - }); - registerNodeCollector({ monitoringCollection, inMemoryMetrics }); - - const metricTypes = Object.keys(metrics); - expect(metricTypes.length).toBe(1); - expect(metricTypes[0]).toBe('node_rules'); - - (inMemoryMetrics.getInMemoryMetric as jest.Mock).mockImplementation((metric) => { - switch (metric) { - case IN_MEMORY_METRICS.RULE_FAILURES: - return 2; - case IN_MEMORY_METRICS.RULE_EXECUTIONS: - return 10; - case IN_MEMORY_METRICS.RULE_TIMEOUTS: - return 1; - } - }); - - const result = (await metrics.node_rules.fetch()) as NodeRulesMetric; - expect(result).toStrictEqual({ failures: 2, executions: 10, timeouts: 1 }); - }); -}); diff --git a/x-pack/plugins/alerting/server/monitoring/register_node_collector.ts b/x-pack/plugins/alerting/server/monitoring/register_node_collector.ts deleted file mode 100644 index 839787c6e78f..000000000000 --- a/x-pack/plugins/alerting/server/monitoring/register_node_collector.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 { MonitoringCollectionSetup } from '@kbn/monitoring-collection-plugin/server'; -import { IN_MEMORY_METRICS } from '.'; -import { InMemoryMetrics } from './in_memory_metrics'; - -export function registerNodeCollector({ - monitoringCollection, - inMemoryMetrics, -}: { - monitoringCollection: MonitoringCollectionSetup; - inMemoryMetrics: InMemoryMetrics; -}) { - monitoringCollection.registerMetric({ - type: 'node_rules', - schema: { - failures: { - type: 'long', - }, - executions: { - type: 'long', - }, - timeouts: { - type: 'long', - }, - }, - fetch: async () => { - return { - failures: inMemoryMetrics.getInMemoryMetric(IN_MEMORY_METRICS.RULE_FAILURES), - executions: inMemoryMetrics.getInMemoryMetric(IN_MEMORY_METRICS.RULE_EXECUTIONS), - timeouts: inMemoryMetrics.getInMemoryMetric(IN_MEMORY_METRICS.RULE_TIMEOUTS), - }; - }, - }); -} diff --git a/x-pack/plugins/alerting/server/monitoring/types.ts b/x-pack/plugins/alerting/server/monitoring/types.ts deleted file mode 100644 index 4b159dd8349a..000000000000 --- a/x-pack/plugins/alerting/server/monitoring/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 { MetricResult } from '@kbn/monitoring-collection-plugin/server'; - -export type ClusterRulesMetric = MetricResult<{ - overdue: { - count: number; - delay: { - p50: number; - p99: number; - }; - }; -}>; - -export type NodeRulesMetric = MetricResult<{ - failures: number | null; - executions: number | null; - timeouts: number | null; -}>; diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index cfd2701e1f88..53efb0677eff 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -47,7 +47,10 @@ import { } from '@kbn/event-log-plugin/server'; import { PluginStartContract as FeaturesPluginStart } from '@kbn/features-plugin/server'; import { PluginStart as DataPluginStart } from '@kbn/data-plugin/server'; -import { MonitoringCollectionSetup } from '@kbn/monitoring-collection-plugin/server'; +import { + MonitoringCollectionSetup, + MonitoringCollectionStart, +} from '@kbn/monitoring-collection-plugin/server'; import { RulesClient } from './rules_client'; import { RuleTypeRegistry } from './rule_type_registry'; import { TaskRunnerFactory } from './task_runner'; @@ -76,7 +79,7 @@ import { getHealth } from './health/get_health'; import { AlertingAuthorizationClientFactory } from './alerting_authorization_client_factory'; import { AlertingAuthorization } from './authorization'; import { getSecurityHealth, SecurityHealth } from './lib/get_security_health'; -import { registerNodeCollector, registerClusterCollector, InMemoryMetrics } from './monitoring'; +import { registerClusterLevelMetrics, NodeLevelMetrics } from './monitoring'; import { getRuleTaskTimeout } from './lib/get_rule_task_timeout'; import { getActionsConfigMap } from './lib/get_actions_config_map'; @@ -152,6 +155,7 @@ export interface AlertingPluginsStart { spaces?: SpacesPluginStart; security?: SecurityPluginStart; data: DataPluginStart; + monitoringCollection?: MonitoringCollectionStart; } export class AlertingPlugin { @@ -170,7 +174,7 @@ export class AlertingPlugin { private eventLogger?: IEventLogger; private kibanaBaseUrl: string | undefined; private usageCounter: UsageCounter | undefined; - private inMemoryMetrics: InMemoryMetrics; + private nodeLevelMetrics?: NodeLevelMetrics; constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.get(); @@ -180,7 +184,6 @@ export class AlertingPlugin { this.alertingAuthorizationClientFactory = new AlertingAuthorizationClientFactory(); this.telemetryLogger = initializerContext.logger.get('usage'); this.kibanaVersion = initializerContext.env.packageInfo.version; - this.inMemoryMetrics = new InMemoryMetrics(initializerContext.logger.get('in_memory_metrics')); } public setup( @@ -224,7 +227,6 @@ export class AlertingPlugin { licenseState: this.licenseState, licensing: plugins.licensing, minimumScheduleInterval: this.config.rules.minimumScheduleInterval, - inMemoryMetrics: this.inMemoryMetrics, }); this.ruleTypeRegistry = ruleTypeRegistry; @@ -262,6 +264,13 @@ export class AlertingPlugin { this.config ); + if (plugins.monitoringCollection) { + registerClusterLevelMetrics({ + monitoringCollection: plugins.monitoringCollection, + core, + }); + } + const serviceStatus$ = new BehaviorSubject({ level: ServiceStatusLevels.available, summary: 'Alerting is (probably) ready', @@ -275,17 +284,6 @@ export class AlertingPlugin { this.createRouteHandlerContext(core) ); - if (plugins.monitoringCollection) { - registerNodeCollector({ - monitoringCollection: plugins.monitoringCollection, - inMemoryMetrics: this.inMemoryMetrics, - }); - registerClusterCollector({ - monitoringCollection: plugins.monitoringCollection, - core, - }); - } - // Routes const router = core.http.createRouter(); // Register routes @@ -294,6 +292,7 @@ export class AlertingPlugin { licenseState: this.licenseState, usageCounter: this.usageCounter, encryptedSavedObjects: plugins.encryptedSavedObjects, + monitoringCollection: plugins.monitoringCollection, }); return { @@ -363,6 +362,10 @@ export class AlertingPlugin { includedHiddenTypes: ['alert'], }); + if (plugins.monitoringCollection) { + this.nodeLevelMetrics = new NodeLevelMetrics(plugins.monitoringCollection); + } + const spaceIdToNamespace = (spaceId?: string) => { return plugins.spaces && spaceId ? plugins.spaces.spacesService.spaceIdToNamespace(spaceId) @@ -435,6 +438,7 @@ export class AlertingPlugin { cancelAlertsOnRuleTimeout: this.config.cancelAlertsOnRuleTimeout, actionsConfigMap: getActionsConfigMap(this.config.rules.run.actions), usageCounter: this.usageCounter, + nodeLevelMetrics: this.nodeLevelMetrics, }); this.eventLogService!.registerSavedObjectProvider('alert', (request) => { diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts index 392ec591d960..f73781a5c305 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -8,6 +8,7 @@ import { IRouter } from '@kbn/core/server'; import { UsageCounter } from '@kbn/usage-collection-plugin/server'; import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server'; +import { MonitoringCollectionSetup } from '@kbn/monitoring-collection-plugin/server'; import { ILicenseState } from '../lib'; import { defineLegacyRoutes } from './legacy'; import { AlertingRequestHandlerContext } from '../types'; @@ -32,11 +33,13 @@ import { unmuteAlertRoute } from './unmute_alert'; import { updateRuleApiKeyRoute } from './update_rule_api_key'; import { snoozeRuleRoute } from './snooze_rule'; import { unsnoozeRuleRoute } from './unsnooze_rule'; +import { defineMonitoringRoutes } from './monitoring'; export interface RouteOptions { router: IRouter; licenseState: ILicenseState; encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; + monitoringCollection?: MonitoringCollectionSetup; usageCounter?: UsageCounter; } @@ -67,4 +70,5 @@ export function defineRoutes(opts: RouteOptions) { updateRuleApiKeyRoute(router, licenseState); snoozeRuleRoute(router, licenseState); unsnoozeRuleRoute(router, licenseState); + defineMonitoringRoutes(opts); } diff --git a/x-pack/plugins/alerting/server/routes/monitoring/index.ts b/x-pack/plugins/alerting/server/routes/monitoring/index.ts new file mode 100644 index 000000000000..1773d65571ff --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/monitoring/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { RouteOptions } from '..'; +import { monitoringRuleAggregateRoute } from './rule_aggregate'; + +export function defineMonitoringRoutes(opts: RouteOptions) { + const { router, licenseState, monitoringCollection } = opts; + + monitoringRuleAggregateRoute(router, licenseState, monitoringCollection); +} diff --git a/x-pack/plugins/alerting/server/routes/monitoring/rule_aggregate.ts b/x-pack/plugins/alerting/server/routes/monitoring/rule_aggregate.ts new file mode 100644 index 000000000000..d95219b2052b --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/monitoring/rule_aggregate.ts @@ -0,0 +1,133 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { IRouter } from '@kbn/core/server'; +import { MonitoringCollectionSetup } from '@kbn/monitoring-collection-plugin/server'; +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { ILicenseState } from '../../lib'; +import { verifyAccessAndContext } from '../lib'; +import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types'; + +const paramSchema = schema.object({ + id: schema.string(), +}); + +export const monitoringRuleAggregateRoute = ( + router: IRouter, + licenseState: ILicenseState, + monitoringCollection?: MonitoringCollectionSetup +) => { + router.get( + { + path: `${INTERNAL_BASE_ALERTING_API_PATH}/rule/{id}/monitoring/aggregate`, + validate: { + params: paramSchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const { id } = req.params; + try { + const query: estypes.QueryDslQueryContainer = { + bool: { + must: [ + { + term: { + 'data_stream.dataset': { + value: 'apm.app.kibana', + }, + }, + }, + { + term: { + 'labels.rule_id': { + value: id, + }, + }, + }, + { + range: { + '@timestamp': { + gte: 'now-15m', + }, + }, + }, + ], + }, + }; + const aggs: Record = { + time_buckets: { + date_histogram: { + field: '@timestamp', + fixed_interval: '30s', + }, + aggs: { + executions_raw: { + max: { + field: 'kibana_alerting_node_rule_executions', + }, + }, + executions: { + derivative: { + buckets_path: 'executions_raw', + // @ts-ignore + unit: '1s', + }, + }, + + execution_time_raw: { + max: { + field: 'kibana_alerting_node_rule_execution_time', + }, + }, + execution_time: { + derivative: { + buckets_path: 'execution_time_raw', + // @ts-ignore + unit: '1s', + }, + }, + }, + }, + }; + const results = await monitoringCollection?.aggregateMonitoringData(query, aggs); + const buckets = ( + results?.time_buckets as { + buckets: Array<{ + executions: { value?: number }; + execution_time: { value?: number }; + key: string; + }>; + } + )?.buckets; + + return res.ok({ + body: buckets?.reduce< + Record }> + >( + (accum, bucket) => { + const timestamp = bucket.key; + accum.execution.data.push({ timestamp, value: bucket.executions?.value ?? 0 }); + accum.execution_time.data.push({ + timestamp, + value: bucket.execution_time?.value ?? 0, + }); + return accum; + }, + { + execution: { data: [] }, + execution_time: { data: [] }, + } + ), + }); + } catch (error) { + return res.badRequest({ body: error }); + } + }) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/rule_type_registry.test.ts b/x-pack/plugins/alerting/server/rule_type_registry.test.ts index 9a6b2232c47d..8f738710f63d 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.test.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.test.ts @@ -13,7 +13,6 @@ import { ILicenseState } from './lib/license_state'; import { licenseStateMock } from './lib/license_state.mock'; import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; import { loggingSystemMock } from '@kbn/core/server/mocks'; -import { inMemoryMetricsMock } from './monitoring/in_memory_metrics.mock'; const logger = loggingSystemMock.create().get(); let mockedLicenseState: jest.Mocked; @@ -21,8 +20,6 @@ let ruleTypeRegistryParams: ConstructorOptions; const taskManager = taskManagerMock.createSetup(); -const inMemoryMetrics = inMemoryMetricsMock.create(); - beforeEach(() => { jest.resetAllMocks(); mockedLicenseState = licenseStateMock.create(); @@ -33,7 +30,6 @@ beforeEach(() => { licenseState: mockedLicenseState, licensing: licensingMock.createSetup(), minimumScheduleInterval: { value: '1m', enforce: false }, - inMemoryMetrics, }; }); diff --git a/x-pack/plugins/alerting/server/rule_type_registry.ts b/x-pack/plugins/alerting/server/rule_type_registry.ts index 338450746781..89327cc66029 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.ts @@ -31,7 +31,6 @@ import { } from '../common'; import { ILicenseState } from './lib/license_state'; import { getRuleTypeFeatureUsageName } from './lib/get_rule_type_feature_usage_name'; -import { InMemoryMetrics } from './monitoring'; import { AlertingRulesConfig } from '.'; export interface ConstructorOptions { @@ -41,7 +40,6 @@ export interface ConstructorOptions { licenseState: ILicenseState; licensing: LicensingPluginSetup; minimumScheduleInterval: AlertingRulesConfig['minimumScheduleInterval']; - inMemoryMetrics: InMemoryMetrics; } export interface RegistryRuleType @@ -138,7 +136,6 @@ export class RuleTypeRegistry { private readonly licenseState: ILicenseState; private readonly minimumScheduleInterval: AlertingRulesConfig['minimumScheduleInterval']; private readonly licensing: LicensingPluginSetup; - private readonly inMemoryMetrics: InMemoryMetrics; constructor({ logger, @@ -147,7 +144,6 @@ export class RuleTypeRegistry { licenseState, licensing, minimumScheduleInterval, - inMemoryMetrics, }: ConstructorOptions) { this.logger = logger; this.taskManager = taskManager; @@ -155,7 +151,6 @@ export class RuleTypeRegistry { this.licenseState = licenseState; this.licensing = licensing; this.minimumScheduleInterval = minimumScheduleInterval; - this.inMemoryMetrics = inMemoryMetrics; } public has(id: string) { @@ -274,7 +269,7 @@ export class RuleTypeRegistry { InstanceContext, ActionGroupIds, RecoveryActionGroupId | RecoveredActionGroupId - >(normalizedRuleType, context, this.inMemoryMetrics), + >(normalizedRuleType, context), }, }); // No need to notify usage on basic alert types diff --git a/x-pack/plugins/alerting/server/saved_objects/is_rule_exportable.test.ts b/x-pack/plugins/alerting/server/saved_objects/is_rule_exportable.test.ts index 0f2677dc4975..d370225b9e12 100644 --- a/x-pack/plugins/alerting/server/saved_objects/is_rule_exportable.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/is_rule_exportable.test.ts @@ -13,14 +13,12 @@ import { ILicenseState } from '../lib/license_state'; import { licenseStateMock } from '../lib/license_state.mock'; import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; import { isRuleExportable } from './is_rule_exportable'; -import { inMemoryMetricsMock } from '../monitoring/in_memory_metrics.mock'; import { loggingSystemMock } from '@kbn/core/server/mocks'; let ruleTypeRegistryParams: ConstructorOptions; let logger: MockedLogger; let mockedLicenseState: jest.Mocked; const taskManager = taskManagerMock.createSetup(); -const inMemoryMetrics = inMemoryMetricsMock.create(); beforeEach(() => { jest.resetAllMocks(); @@ -33,7 +31,6 @@ beforeEach(() => { licenseState: mockedLicenseState, licensing: licensingMock.createSetup(), minimumScheduleInterval: { value: '1m', enforce: false }, - inMemoryMetrics, }; }); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 111c0a768950..6ff57d6a9e4d 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -41,8 +41,8 @@ import { IEventLogger } from '@kbn/event-log-plugin/server'; import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import { omit } from 'lodash'; import { ruleTypeRegistryMock } from '../rule_type_registry.mock'; +import { nodeLevelMetricsMock } from '../monitoring/node_level_metrics.mock'; import { ExecuteOptions } from '@kbn/actions-plugin/server/create_execute_function'; -import { inMemoryMetricsMock } from '../monitoring/in_memory_metrics.mock'; import moment from 'moment'; import { generateActionSO, @@ -68,7 +68,6 @@ import { DATE_9999, } from './fixtures'; import { EVENT_LOG_ACTIONS } from '../plugin'; -import { IN_MEMORY_METRICS } from '../monitoring'; import { translations } from '../constants/translations'; import { dataPluginMock } from '@kbn/data-plugin/server/mocks'; @@ -104,7 +103,7 @@ describe('Task Runner', () => { const elasticsearchService = elasticsearchServiceMock.createInternalStart(); const dataPlugin = dataPluginMock.createStartContract(); const uiSettingsService = uiSettingsServiceMock.createStartContract(); - const inMemoryMetrics = inMemoryMetricsMock.create(); + const mockNodeLevelMetrics = nodeLevelMetricsMock.create(); type TaskRunnerFactoryInitializerParamsType = jest.Mocked & { actionsPlugin: jest.Mocked; @@ -134,6 +133,7 @@ describe('Task Runner', () => { maxEphemeralActionsPerRule: 10, cancelAlertsOnRuleTimeout: true, usageCounter: mockUsageCounter, + nodeLevelMetrics: mockNodeLevelMetrics, actionsConfigMap: { default: { max: 10000, @@ -198,8 +198,7 @@ describe('Task Runner', () => { previousStartedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), }, }, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); @@ -306,8 +305,7 @@ describe('Task Runner', () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, - customTaskRunnerFactoryInitializerParams, - inMemoryMetrics + customTaskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); @@ -414,8 +412,7 @@ describe('Task Runner', () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue({ ...mockedRuleTypeSavedObject, @@ -533,8 +530,7 @@ describe('Task Runner', () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue({ ...mockedRuleTypeSavedObject, @@ -585,8 +581,7 @@ describe('Task Runner', () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, - customTaskRunnerFactoryInitializerParams, - inMemoryMetrics + customTaskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue({ ...mockedRuleTypeSavedObject, @@ -663,8 +658,7 @@ describe('Task Runner', () => { }, }, }, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue({ ...mockedRuleTypeSavedObject, @@ -706,8 +700,7 @@ describe('Task Runner', () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, - customTaskRunnerFactoryInitializerParams, - inMemoryMetrics + customTaskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue({ ...mockedRuleTypeSavedObject, @@ -762,8 +755,7 @@ describe('Task Runner', () => { }, }, }, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue({ ...mockedRuleTypeSavedObject, @@ -849,8 +841,7 @@ describe('Task Runner', () => { }, }, }, - customTaskRunnerFactoryInitializerParams, - inMemoryMetrics + customTaskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue({ ...mockedRuleTypeSavedObject, @@ -924,8 +915,7 @@ describe('Task Runner', () => { }, }, }, - customTaskRunnerFactoryInitializerParams, - inMemoryMetrics + customTaskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue({ ...mockedRuleTypeSavedObject, @@ -981,8 +971,7 @@ describe('Task Runner', () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, - customTaskRunnerFactoryInitializerParams, - inMemoryMetrics + customTaskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT); @@ -1123,8 +1112,7 @@ describe('Task Runner', () => { }, }, }, - customTaskRunnerFactoryInitializerParams, - inMemoryMetrics + customTaskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); @@ -1274,8 +1262,7 @@ describe('Task Runner', () => { alertId, }, }, - customTaskRunnerFactoryInitializerParams, - inMemoryMetrics + customTaskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); @@ -1356,8 +1343,7 @@ describe('Task Runner', () => { }, }, }, - customTaskRunnerFactoryInitializerParams, - inMemoryMetrics + customTaskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue({ ...mockedRuleTypeSavedObject, @@ -1433,8 +1419,7 @@ describe('Task Runner', () => { }, }, }, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); @@ -1512,8 +1497,7 @@ describe('Task Runner', () => { spaceId: 'foo', }, }, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT); @@ -1529,8 +1513,7 @@ describe('Task Runner', () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT); @@ -1557,8 +1540,7 @@ describe('Task Runner', () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ @@ -1587,8 +1569,7 @@ describe('Task Runner', () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValueOnce(mockedRuleTypeSavedObject); @@ -1623,8 +1604,7 @@ describe('Task Runner', () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); @@ -1666,8 +1646,7 @@ describe('Task Runner', () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); @@ -1709,8 +1688,7 @@ describe('Task Runner', () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); @@ -1753,8 +1731,7 @@ describe('Task Runner', () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); @@ -1789,8 +1766,7 @@ describe('Task Runner', () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); @@ -1828,8 +1804,7 @@ describe('Task Runner', () => { const taskRunner = new TaskRunner( ruleType, legacyTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT); @@ -1865,8 +1840,7 @@ describe('Task Runner', () => { ...mockedTaskInstance, state: originalAlertSate, }, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); @@ -1894,8 +1868,7 @@ describe('Task Runner', () => { spaceId: 'foo', }, }, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); @@ -1924,8 +1897,7 @@ describe('Task Runner', () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); @@ -1947,8 +1919,7 @@ describe('Task Runner', () => { interval: '1d', }, }, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); @@ -1972,8 +1943,7 @@ describe('Task Runner', () => { spaceId: 'test space', }, }, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); @@ -2019,8 +1989,7 @@ describe('Task Runner', () => { alertInstances: {}, }, }, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue({ ...mockedRuleTypeSavedObject, @@ -2146,8 +2115,7 @@ describe('Task Runner', () => { }, }, }, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue({ ...mockedRuleTypeSavedObject, @@ -2243,8 +2211,7 @@ describe('Task Runner', () => { }, }, }, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue({ ...mockedRuleTypeSavedObject, @@ -2329,8 +2296,7 @@ describe('Task Runner', () => { }, }, }, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue({ ...mockedRuleTypeSavedObject, @@ -2422,8 +2388,7 @@ describe('Task Runner', () => { }, }, }, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue({ ...mockedRuleTypeSavedObject, @@ -2490,8 +2455,7 @@ describe('Task Runner', () => { { ...taskRunnerFactoryInitializerParams, supportsEphemeralTasks: true, - }, - inMemoryMetrics + } ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); @@ -2567,8 +2531,7 @@ describe('Task Runner', () => { ...mockedTaskInstance, state, }, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ @@ -2608,8 +2571,7 @@ describe('Task Runner', () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); @@ -2622,8 +2584,7 @@ describe('Task Runner', () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT); @@ -2648,8 +2609,7 @@ describe('Task Runner', () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); @@ -2681,8 +2641,7 @@ describe('Task Runner', () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); @@ -2758,15 +2717,10 @@ describe('Task Runner', () => { ruleTypeRegistry.get.mockReturnValue(ruleType); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT); - const taskRunner = new TaskRunner( - ruleType, - mockedTaskInstance, - { - ...taskRunnerFactoryInitializerParams, - actionsConfigMap, - }, - inMemoryMetrics - ); + const taskRunner = new TaskRunner(ruleType, mockedTaskInstance, { + ...taskRunnerFactoryInitializerParams, + actionsConfigMap, + }); const runnerResult = await taskRunner.run(); @@ -2957,15 +2911,10 @@ describe('Task Runner', () => { encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT); - const taskRunner = new TaskRunner( - ruleType, - mockedTaskInstance, - { - ...taskRunnerFactoryInitializerParams, - actionsConfigMap, - }, - inMemoryMetrics - ); + const taskRunner = new TaskRunner(ruleType, mockedTaskInstance, { + ...taskRunnerFactoryInitializerParams, + actionsConfigMap, + }); const runnerResult = await taskRunner.run(); @@ -3025,8 +2974,7 @@ describe('Task Runner', () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ @@ -3059,12 +3007,8 @@ describe('Task Runner', () => { await taskRunner.run(); await taskRunner.cancel(); - expect(inMemoryMetrics.increment).toHaveBeenCalledTimes(6); - expect(inMemoryMetrics.increment.mock.calls[0][0]).toBe(IN_MEMORY_METRICS.RULE_EXECUTIONS); - expect(inMemoryMetrics.increment.mock.calls[1][0]).toBe(IN_MEMORY_METRICS.RULE_EXECUTIONS); - expect(inMemoryMetrics.increment.mock.calls[2][0]).toBe(IN_MEMORY_METRICS.RULE_EXECUTIONS); - expect(inMemoryMetrics.increment.mock.calls[3][0]).toBe(IN_MEMORY_METRICS.RULE_EXECUTIONS); - expect(inMemoryMetrics.increment.mock.calls[4][0]).toBe(IN_MEMORY_METRICS.RULE_FAILURES); - expect(inMemoryMetrics.increment.mock.calls[5][0]).toBe(IN_MEMORY_METRICS.RULE_TIMEOUTS); + expect(taskRunnerFactoryInitializerParams.nodeLevelMetrics?.execution).toHaveBeenCalledTimes(4); + expect(taskRunnerFactoryInitializerParams.nodeLevelMetrics?.failure).toHaveBeenCalledTimes(1); + expect(taskRunnerFactoryInitializerParams.nodeLevelMetrics?.timeout).toHaveBeenCalledTimes(1); }); }); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 6c161332bb58..e714033c8b66 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -61,7 +61,6 @@ import { createAlertEventLogRecordObject, Event, } from '../lib/create_alert_event_log_record_object'; -import { InMemoryMetrics, IN_MEMORY_METRICS } from '../monitoring'; import { GenerateNewAndRecoveredAlertEventsParams, LogActiveAndRecoveredAlertsParams, @@ -115,7 +114,6 @@ export class TaskRunner< >; private readonly executionId: string; private readonly ruleTypeRegistry: RuleTypeRegistry; - private readonly inMemoryMetrics: InMemoryMetrics; private usageCounter?: UsageCounter; private searchAbortController: AbortController; private cancelled: boolean; @@ -131,8 +129,7 @@ export class TaskRunner< RecoveryActionGroupId >, taskInstance: ConcreteTaskInstance, - context: TaskRunnerContext, - inMemoryMetrics: InMemoryMetrics + context: TaskRunnerContext ) { this.context = context; this.logger = context.logger; @@ -145,7 +142,6 @@ export class TaskRunner< this.searchAbortController = new AbortController(); this.cancelled = false; this.executionId = uuid.v4(); - this.inMemoryMetrics = inMemoryMetrics; } private async getDecryptedAttributes( @@ -885,9 +881,12 @@ export class TaskRunner< eventLogger.logEvent(event); if (!this.cancelled) { - this.inMemoryMetrics.increment(IN_MEMORY_METRICS.RULE_EXECUTIONS); + this.context.nodeLevelMetrics?.execution( + ruleId, + event.event?.duration ? event.event?.duration / Millis2Nanos : undefined + ); if (executionStatus.error) { - this.inMemoryMetrics.increment(IN_MEMORY_METRICS.RULE_FAILURES); + this.context.nodeLevelMetrics?.failure(ruleId, executionStatus.error.reason); } this.logger.debug( `Updating rule task for ${this.ruleType.id} rule with id ${ruleId} - ${JSON.stringify( @@ -1020,7 +1019,7 @@ export class TaskRunner< }; eventLogger.logEvent(event); - this.inMemoryMetrics.increment(IN_MEMORY_METRICS.RULE_TIMEOUTS); + this.context.nodeLevelMetrics?.timeout(ruleId, this.ruleType.ruleTaskTimeout); // Update the rule saved object with execution status const executionStatus: RuleExecutionStatus = { diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts index 498635911537..b368fb270021 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts @@ -36,7 +36,6 @@ import { Rule, RecoveredActionGroup } from '../../common'; import { UntypedNormalizedRuleType } from '../rule_type_registry'; import { ruleTypeRegistryMock } from '../rule_type_registry.mock'; import { dataPluginMock } from '@kbn/data-plugin/server/mocks'; -import { inMemoryMetricsMock } from '../monitoring/in_memory_metrics.mock'; jest.mock('uuid', () => ({ v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -101,7 +100,6 @@ describe('Task Runner Cancel', () => { const elasticsearchService = elasticsearchServiceMock.createInternalStart(); const uiSettingsService = uiSettingsServiceMock.createStartContract(); const dataPlugin = dataPluginMock.createStartContract(); - const inMemoryMetrics = inMemoryMetricsMock.create(); type TaskRunnerFactoryInitializerParamsType = jest.Mocked & { actionsPlugin: jest.Mocked; @@ -227,8 +225,7 @@ describe('Task Runner Cancel', () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); const promise = taskRunner.run(); @@ -417,15 +414,10 @@ describe('Task Runner Cancel', () => { } ); // setting cancelAlertsOnRuleTimeout to false here - const taskRunner = new TaskRunner( - ruleType, - mockedTaskInstance, - { - ...taskRunnerFactoryInitializerParams, - cancelAlertsOnRuleTimeout: false, - }, - inMemoryMetrics - ); + const taskRunner = new TaskRunner(ruleType, mockedTaskInstance, { + ...taskRunnerFactoryInitializerParams, + cancelAlertsOnRuleTimeout: false, + }); const promise = taskRunner.run(); await Promise.resolve(); @@ -462,8 +454,7 @@ describe('Task Runner Cancel', () => { cancelAlertsOnRuleTimeout: false, }, mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); const promise = taskRunner.run(); @@ -493,8 +484,7 @@ describe('Task Runner Cancel', () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics + taskRunnerFactoryInitializerParams ); const promise = taskRunner.run(); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts index e78761780035..af28ea358279 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts @@ -25,9 +25,7 @@ import { UntypedNormalizedRuleType } from '../rule_type_registry'; import { ruleTypeRegistryMock } from '../rule_type_registry.mock'; import { executionContextServiceMock } from '@kbn/core/server/mocks'; import { dataPluginMock } from '@kbn/data-plugin/server/mocks'; -import { inMemoryMetricsMock } from '../monitoring/in_memory_metrics.mock'; -const inMemoryMetrics = inMemoryMetricsMock.create(); const executionContext = executionContextServiceMock.createSetupContract(); const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); @@ -115,7 +113,7 @@ describe('Task Runner Factory', () => { test(`throws an error if factory isn't initialized`, () => { const factory = new TaskRunnerFactory(); expect(() => - factory.create(ruleType, { taskInstance: mockedTaskInstance }, inMemoryMetrics) + factory.create(ruleType, { taskInstance: mockedTaskInstance }) ).toThrowErrorMatchingInlineSnapshot(`"TaskRunnerFactory not initialized"`); }); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts index e7c483b944ed..6eff5a08129c 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts @@ -33,7 +33,7 @@ import { import { TaskRunner } from './task_runner'; import { RulesClient } from '../rules_client'; import { NormalizedRuleType } from '../rule_type_registry'; -import { InMemoryMetrics } from '../monitoring'; +import { NodeLevelMetrics } from '../monitoring'; import { ActionsConfigMap } from '../lib/get_actions_config_map'; export interface TaskRunnerContext { @@ -57,6 +57,7 @@ export interface TaskRunnerContext { actionsConfigMap: ActionsConfigMap; cancelAlertsOnRuleTimeout: boolean; usageCounter?: UsageCounter; + nodeLevelMetrics?: NodeLevelMetrics; } export class TaskRunnerFactory { @@ -89,8 +90,7 @@ export class TaskRunnerFactory { ActionGroupIds, RecoveryActionGroupId >, - { taskInstance }: RunContext, - inMemoryMetrics: InMemoryMetrics + { taskInstance }: RunContext ) { if (!this.isInitialized) { throw new Error('TaskRunnerFactory not initialized'); @@ -104,6 +104,6 @@ export class TaskRunnerFactory { InstanceContext, ActionGroupIds, RecoveryActionGroupId - >(ruleType, taskInstance, this.taskRunnerContext!, inMemoryMetrics); + >(ruleType, taskInstance, this.taskRunnerContext!); } } diff --git a/x-pack/plugins/alerting/tsconfig.json b/x-pack/plugins/alerting/tsconfig.json index 357f4ca94087..3b698f68434a 100644 --- a/x-pack/plugins/alerting/tsconfig.json +++ b/x-pack/plugins/alerting/tsconfig.json @@ -11,7 +11,8 @@ // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 "server/**/*.json", "public/**/*", - "common/*" + "common/*", + "common/monitoring/*" ], "references": [ { "path": "../../../src/core/tsconfig.json" }, diff --git a/x-pack/plugins/monitoring/kibana.json b/x-pack/plugins/monitoring/kibana.json index bbb6eb374e91..9a6be4dd5c3d 100644 --- a/x-pack/plugins/monitoring/kibana.json +++ b/x-pack/plugins/monitoring/kibana.json @@ -16,7 +16,8 @@ "triggersActionsUi", "alerting", "actions", - "encryptedSavedObjects" + "encryptedSavedObjects", + "monitoringCollection" ], "server": true, "ui": true, diff --git a/x-pack/plugins/monitoring/server/index.ts b/x-pack/plugins/monitoring/server/index.ts index 62db584a3528..28e910783fae 100644 --- a/x-pack/plugins/monitoring/server/index.ts +++ b/x-pack/plugins/monitoring/server/index.ts @@ -13,7 +13,7 @@ import { deprecations } from './deprecations'; export type { KibanaSettingsCollector } from './kibana_monitoring/collectors'; export type { MonitoringConfig } from './config'; -export type { MonitoringPluginSetup, IBulkUploader } from './types'; +export type { MonitoringPluginSetup, IBulkUploader, MonitoringPluginStart } from './types'; export const plugin = (initContext: PluginInitializerContext) => new MonitoringPlugin(initContext); export const config: PluginConfigDescriptor> = { diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 433a3358558d..62660480cfa3 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -48,6 +48,7 @@ import { MonitoringCore, MonitoringLicenseService, MonitoringPluginSetup, + MonitoringPluginStart, PluginsSetup, PluginsStart, RequestHandlerContextMonitoringPlugin, @@ -206,7 +207,10 @@ export class MonitoringPlugin } } - start(coreStart: CoreStart, { licensing }: PluginsStart) { + start( + coreStart: CoreStart, + { licensing, monitoringCollection }: PluginsStart + ): MonitoringPluginStart { const config = this.config!; this.cluster = instantiateClient( config.ui.elasticsearch, @@ -216,6 +220,10 @@ export class MonitoringPlugin this.init(this.cluster, coreStart); + if (monitoringCollection) { + monitoringCollection.registerCustomElasticsearchClient(this.cluster); + } + // Start our license service which will ensure // the appropriate licenses are present this.licenseService = new LicenseService().setup({ @@ -253,6 +261,12 @@ export class MonitoringPlugin 'Internal collection for Kibana monitoring is disabled per configuration.' ); } + + return { + getMonitoringCluster: () => { + return this.cluster; + }, + }; } stop() { diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index bcd40fe38e41..ac4939c437b4 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -34,6 +34,7 @@ import { LicensingPluginStart } from '@kbn/licensing-plugin/server'; import { PluginSetupContract as FeaturesPluginSetupContract } from '@kbn/features-plugin/server'; import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server'; import { CloudSetup } from '@kbn/cloud-plugin/server'; +import { MonitoringCollectionStart } from '@kbn/monitoring-collection-plugin/server'; import { RouteConfig, RouteMethod } from '@kbn/core/server'; import { ElasticsearchModifiedSource } from '../common/types/es'; import { RulesByType } from '../common/types/alerts'; @@ -69,6 +70,11 @@ export interface PluginsStart { alerting: AlertingPluginStartContract; actions: ActionsPluginsStartContact; licensing: LicensingPluginStart; + monitoringCollection: MonitoringCollectionStart; +} + +export interface MonitoringPluginStart { + getMonitoringCluster: () => ICustomClusterClient; } export interface RouteDependencies { diff --git a/x-pack/plugins/monitoring/tsconfig.json b/x-pack/plugins/monitoring/tsconfig.json index 79fcff4d840f..0d6e346511ae 100644 --- a/x-pack/plugins/monitoring/tsconfig.json +++ b/x-pack/plugins/monitoring/tsconfig.json @@ -30,5 +30,6 @@ { "path": "../observability/tsconfig.json" }, { "path": "../telemetry_collection_xpack/tsconfig.json" }, { "path": "../triggers_actions_ui/tsconfig.json" }, + { "path": "../monitoring_collection/tsconfig.json" }, ] } diff --git a/x-pack/plugins/monitoring_collection/server/routes/index.ts b/x-pack/plugins/monitoring_collection/common/types.ts similarity index 81% rename from x-pack/plugins/monitoring_collection/server/routes/index.ts rename to x-pack/plugins/monitoring_collection/common/types.ts index eb96ce19f764..670aeecf1bc7 100644 --- a/x-pack/plugins/monitoring_collection/server/routes/index.ts +++ b/x-pack/plugins/monitoring_collection/common/types.ts @@ -4,5 +4,4 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -export { registerDynamicRoute } from './dynamic_route'; +export type ValidMetricSet = Record; diff --git a/x-pack/plugins/monitoring_collection/kibana.json b/x-pack/plugins/monitoring_collection/kibana.json index d88b7e87861e..0e6bae7871f5 100644 --- a/x-pack/plugins/monitoring_collection/kibana.json +++ b/x-pack/plugins/monitoring_collection/kibana.json @@ -6,7 +6,7 @@ "name": "Stack Monitoring", "githubTeam": "stack-monitoring-ui" }, - "configPath": ["monitoring_collection"], + "configPath": ["xpack", "monitoring_collection"], "requiredPlugins": [], "optionalPlugins": [ ], diff --git a/x-pack/plugins/monitoring_collection/server/config.ts b/x-pack/plugins/monitoring_collection/server/config.ts index 275d2f31e505..430933a6c07d 100644 --- a/x-pack/plugins/monitoring_collection/server/config.ts +++ b/x-pack/plugins/monitoring_collection/server/config.ts @@ -9,6 +9,8 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), + interval: schema.number({ defaultValue: 10000 }), + metricsIndex: schema.string({ defaultValue: 'metrics-apm*' }), }); export type MonitoringCollectionConfig = ReturnType; diff --git a/x-pack/plugins/monitoring_collection/server/constants.ts b/x-pack/plugins/monitoring_collection/server/constants.ts index 86231dec6c6c..644afcfac62c 100644 --- a/x-pack/plugins/monitoring_collection/server/constants.ts +++ b/x-pack/plugins/monitoring_collection/server/constants.ts @@ -4,4 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -export const TYPE_ALLOWLIST = ['node_rules', 'cluster_rules', 'node_actions', 'cluster_actions']; +export const METRICSET_ALLOWLIST = [ + // 'node_rules', + // 'cluster_rules', + // 'node_actions', + // 'cluster_actions', + 'kibana_alerting_cluster_rules', +]; diff --git a/x-pack/plugins/monitoring_collection/server/index.ts b/x-pack/plugins/monitoring_collection/server/index.ts index e6f15bd297dc..bf4a8f4f2b47 100644 --- a/x-pack/plugins/monitoring_collection/server/index.ts +++ b/x-pack/plugins/monitoring_collection/server/index.ts @@ -12,7 +12,7 @@ import { configSchema } from './config'; export type { MonitoringCollectionConfig } from './config'; -export type { MonitoringCollectionSetup, MetricResult, Metric } from './plugin'; +export type { MonitoringCollectionSetup, MonitoringCollectionStart, MetricSet } from './plugin'; export const plugin = (initContext: PluginInitializerContext) => new MonitoringCollectionPlugin(initContext); diff --git a/x-pack/plugins/monitoring_collection/server/mocks.ts b/x-pack/plugins/monitoring_collection/server/mocks.ts index 9858653df648..38384cfca4cf 100644 --- a/x-pack/plugins/monitoring_collection/server/mocks.ts +++ b/x-pack/plugins/monitoring_collection/server/mocks.ts @@ -6,15 +6,26 @@ */ import { MonitoringCollectionSetup } from '.'; +import { MonitoringCollectionStart } from './plugin'; const createSetupMock = (): jest.Mocked => { const mock = { - registerMetric: jest.fn(), - getMetrics: jest.fn(), + registerMetricSet: jest.fn(), + aggregateMonitoringData: jest.fn(), + }; + return mock; +}; + +const createStartMock = (): jest.Mocked => { + const mock = { + reportGauge: jest.fn(), + reportCounter: jest.fn(), + registerCustomElasticsearchClient: jest.fn(), }; return mock; }; export const monitoringCollectionMock = { createSetup: createSetupMock, + createStart: createStartMock, }; diff --git a/x-pack/plugins/monitoring_collection/server/plugin.test.ts b/x-pack/plugins/monitoring_collection/server/plugin.test.ts deleted file mode 100644 index 15b9934525b9..000000000000 --- a/x-pack/plugins/monitoring_collection/server/plugin.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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 { coreMock } from '@kbn/core/server/mocks'; -import { MonitoringCollectionPlugin } from './plugin'; - -describe('monitoring_collection plugin', () => { - describe('setup()', () => { - let context: ReturnType; - let plugin: MonitoringCollectionPlugin; - let coreSetup: ReturnType; - - beforeEach(() => { - context = coreMock.createPluginInitializerContext(); - plugin = new MonitoringCollectionPlugin(context); - coreSetup = coreMock.createSetup(); - coreSetup.getStartServices = jest.fn().mockResolvedValue([ - { - application: {}, - }, - { triggersActionsUi: {} }, - ]); - }); - - it('should allow registering a collector and getting data from it', async () => { - const { registerMetric } = plugin.setup(coreSetup); - registerMetric<{ name: string }>({ - type: 'cluster_actions', - schema: { - name: { - type: 'text', - }, - }, - fetch: async () => { - return [ - { - name: 'foo', - }, - ]; - }, - }); - - const metrics = await plugin.getMetric('cluster_actions'); - expect(metrics).toStrictEqual([{ name: 'foo' }]); - }); - - it('should allow registering multiple ollectors and getting data from it', async () => { - const { registerMetric } = plugin.setup(coreSetup); - registerMetric<{ name: string }>({ - type: 'cluster_actions', - schema: { - name: { - type: 'text', - }, - }, - fetch: async () => { - return [ - { - name: 'foo', - }, - ]; - }, - }); - registerMetric<{ name: string }>({ - type: 'cluster_rules', - schema: { - name: { - type: 'text', - }, - }, - fetch: async () => { - return [ - { - name: 'foo', - }, - { - name: 'bar', - }, - { - name: 'foobar', - }, - ]; - }, - }); - - const metrics = await Promise.all([ - plugin.getMetric('cluster_actions'), - plugin.getMetric('cluster_rules'), - ]); - expect(metrics).toStrictEqual([ - [{ name: 'foo' }], - [{ name: 'foo' }, { name: 'bar' }, { name: 'foobar' }], - ]); - }); - - it('should NOT allow registering a collector that is not in the allowlist', async () => { - const logger = context.logger.get(); - const { registerMetric } = plugin.setup(coreSetup); - registerMetric<{ name: string }>({ - type: 'test', - schema: { - name: { - type: 'text', - }, - }, - fetch: async () => { - return [ - { - name: 'foo', - }, - ]; - }, - }); - const metrics = await plugin.getMetric('test'); - expect((logger.warn as jest.Mock).mock.calls.length).toBe(2); - expect((logger.warn as jest.Mock).mock.calls[0][0]).toBe( - `Skipping registration of metric type 'test'. This type is not supported in the allowlist.` - ); - expect((logger.warn as jest.Mock).mock.calls[1][0]).toBe( - `Call to 'getMetric' failed because type 'test' does not exist.` - ); - expect(metrics).toBeUndefined(); - }); - }); -}); diff --git a/x-pack/plugins/monitoring_collection/server/plugin.ts b/x-pack/plugins/monitoring_collection/server/plugin.ts index e1c3a5064a57..cc24e3b5aa82 100644 --- a/x-pack/plugins/monitoring_collection/server/plugin.ts +++ b/x-pack/plugins/monitoring_collection/server/plugin.ts @@ -4,88 +4,162 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { JsonObject } from '@kbn/utility-types'; -import { CoreSetup, Plugin, PluginInitializerContext, Logger } from '@kbn/core/server'; -import { MakeSchemaFrom } from '@kbn/usage-collection-plugin/server'; -import { ServiceStatus } from '@kbn/core/server'; -import { registerDynamicRoute } from './routes'; -import { TYPE_ALLOWLIST } from './constants'; +import apm from 'elastic-apm-node'; +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { + Plugin, + PluginInitializerContext, + Logger, + CoreStart, + ICustomClusterClient, + IClusterClient, +} from '@kbn/core/server'; +import { METRICSET_ALLOWLIST } from './constants'; +import { MonitoringCollectionConfig } from './config'; +import { ValidMetricSet } from '../common/types'; export interface MonitoringCollectionSetup { - registerMetric: (metric: Metric) => void; + registerMetricSet: (metric: MetricSet) => void; + aggregateMonitoringData: ( + query: estypes.QueryDslQueryContainer, + aggs: Record + ) => Promise | undefined>; +} +export interface MonitoringCollectionStart { + reportGauge: (name: string, dimensions: Record, value: number) => void; + reportCounter: (name: string, dimensions: Record, amount?: number) => void; + registerCustomElasticsearchClient: (customClient: ICustomClusterClient) => void; } -export type MetricResult = T & JsonObject; - -export interface Metric { - type: string; - schema: MakeSchemaFrom; - fetch: () => Promise | Array>>; +export interface MetricSet { + id: string; + fetch: () => Promise; } +export type KibanaIdentifier = Record & { + kibana_version: string; + kibana_uuid: string; + es_cluster_uuid?: string; +}; + export class MonitoringCollectionPlugin implements Plugin { private readonly initializerContext: PluginInitializerContext; private readonly logger: Logger; + private readonly config: MonitoringCollectionConfig; - private metrics: Record> = {}; + private metricSets: Record> = {}; + private results: Record = {}; + private client?: IClusterClient | ICustomClusterClient; + private kibanaVersion?: string; + private kibanaUuid?: string; constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; this.logger = initializerContext.logger.get(); + this.config = initializerContext.config.get(); } - async getMetric(type: string) { - if (this.metrics.hasOwnProperty(type)) { - return await this.metrics[type].fetch(); - } - this.logger.warn(`Call to 'getMetric' failed because type '${type}' does not exist.`); - return undefined; - } - - setup(core: CoreSetup) { - const router = core.http.createRouter(); - const kibanaIndex = core.savedObjects.getKibanaIndex(); - - let status: ServiceStatus; - core.status.overall$.subscribe((newStatus) => { - status = newStatus; - }); - - registerDynamicRoute({ - router, - config: { - kibanaIndex, - kibanaVersion: this.initializerContext.env.packageInfo.version, - server: core.http.getServerInfo(), - uuid: this.initializerContext.env.instanceUuid, - }, - getStatus: () => status, - getMetric: async (type: string) => { - return await this.getMetric(type); - }, - }); + setup() { + this.kibanaVersion = this.initializerContext.env.packageInfo.version; + this.kibanaUuid = this.initializerContext.env.instanceUuid; return { - registerMetric: (metric: Metric) => { - if (this.metrics.hasOwnProperty(metric.type)) { + registerMetricSet: (metricSet: MetricSet) => { + if (this.metricSets.hasOwnProperty(metricSet.id)) { this.logger.warn( - `Skipping registration of metric type '${metric.type}'. This type has already been registered.` + `Skipping registration of metric set '${metricSet.id}'. It was has already been registered.` ); return; } - if (!TYPE_ALLOWLIST.includes(metric.type)) { + if (!METRICSET_ALLOWLIST.includes(metricSet.id)) { this.logger.warn( - `Skipping registration of metric type '${metric.type}'. This type is not supported in the allowlist.` + `Skipping registration of metric set '${metricSet.id}'. This id is not supported in the allowlist.` ); return; } - this.metrics[metric.type] = metric; + + this.results[metricSet.id] = {}; + this.metricSets[metricSet.id] = metricSet; + }, + // Maybe this belongs in start but this will be used by routes which are usually created in setup + aggregateMonitoringData: async ( + query: estypes.QueryDslQueryContainer, + aggs: Record + ) => { + const request: estypes.SearchRequest = { + index: this.config.metricsIndex, + size: 0, + body: { + query, + aggs, + }, + }; + + try { + const results = await this.client?.asInternalUser.search(request); + return results?.aggregations; + } catch (err) { + return {}; + } }, }; } - start() {} + start(core: CoreStart) { + this.client = core.elasticsearch.client; + const kibanaDimensions: KibanaIdentifier = { + kibana_version: this.kibanaVersion!, + kibana_uuid: this.kibanaUuid!, + }; + + (async () => { + const response = await core.elasticsearch.client.asInternalUser.info({ + filter_path: 'cluster_uuid', + }); + + kibanaDimensions.es_cluster_uuid = response.cluster_uuid; + })(); + + setInterval(async () => { + const ids = Object.keys(this.metricSets); + // TODO: think about how to make this not suck + // abort controller, maybe chunk it and perform with some backoff? + const results = await Promise.all(ids.map((id) => this.metricSets[id].fetch())); + ids.forEach((id, idIndex) => { + this.results[id] = results[idIndex]; + }); + }, this.config.interval); + + return { + reportCounter: (name: string, dimensions: Record, amount: number = 1) => { + try { + // @ts-ignore-line + const counter = apm.registerMetricCounter(name, { + ...dimensions, + ...kibanaDimensions, + }); + if (counter) { + counter.inc(amount); + } + } catch (err) { + this.logger.warn(`Unable to report counter for ${name} due to ${err.message}`); + } + }, + reportGauge: (name: string, dimensions: Record, value: number) => { + // if (typeof name !== 'string') { + + // } + try { + apm.registerMetric(name, { ...dimensions, ...kibanaDimensions }, () => value); + } catch (err) { + this.logger.warn(`Unable to report gauge for ${name} due to ${err.message}`); + } + }, + registerCustomElasticsearchClient: (customClient: ICustomClusterClient) => { + this.client = customClient; + }, + }; + } stop() {} } diff --git a/x-pack/plugins/monitoring_collection/server/routes/dynamic_route.test.ts b/x-pack/plugins/monitoring_collection/server/routes/dynamic_route.test.ts deleted file mode 100644 index deacfb3fdd99..000000000000 --- a/x-pack/plugins/monitoring_collection/server/routes/dynamic_route.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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 { registerDynamicRoute } from './dynamic_route'; -import { KibanaRequest, KibanaResponseFactory, ServiceStatusLevels } from '@kbn/core/server'; -import { httpServerMock, httpServiceMock } from '@kbn/core/server/mocks'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { elasticsearchClientMock } from '@kbn/core/server/elasticsearch/client/mocks'; - -beforeEach(() => { - jest.resetAllMocks(); -}); - -jest.mock('../lib', () => ({ - getESClusterUuid: () => 'clusterA', - getKibanaStats: () => ({ name: 'myKibana' }), -})); - -describe('dynamic route', () => { - const kibanaStatsConfig = { - allowAnonymous: true, - kibanaIndex: '.kibana', - kibanaVersion: '8.0.0', - uuid: 'abc123', - server: { - name: 'server', - hostname: 'host', - port: 123, - }, - }; - - const getStatus = () => ({ - level: ServiceStatusLevels.available, - summary: 'Service is working', - }); - - it('returns for a valid type', async () => { - const router = httpServiceMock.createRouter(); - - const getMetric = async () => { - return { foo: 1 }; - }; - registerDynamicRoute({ - router, - config: kibanaStatsConfig, - getStatus, - getMetric, - }); - - const [_, handler] = router.get.mock.calls[0]; - - const esClientMock = elasticsearchClientMock.createScopedClusterClient(); - const context = { - core: { - elasticsearch: { - client: esClientMock, - }, - }, - }; - const req = { params: { type: 'test' } } as KibanaRequest; - const factory: jest.Mocked = httpServerMock.createResponseFactory(); - - await handler(context, req, factory); - - expect(factory.ok).toHaveBeenCalledWith({ - body: { - cluster_uuid: 'clusterA', - kibana: { name: 'myKibana' }, - test: { - foo: 1, - }, - }, - }); - }); - - it('returns the an empty object if there is no data', async () => { - const router = httpServiceMock.createRouter(); - - const getMetric = async () => { - return {}; - }; - registerDynamicRoute({ router, config: kibanaStatsConfig, getStatus, getMetric }); - - const [_, handler] = router.get.mock.calls[0]; - - const esClientMock = elasticsearchClientMock.createScopedClusterClient(); - const context = { - core: { - elasticsearch: { - client: esClientMock, - }, - }, - }; - const req = { params: { type: 'test' } } as KibanaRequest; - const factory: jest.Mocked = httpServerMock.createResponseFactory(); - - await handler(context, req, factory); - - expect(factory.ok).toHaveBeenCalledWith({ - body: { - cluster_uuid: 'clusterA', - kibana: { name: 'myKibana' }, - test: {}, - }, - }); - }); -}); diff --git a/x-pack/plugins/monitoring_collection/server/routes/dynamic_route.ts b/x-pack/plugins/monitoring_collection/server/routes/dynamic_route.ts deleted file mode 100644 index 944037dd17a7..000000000000 --- a/x-pack/plugins/monitoring_collection/server/routes/dynamic_route.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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 { JsonObject } from '@kbn/utility-types'; -import { schema } from '@kbn/config-schema'; -import { IRouter, ServiceStatus } from '@kbn/core/server'; -import { getESClusterUuid, getKibanaStats } from '../lib'; -import { MetricResult } from '../plugin'; - -export function registerDynamicRoute({ - router, - config, - getStatus, - getMetric, -}: { - router: IRouter; - config: { - kibanaIndex: string; - kibanaVersion: string; - uuid: string; - server: { - name: string; - hostname: string; - port: number; - }; - }; - getStatus: () => ServiceStatus | undefined; - getMetric: ( - type: string - ) => Promise> | MetricResult | undefined>; -}) { - router.get( - { - path: `/api/monitoring_collection/{type}`, - options: { - authRequired: true, - tags: ['api'], // ensures that unauthenticated calls receive a 401 rather than a 302 redirect to login page - }, - validate: { - params: schema.object({ - type: schema.string(), - }), - }, - }, - async (context, req, res) => { - const type = req.params.type; - const esClient = (await context.core).elasticsearch.client; - const [data, clusterUuid, kibana] = await Promise.all([ - getMetric(type), - getESClusterUuid(esClient), - getKibanaStats({ config, getStatus }), - ]); - - return res.ok({ - body: { - [type]: data, - cluster_uuid: clusterUuid, - kibana, - }, - }); - } - ); -} diff --git a/x-pack/plugins/monitoring_collection/tsconfig.json b/x-pack/plugins/monitoring_collection/tsconfig.json index c382b243b3fe..5d1fa61ec377 100644 --- a/x-pack/plugins/monitoring_collection/tsconfig.json +++ b/x-pack/plugins/monitoring_collection/tsconfig.json @@ -7,7 +7,8 @@ "declarationMap": true }, "include": [ - "server/**/*" + "server/**/*", + "common/**/*" ], "references": [ { "path": "../../../src/core/tsconfig.json" }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts index 89ede79f4a21..140479a2c2d2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts @@ -27,3 +27,4 @@ export { updateRule } from './update'; export { resolveRule } from './resolve_rule'; export { snoozeRule } from './snooze'; export { unsnoozeRule } from './unsnooze'; +export { loadMonitoring } from './load_monitoring'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_monitoring.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_monitoring.ts new file mode 100644 index 000000000000..da0329cd8a84 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_monitoring.ts @@ -0,0 +1,25 @@ +/* + * 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 { HttpSetup } from '@kbn/core/public'; +import { AsApiContract } from '@kbn/actions-plugin/common'; +import { RuleMonitoringMetrics } from '@kbn/alerting-plugin/common/monitoring/types'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; + +export interface LoadMonitoringProps { + ruleId: string; +} + +export const loadMonitoring = async ({ + ruleId, + http, +}: LoadMonitoringProps & { http: HttpSetup }) => { + const result = await http.get>( + `${INTERNAL_BASE_ALERTING_API_PATH}/rule/${ruleId}/monitoring/aggregate` + ); + return result; // rewriteBodyRes(result); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.tsx index e82bbaad2674..fe6afd716a7c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { IExecutionLogWithErrorsResult } from '@kbn/alerting-plugin/common'; +import { RuleMonitoringMetrics } from '@kbn/alerting-plugin/common/monitoring/types'; import { Rule, RuleType, @@ -38,6 +39,7 @@ import { LoadExecutionLogAggregationsProps, snoozeRule, unsnoozeRule, + loadMonitoring, } from '../../../lib/rule_api'; import { useKibana } from '../../../../common/lib/kibana'; @@ -71,6 +73,7 @@ export interface ComponentOpts { resolveRule: (id: Rule['id']) => Promise; snoozeRule: (rule: Rule, snoozeEndTime: string | -1) => Promise; unsnoozeRule: (rule: Rule) => Promise; + loadMonitoring: (id: Rule['id']) => Promise; } export type PropsWithOptionalApiHandlers = Omit & Partial; @@ -155,6 +158,7 @@ export function withBulkRuleOperations( unsnoozeRule={async (rule: Rule) => { return await unsnoozeRule({ http, id: rule.id }); }} + loadMonitoring={async (ruleId: Rule['id']) => loadMonitoring({ http, ruleId })} /> ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx index b70eaf20a051..11c8741520ef 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx @@ -21,6 +21,7 @@ import { // @ts-ignore import { RIGHT_ALIGNMENT, CENTER_ALIGNMENT } from '@elastic/eui/lib/services'; import { FormattedMessage } from '@kbn/i18n-react'; +import { RuleMonitoringMetrics } from '@kbn/alerting-plugin/common/monitoring/types'; import moment from 'moment'; import { ActionGroup, @@ -50,6 +51,7 @@ import { suspendedComponentWithProps } from '../../../lib/suspended_component_wi const RuleEventLogListWithApi = lazy(() => import('./rule_event_log_list')); const RuleErrorLogWithApi = lazy(() => import('./rule_error_log')); +const RuleMetricsWithApi = lazy(() => import('./rule_metrics')); const RuleAlertList = lazy(() => import('./rule_alert_list')); @@ -64,11 +66,13 @@ type RuleProps = { onChangeDuration: (length: number) => void; durationEpoch?: number; isLoadingChart?: boolean; + monitoring?: RuleMonitoringMetrics; } & Pick; const EVENT_LOG_LIST_TAB = 'rule_event_log_list'; const ALERT_LIST_TAB = 'rule_alert_list'; const EVENT_ERROR_LOG_TAB = 'rule_error_log_list'; +const METRICS_TAB = 'rule_metrics'; export function RuleComponent({ rule, @@ -83,6 +87,7 @@ export function RuleComponent({ onChangeDuration, durationEpoch = Date.now(), isLoadingChart, + monitoring, }: RuleProps) { const alerts = Object.entries(ruleSummary.alerts) .map(([alertId, alert]) => alertToListItem(durationEpoch, ruleType, alertId, alert)) @@ -149,6 +154,14 @@ export function RuleComponent({ 'xl' )({ requestRefresh, rule, refreshToken }), }, + { + id: METRICS_TAB, + name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.rule.metricsTabText', { + defaultMessage: 'Metrics', + }), + 'data-test-subj': 'metricsTab', + content: suspendedComponentWithProps(RuleMetricsWithApi, 'xl')({ monitoring }), + }, ]; const renderTabs = () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_metrics.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_metrics.tsx new file mode 100644 index 000000000000..cd4d9be291f9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_metrics.tsx @@ -0,0 +1,88 @@ +/* + * 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 from 'react'; +import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { RuleMonitoringMetrics } from '@kbn/alerting-plugin/common/monitoring/types'; +import { + Chart, + LineSeries, + ScaleType, + Settings, + DARK_THEME, + Axis, + Position, + timeFormatter, +} from '@elastic/charts'; + +const dateFormatter = timeFormatter('HH:mm'); + +const METRICS = [ + { + label: 'Rate of executions', + field: 'execution', + }, + { + label: 'Average execution time', + field: 'execution_time', + }, +]; + +export interface RuleMetricsProps { + monitoring?: RuleMonitoringMetrics; +} +export const RuleMetrics = (props: RuleMetricsProps) => { + const monitoring = props.monitoring; + if (!monitoring) { + return ( + <> + + No metrics found} + body={

There was an error searching for metrics.

} + /> + + ); + } + + return ( + <> + + + {METRICS.map((metric) => ( + + +

{metric.label}

+
+ + + + + + +
+ ))} +
+ + ); +}; +// eslint-disable-next-line import/no-default-export +export { RuleMetrics as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.tsx index 7b72f34d05ba..5f7d1bb9b974 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.tsx @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ToastsApi } from '@kbn/core/public'; +import { RuleMonitoringMetrics } from '@kbn/alerting-plugin/common/monitoring/types'; import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Rule, RuleSummary, RuleType } from '../../../../types'; import { @@ -23,13 +24,14 @@ type WithRuleSummaryProps = { readOnly: boolean; requestRefresh: () => Promise; refreshToken?: number; -} & Pick; +} & Pick; export const RuleRoute: React.FunctionComponent = ({ rule, ruleType, readOnly, requestRefresh, + loadMonitoring, loadRuleSummary: loadRuleSummary, refreshToken, }) => { @@ -38,6 +40,7 @@ export const RuleRoute: React.FunctionComponent = ({ } = useKibana().services; const [ruleSummary, setRuleSummary] = useState(null); + const [monitoring, setMonitoring] = useState(); const [numberOfExecutions, setNumberOfExecutions] = useState(60); const [isLoadingChart, setIsLoadingChart] = useState(true); const ruleID = useRef(null); @@ -52,6 +55,14 @@ export const RuleRoute: React.FunctionComponent = ({ [setIsLoadingChart, ruleID, loadRuleSummary, setRuleSummary, toasts, numberOfExecutions] ); + useEffect(() => { + if (!rule.id) return; + (async () => { + const result = await loadMonitoring(rule.id); + setMonitoring(result); + })(); + }, [loadMonitoring, rule.id]); + useEffect(() => { if (ruleID.current !== rule.id) { ruleID.current = rule.id; @@ -80,6 +91,7 @@ export const RuleRoute: React.FunctionComponent = ({ refreshToken={refreshToken} rule={rule} ruleType={ruleType} + monitoring={monitoring} readOnly={readOnly} ruleSummary={ruleSummary} numberOfExecutions={numberOfExecutions}