From 19de94e201ff2a108aaeda2ad54fed9902701097 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 8 Jan 2026 17:50:23 +0200 Subject: [PATCH 01/10] Create basic services --- .../server/lib/services/esql_service.test.ts | 155 ++++++++++++++++++ .../server/lib/services/esql_service.ts | 74 +++++++++ .../lib/services/logger_service.test.ts | 70 ++++++++ .../server/lib/services/logger_service.ts | 45 +++++ .../lib/services/storage_service.test.ts | 138 ++++++++++++++++ .../server/lib/services/storage_service.ts | 71 ++++++++ 6 files changed, 553 insertions(+) create mode 100644 x-pack/platform/plugins/shared/alerting_v2/server/lib/services/esql_service.test.ts create mode 100644 x-pack/platform/plugins/shared/alerting_v2/server/lib/services/esql_service.ts create mode 100644 x-pack/platform/plugins/shared/alerting_v2/server/lib/services/logger_service.test.ts create mode 100644 x-pack/platform/plugins/shared/alerting_v2/server/lib/services/logger_service.ts create mode 100644 x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service.test.ts create mode 100644 x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service.ts diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/esql_service.test.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/esql_service.test.ts new file mode 100644 index 0000000000000..d056cded7111f --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/esql_service.test.ts @@ -0,0 +1,155 @@ +/* + * 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 type { EsqlEsqlResult } from '@elastic/elasticsearch/lib/api/types'; +import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; +import { loggerMock } from '@kbn/logging-mocks'; +import { EsqlService } from './esql_service'; +import { LoggerService } from './logger_service'; + +describe('EsqlService', () => { + let mockEsClient: jest.Mocked; + let mockLogger: jest.Mocked; + let mockLoggerService: LoggerService; + let esqlService: EsqlService; + + beforeEach(() => { + mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); + mockLogger = loggerMock.create(); + mockLoggerService = new LoggerService(mockLogger); + esqlService = new EsqlService(mockEsClient, mockLoggerService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('executeQuery', () => { + const mockQuery = 'FROM .alerts-* | LIMIT 10'; + + const mockResponse: EsqlEsqlResult = { + 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 () => { + mockEsClient.esql.query = jest.fn().mockResolvedValue(mockResponse); + + const result = await esqlService.executeQuery({ query: mockQuery }); + + expect(mockEsClient.esql.query).toHaveBeenCalledTimes(1); + expect(mockEsClient.esql.query).toHaveBeenCalledWith({ + query: mockQuery, + drop_null_columns: false, + }); + + 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'); + mockEsClient.esql.query = jest.fn().mockRejectedValue(error); + + await expect(esqlService.executeQuery({ query: mockQuery })).rejects.toThrow( + 'ES|QL syntax error' + ); + + expect(mockLogger.error).toHaveBeenCalled(); + }); + }); + + describe('queryResponseToObject', () => { + it('should convert ES|QL response to array of objects', () => { + const mockResponse: EsqlEsqlResult = { + 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.queryResponseToObject(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: EsqlEsqlResult = { + 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.queryResponseToObject(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: EsqlEsqlResult = { + columns: [{ name: 'field', type: 'keyword' }], + values: [], + }; + + const result = esqlService.queryResponseToObject<{ field: string }>(mockResponse); + + expect(result).toHaveLength(0); + expect(result).toEqual([]); + }); + + it('should handle empty columns response', () => { + const mockResponse: EsqlEsqlResult = { + columns: [], + values: [['value']], + }; + + const result = esqlService.queryResponseToObject<{ field: string }>(mockResponse); + + expect(result).toHaveLength(0); + expect(result).toEqual([]); + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/esql_service.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/esql_service.ts new file mode 100644 index 0000000000000..347d35ccd63a5 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/esql_service.ts @@ -0,0 +1,74 @@ +/* + * 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 { ElasticsearchClient } from '@kbn/core-di'; +import type { EsqlEsqlResult } from '@elastic/elasticsearch/lib/api/types'; +import { LoggerService } from './logger_service'; + +interface ExecuteEsqlQueryParams { + query: string; +} + +@injectable() +export class EsqlService { + constructor( + @inject(ElasticsearchClient) private readonly esClient: ElasticsearchClient, + @inject(LoggerService) private readonly logger: LoggerService + ) {} + + async executeQuery({ query }: ExecuteEsqlQueryParams): Promise { + try { + this.logger.debug({ message: 'EsqlService: Executing ES|QL query' }); + + const queryRequest = { + query, + drop_null_columns: false, + }; + + const response = await this.esClient.esql.query(queryRequest); + + this.logger.debug({ + message: `EsqlService: Query executed successfully, returned ${response.values.length} rows`, + }); + + return response; + } catch (error) { + this.logger.error({ + error, + code: 'ESQL_QUERY_ERROR', + type: 'EsqlServiceError', + }); + + throw error; + } + } + + public queryResponseToObject>(response: EsqlEsqlResult): 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/logger_service.test.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/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.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.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/logger_service.ts new file mode 100644 index 0000000000000..86fb3777eae09 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/logger_service.ts @@ -0,0 +1,45 @@ +/* + * 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 { Logger } from '@kbn/core-di'; +import type { EcsError } from '@elastic/ecs'; + +interface DebugParams { + message: string; +} + +interface ErrorParams { + error: Error; + code?: string; + type?: string; +} + +@injectable() +export class LoggerService { + constructor(@inject(Logger) 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, + }); + } + + 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/storage_service.test.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service.test.ts new file mode 100644 index 0000000000000..6b18896fa59d4 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/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'; + +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.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service.ts new file mode 100644 index 0000000000000..847dabb0c6689 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service.ts @@ -0,0 +1,71 @@ +/* + * 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 { ElasticsearchClient } from '@kbn/core-di'; +import type { BulkResponse } from '@elastic/elasticsearch/lib/api/types'; +import { LoggerService } from './logger_service'; + +interface BulkIndexDocsParams { + index: string; + docs: Record[]; +} + +@injectable() +export class StorageService { + constructor( + @inject(ElasticsearchClient) private readonly esClient: ElasticsearchClient, + @inject(LoggerService) private readonly logger: LoggerService + ) {} + + public async bulkIndexDocs({ index, docs }: BulkIndexDocsParams): Promise { + if (docs.length === 0) { + return; + } + + const operations = docs.flatMap((doc) => [{ index: { _index: index } }, doc]); + + try { + const response = await this.esClient.bulk({ + operations, + refresh: 'wait_for', + }); + + this.logFirstError(response); + + this.logger.debug({ + message: `StorageService: Successfully bulk indexed ${docs.length} documents to index: ${index}`, + }); + } catch (error) { + this.logger.error({ + error, + code: 'BULK_INDEX_ERROR', + type: 'StorageServiceError', + }); + + throw error; + } + } + + private logFirstError(response: BulkResponse): void { + if (response.errors) { + const firstErrorItem = response.items.find((item) => item.index?.error); + + if (firstErrorItem) { + 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', + }); + } + } + } +} From 13cd8364ffa7f6369ae5841c0359d9127751cc2f Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 8 Jan 2026 17:56:18 +0200 Subject: [PATCH 02/10] Instatiate the services in the plugin --- x-pack/platform/plugins/shared/alerting_v2/server/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) 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 15db2eef1abfc..c78e3e2546859 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/index.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/index.ts @@ -19,6 +19,9 @@ import { CreateRuleRoute } from './routes/create_rule_route'; import { UpdateRuleRoute } from './routes/update_rule_route'; import { initializeRuleExecutorTaskDefinition } from './lib/rule_executor'; import { AlertingResourcesService } from './lib/services/alerting_resources_service'; +import { LoggerService } from './lib/services/logger_service'; +import { StorageService } from './lib/services/storage_service'; +import { EsqlService } from './lib/services/esql_service'; import { registerSavedObjects } from './saved_objects'; import type { AlertingServerStartDependencies } from './types'; @@ -37,6 +40,9 @@ export const module = new ContainerModule(({ bind }) => { // Singleton services bind(AlertingRetryService).toSelf().inSingletonScope(); bind(AlertingResourcesService).toSelf().inSingletonScope(); + bind(LoggerService).toSelf().inSingletonScope(); + bind(StorageService).toSelf().inSingletonScope(); + bind(EsqlService).toSelf().inSingletonScope(); bind(OnSetup).toConstantValue((container) => { const logger = container.get(Logger); From 64e54e54cc6ba422f7dd7d8b4881165c2491e752 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:13:48 +0000 Subject: [PATCH 03/10] Changes from node scripts/lint_ts_projects --fix --- x-pack/platform/plugins/shared/alerting_v2/tsconfig.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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/**/*" From 587c1593c2736ae09712589a808a52629586ad41 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 9 Jan 2026 13:34:07 +0200 Subject: [PATCH 04/10] Switch to factories --- .../shared/alerting_v2/server/index.ts | 11 +- .../server/lib/services/esql_service.ts | 74 ------------- .../server/lib/services/logger_service.ts | 21 +++- .../query_service.test.ts} | 98 ++++++++++++----- .../services/query_service/query_service.ts | 101 ++++++++++++++++++ .../query_service/query_service_factory.ts | 26 +++++ .../storage_service.test.ts | 2 +- .../{ => storage_service}/storage_service.ts | 10 +- .../storage_service_factory.ts | 31 ++++++ 9 files changed, 257 insertions(+), 117 deletions(-) delete mode 100644 x-pack/platform/plugins/shared/alerting_v2/server/lib/services/esql_service.ts rename x-pack/platform/plugins/shared/alerting_v2/server/lib/services/{esql_service.test.ts => query_service/query_service.test.ts} (56%) create mode 100644 x-pack/platform/plugins/shared/alerting_v2/server/lib/services/query_service/query_service.ts create mode 100644 x-pack/platform/plugins/shared/alerting_v2/server/lib/services/query_service/query_service_factory.ts rename x-pack/platform/plugins/shared/alerting_v2/server/lib/services/{ => storage_service}/storage_service.test.ts (98%) rename x-pack/platform/plugins/shared/alerting_v2/server/lib/services/{ => storage_service}/storage_service.ts (84%) create mode 100644 x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/storage_service_factory.ts 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 c78e3e2546859..8ecafea2cad6b 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/index.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/index.ts @@ -20,8 +20,8 @@ import { UpdateRuleRoute } from './routes/update_rule_route'; import { initializeRuleExecutorTaskDefinition } from './lib/rule_executor'; import { AlertingResourcesService } from './lib/services/alerting_resources_service'; import { LoggerService } from './lib/services/logger_service'; -import { StorageService } from './lib/services/storage_service'; -import { EsqlService } from './lib/services/esql_service'; +import { QueryServiceFactory } from './lib/services/query_service/query_service_factory'; +import { StorageServiceFactory } from './lib/services/storage_service/storage_service_factory'; import { registerSavedObjects } from './saved_objects'; import type { AlertingServerStartDependencies } from './types'; @@ -30,19 +30,16 @@ export const config: PluginConfigDescriptor = { }; export const module = new ContainerModule(({ bind }) => { - // Register HTTP routes via DI bind(Route).toConstantValue(CreateRuleRoute); bind(Route).toConstantValue(UpdateRuleRoute); - // Request-scoped rules client bind(RulesClient).toSelf().inRequestScope(); - // Singleton services bind(AlertingRetryService).toSelf().inSingletonScope(); bind(AlertingResourcesService).toSelf().inSingletonScope(); bind(LoggerService).toSelf().inSingletonScope(); - bind(StorageService).toSelf().inSingletonScope(); - bind(EsqlService).toSelf().inSingletonScope(); + bind(QueryServiceFactory).toSelf().inSingletonScope(); + bind(StorageServiceFactory).toSelf().inSingletonScope(); bind(OnSetup).toConstantValue((container) => { const logger = container.get(Logger); diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/esql_service.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/esql_service.ts deleted file mode 100644 index 347d35ccd63a5..0000000000000 --- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/esql_service.ts +++ /dev/null @@ -1,74 +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 { inject, injectable } from 'inversify'; -import { ElasticsearchClient } from '@kbn/core-di'; -import type { EsqlEsqlResult } from '@elastic/elasticsearch/lib/api/types'; -import { LoggerService } from './logger_service'; - -interface ExecuteEsqlQueryParams { - query: string; -} - -@injectable() -export class EsqlService { - constructor( - @inject(ElasticsearchClient) private readonly esClient: ElasticsearchClient, - @inject(LoggerService) private readonly logger: LoggerService - ) {} - - async executeQuery({ query }: ExecuteEsqlQueryParams): Promise { - try { - this.logger.debug({ message: 'EsqlService: Executing ES|QL query' }); - - const queryRequest = { - query, - drop_null_columns: false, - }; - - const response = await this.esClient.esql.query(queryRequest); - - this.logger.debug({ - message: `EsqlService: Query executed successfully, returned ${response.values.length} rows`, - }); - - return response; - } catch (error) { - this.logger.error({ - error, - code: 'ESQL_QUERY_ERROR', - type: 'EsqlServiceError', - }); - - throw error; - } - } - - public queryResponseToObject>(response: EsqlEsqlResult): 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/logger_service.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/logger_service.ts index 86fb3777eae09..c7c638aa95610 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/logger_service.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/logger_service.ts @@ -6,13 +6,22 @@ */ import { inject, injectable } from 'inversify'; -import { Logger } from '@kbn/core-di'; +import type { Logger } from '@kbn/logging'; +import { Logger as BaseLogger } from '@kbn/core-di'; import type { EcsError } from '@elastic/ecs'; interface DebugParams { message: string; } +interface InfoParams { + message: string; +} + +interface WarnParams { + message: string; +} + interface ErrorParams { error: Error; code?: string; @@ -21,7 +30,7 @@ interface ErrorParams { @injectable() export class LoggerService { - constructor(@inject(Logger) private readonly logger: Logger) {} + constructor(@inject(BaseLogger) private readonly logger: Logger) {} public debug({ message }: DebugParams): void { this.logger.debug(message); @@ -34,6 +43,14 @@ export class LoggerService { }); } + 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', diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/esql_service.test.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/query_service/query_service.test.ts similarity index 56% rename from x-pack/platform/plugins/shared/alerting_v2/server/lib/services/esql_service.test.ts rename to x-pack/platform/plugins/shared/alerting_v2/server/lib/services/query_service/query_service.test.ts index d056cded7111f..28689624f4f18 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/esql_service.test.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/query_service/query_service.test.ts @@ -5,24 +5,31 @@ * 2.0. */ -import type { ElasticsearchClient, Logger } from '@kbn/core/server'; -import type { EsqlEsqlResult } from '@elastic/elasticsearch/lib/api/types'; -import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; +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 { EsqlService } from './esql_service'; -import { LoggerService } from './logger_service'; +import { dataPluginMock } from '@kbn/data-plugin/server/mocks'; +import { QueryService } from './query_service'; +import { LoggerService } from '../logger_service'; +import { httpServerMock } from '@kbn/core/server/mocks'; -describe('EsqlService', () => { - let mockEsClient: jest.Mocked; +describe('QueryService', () => { + let mockSearchClient: jest.Mocked; let mockLogger: jest.Mocked; let mockLoggerService: LoggerService; - let esqlService: EsqlService; + let esqlService: QueryService; beforeEach(() => { - mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); + // @ts-expect-error - dataPluginMock is not typed correctly + mockSearchClient = dataPluginMock + .createStartContract() + .search.asScoped(httpServerMock.createKibanaRequest({})); + mockLogger = loggerMock.create(); mockLoggerService = new LoggerService(mockLogger); - esqlService = new EsqlService(mockEsClient, mockLoggerService); + esqlService = new QueryService(mockSearchClient, mockLoggerService); }); afterEach(() => { @@ -31,8 +38,27 @@ describe('EsqlService', () => { 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 mockResponse: EsqlEsqlResult = { + 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' }, @@ -44,23 +70,41 @@ describe('EsqlService', () => { }; it('should successfully execute ES|QL query', async () => { - mockEsClient.esql.query = jest.fn().mockResolvedValue(mockResponse); - - const result = await esqlService.executeQuery({ query: mockQuery }); + mockSearchClient.search.mockReturnValue( + of({ + isRunning: false, + rawResponse: mockResponse, + }) + ); - expect(mockEsClient.esql.query).toHaveBeenCalledTimes(1); - expect(mockEsClient.esql.query).toHaveBeenCalledWith({ + const result = await esqlService.executeQuery({ query: mockQuery, - drop_null_columns: false, + 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'); - mockEsClient.esql.query = jest.fn().mockRejectedValue(error); + mockSearchClient.search.mockReturnValue(throwError(() => error)); await expect(esqlService.executeQuery({ query: mockQuery })).rejects.toThrow( 'ES|QL syntax error' @@ -70,9 +114,9 @@ describe('EsqlService', () => { }); }); - describe('queryResponseToObject', () => { + describe('queryResponseToRecords', () => { it('should convert ES|QL response to array of objects', () => { - const mockResponse: EsqlEsqlResult = { + const mockResponse: ESQLSearchResponse = { columns: [ { name: 'rule_id', type: 'keyword' }, { name: 'alert_series_id', type: 'keyword' }, @@ -84,7 +128,7 @@ describe('EsqlService', () => { ], }; - const result = esqlService.queryResponseToObject(mockResponse); + const result = esqlService.queryResponseToRecords(mockResponse); expect(result).toHaveLength(2); expect(result).toEqual([ @@ -102,7 +146,7 @@ describe('EsqlService', () => { }); it('should handle missing column names in response', () => { - const mockResponse: EsqlEsqlResult = { + const mockResponse: ESQLSearchResponse = { columns: [ { name: 'rule_id', type: 'keyword' }, { name: 'alert_series_id', type: 'keyword' }, @@ -113,7 +157,7 @@ describe('EsqlService', () => { ], }; - const result = esqlService.queryResponseToObject(mockResponse); + const result = esqlService.queryResponseToRecords(mockResponse); expect(result).toHaveLength(2); expect(result).toEqual([ @@ -129,24 +173,24 @@ describe('EsqlService', () => { }); it('should handle empty values response', () => { - const mockResponse: EsqlEsqlResult = { + const mockResponse: ESQLSearchResponse = { columns: [{ name: 'field', type: 'keyword' }], values: [], }; - const result = esqlService.queryResponseToObject<{ field: string }>(mockResponse); + const result = esqlService.queryResponseToRecords<{ field: string }>(mockResponse); expect(result).toHaveLength(0); expect(result).toEqual([]); }); it('should handle empty columns response', () => { - const mockResponse: EsqlEsqlResult = { + const mockResponse: ESQLSearchResponse = { columns: [], values: [['value']], }; - const result = esqlService.queryResponseToObject<{ field: string }>(mockResponse); + 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..8dd12d5581d05 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/query_service/query_service.ts @@ -0,0 +1,101 @@ +/* + * 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 type { LoggerService } from '../logger_service'; + +interface ExecuteQueryParams { + query: ESQLSearchParams['query']; + filter?: ESQLSearchParams['filter']; + params?: ESQLSearchParams['params']; +} + +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' }); + + 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/query_service/query_service_factory.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/query_service/query_service_factory.ts new file mode 100644 index 0000000000000..4cc9bfe9e8de1 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/query_service/query_service_factory.ts @@ -0,0 +1,26 @@ +/* + * 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 { KibanaRequest } from '@kbn/core-http-server'; +import { PluginStart } from '@kbn/core-di'; +import type { PluginStart as DataPluginStart } from '@kbn/data-plugin/server'; +import { QueryService } from './query_service'; +import { LoggerService } from '../logger_service'; + +@injectable() +export class QueryServiceFactory { + constructor( + @inject(PluginStart('data')) private readonly data: DataPluginStart, + @inject(LoggerService) private readonly loggerService: LoggerService + ) {} + + public createAsScoped(request: KibanaRequest): QueryService { + const searchClient = this.data.search.asScoped(request); + return new QueryService(searchClient, this.loggerService); + } +} diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service.test.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/storage_service.test.ts similarity index 98% rename from x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service.test.ts rename to x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/storage_service.test.ts index 6b18896fa59d4..348ca04280b10 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service.test.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/storage_service.test.ts @@ -9,7 +9,7 @@ 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'; +import { LoggerService } from '../logger_service'; describe('StorageService', () => { let mockEsClient: jest.Mocked; diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/storage_service.ts similarity index 84% rename from x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service.ts rename to x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/storage_service.ts index 847dabb0c6689..0fa300e609a32 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/storage_service.ts @@ -5,21 +5,19 @@ * 2.0. */ -import { inject, injectable } from 'inversify'; -import { ElasticsearchClient } from '@kbn/core-di'; import type { BulkResponse } from '@elastic/elasticsearch/lib/api/types'; -import { LoggerService } from './logger_service'; +import type { ElasticsearchClient } from '@kbn/core/server'; +import type { LoggerService } from '../logger_service'; interface BulkIndexDocsParams { index: string; docs: Record[]; } -@injectable() export class StorageService { constructor( - @inject(ElasticsearchClient) private readonly esClient: ElasticsearchClient, - @inject(LoggerService) private readonly logger: LoggerService + private readonly esClient: ElasticsearchClient, + private readonly logger: LoggerService ) {} public async bulkIndexDocs({ index, docs }: BulkIndexDocsParams): Promise { diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/storage_service_factory.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/storage_service_factory.ts new file mode 100644 index 0000000000000..8cc9e2dcd3022 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/storage_service_factory.ts @@ -0,0 +1,31 @@ +/* + * 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 { KibanaRequest } from '@kbn/core-http-server'; +import type { ElasticsearchServiceStart } from '@kbn/core/server'; +import { CoreStart } from '@kbn/core-di-server'; +import { StorageService } from './storage_service'; +import { LoggerService } from '../logger_service'; + +@injectable() +export class StorageServiceFactory { + constructor( + @inject(CoreStart('elasticsearch')) private readonly elasticsearch: ElasticsearchServiceStart, + @inject(LoggerService) private readonly loggerService: LoggerService + ) {} + + public createAsScoped(request: KibanaRequest): StorageService { + const esClient = this.elasticsearch.client.asScoped(request).asCurrentUser; + return new StorageService(esClient, this.loggerService); + } + + public createAsInternal(): StorageService { + const esClient = this.elasticsearch.client.asInternalUser; + return new StorageService(esClient, this.loggerService); + } +} From ad0b2ae4ff26ab41ffc57c53de14ccedccb0d81f Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 9 Jan 2026 18:43:38 +0200 Subject: [PATCH 05/10] Remove factories and use inversify --- .../alerting_v2/server/di/bind_on_setup.ts | 57 +++++++++++++++ .../alerting_v2/server/di/bind_routes.ts | 16 +++++ .../alerting_v2/server/di/bind_services.ts | 59 +++++++++++++++ .../shared/alerting_v2/server/index.ts | 72 +++---------------- .../logger_service.test.ts | 0 .../{ => logger_service}/logger_service.ts | 0 .../query_service/query_service.test.ts | 2 +- .../services/query_service/query_service.ts | 4 +- .../query_service/query_service_factory.ts | 26 ------- .../storage_service/storage_service.test.ts | 2 +- .../storage_service/storage_service.ts | 4 +- .../storage_service_factory.ts | 31 -------- .../lib/services/storage_service/tokens.ts | 25 +++++++ 13 files changed, 173 insertions(+), 125 deletions(-) create mode 100644 x-pack/platform/plugins/shared/alerting_v2/server/di/bind_on_setup.ts create mode 100644 x-pack/platform/plugins/shared/alerting_v2/server/di/bind_routes.ts create mode 100644 x-pack/platform/plugins/shared/alerting_v2/server/di/bind_services.ts rename x-pack/platform/plugins/shared/alerting_v2/server/lib/services/{ => logger_service}/logger_service.test.ts (100%) rename x-pack/platform/plugins/shared/alerting_v2/server/lib/services/{ => logger_service}/logger_service.ts (100%) delete mode 100644 x-pack/platform/plugins/shared/alerting_v2/server/lib/services/query_service/query_service_factory.ts delete mode 100644 x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/storage_service_factory.ts create mode 100644 x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/tokens.ts diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/di/bind_on_setup.ts b/x-pack/platform/plugins/shared/alerting_v2/server/di/bind_on_setup.ts new file mode 100644 index 0000000000000..735ad6d732363 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/di/bind_on_setup.ts @@ -0,0 +1,57 @@ +/* + * 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, PluginInitializerContext } from '@kbn/core/server'; +import type { TaskManagerSetupContract } from '@kbn/task-manager-plugin/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 { 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') + ) 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 + ); + }); +} diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/di/bind_routes.ts b/x-pack/platform/plugins/shared/alerting_v2/server/di/bind_routes.ts new file mode 100644 index 0000000000000..cdb1411ccc0c5 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/di/bind_routes.ts @@ -0,0 +1,16 @@ +/* + * 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'; + +export function bindRoutes({ bind }: ContainerModuleLoadOptions) { + bind(Route).toConstantValue(CreateRuleRoute); + bind(Route).toConstantValue(UpdateRuleRoute); +} diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/di/bind_services.ts b/x-pack/platform/plugins/shared/alerting_v2/server/di/bind_services.ts new file mode 100644 index 0000000000000..4e445ab05718b --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/di/bind_services.ts @@ -0,0 +1,59 @@ +/* + * 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 { ElasticsearchServiceStart } from '@kbn/core/server'; +import type { PluginStart as DataPluginStart } from '@kbn/data-plugin/server'; +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'; + +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')) as DataPluginStart; + 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')) as ElasticsearchServiceStart; + 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')) as ElasticsearchServiceStart; + 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/server/index.ts b/x-pack/platform/plugins/shared/alerting_v2/server/index.ts index 8ecafea2cad6b..5c0bb721548ba 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/index.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/index.ts @@ -5,78 +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 { UpdateRuleRoute } from './routes/update_rule_route'; -import { initializeRuleExecutorTaskDefinition } from './lib/rule_executor'; -import { AlertingResourcesService } from './lib/services/alerting_resources_service'; -import { LoggerService } from './lib/services/logger_service'; -import { QueryServiceFactory } from './lib/services/query_service/query_service_factory'; -import { StorageServiceFactory } from './lib/services/storage_service/storage_service_factory'; -import { registerSavedObjects } from './saved_objects'; -import type { AlertingServerStartDependencies } from './types'; +import { bindOnSetup } from './di/bind_on_setup'; +import { bindRoutes } from './di/bind_routes'; +import { bindServices } from './di/bind_services'; export const config: PluginConfigDescriptor = { schema: configSchema, }; -export const module = new ContainerModule(({ bind }) => { - bind(Route).toConstantValue(CreateRuleRoute); - bind(Route).toConstantValue(UpdateRuleRoute); - - bind(RulesClient).toSelf().inRequestScope(); - - bind(AlertingRetryService).toSelf().inSingletonScope(); - bind(AlertingResourcesService).toSelf().inSingletonScope(); - bind(LoggerService).toSelf().inSingletonScope(); - bind(QueryServiceFactory).toSelf().inSingletonScope(); - bind(StorageServiceFactory).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.test.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/logger_service/logger_service.test.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting_v2/server/lib/services/logger_service.test.ts rename to x-pack/platform/plugins/shared/alerting_v2/server/lib/services/logger_service/logger_service.test.ts diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/logger_service.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/logger_service/logger_service.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting_v2/server/lib/services/logger_service.ts rename to x-pack/platform/plugins/shared/alerting_v2/server/lib/services/logger_service/logger_service.ts 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 index 28689624f4f18..b519303a61d55 100644 --- 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 @@ -12,7 +12,7 @@ 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'; +import { LoggerService } from '../logger_service/logger_service'; import { httpServerMock } from '@kbn/core/server/mocks'; describe('QueryService', () => { 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 index 8dd12d5581d05..14c98687f71ed 100644 --- 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 @@ -10,7 +10,8 @@ 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 type { LoggerService } from '../logger_service'; +import { injectable } from 'inversify'; +import type { LoggerService } from '../logger_service/logger_service'; interface ExecuteQueryParams { query: ESQLSearchParams['query']; @@ -18,6 +19,7 @@ interface ExecuteQueryParams { params?: ESQLSearchParams['params']; } +@injectable() export class QueryService { constructor( private readonly searchClient: IScopedSearchClient, diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/query_service/query_service_factory.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/query_service/query_service_factory.ts deleted file mode 100644 index 4cc9bfe9e8de1..0000000000000 --- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/query_service/query_service_factory.ts +++ /dev/null @@ -1,26 +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 { inject, injectable } from 'inversify'; -import type { KibanaRequest } from '@kbn/core-http-server'; -import { PluginStart } from '@kbn/core-di'; -import type { PluginStart as DataPluginStart } from '@kbn/data-plugin/server'; -import { QueryService } from './query_service'; -import { LoggerService } from '../logger_service'; - -@injectable() -export class QueryServiceFactory { - constructor( - @inject(PluginStart('data')) private readonly data: DataPluginStart, - @inject(LoggerService) private readonly loggerService: LoggerService - ) {} - - public createAsScoped(request: KibanaRequest): QueryService { - const searchClient = this.data.search.asScoped(request); - return new QueryService(searchClient, this.loggerService); - } -} 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 index 348ca04280b10..c94c61d84c15c 100644 --- 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 @@ -9,7 +9,7 @@ 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'; +import { LoggerService } from '../logger_service/logger_service'; describe('StorageService', () => { let mockEsClient: jest.Mocked; 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 index 0fa300e609a32..6f2260da3a8bd 100644 --- 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 @@ -7,13 +7,15 @@ import type { BulkResponse } from '@elastic/elasticsearch/lib/api/types'; import type { ElasticsearchClient } from '@kbn/core/server'; -import type { LoggerService } from '../logger_service'; +import { injectable } from 'inversify'; +import type { LoggerService } from '../logger_service/logger_service'; interface BulkIndexDocsParams { index: string; docs: Record[]; } +@injectable() export class StorageService { constructor( private readonly esClient: ElasticsearchClient, diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/storage_service_factory.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/storage_service_factory.ts deleted file mode 100644 index 8cc9e2dcd3022..0000000000000 --- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/storage_service/storage_service_factory.ts +++ /dev/null @@ -1,31 +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 { inject, injectable } from 'inversify'; -import type { KibanaRequest } from '@kbn/core-http-server'; -import type { ElasticsearchServiceStart } from '@kbn/core/server'; -import { CoreStart } from '@kbn/core-di-server'; -import { StorageService } from './storage_service'; -import { LoggerService } from '../logger_service'; - -@injectable() -export class StorageServiceFactory { - constructor( - @inject(CoreStart('elasticsearch')) private readonly elasticsearch: ElasticsearchServiceStart, - @inject(LoggerService) private readonly loggerService: LoggerService - ) {} - - public createAsScoped(request: KibanaRequest): StorageService { - const esClient = this.elasticsearch.client.asScoped(request).asCurrentUser; - return new StorageService(esClient, this.loggerService); - } - - public createAsInternal(): StorageService { - const esClient = this.elasticsearch.client.asInternalUser; - return new StorageService(esClient, this.loggerService); - } -} 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; From d8ed208cf50645d3ec9a0f480a56657013691572 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 9 Jan 2026 18:51:38 +0200 Subject: [PATCH 06/10] Rename di folder --- x-pack/platform/plugins/shared/alerting_v2/server/index.ts | 6 +++--- .../alerting_v2/server/{di => setup}/bind_on_setup.ts | 0 .../shared/alerting_v2/server/{di => setup}/bind_routes.ts | 0 .../alerting_v2/server/{di => setup}/bind_services.ts | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename x-pack/platform/plugins/shared/alerting_v2/server/{di => setup}/bind_on_setup.ts (100%) rename x-pack/platform/plugins/shared/alerting_v2/server/{di => setup}/bind_routes.ts (100%) rename x-pack/platform/plugins/shared/alerting_v2/server/{di => setup}/bind_services.ts (100%) 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 5c0bb721548ba..0201326c043f0 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/index.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/index.ts @@ -9,9 +9,9 @@ import { ContainerModule } from 'inversify'; import type { PluginConfigDescriptor } from '@kbn/core/server'; import type { PluginConfig } from './config'; import { configSchema } from './config'; -import { bindOnSetup } from './di/bind_on_setup'; -import { bindRoutes } from './di/bind_routes'; -import { bindServices } from './di/bind_services'; +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, diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/di/bind_on_setup.ts b/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_on_setup.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting_v2/server/di/bind_on_setup.ts rename to x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_on_setup.ts diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/di/bind_routes.ts b/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_routes.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting_v2/server/di/bind_routes.ts rename to x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_routes.ts diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/di/bind_services.ts b/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_services.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting_v2/server/di/bind_services.ts rename to x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_services.ts From e417b14ea1104948707985b3e84b49e8ea3f4346 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Sun, 11 Jan 2026 21:10:14 +0200 Subject: [PATCH 07/10] Better types --- .../alerting_v2/server/setup/bind_on_setup.ts | 18 ++++++++---------- .../alerting_v2/server/setup/bind_services.ts | 9 ++++----- 2 files changed, 12 insertions(+), 15 deletions(-) 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 index 735ad6d732363..3684bcf137168 100644 --- 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 @@ -6,39 +6,37 @@ */ import type { ContainerModuleLoadOptions } from 'inversify'; -import type { CoreStart, PluginInitializerContext } from '@kbn/core/server'; -import type { TaskManagerSetupContract } from '@kbn/task-manager-plugin/server'; +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 { AlertingServerStartDependencies } from '../types'; +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') - ) as PluginInitializerContext['config']; + const pluginConfig = container.get(PluginInitializer('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 taskManagerSetup = container.get( + PluginSetup('taskManager') + ); + const getStartServices = container.get(CoreSetup('getStartServices')) as () => Promise< [CoreStart, AlertingServerStartDependencies, unknown] >; + const startServices = getStartServices(); const resourcesService = container.get(AlertingResourcesService); 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 index 4e445ab05718b..144ad9cf2d917 100644 --- 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 @@ -6,8 +6,6 @@ */ import type { ContainerModuleLoadOptions } from 'inversify'; -import type { ElasticsearchServiceStart } from '@kbn/core/server'; -import type { PluginStart as DataPluginStart } from '@kbn/data-plugin/server'; import { PluginStart } from '@kbn/core-di'; import { CoreStart, Request } from '@kbn/core-di-server'; import { RulesClient } from '../lib/rules_client'; @@ -20,6 +18,7 @@ import { StorageServiceInternalToken, StorageServiceScopedToken, } from '../lib/services/storage_service/tokens'; +import type { AlertingServerStartDependencies } from '../types'; export function bindServices({ bind }: ContainerModuleLoadOptions) { bind(RulesClient).toSelf().inRequestScope(); @@ -31,7 +30,7 @@ export function bindServices({ bind }: ContainerModuleLoadOptions) { bind(QueryService) .toDynamicValue(({ get }) => { const request = get(Request); - const data = get(PluginStart('data')) as DataPluginStart; + const data = get(PluginStart('data')); const loggerService = get(LoggerService); const searchClient = data.search.asScoped(request); return new QueryService(searchClient, loggerService); @@ -41,7 +40,7 @@ export function bindServices({ bind }: ContainerModuleLoadOptions) { bind(StorageServiceScopedToken) .toDynamicValue(({ get }) => { const request = get(Request); - const elasticsearch = get(CoreStart('elasticsearch')) as ElasticsearchServiceStart; + const elasticsearch = get(CoreStart('elasticsearch')); const loggerService = get(LoggerService); const esClient = elasticsearch.client.asScoped(request).asCurrentUser; return new StorageService(esClient, loggerService); @@ -50,7 +49,7 @@ export function bindServices({ bind }: ContainerModuleLoadOptions) { bind(StorageServiceInternalToken) .toDynamicValue(({ get }) => { - const elasticsearch = get(CoreStart('elasticsearch')) as ElasticsearchServiceStart; + const elasticsearch = get(CoreStart('elasticsearch')); const loggerService = get(LoggerService); const esClient = elasticsearch.client.asInternalUser; return new StorageService(esClient, loggerService); From 802a9ac772c02f80ea81609c1bc2a6dc514e6268 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 12 Jan 2026 11:50:22 +0200 Subject: [PATCH 08/10] Use generics for the Storage service --- .../services/storage_service/storage_service.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) 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 index 6f2260da3a8bd..c790f751fe6f0 100644 --- 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 @@ -5,14 +5,14 @@ * 2.0. */ -import type { BulkResponse } from '@elastic/elasticsearch/lib/api/types'; +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 { +interface BulkIndexDocsParams> { index: string; - docs: Record[]; + docs: TDocument[]; } @injectable() @@ -22,12 +22,18 @@ export class StorageService { private readonly logger: LoggerService ) {} - public async bulkIndexDocs({ index, docs }: BulkIndexDocsParams): Promise { + public async bulkIndexDocs>({ + index, + docs, + }: BulkIndexDocsParams): Promise { if (docs.length === 0) { return; } - const operations = docs.flatMap((doc) => [{ index: { _index: index } }, doc]); + const operations: NonNullable['operations']> = docs.flatMap((doc) => [ + { index: { _index: index } }, + doc, + ]); try { const response = await this.esClient.bulk({ From 401e440640c7bd0074e1e63cca20298fb9f043a5 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 12 Jan 2026 11:58:18 +0200 Subject: [PATCH 09/10] Better logging in storage service --- .../storage_service/storage_service.ts | 65 ++++++++++++++----- 1 file changed, 47 insertions(+), 18 deletions(-) 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 index c790f751fe6f0..9b780bd63a339 100644 --- 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 @@ -41,11 +41,7 @@ export class StorageService { refresh: 'wait_for', }); - this.logFirstError(response); - - this.logger.debug({ - message: `StorageService: Successfully bulk indexed ${docs.length} documents to index: ${index}`, - }); + this.logBulkIndexResponse({ index, docsCount: docs.length, response }); } catch (error) { this.logger.error({ error, @@ -57,21 +53,54 @@ export class StorageService { } } - private logFirstError(response: BulkResponse): void { - if (response.errors) { - const firstErrorItem = response.items.find((item) => item.index?.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; + } - if (firstErrorItem) { - const error = firstErrorItem.index?.error; + 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', + }); + } - 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})`; } } From 280fb89ac4b6aeb56ecbd0f7772dfb6491e6569e Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 12 Jan 2026 12:05:54 +0200 Subject: [PATCH 10/10] Better debug in query service --- .../server/lib/services/logger_service/logger_service.ts | 8 ++++---- .../server/lib/services/query_service/query_service.ts | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) 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 index c7c638aa95610..f373ff21e25b9 100644 --- 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 @@ -6,20 +6,20 @@ */ import { inject, injectable } from 'inversify'; -import type { Logger } from '@kbn/logging'; +import type { Logger, LogMessageSource } from '@kbn/logging'; import { Logger as BaseLogger } from '@kbn/core-di'; import type { EcsError } from '@elastic/ecs'; interface DebugParams { - message: string; + message: LogMessageSource; } interface InfoParams { - message: string; + message: LogMessageSource; } interface WarnParams { - message: string; + message: LogMessageSource; } interface ErrorParams { 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 index 14c98687f71ed..4a270620a96bf 100644 --- 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 @@ -28,7 +28,10 @@ export class QueryService { async executeQuery({ query, filter, params }: ExecuteQueryParams): Promise { try { - this.logger.debug({ message: 'QueryService: Executing query' }); + this.logger.debug({ + message: () => + `QueryService: Executing query - ${JSON.stringify({ query, filter, params })}`, + }); const request: IKibanaSearchRequest = { params: {