diff --git a/packages/kbn-rule-data-utils/src/alerts_as_data_rbac.ts b/packages/kbn-rule-data-utils/src/alerts_as_data_rbac.ts index 795d194ee8a92..d32a7dc99faff 100644 --- a/packages/kbn-rule-data-utils/src/alerts_as_data_rbac.ts +++ b/packages/kbn-rule-data-utils/src/alerts_as_data_rbac.ts @@ -25,6 +25,7 @@ export const AlertConsumers = { OBSERVABILITY: 'observability', SIEM: 'siem', SYNTHETICS: 'synthetics', + ML: 'ml', } as const; export type AlertConsumers = typeof AlertConsumers[keyof typeof AlertConsumers]; export type STATUS_VALUES = 'open' | 'acknowledged' | 'closed'; @@ -36,6 +37,7 @@ export const mapConsumerToIndexName: Record = observability: '.alerts-observability', siem: ['.alerts-security.alerts', '.siem-signals'], synthetics: '.alerts-observability-synthetics', + ml: '.alerts-ml', }; export type ValidFeatureId = keyof typeof mapConsumerToIndexName; diff --git a/x-pack/plugins/ml/common/constants/alerts.ts b/x-pack/plugins/ml/common/constants/alerts.ts index 2192b2b504b59..feb3a5659442e 100644 --- a/x-pack/plugins/ml/common/constants/alerts.ts +++ b/x-pack/plugins/ml/common/constants/alerts.ts @@ -21,8 +21,12 @@ export const TOP_N_BUCKETS_COUNT = 1; export const ALL_JOBS_SELECTION = '*'; -export const HEALTH_CHECK_NAMES: Record = { +export const HEALTH_CHECK_NAMES: Record< + JobsHealthTests, + { id: string; name: string; description: string } +> = { datafeed: { + id: 'datafeed_not_started', name: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.datafeedCheckName', { defaultMessage: 'Datafeed is not started', }), @@ -34,6 +38,7 @@ export const HEALTH_CHECK_NAMES: Record { + const componentTemplateName = ruleDataService.getFullAssetName('ml-mappings'); + + if (!ruleDataService.isWriteEnabled()) { + return; + } + + await ruleDataService.createOrUpdateComponentTemplate({ + name: componentTemplateName, + body: { + template: { + settings: { + number_of_shards: 1, + }, + // Mappings based on {@link AnomalyDetectionJobHealthResult} + mappings: mappingFromFieldMap( + { + [JOB_ID]: { + type: 'keyword', + }, + [JOB_STATE]: { + type: 'keyword', + }, + // datafeed + [DATAFEED_ID]: { + type: 'keyword', + }, + [DATAFEED_STATE]: { + type: 'keyword', + }, + // mml + [MEMORY_STATUS]: { + type: 'keyword', + }, + [MEMORY_LOG_TIME]: { + type: 'date', + }, + [MODEL_BYTES]: { + type: 'long', + }, + [MODEL_BYTES_MEMORY_LIMIT]: { + type: 'long', + }, + [PEAK_MODEL_BYTES]: { + type: 'long', + }, + [MODEL_BYTES_EXCEEDED]: { + type: 'long', + }, + // {@link DelayedDataResponse) + [ANNOTATION]: { + type: 'text', + }, + [MISSED_DOC_COUNT]: { + type: 'long', + }, + [END_TIMESTAMP]: { + type: 'date', + }, + }, + 'strict' + ), + }, + }, + }); + + await ruleDataService.createOrUpdateIndexTemplate({ + name: ruleDataService.getFullAssetName('ml-index-template'), + body: { + index_patterns: [alertsIndexPattern], + composed_of: [ + ruleDataService.getFullAssetName(TECHNICAL_COMPONENT_TEMPLATE_NAME), + componentTemplateName, + ], + }, + }); + await ruleDataService.updateIndexMappingsMatchingPattern(alertsIndexPattern); + }); + + const initializeRuleDataTemplatesPromise = initializeRuleDataTemplates().catch((err) => { + logger!.error(err); + }); + + const ruleDataClient = ruleDataService.getRuleDataClient( + 'ml', + ruleDataService.getFullAssetName('ml'), + () => initializeRuleDataTemplatesPromise + ); + + return ruleDataClient; +} diff --git a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts index b345cf8c1245c..053f5a42e22b1 100644 --- a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts +++ b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts @@ -11,9 +11,34 @@ import type { Logger } from 'kibana/server'; import { MlClient } from '../ml_client'; import { MlJob, MlJobStats } from '@elastic/elasticsearch/api/types'; import { AnnotationService } from '../../models/annotation_service/annotation'; +import { JobsHealthExecutorOptions } from './register_jobs_monitoring_rule_type'; const MOCK_DATE_NOW = 1487076708000; +function getDefaultExecutorOptions(): JobsHealthExecutorOptions { + return ({ + state: {}, + startedAt: new Date('2021-08-12T13:13:39.396Z'), + previousStartedAt: new Date('2021-08-12T13:13:27.396Z'), + spaceId: 'default', + namespace: undefined, + name: 'ml-health-check', + tags: [], + createdBy: 'elastic', + updatedBy: 'elastic', + rule: { + name: 'ml-health-check', + tags: [], + consumer: 'alerts', + producer: 'ml', + ruleTypeId: 'xpack.ml.anomaly_detection_jobs_health', + ruleTypeName: 'Anomaly detection jobs health', + enabled: true, + schedule: { interval: '10s' }, + }, + } as unknown) as JobsHealthExecutorOptions; +} + describe('JobsHealthService', () => { const mlClient = ({ getJobs: jest.fn().mockImplementation(({ job_id: jobIds = [] }) => { @@ -61,7 +86,7 @@ describe('JobsHealthService', () => { state: j === 'test_job_02' || 'test_job_01' ? 'opened' : 'closed', model_size_stats: { memory_status: j === 'test_job_01' ? 'hard_limit' : 'ok', - log_time: 1626935914540, + memory_log_time: 1626935914540, }, }; }) as MlJobStats, @@ -123,11 +148,14 @@ describe('JobsHealthService', () => { debug: jest.fn(), } as unknown) as jest.Mocked; + const ruleDataClient = null; + const jobHealthService: JobsHealthService = jobsHealthServiceProvider( mlClient, datafeedsService, annotationService, - logger + logger, + ruleDataClient ); let dateNowSpy: jest.SpyInstance; @@ -143,7 +171,7 @@ describe('JobsHealthService', () => { test('returns empty results when no jobs provided', async () => { // act - const executionResult = await jobHealthService.getTestsResults('testRule', { + const executionResult = await jobHealthService.getTestsResults(getDefaultExecutorOptions(), { testsConfig: null, includeJobs: { jobIds: ['*'], @@ -151,13 +179,15 @@ describe('JobsHealthService', () => { }, excludeJobs: null, }); - expect(logger.warn).toHaveBeenCalledWith('Rule "testRule" does not have associated jobs.'); + expect(logger.warn).toHaveBeenCalledWith( + 'Rule "ml-health-check" does not have associated jobs.' + ); expect(datafeedsService.getDatafeedByJobId).not.toHaveBeenCalled(); expect(executionResult).toEqual([]); }); test('returns empty results and does not perform datafeed check when test is disabled', async () => { - const executionResult = await jobHealthService.getTestsResults('testRule', { + const executionResult = await jobHealthService.getTestsResults(getDefaultExecutorOptions(), { testsConfig: { datafeed: { enabled: false, @@ -186,7 +216,7 @@ describe('JobsHealthService', () => { }); test('takes into account delayed data params', async () => { - const executionResult = await jobHealthService.getTestsResults('testRule_04', { + const executionResult = await jobHealthService.getTestsResults(getDefaultExecutorOptions(), { testsConfig: { delayedData: { enabled: true, @@ -216,6 +246,7 @@ describe('JobsHealthService', () => { expect(executionResult).toEqual([ { + id: 'delayed_data', name: 'Data delay has occurred', context: { results: [ @@ -234,7 +265,7 @@ describe('JobsHealthService', () => { }); test('returns results based on provided selection', async () => { - const executionResult = await jobHealthService.getTestsResults('testRule_03', { + const executionResult = await jobHealthService.getTestsResults(getDefaultExecutorOptions(), { testsConfig: null, includeJobs: { jobIds: [], @@ -266,6 +297,7 @@ describe('JobsHealthService', () => { expect(executionResult).toEqual([ { + id: 'datafeed_not_started', name: 'Datafeed is not started', context: { results: [ @@ -280,12 +312,12 @@ describe('JobsHealthService', () => { }, }, { + id: 'mml', name: 'Model memory limit reached', context: { results: [ { job_id: 'test_job_01', - log_time: 1626935914540, memory_status: 'hard_limit', }, ], @@ -294,6 +326,7 @@ describe('JobsHealthService', () => { }, }, { + id: 'delayed_data', name: 'Data delay has occurred', context: { results: [ diff --git a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts index 52e17fed7a414..b79ef059aa216 100644 --- a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts +++ b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { memoize, keyBy } from 'lodash'; -import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; +import { keyBy, memoize } from 'lodash'; +import { KibanaRequest, Logger, SavedObjectsClientContract } from 'kibana/server'; import { i18n } from '@kbn/i18n'; -import { Logger } from 'kibana/server'; import { MlJob } from '@elastic/elasticsearch/api/types'; +import { Mutable } from 'utility-types'; +import { ALERT_ID, ALERT_RULE_NAME, EVENT_KIND, TIMESTAMP } from '@kbn/rule-data-utils'; import { MlClient } from '../ml_client'; import { AnomalyDetectionJobsHealthRuleParams, @@ -22,6 +23,7 @@ import { GetGuards } from '../../shared_services/shared_services'; import { AnomalyDetectionJobsHealthAlertContext, DelayedDataResponse, + JobsHealthExecutorOptions, MmlTestResponse, NotStartedDatafeedResponse, } from './register_jobs_monitoring_rule_type'; @@ -33,8 +35,12 @@ import { AnnotationService } from '../../models/annotation_service/annotation'; import { annotationServiceProvider } from '../../models/annotation_service'; import { parseInterval } from '../../../common/util/parse_interval'; import { isDefined } from '../../../common/types/guards'; +import type { IRuleDataClient } from '../../../../rule_registry/server'; +import { getRuleData } from '../../../../rule_registry/server'; +import { ParsedTechnicalFields } from '../../../../rule_registry/common/parse_technical_fields'; interface TestResult { + id: string; name: string; context: AnomalyDetectionJobsHealthAlertContext; } @@ -45,7 +51,8 @@ export function jobsHealthServiceProvider( mlClient: MlClient, datafeedsService: DatafeedsService, annotationService: AnnotationService, - logger: Logger + logger: Logger, + ruleDataClient: IRuleDataClient | null ) { /** * Extracts result list of jobs based on included and excluded selection of jobs and groups. @@ -167,7 +174,7 @@ export function jobsHealthServiceProvider( return { job_id: jobId, memory_status: modelSizeStats.memory_status, - log_time: modelSizeStats.log_time, + memory_log_time: modelSizeStats.log_time, model_bytes: modelSizeStats.model_bytes, model_bytes_memory_limit: modelSizeStats.model_bytes_memory_limit, peak_model_bytes: modelSizeStats.peak_model_bytes, @@ -237,10 +244,10 @@ export function jobsHealthServiceProvider( return annotations; }, /** - * Retrieves report grouped by test. + * Retrieves report grouped by test and updates alerting indices. */ async getTestsResults( - ruleInstanceName: string, + executorOptions: JobsHealthExecutorOptions, { testsConfig, includeJobs, excludeJobs }: AnomalyDetectionJobsHealthRuleParams ): Promise { const config = getResultJobsHealthRuleConfig(testsConfig); @@ -250,8 +257,11 @@ export function jobsHealthServiceProvider( const jobs = await getResultJobs(includeJobs, excludeJobs); const jobIds = getJobIds(jobs); + const ruleExecutorData = getRuleData(executorOptions); + const timestamp = executorOptions.startedAt.toISOString(); + if (jobIds.length === 0) { - logger.warn(`Rule "${ruleInstanceName}" does not have associated jobs.`); + logger.warn(`Rule "${ruleExecutorData[ALERT_RULE_NAME]}" does not have associated jobs.`); return results; } @@ -261,6 +271,7 @@ export function jobsHealthServiceProvider( const response = await this.getNotStartedDatafeeds(jobIds); if (response && response.length > 0) { results.push({ + id: HEALTH_CHECK_NAMES.datafeed.id, name: HEALTH_CHECK_NAMES.datafeed.name, context: { results: response, @@ -283,6 +294,7 @@ export function jobsHealthServiceProvider( }, 0); results.push({ + id: HEALTH_CHECK_NAMES.mml.id, name: HEALTH_CHECK_NAMES.mml.name, context: { results: response, @@ -318,6 +330,7 @@ export function jobsHealthServiceProvider( if (response.length > 0) { results.push({ + id: HEALTH_CHECK_NAMES.delayedData.id, name: HEALTH_CHECK_NAMES.delayedData.name, context: { results: response, @@ -334,6 +347,50 @@ export function jobsHealthServiceProvider( } } + if (ruleDataClient && results.length) { + logger.debug('Updating docs in the alert index'); + + if (ruleDataClient.isWriteEnabled()) { + logger.info('Writing results to the index'); + + const events: Array>> = []; + for (const alertResult of results) { + const checkResults = alertResult.context.results; + for (const checkResult of checkResults) { + const prefixResult = Object.entries(checkResult).reduce((acc, [key, val]) => { + acc[`ml.${key}`] = val; + return acc; + }, {} as Record); + + events.push({ + ...prefixResult, + ...ruleExecutorData, + [TIMESTAMP]: timestamp, + // TODO confirm which event kind to use + [EVENT_KIND]: 'signal', + // we use name for creating alert instances + [ALERT_ID]: alertResult.name, + }); + } + } + + const body = events.flatMap((event) => { + return [{ index: {} }, event]; + }); + + try { + await ruleDataClient.getWriter().bulk({ + body, + }); + logger.debug('.alerts-ml is successfully updated'); + } catch (e) { + logger.error('Unable to update .alerts-ml index', e.meta); + } + } else { + logger.warn('Writing is disabled'); + } + } + return results; }, }; @@ -355,12 +412,13 @@ export function getJobsHealthServiceProvider(getGuards: GetGuards) { return await getGuards(request, savedObjectsClient) .isFullLicense() .hasMlCapabilities(['canGetJobs']) - .ok(({ mlClient, scopedClient }) => + .ok(({ mlClient, scopedClient, ruleDataClient }) => jobsHealthServiceProvider( mlClient, datafeedsProvider(scopedClient, mlClient), annotationServiceProvider(scopedClient), - logger + logger, + ruleDataClient ).getTestsResults(...args) ); }, diff --git a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts index 063d8ad5a8980..8763b82c9071d 100644 --- a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts +++ b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts @@ -6,8 +6,8 @@ */ import { i18n } from '@kbn/i18n'; -import { KibanaRequest } from 'kibana/server'; -import { MlDatafeedState, MlJobState, MlJobStats } from '@elastic/elasticsearch/api/types'; +import type { KibanaRequest } from 'kibana/server'; +import type { MlDatafeedState, MlJobState, MlJobStats } from '@elastic/elasticsearch/api/types'; import { ML_ALERT_TYPES } from '../../../common/constants/alerts'; import { PLUGIN_ID } from '../../../common/constants/app'; import { MINIMUM_FULL_LICENSE } from '../../../common/license'; @@ -15,20 +15,21 @@ import { anomalyDetectionJobsHealthRuleParams, AnomalyDetectionJobsHealthRuleParams, } from '../../routes/schemas/alerting_schema'; -import { RegisterAlertParams } from './register_ml_alerts'; -import { +import type { RegisterAlertParams } from './register_ml_alerts'; +import type { ActionGroup, AlertInstanceContext, AlertInstanceState, AlertTypeState, } from '../../../../alerting/common'; +import type { AlertExecutorOptions } from '../../../../alerting/server'; type ModelSizeStats = MlJobStats['model_size_stats']; export interface MmlTestResponse { job_id: string; memory_status: ModelSizeStats['memory_status']; - log_time: ModelSizeStats['log_time']; + memory_log_time: ModelSizeStats['log_time']; model_bytes: ModelSizeStats['model_bytes']; model_bytes_memory_limit: ModelSizeStats['model_bytes_memory_limit']; peak_model_bytes: ModelSizeStats['peak_model_bytes']; @@ -66,6 +67,14 @@ export const ANOMALY_DETECTION_JOB_REALTIME_ISSUE = 'anomaly_detection_realtime_ export type AnomalyDetectionJobRealtimeIssue = typeof ANOMALY_DETECTION_JOB_REALTIME_ISSUE; +export type JobsHealthExecutorOptions = AlertExecutorOptions< + AnomalyDetectionJobsHealthRuleParams, + Record, + Record, + AnomalyDetectionJobsHealthAlertContext, + AnomalyDetectionJobRealtimeIssue +>; + export const REALTIME_ISSUE_DETECTED: ActionGroup = { id: ANOMALY_DETECTION_JOB_REALTIME_ISSUE, name: i18n.translate('xpack.ml.jobsHealthAlertingRule.actionGroupName', { @@ -120,14 +129,17 @@ export function registerJobsMonitoringRuleType({ producer: PLUGIN_ID, minimumLicenseRequired: MINIMUM_FULL_LICENSE, isExportable: true, - async executor({ services, params, alertId, state, previousStartedAt, startedAt, name, rule }) { + async executor(options) { + const { services, params, name } = options; + const fakeRequest = {} as KibanaRequest; const { getTestsResults } = mlServicesProviders.jobsHealthServiceProvider( services.savedObjectsClient, fakeRequest, logger ); - const executionResult = await getTestsResults(name, params); + + const executionResult = await getTestsResults(options, params); if (executionResult.length > 0) { logger.info( diff --git a/x-pack/plugins/ml/server/lib/alerts/register_ml_alerts.ts b/x-pack/plugins/ml/server/lib/alerts/register_ml_alerts.ts index 6f1e000c9a430..00e10e0237197 100644 --- a/x-pack/plugins/ml/server/lib/alerts/register_ml_alerts.ts +++ b/x-pack/plugins/ml/server/lib/alerts/register_ml_alerts.ts @@ -5,18 +5,20 @@ * 2.0. */ -import { Logger } from 'kibana/server'; -import { AlertingPlugin } from '../../../../alerting/server'; +import type { Logger } from 'kibana/server'; +import type { AlertingPlugin } from '../../../../alerting/server'; import { registerAnomalyDetectionAlertType } from './register_anomaly_detection_alert_type'; -import { SharedServices } from '../../shared_services'; +import type { SharedServices } from '../../shared_services'; import { registerJobsMonitoringRuleType } from './register_jobs_monitoring_rule_type'; -import { MlServicesProviders } from '../../shared_services/shared_services'; +import type { MlServicesProviders } from '../../shared_services/shared_services'; +import type { IRuleDataClient } from '../../../../rule_registry/server'; export interface RegisterAlertParams { alerting: AlertingPlugin['setup']; logger: Logger; mlSharedServices: SharedServices; mlServicesProviders: MlServicesProviders; + ruleDataClient: IRuleDataClient | null; } export function registerMlAlerts(params: RegisterAlertParams) { diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 35f66e86b955a..3694023563ca3 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -60,6 +60,8 @@ import { registerMlAlerts } from './lib/alerts/register_ml_alerts'; import { ML_ALERT_TYPES } from '../common/constants/alerts'; import { alertingRoutes } from './routes/alerting'; import { registerCollector } from './usage'; +import { getRuleDataClient } from './lib/alerts/get_rule_data_client'; +import { IRuleDataClient } from '../../rule_registry/server'; export type MlPluginSetup = SharedServices; export type MlPluginStart = void; @@ -196,6 +198,11 @@ export class MlServerPlugin initMlServerLog({ log: this.log }); + let ruleDataClient: IRuleDataClient | null = null; + if (plugins.ruleRegistry) { + ruleDataClient = getRuleDataClient(plugins.ruleRegistry, this.log); + } + const { internalServicesProviders, sharedServicesProviders } = createSharedServices( this.mlLicense, getSpaces, @@ -204,7 +211,8 @@ export class MlServerPlugin resolveMlCapabilities, () => this.clusterClient, () => getInternalSavedObjectsClient(), - () => this.isMlReady + () => this.isMlReady, + ruleDataClient ); if (plugins.alerting) { @@ -213,6 +221,7 @@ export class MlServerPlugin logger: this.log, mlSharedServices: sharedServicesProviders, mlServicesProviders: internalServicesProviders, + ruleDataClient, }); } diff --git a/x-pack/plugins/ml/server/shared_services/shared_services.ts b/x-pack/plugins/ml/server/shared_services/shared_services.ts index 3766a48b0537d..afcd0fba068e8 100644 --- a/x-pack/plugins/ml/server/shared_services/shared_services.ts +++ b/x-pack/plugins/ml/server/shared_services/shared_services.ts @@ -34,6 +34,7 @@ import { getJobsHealthServiceProvider, JobsHealthServiceProvider, } from '../lib/alerts/jobs_health_service'; +import type { IRuleDataClient } from '../../../rule_registry/server'; export type SharedServices = JobServiceProvider & AnomalyDetectorsProvider & @@ -64,6 +65,7 @@ interface OkParams { scopedClient: IScopedClusterClient; mlClient: MlClient; jobSavedObjectService: JobSavedObjectService; + ruleDataClient: IRuleDataClient | null; } type OkCallback = (okParams: OkParams) => any; @@ -76,7 +78,8 @@ export function createSharedServices( resolveMlCapabilities: ResolveMlCapabilities, getClusterClient: () => IClusterClient | null, getInternalSavedObjectsClient: () => SavedObjectsClientContract | null, - isMlReady: () => Promise + isMlReady: () => Promise, + ruleDataClient: IRuleDataClient | null ): { sharedServicesProviders: SharedServices; internalServicesProviders: MlServicesProviders; @@ -120,7 +123,7 @@ export function createSharedServices( }, async ok(callback: OkCallback) { await Promise.all(asyncGuards); - return callback({ scopedClient, mlClient, jobSavedObjectService }); + return callback({ scopedClient, mlClient, jobSavedObjectService, ruleDataClient }); }, }; return guards; diff --git a/x-pack/plugins/ml/server/types.ts b/x-pack/plugins/ml/server/types.ts index b04b8d8601772..fc7465e9fb1b6 100644 --- a/x-pack/plugins/ml/server/types.ts +++ b/x-pack/plugins/ml/server/types.ts @@ -23,6 +23,10 @@ import type { PluginSetup as DataPluginSetup, PluginStart as DataPluginStart, } from '../../../../src/plugins/data/server'; +import { + RuleRegistryPluginSetupContract, + RuleRegistryPluginStartContract, +} from '../../rule_registry/server'; export interface LicenseCheckResult { isAvailable: boolean; @@ -55,11 +59,13 @@ export interface PluginsSetup { alerting?: AlertingPlugin['setup']; actions?: ActionsPlugin['setup']; usageCollection?: UsageCollectionSetup; + ruleRegistry?: RuleRegistryPluginSetupContract; } export interface PluginsStart { data: DataPluginStart; spaces?: SpacesPluginStart; + ruleRegistry?: RuleRegistryPluginStartContract; } export interface RouteInitialization { diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json index db8fc463b0550..a9076f420b20d 100644 --- a/x-pack/plugins/ml/tsconfig.json +++ b/x-pack/plugins/ml/tsconfig.json @@ -30,6 +30,7 @@ { "path": "../security/tsconfig.json" }, { "path": "../spaces/tsconfig.json" }, { "path": "../alerting/tsconfig.json" }, - { "path": "../triggers_actions_ui/tsconfig.json" } + { "path": "../triggers_actions_ui/tsconfig.json" }, + { "path": "../rule_registry/tsconfig.json" } ] }