diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/index.ts b/x-pack/platform/plugins/shared/alerting_v2/server/index.ts index bdcda165aecb7..0201326c043f0 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/index.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/index.ts @@ -5,81 +5,22 @@ * 2.0. */ -import { Logger, OnSetup, PluginSetup } from '@kbn/core-di'; -import { CoreSetup, PluginInitializer, Route } from '@kbn/core-di-server'; -import type { CoreStart, PluginConfigDescriptor, PluginInitializerContext } from '@kbn/core/server'; -import type { TaskManagerSetupContract } from '@kbn/task-manager-plugin/server'; import { ContainerModule } from 'inversify'; -import { RulesClient } from './lib/rules_client'; +import type { PluginConfigDescriptor } from '@kbn/core/server'; import type { PluginConfig } from './config'; import { configSchema } from './config'; -import { AlertingRetryService } from './lib/services/retry_service'; -import { registerFeaturePrivileges } from './lib/security/privileges'; -import { CreateRuleRoute } from './routes/create_rule_route'; -import { DeleteRuleRoute } from './routes/delete_rule_route'; -import { GetRuleRoute } from './routes/get_rule_route'; -import { GetRulesRoute } from './routes/get_rules_route'; -import { UpdateRuleRoute } from './routes/update_rule_route'; -import { initializeRuleExecutorTaskDefinition } from './lib/rule_executor'; -import { AlertingResourcesService } from './lib/services/alerting_resources_service'; -import { registerSavedObjects } from './saved_objects'; -import type { AlertingServerStartDependencies } from './types'; +import { bindOnSetup } from './setup/bind_on_setup'; +import { bindRoutes } from './setup/bind_routes'; +import { bindServices } from './setup/bind_services'; export const config: PluginConfigDescriptor = { schema: configSchema, }; -export const module = new ContainerModule(({ bind }) => { - // Register HTTP routes via DI - bind(Route).toConstantValue(CreateRuleRoute); - bind(Route).toConstantValue(DeleteRuleRoute); - bind(Route).toConstantValue(GetRuleRoute); - bind(Route).toConstantValue(GetRulesRoute); - bind(Route).toConstantValue(UpdateRuleRoute); - - // Request-scoped rules client - bind(RulesClient).toSelf().inRequestScope(); - - // Singleton services - bind(AlertingRetryService).toSelf().inSingletonScope(); - bind(AlertingResourcesService).toSelf().inSingletonScope(); - - bind(OnSetup).toConstantValue((container) => { - const logger = container.get(Logger); - const pluginConfig = container.get( - PluginInitializer('config') - ) as PluginInitializerContext['config']; - const alertingConfig = pluginConfig.get(); - - // Register feature privileges - registerFeaturePrivileges(container.get(PluginSetup('features'))); - - // Saved Objects + Encrypted Saved Objects registration - registerSavedObjects({ - savedObjects: container.get(CoreSetup('savedObjects')), - logger, - }); - - // Task type registration - const taskManagerSetup = container.get(PluginSetup('taskManager')); - const getStartServices = container.get(CoreSetup('getStartServices')) as () => Promise< - [CoreStart, AlertingServerStartDependencies, unknown] - >; - const startServices = getStartServices(); - - const resourcesService = container.get(AlertingResourcesService); - resourcesService.startInitialization({ - enabled: alertingConfig.enabled, - }); - - initializeRuleExecutorTaskDefinition( - logger, - taskManagerSetup, - startServices, - alertingConfig, - resourcesService - ); - }); +export const module = new ContainerModule((options) => { + bindRoutes(options); + bindServices(options); + bindOnSetup(options); }); export type { PluginConfig as AlertingV2Config } from './config'; diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/logger_service/logger_service.test.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/logger_service/logger_service.test.ts new file mode 100644 index 0000000000000..b2cbcb26ad064 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/logger_service/logger_service.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from '@kbn/core/server'; +import { loggerMock } from '@kbn/logging-mocks'; +import { LoggerService } from './logger_service'; + +describe('LoggerService', () => { + let mockLogger: jest.Mocked; + let loggerService: LoggerService; + + beforeEach(() => { + mockLogger = loggerMock.create(); + loggerService = new LoggerService(mockLogger); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('debug', () => { + it('should call logger.debug with the message', () => { + const message = 'Test debug message'; + + loggerService.debug({ message }); + + expect(mockLogger.debug).toHaveBeenCalledTimes(1); + expect(mockLogger.debug).toHaveBeenCalledWith(message); + }); + }); + + describe('error', () => { + it('should call logger.error with error message and EcsError when only error is provided', () => { + const error = new Error('Test error'); + + loggerService.error({ error }); + + expect(mockLogger.error).toHaveBeenCalledTimes(1); + expect(mockLogger.error).toHaveBeenCalledWith(error.message, { + error: { + code: 'UNKNOWN_ERROR', + message: error.message, + stack_trace: error.stack, + type: 'Error', + }, + }); + }); + + it('should use the code and the type if provided', () => { + const error = new Error('Test error'); + const code = 'CUSTOM_ERROR_CODE'; + const type = 'CustomErrorType'; + + loggerService.error({ error, code, type }); + + expect(mockLogger.error).toHaveBeenCalledWith(error.message, { + error: { + code, + message: error.message, + stack_trace: error.stack, + type, + }, + }); + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/logger_service/logger_service.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/logger_service/logger_service.ts new file mode 100644 index 0000000000000..f373ff21e25b9 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/logger_service/logger_service.ts @@ -0,0 +1,62 @@ +/* + * 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 { inject, injectable } from 'inversify'; +import type { Logger, LogMessageSource } from '@kbn/logging'; +import { Logger as BaseLogger } from '@kbn/core-di'; +import type { EcsError } from '@elastic/ecs'; + +interface DebugParams { + message: LogMessageSource; +} + +interface InfoParams { + message: LogMessageSource; +} + +interface WarnParams { + message: LogMessageSource; +} + +interface ErrorParams { + error: Error; + code?: string; + type?: string; +} + +@injectable() +export class LoggerService { + constructor(@inject(BaseLogger) private readonly logger: Logger) {} + + public debug({ message }: DebugParams): void { + this.logger.debug(message); + } + + public error({ error, code, type }: ErrorParams): void { + const ecsError = this.buildError({ error, code, type }); + this.logger.error(error.message, { + error: ecsError, + }); + } + + public info({ message }: InfoParams): void { + this.logger.info(message); + } + + public warn({ message }: WarnParams): void { + this.logger.warn(message); + } + + private buildError({ error, code, type }: ErrorParams): EcsError { + return { + code: code ?? 'UNKNOWN_ERROR', + message: error.message, + stack_trace: error.stack, + type: type ?? 'Error', + }; + } +} diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/query_service/query_service.test.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/query_service/query_service.test.ts new file mode 100644 index 0000000000000..b519303a61d55 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/query_service/query_service.test.ts @@ -0,0 +1,199 @@ +/* + * 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 { of, throwError } from 'rxjs'; +import type { Logger } from '@kbn/core/server'; +import type { IScopedSearchClient } from '@kbn/data-plugin/server'; +import type { ESQLSearchResponse } from '@kbn/es-types'; +import { loggerMock } from '@kbn/logging-mocks'; +import { dataPluginMock } from '@kbn/data-plugin/server/mocks'; +import { QueryService } from './query_service'; +import { LoggerService } from '../logger_service/logger_service'; +import { httpServerMock } from '@kbn/core/server/mocks'; + +describe('QueryService', () => { + let mockSearchClient: jest.Mocked; + let mockLogger: jest.Mocked; + let mockLoggerService: LoggerService; + let esqlService: QueryService; + + beforeEach(() => { + // @ts-expect-error - dataPluginMock is not typed correctly + mockSearchClient = dataPluginMock + .createStartContract() + .search.asScoped(httpServerMock.createKibanaRequest({})); + + mockLogger = loggerMock.create(); + mockLoggerService = new LoggerService(mockLogger); + esqlService = new QueryService(mockSearchClient, mockLoggerService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('executeQuery', () => { + const mockQuery = 'FROM .alerts-* | LIMIT 10'; + const mockFilter = { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: '2025-01-01T00:00:00.000Z', + lte: '2025-01-02T00:00:00.000Z', + }, + }, + }, + ], + }, + }; + + const mockParams = [ + { _tstart: '2025-01-01T00:00:00.000Z' }, + { _tend: '2025-01-02T00:00:00.000Z' }, + ]; + + const mockResponse: ESQLSearchResponse = { + columns: [ + { name: '@timestamp', type: 'date' }, + { name: 'rule_id', type: 'keyword' }, + ], + values: [ + [new Date().toISOString(), 'rule-1'], + [new Date().toISOString(), 'rule-2'], + ], + }; + + it('should successfully execute ES|QL query', async () => { + mockSearchClient.search.mockReturnValue( + of({ + isRunning: false, + rawResponse: mockResponse, + }) + ); + + const result = await esqlService.executeQuery({ + query: mockQuery, + filter: mockFilter, + params: mockParams, + }); + + expect(mockSearchClient.search).toHaveBeenCalledTimes(1); + expect(mockSearchClient.search).toHaveBeenCalledWith( + { + params: { + query: mockQuery, + dropNullColumns: false, + filter: mockFilter, + params: mockParams, + }, + }, + { + strategy: 'esql', + } + ); + + expect(result).toEqual(mockResponse); + expect(mockLogger.error).not.toHaveBeenCalled(); + }); + + it('should throw and log error when query execution fails', async () => { + const error = new Error('ES|QL syntax error'); + mockSearchClient.search.mockReturnValue(throwError(() => error)); + + await expect(esqlService.executeQuery({ query: mockQuery })).rejects.toThrow( + 'ES|QL syntax error' + ); + + expect(mockLogger.error).toHaveBeenCalled(); + }); + }); + + describe('queryResponseToRecords', () => { + it('should convert ES|QL response to array of objects', () => { + const mockResponse: ESQLSearchResponse = { + columns: [ + { name: 'rule_id', type: 'keyword' }, + { name: 'alert_series_id', type: 'keyword' }, + { name: '@timestamp', type: 'date' }, + ], + values: [ + ['rule-1', 'series-1', '2026-01-02T10:29:31.019Z'], + ['rule-2', 'series-2', '2026-01-02T10:29:31.019Z'], + ], + }; + + const result = esqlService.queryResponseToRecords(mockResponse); + + expect(result).toHaveLength(2); + expect(result).toEqual([ + { + '@timestamp': '2026-01-02T10:29:31.019Z', + rule_id: 'rule-1', + alert_series_id: 'series-1', + }, + { + '@timestamp': '2026-01-02T10:29:31.019Z', + rule_id: 'rule-2', + alert_series_id: 'series-2', + }, + ]); + }); + + it('should handle missing column names in response', () => { + const mockResponse: ESQLSearchResponse = { + columns: [ + { name: 'rule_id', type: 'keyword' }, + { name: 'alert_series_id', type: 'keyword' }, + ], + values: [ + ['rule-1', 'series-1', '2026-01-02T10:29:31.019Z'], + ['rule-2', 'series-2', '2026-01-02T10:29:31.019Z'], + ], + }; + + const result = esqlService.queryResponseToRecords(mockResponse); + + expect(result).toHaveLength(2); + expect(result).toEqual([ + { + rule_id: 'rule-1', + alert_series_id: 'series-1', + }, + { + rule_id: 'rule-2', + alert_series_id: 'series-2', + }, + ]); + }); + + it('should handle empty values response', () => { + const mockResponse: ESQLSearchResponse = { + columns: [{ name: 'field', type: 'keyword' }], + values: [], + }; + + const result = esqlService.queryResponseToRecords<{ field: string }>(mockResponse); + + expect(result).toHaveLength(0); + expect(result).toEqual([]); + }); + + it('should handle empty columns response', () => { + const mockResponse: ESQLSearchResponse = { + columns: [], + values: [['value']], + }; + + const result = esqlService.queryResponseToRecords<{ field: string }>(mockResponse); + + expect(result).toHaveLength(0); + expect(result).toEqual([]); + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/query_service/query_service.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/query_service/query_service.ts new file mode 100644 index 0000000000000..4a270620a96bf --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/query_service/query_service.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IScopedSearchClient } from '@kbn/data-plugin/server'; +import { ESQL_SEARCH_STRATEGY, isRunningResponse } from '@kbn/data-plugin/common'; +import type { ESQLSearchParams, ESQLSearchResponse } from '@kbn/es-types'; +import type { IKibanaSearchRequest, IKibanaSearchResponse } from '@kbn/search-types'; +import { catchError, filter as rxFilter, lastValueFrom, map, throwError } from 'rxjs'; +import { injectable } from 'inversify'; +import type { LoggerService } from '../logger_service/logger_service'; + +interface ExecuteQueryParams { + query: ESQLSearchParams['query']; + filter?: ESQLSearchParams['filter']; + params?: ESQLSearchParams['params']; +} + +@injectable() +export class QueryService { + constructor( + private readonly searchClient: IScopedSearchClient, + private readonly logger: LoggerService + ) {} + + async executeQuery({ query, filter, params }: ExecuteQueryParams): Promise { + try { + this.logger.debug({ + message: () => + `QueryService: Executing query - ${JSON.stringify({ query, filter, params })}`, + }); + + const request: IKibanaSearchRequest = { + params: { + query, + dropNullColumns: false, + filter, + params, + }, + }; + + const searchResponse = await lastValueFrom( + this.searchClient + .search< + IKibanaSearchRequest, + IKibanaSearchResponse + >(request, { + strategy: ESQL_SEARCH_STRATEGY, + }) + .pipe( + catchError((error) => { + this.logger.error({ + error, + code: 'ESQL_QUERY_ERROR', + type: 'QueryServiceError', + }); + return throwError(() => error); + }), + rxFilter((resp) => !isRunningResponse(resp)), + map((resp) => resp.rawResponse) + ) + ); + + this.logger.debug({ + message: `QueryService: Query executed successfully, returned ${searchResponse.values.length} rows`, + }); + + return searchResponse; + } catch (error) { + this.logger.error({ + error, + code: 'ESQL_QUERY_ERROR', + type: 'QueryServiceError', + }); + + throw error; + } + } + + public queryResponseToRecords>(response: ESQLSearchResponse): T[] { + const objects: T[] = []; + + if (response.columns.length === 0 || response.values.length === 0) { + return []; + } + + for (const row of response.values) { + const object: T = {} as T; + + for (const [columnIndex, value] of row.entries()) { + const columnName = response.columns[columnIndex]?.name as keyof T; + + if (columnName) { + object[columnName] = value as T[keyof T]; + } + } + + objects.push(object); + } + + return objects; + } +} diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/storage_service.test.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/storage_service.test.ts new file mode 100644 index 0000000000000..c94c61d84c15c --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/storage_service.test.ts @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient, Logger } from '@kbn/core/server'; +import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; +import { loggerMock } from '@kbn/logging-mocks'; +import { StorageService } from './storage_service'; +import { LoggerService } from '../logger_service/logger_service'; + +describe('StorageService', () => { + let mockEsClient: jest.Mocked; + let mockLogger: jest.Mocked; + let mockLoggerService: LoggerService; + let storageService: StorageService; + + beforeEach(() => { + mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); + mockLogger = loggerMock.create(); + mockLoggerService = new LoggerService(mockLogger); + storageService = new StorageService(mockEsClient, mockLoggerService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('bulkIndexDocs', () => { + const index = 'my-index'; + const mockDocs = [ + { '@timestamp': '2024-01-01T00:00:00Z', rule_id: 'rule-1', alert_series_id: 'series-1' }, + { '@timestamp': '2024-01-01T00:01:00Z', rule_id: 'rule-2', alert_series_id: 'series-2' }, + ]; + + it('should return early when docs array is empty', async () => { + await storageService.bulkIndexDocs({ index, docs: [] }); + + expect(mockEsClient.bulk).not.toHaveBeenCalled(); + }); + + it('should successfully bulk index documents', async () => { + const mockBulkResponse = { + items: [{ index: { _id: '1', status: 201 } }, { index: { _id: '2', status: 201 } }], + errors: false, + }; + + // @ts-expect-error - not all fields are used + mockEsClient.bulk.mockResolvedValue(mockBulkResponse); + + await storageService.bulkIndexDocs({ index, docs: mockDocs }); + + expect(mockEsClient.bulk).toHaveBeenCalledTimes(1); + expect(mockEsClient.bulk).toHaveBeenCalledWith({ + operations: [ + { index: { _index: index } }, + mockDocs[0], + { index: { _index: index } }, + mockDocs[1], + ], + refresh: 'wait_for', + }); + }); + + it('should format operations correctly for bulk indexing', async () => { + const mockBulkResponse = { + items: [{ index: { _id: '1', status: 201 } }], + errors: false, + }; + + // @ts-expect-error - not all fields are used + mockEsClient.bulk.mockResolvedValue(mockBulkResponse); + + const docs = [mockDocs[0]]; + await storageService.bulkIndexDocs({ index, docs }); + + expect(mockEsClient.bulk).toHaveBeenCalledWith({ + operations: [{ index: { _index: index } }, docs[0]], + refresh: 'wait_for', + }); + }); + + it('should log error when bulk response contains errors', async () => { + const mockBulkResponse = { + items: [ + { index: { _id: '1', status: 201 } }, + { + index: { + _id: '2', + status: 400, + error: { + type: 'mapper_parsing_exception', + reason: 'failed to parse', + status: 400, + }, + }, + }, + ], + errors: true, + }; + + // @ts-expect-error - not all fields are used + mockEsClient.bulk.mockResolvedValue(mockBulkResponse); + + await storageService.bulkIndexDocs({ index, docs: mockDocs }); + + expect(mockLogger.error).toHaveBeenCalled(); + }); + + it('should handle bulk response with errors but no error items gracefully', async () => { + const mockBulkResponse = { + items: [{ index: { _id: '1', status: 201 } }], + errors: true, + took: 5, + }; + + // @ts-expect-error - not all fields are used + mockEsClient.bulk.mockResolvedValue(mockBulkResponse); + + await storageService.bulkIndexDocs({ index, docs: [mockDocs[0]] }); + + expect(mockLogger.error).not.toHaveBeenCalled(); + }); + + it('should throw error and log when bulk operation fails', async () => { + const error = new Error('Elasticsearch connection failed'); + mockEsClient.bulk.mockRejectedValue(error); + + await expect(storageService.bulkIndexDocs({ index, docs: mockDocs })).rejects.toThrow( + 'Elasticsearch connection failed' + ); + + expect(mockLogger.error).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/storage_service.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/storage_service.ts new file mode 100644 index 0000000000000..9b780bd63a339 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/storage_service.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { BulkRequest, BulkResponse } from '@elastic/elasticsearch/lib/api/types'; +import type { ElasticsearchClient } from '@kbn/core/server'; +import { injectable } from 'inversify'; +import type { LoggerService } from '../logger_service/logger_service'; + +interface BulkIndexDocsParams> { + index: string; + docs: TDocument[]; +} + +@injectable() +export class StorageService { + constructor( + private readonly esClient: ElasticsearchClient, + private readonly logger: LoggerService + ) {} + + public async bulkIndexDocs>({ + index, + docs, + }: BulkIndexDocsParams): Promise { + if (docs.length === 0) { + return; + } + + const operations: NonNullable['operations']> = docs.flatMap((doc) => [ + { index: { _index: index } }, + doc, + ]); + + try { + const response = await this.esClient.bulk({ + operations, + refresh: 'wait_for', + }); + + this.logBulkIndexResponse({ index, docsCount: docs.length, response }); + } catch (error) { + this.logger.error({ + error, + code: 'BULK_INDEX_ERROR', + type: 'StorageServiceError', + }); + + throw error; + } + } + + private logBulkIndexResponse({ + index, + docsCount, + response, + }: { + index: string; + docsCount: number; + response: BulkResponse; + }): void { + this.logFirstBulkIndexItemError(response); + const message = this.getBulkIndexDebugMessage({ index, docsCount, response }); + this.logger.debug({ message }); + } + + private logFirstBulkIndexItemError(response: BulkResponse): void { + if (!response.errors) { + return; + } + + const firstErrorItem = response.items.find((item) => item.index?.error); + if (!firstErrorItem) { + return; + } + + const error = firstErrorItem.index?.error; + this.logger.error({ + error: new Error(`[${error?.type ?? 'UNKNOWN_ERROR'}] ${error?.reason ?? 'UNKNOWN_REASON'}`), + code: 'BULK_INDEX_ERROR', + type: 'StorageServiceError', + }); + } + + private getBulkIndexDebugMessage({ + index, + docsCount, + response, + }: { + index: string; + docsCount: number; + response: BulkResponse; + }): string { + const failedItemCount = response.items.filter((item) => item.index?.error).length; + + if (!response.errors) { + return `StorageService: Successfully bulk indexed ${docsCount} documents to index: ${index}`; + } + + const successItemCount = docsCount - failedItemCount; + return `StorageService: Bulk index completed with errors for index: ${index} (successful: ${successItemCount}, failed: ${failedItemCount}, total: ${docsCount})`; + } +} diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/tokens.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/tokens.ts new file mode 100644 index 0000000000000..f882332d491bf --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/tokens.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 type { ServiceIdentifier } from 'inversify'; +import type { StorageService } from './storage_service'; + +/** + * StorageService flavor that uses an Elasticsearch client scoped to the current request user: + * `elasticsearch.client.asScoped(request).asCurrentUser` + */ +export const StorageServiceScopedToken = Symbol.for( + 'alerting_v2.StorageServiceScoped' +) as ServiceIdentifier; + +/** + * StorageService flavor that uses the internal Kibana system user: + * `elasticsearch.client.asInternalUser` + */ +export const StorageServiceInternalToken = Symbol.for( + 'alerting_v2.StorageServiceInternal' +) as ServiceIdentifier; diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_on_setup.ts b/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_on_setup.ts new file mode 100644 index 0000000000000..3684bcf137168 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_on_setup.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 type { ContainerModuleLoadOptions } from 'inversify'; +import type { CoreStart } from '@kbn/core/server'; +import { Logger, OnSetup, PluginSetup } from '@kbn/core-di'; +import { CoreSetup, PluginInitializer } from '@kbn/core-di-server'; +import { initializeRuleExecutorTaskDefinition } from '../lib/rule_executor'; +import { registerFeaturePrivileges } from '../lib/security/privileges'; +import { AlertingResourcesService } from '../lib/services/alerting_resources_service'; +import type { PluginConfig } from '../config'; +import type { AlertingServerSetupDependencies, AlertingServerStartDependencies } from '../types'; +import { registerSavedObjects } from '../saved_objects'; + +export function bindOnSetup({ bind }: ContainerModuleLoadOptions) { + bind(OnSetup).toConstantValue((container) => { + const logger = container.get(Logger); + const pluginConfig = container.get(PluginInitializer('config')); + const alertingConfig = pluginConfig.get(); + + registerFeaturePrivileges(container.get(PluginSetup('features'))); + + registerSavedObjects({ + savedObjects: container.get(CoreSetup('savedObjects')), + logger, + }); + + const taskManagerSetup = container.get( + PluginSetup('taskManager') + ); + + const getStartServices = container.get(CoreSetup('getStartServices')) as () => Promise< + [CoreStart, AlertingServerStartDependencies, unknown] + >; + + const startServices = getStartServices(); + + const resourcesService = container.get(AlertingResourcesService); + resourcesService.startInitialization({ + enabled: alertingConfig.enabled, + }); + + initializeRuleExecutorTaskDefinition( + logger, + taskManagerSetup, + startServices, + alertingConfig, + resourcesService + ); + }); +} diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_routes.ts b/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_routes.ts new file mode 100644 index 0000000000000..d57a9c4d7207f --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_routes.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ContainerModuleLoadOptions } from 'inversify'; +import { Route } from '@kbn/core-di-server'; +import { CreateRuleRoute } from '../routes/create_rule_route'; +import { UpdateRuleRoute } from '../routes/update_rule_route'; +import { GetRulesRoute } from '../routes/get_rules_route'; +import { GetRuleRoute } from '../routes/get_rule_route'; +import { DeleteRuleRoute } from '../routes/delete_rule_route'; + +export function bindRoutes({ bind }: ContainerModuleLoadOptions) { + bind(Route).toConstantValue(CreateRuleRoute); + bind(Route).toConstantValue(UpdateRuleRoute); + bind(Route).toConstantValue(GetRulesRoute); + bind(Route).toConstantValue(GetRuleRoute); + bind(Route).toConstantValue(DeleteRuleRoute); +} diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_services.ts b/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_services.ts new file mode 100644 index 0000000000000..144ad9cf2d917 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_services.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ContainerModuleLoadOptions } from 'inversify'; +import { PluginStart } from '@kbn/core-di'; +import { CoreStart, Request } from '@kbn/core-di-server'; +import { RulesClient } from '../lib/rules_client'; +import { AlertingResourcesService } from '../lib/services/alerting_resources_service'; +import { LoggerService } from '../lib/services/logger_service/logger_service'; +import { QueryService } from '../lib/services/query_service/query_service'; +import { AlertingRetryService } from '../lib/services/retry_service'; +import { StorageService } from '../lib/services/storage_service/storage_service'; +import { + StorageServiceInternalToken, + StorageServiceScopedToken, +} from '../lib/services/storage_service/tokens'; +import type { AlertingServerStartDependencies } from '../types'; + +export function bindServices({ bind }: ContainerModuleLoadOptions) { + bind(RulesClient).toSelf().inRequestScope(); + + bind(AlertingRetryService).toSelf().inSingletonScope(); + bind(AlertingResourcesService).toSelf().inSingletonScope(); + bind(LoggerService).toSelf().inSingletonScope(); + + bind(QueryService) + .toDynamicValue(({ get }) => { + const request = get(Request); + const data = get(PluginStart('data')); + const loggerService = get(LoggerService); + const searchClient = data.search.asScoped(request); + return new QueryService(searchClient, loggerService); + }) + .inRequestScope(); + + bind(StorageServiceScopedToken) + .toDynamicValue(({ get }) => { + const request = get(Request); + const elasticsearch = get(CoreStart('elasticsearch')); + const loggerService = get(LoggerService); + const esClient = elasticsearch.client.asScoped(request).asCurrentUser; + return new StorageService(esClient, loggerService); + }) + .inRequestScope(); + + bind(StorageServiceInternalToken) + .toDynamicValue(({ get }) => { + const elasticsearch = get(CoreStart('elasticsearch')); + const loggerService = get(LoggerService); + const esClient = elasticsearch.client.asInternalUser; + return new StorageService(esClient, loggerService); + }) + .inSingletonScope(); +} diff --git a/x-pack/platform/plugins/shared/alerting_v2/tsconfig.json b/x-pack/platform/plugins/shared/alerting_v2/tsconfig.json index dee4429f877cc..735ee3b7577d5 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/tsconfig.json +++ b/x-pack/platform/plugins/shared/alerting_v2/tsconfig.json @@ -32,7 +32,9 @@ "@kbn/core-http-server-mocks", "@kbn/core-saved-objects-server-mocks", "@kbn/core-saved-objects-api-server-mocks", - "@kbn/core-security-common" + "@kbn/core-security-common", + "@kbn/core-elasticsearch-server-mocks", + "@kbn/logging-mocks" ], "exclude": [ "target/**/*"