From 8acb9a1c2558445fe1f8c805d5a3794a264f4a61 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 17 Aug 2021 18:30:14 +0200 Subject: [PATCH 01/12] [ML] extend alert messages with job ids --- .../lib/alerts/jobs_health_service.test.ts | 8 ++-- .../server/lib/alerts/jobs_health_service.ts | 40 ++++++++++++------- 2 files changed, 30 insertions(+), 18 deletions(-) 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 ffaa26fc949ee..f2c47466d358f 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 @@ -279,7 +279,7 @@ describe('JobsHealthService', () => { missed_docs_count: 11, }, ], - message: '1 job is suffering from delayed data.', + message: 'Job test_job_01 is suffering from delayed data.', }, }, ]); @@ -333,7 +333,7 @@ describe('JobsHealthService', () => { datafeed_state: 'stopped', }, ], - message: 'Datafeed is not started for the following jobs:', + message: 'Datafeed is not started for job test_job_02', }, }, { @@ -347,7 +347,7 @@ describe('JobsHealthService', () => { }, ], message: - '1 job reached the hard model memory limit. Assign the job more memory and restore from a snapshot from prior to reaching the hard limit.', + 'Job test_job_01 reached the hard model memory limit. Assign the job more memory and restore from a snapshot from prior to reaching the hard limit.', }, }, { @@ -369,7 +369,7 @@ describe('JobsHealthService', () => { missed_docs_count: 8, }, ], - message: '2 jobs are suffering from delayed data.', + message: 'Jobs test_job_01, test_job_02 are suffering from delayed data.', }, }, ]); 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 bcae57e558573..0fadda9a86da2 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,7 +5,7 @@ * 2.0. */ -import { memoize, keyBy } from 'lodash'; +import { memoize, keyBy, groupBy } from 'lodash'; import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; import { i18n } from '@kbn/i18n'; import { Logger } from 'kibana/server'; @@ -121,6 +121,15 @@ export function jobsHealthServiceProvider( async (jobIds: string[]) => (await mlClient.getJobStats({ job_id: jobIds.join(',') })).body.jobs ); + /** Gets values for translation string */ + const getJobsAlertingMessageValues = >(response: T) => { + const jobIds = response.map((v) => v.job_id); + return { + count: jobIds.length, + jobsString: jobIds.join(', '), + }; + }; + return { /** * Gets not started datafeeds for opened jobs. @@ -282,7 +291,9 @@ export function jobsHealthServiceProvider( message: i18n.translate( 'xpack.ml.alertTypes.jobsHealthAlertingRule.datafeedStateMessage', { - defaultMessage: 'Datafeed is not started for the following jobs:', + defaultMessage: + 'Datafeed is not started for {count, plural, one {job} other {jobs}} {jobsString}', + values: getJobsAlertingMessageValues(response), } ), }, @@ -293,30 +304,31 @@ export function jobsHealthServiceProvider( if (config.mml.enabled) { const response = await this.getMmlReport(jobIds); if (response && response.length > 0) { - const hardLimitJobsCount = response.reduce((acc, curr) => { - return acc + (curr.memory_status === 'hard_limit' ? 1 : 0); - }, 0); + const { hard_limit: hardLimitJobs, soft_limit: sofLimitJobs } = groupBy( + response, + 'memory_status' + ); results.push({ name: HEALTH_CHECK_NAMES.mml.name, context: { results: response, message: - hardLimitJobsCount > 0 + hardLimitJobs.length > 0 ? i18n.translate( 'xpack.ml.alertTypes.jobsHealthAlertingRule.mmlHardLimitMessage', { defaultMessage: - '{jobsCount, plural, one {# job} other {# jobs}} reached the hard model memory limit. Assign the job more memory and restore from a snapshot from prior to reaching the hard limit.', - values: { jobsCount: hardLimitJobsCount }, + '{count, plural, one {Job} other {Jobs}} {jobsString} reached the hard model memory limit. Assign the job more memory and restore from a snapshot from prior to reaching the hard limit.', + values: getJobsAlertingMessageValues(hardLimitJobs), } ) : i18n.translate( 'xpack.ml.alertTypes.jobsHealthAlertingRule.mmlSoftLimitMessage', { defaultMessage: - '{jobsCount, plural, one {# job} other {# jobs}} reached the soft model memory limit. Assign the job more memory or edit the datafeed filter to limit scope of analysis.', - values: { jobsCount: response.length }, + '{count, plural, one {Job} other {Jobs}} {jobsString} reached the soft model memory limit. Assign the job more memory or edit the datafeed filter to limit scope of analysis.', + values: getJobsAlertingMessageValues(sofLimitJobs), } ), }, @@ -340,8 +352,8 @@ export function jobsHealthServiceProvider( 'xpack.ml.alertTypes.jobsHealthAlertingRule.delayedDataMessage', { defaultMessage: - '{jobsCount, plural, one {# job is} other {# jobs are}} suffering from delayed data.', - values: { jobsCount: response.length }, + '{count, plural, one {Job} other {Jobs}} {jobsString} {count, plural, one {is} other {are}} suffering from delayed data.', + values: getJobsAlertingMessageValues(response), } ), }, @@ -360,8 +372,8 @@ export function jobsHealthServiceProvider( 'xpack.ml.alertTypes.jobsHealthAlertingRule.errorMessagesMessage', { defaultMessage: - '{jobsCount, plural, one {# job contains} other {# jobs contain}} errors in the messages.', - values: { jobsCount: response.length }, + '{count, plural, one {Job} other {Jobs}} {jobsString} {count, plural, one {contains} other {contain}} errors in the messages.', + values: getJobsAlertingMessageValues(response), } ), }, From 6dc53ae7563ecce8b32e5a58ea65129707f48432 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 17 Aug 2021 18:34:15 +0200 Subject: [PATCH 02/12] [ML] rename getJobsErrorMessages --- .../ml/server/lib/alerts/jobs_health_service.test.ts | 2 +- x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts | 5 ++++- .../server/models/job_audit_messages/job_audit_messages.ts | 7 +++++-- 3 files changed, 10 insertions(+), 4 deletions(-) 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 f2c47466d358f..d7fd15914d270 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 @@ -148,7 +148,7 @@ describe('JobsHealthService', () => { } as unknown) as jest.Mocked; const jobAuditMessagesService = ({ - getJobsErrors: jest.fn().mockImplementation((jobIds: string) => { + getJobsErrorMessages: jest.fn().mockImplementation((jobIds: string) => { return Promise.resolve({}); }), } as unknown) as jest.Mocked; 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 0fadda9a86da2..b59e494a77f25 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 @@ -255,7 +255,10 @@ export function jobsHealthServiceProvider( * about an error only once, limit the scope of the errors search. */ async getErrorsReport(jobIds: string[], previousStartedAt: Date) { - return await jobAuditMessagesService.getJobsErrors(jobIds, previousStartedAt.getTime()); + return await jobAuditMessagesService.getJobsErrorMessages( + jobIds, + previousStartedAt.getTime() + ); }, /** * Retrieves report grouped by test. diff --git a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts index fcda1a2a3ea73..69f5c8b36f10c 100644 --- a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts @@ -411,7 +411,10 @@ export function jobAuditMessagesProvider( * Retrieve list of errors per job. * @param jobIds */ - async function getJobsErrors(jobIds: string[], earliestMs?: number): Promise { + async function getJobsErrorMessages( + jobIds: string[], + earliestMs?: number + ): Promise { const { body } = await asInternalUser.search({ index: ML_NOTIFICATION_INDEX_PATTERN, ignore_unavailable: true, @@ -471,6 +474,6 @@ export function jobAuditMessagesProvider( getJobAuditMessages, getAuditMessagesSummary, clearJobAuditMessages, - getJobsErrors, + getJobsErrorMessages, }; } From 4e61b87766edb5f9b1ea4c417e30440702d29878 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Wed, 18 Aug 2021 12:11:54 +0200 Subject: [PATCH 03/12] [ML] add rule name to a template --- .../jobs_health_rule/register_jobs_health_alerting_rule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts b/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts index a5f433bcc3752..68a06919d03a3 100644 --- a/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts +++ b/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts @@ -87,7 +87,7 @@ export function registerJobsHealthAlertingRule( defaultActionMessage: i18n.translate( 'xpack.ml.alertTypes.jobsHealthAlertingRule.defaultActionMessage', { - defaultMessage: `Anomaly detection jobs health check result: + defaultMessage: `[\\{\\{rule.name\\}\\}] Anomaly detection jobs health check result: \\{\\{context.message\\}\\} \\{\\{#context.results\\}\\} Job ID: \\{\\{job_id\\}\\} From e1a082b9849e2c6a1780c344df3765c1c8208616 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Wed, 18 Aug 2021 12:13:21 +0200 Subject: [PATCH 04/12] [ML] utilize fieldFormats plugin for date formatting --- x-pack/plugins/ml/common/types/kibana.ts | 7 ++- x-pack/plugins/ml/kibana.json | 3 +- .../lib/alerts/jobs_health_service.test.ts | 6 ++ .../server/lib/alerts/jobs_health_service.ts | 58 ++++++++++++++----- .../register_jobs_monitoring_rule_type.ts | 2 +- x-pack/plugins/ml/server/plugin.ts | 10 +++- .../server/shared_services/shared_services.ts | 44 +++++++++++--- x-pack/plugins/ml/server/types.ts | 6 ++ 8 files changed, 110 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/ml/common/types/kibana.ts b/x-pack/plugins/ml/common/types/kibana.ts index cbdc09fa01be3..7783a02c2dd37 100644 --- a/x-pack/plugins/ml/common/types/kibana.ts +++ b/x-pack/plugins/ml/common/types/kibana.ts @@ -7,8 +7,9 @@ // custom edits or fixes for default kibana types which are incomplete -import { SimpleSavedObject } from 'kibana/public'; -import { IndexPatternAttributes } from 'src/plugins/data/common'; +import type { SimpleSavedObject } from 'kibana/public'; +import type { IndexPatternAttributes } from 'src/plugins/data/common'; +import type { FieldFormatsRegistry } from '../../../../../src/plugins/field_formats/common'; export type IndexPatternTitle = string; @@ -26,3 +27,5 @@ export function isSavedSearchSavedObject( ): ss is SavedSearchSavedObject { return ss !== null; } + +export type FieldFormatsRegistryProvider = () => Promise; diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index 1196247fe4629..310ac5d65c986 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -17,7 +17,8 @@ "uiActions", "kibanaLegacy", "discover", - "triggersActionsUi" + "triggersActionsUi", + "fieldFormats" ], "optionalPlugins": [ "alerting", 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 d7fd15914d270..d02f64e6e3fff 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 @@ -14,6 +14,7 @@ import { AnnotationService } from '../../models/annotation_service/annotation'; import { JobsHealthExecutorOptions } from './register_jobs_monitoring_rule_type'; import { JobAuditMessagesService } from '../../models/job_audit_messages/job_audit_messages'; import { DeepPartial } from '../../../common/types/common'; +import { FieldFormatsRegistryProvider } from '../../../common/types/kibana'; const MOCK_DATE_NOW = 1487076708000; @@ -159,11 +160,16 @@ describe('JobsHealthService', () => { debug: jest.fn(), } as unknown) as jest.Mocked; + const getFieldsFormatRegistry = jest.fn().mockImplementation(() => { + return Promise.resolve({}); + }) as jest.Mocked; + const jobHealthService: JobsHealthService = jobsHealthServiceProvider( mlClient, datafeedsService, annotationService, jobAuditMessagesService, + getFieldsFormatRegistry, logger ); 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 b59e494a77f25..27e57c3bd7807 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,10 +5,9 @@ * 2.0. */ -import { memoize, keyBy, groupBy } from 'lodash'; -import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; +import { groupBy, 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 { MlClient } from '../ml_client'; import { JobSelection } from '../../routes/schemas/alerting_schema'; @@ -35,6 +34,7 @@ import { jobAuditMessagesProvider, JobAuditMessagesService, } from '../../models/job_audit_messages/job_audit_messages'; +import type { FieldFormatsRegistryProvider } from '../../../common/types/kibana'; interface TestResult { name: string; @@ -48,8 +48,18 @@ export function jobsHealthServiceProvider( datafeedsService: DatafeedsService, annotationService: AnnotationService, jobAuditMessagesService: JobAuditMessagesService, + getFieldsFormatRegistry: FieldFormatsRegistryProvider, logger: Logger ) { + /** + * Provides a callback for date formatting based on the Kibana settings. + */ + const getDateFormatter = memoize(async () => { + const fieldFormatsRegistry = await getFieldsFormatRegistry(); + const dateFormatter = fieldFormatsRegistry.deserialize({ id: 'date' }); + return dateFormatter.convert.bind(dateFormatter); + }); + /** * Extracts result list of jobs based on included and excluded selection of jobs and groups. * @param includeJobs @@ -122,8 +132,8 @@ export function jobsHealthServiceProvider( ); /** Gets values for translation string */ - const getJobsAlertingMessageValues = >(response: T) => { - const jobIds = response.map((v) => v.job_id); + const getJobsAlertingMessageValues = >(results: T) => { + const jobIds = results.map((v) => v.job_id); return { count: jobIds.length, jobsString: jobIds.join(', '), @@ -173,13 +183,15 @@ export function jobsHealthServiceProvider( async getMmlReport(jobIds: string[]): Promise { const jobsStats = await getJobStats(jobIds); + const dateFormatter = await getDateFormatter(); + return jobsStats .filter((j) => j.state === 'opened' && j.model_size_stats.memory_status !== 'ok') .map(({ job_id: jobId, model_size_stats: modelSizeStats }) => { return { job_id: jobId, memory_status: modelSizeStats.memory_status, - log_time: modelSizeStats.log_time, + log_time: dateFormatter(modelSizeStats.log_time), model_bytes: modelSizeStats.model_bytes, model_bytes_memory_limit: modelSizeStats.model_bytes_memory_limit, peak_model_bytes: modelSizeStats.peak_model_bytes, @@ -212,6 +224,8 @@ export function jobsHealthServiceProvider( const defaultLookbackInterval = resolveLookbackInterval(resultJobs, datafeeds!); const earliestMs = getDelayedDataLookbackTimestamp(timeInterval, defaultLookbackInterval); + const getFormattedDate = await getDateFormatter(); + const annotations: DelayedDataResponse[] = ( await annotationService.getDelayedDataAnnotations({ jobIds: resultJobIds, @@ -244,6 +258,12 @@ export function jobsHealthServiceProvider( v.end_timestamp > getDelayedDataLookbackTimestamp(timeInterval, jobLookbackInterval); return isDocCountExceededThreshold && isEndTimestampWithinRange; + }) + .map((v) => { + return { + ...v, + end_timestamp: getFormattedDate(v.end_timestamp), + }; }); return annotations; @@ -255,10 +275,21 @@ export function jobsHealthServiceProvider( * about an error only once, limit the scope of the errors search. */ async getErrorsReport(jobIds: string[], previousStartedAt: Date) { - return await jobAuditMessagesService.getJobsErrorMessages( - jobIds, - previousStartedAt.getTime() - ); + const getFormattedDate = await getDateFormatter(); + + return ( + await jobAuditMessagesService.getJobsErrorMessages(jobIds, previousStartedAt.getTime()) + ).map((v) => { + return { + ...v, + errors: v.errors.map((e) => { + return { + ...e, + timestamp: getFormattedDate(e.timestamp), + }; + }), + }; + }); }, /** * Retrieves report grouped by test. @@ -307,7 +338,7 @@ export function jobsHealthServiceProvider( if (config.mml.enabled) { const response = await this.getMmlReport(jobIds); if (response && response.length > 0) { - const { hard_limit: hardLimitJobs, soft_limit: sofLimitJobs } = groupBy( + const { hard_limit: hardLimitJobs, soft_limit: softLimitJobs } = groupBy( response, 'memory_status' ); @@ -331,7 +362,7 @@ export function jobsHealthServiceProvider( { defaultMessage: '{count, plural, one {Job} other {Jobs}} {jobsString} reached the soft model memory limit. Assign the job more memory or edit the datafeed filter to limit scope of analysis.', - values: getJobsAlertingMessageValues(sofLimitJobs), + values: getJobsAlertingMessageValues(softLimitJobs), } ), }, @@ -405,12 +436,13 @@ export function getJobsHealthServiceProvider(getGuards: GetGuards) { return await getGuards(request, savedObjectsClient) .isFullLicense() .hasMlCapabilities(['canGetJobs']) - .ok(({ mlClient, scopedClient }) => + .ok(({ mlClient, scopedClient, getFieldsFormatRegistry }) => jobsHealthServiceProvider( mlClient, datafeedsProvider(scopedClient, mlClient), annotationServiceProvider(scopedClient), jobAuditMessagesProvider(scopedClient, mlClient), + getFieldsFormatRegistry, logger ).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 c49c169d3bd21..d814c2cc3dea4 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 @@ -143,7 +143,7 @@ export function registerJobsMonitoringRuleType({ const executionResult = await getTestsResults(options); if (executionResult.length > 0) { - logger.info( + logger.debug( `"${name}" rule is scheduling actions for tests: ${executionResult .map((v) => v.name) .join(', ')}` diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 35f66e86b955a..4dea3cc072ca5 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -17,6 +17,7 @@ import { IClusterClient, SavedObjectsServiceStart, SharedGlobalConfig, + UiSettingsServiceStart, } from 'kibana/server'; import type { SecurityPluginSetup } from '../../security/server'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; @@ -60,6 +61,7 @@ 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 { FieldFormatsStart } from '../../../../src/plugins/field_formats/server'; export type MlPluginSetup = SharedServices; export type MlPluginStart = void; @@ -70,6 +72,8 @@ export class MlServerPlugin private mlLicense: MlLicense; private capabilities: CapabilitiesStart | null = null; private clusterClient: IClusterClient | null = null; + private fieldsFormat: FieldFormatsStart | null = null; + private uiSettings: UiSettingsServiceStart | null = null; private savedObjectsStart: SavedObjectsServiceStart | null = null; private spacesPlugin: SpacesPluginSetup | undefined; private security: SecurityPluginSetup | undefined; @@ -204,6 +208,8 @@ export class MlServerPlugin resolveMlCapabilities, () => this.clusterClient, () => getInternalSavedObjectsClient(), + () => this.uiSettings, + () => this.fieldsFormat, () => this.isMlReady ); @@ -223,7 +229,9 @@ export class MlServerPlugin return sharedServicesProviders; } - public start(coreStart: CoreStart): MlPluginStart { + public start(coreStart: CoreStart, plugins: PluginsStart): MlPluginStart { + this.uiSettings = coreStart.uiSettings; + this.fieldsFormat = plugins.fieldFormats; this.capabilities = coreStart.capabilities; this.clusterClient = coreStart.elasticsearch.client; this.savedObjectsStart = coreStart.savedObjects; 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..4c1f7e491c7f7 100644 --- a/x-pack/plugins/ml/server/shared_services/shared_services.ts +++ b/x-pack/plugins/ml/server/shared_services/shared_services.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { IClusterClient, IScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; +import { + IClusterClient, + IScopedClusterClient, + SavedObjectsClientContract, + UiSettingsServiceStart, +} from 'kibana/server'; import { SpacesPluginStart } from '../../../spaces/server'; import { KibanaRequest } from '../../.././../../src/core/server'; import { MlLicense } from '../../common/license'; @@ -34,6 +39,8 @@ import { getJobsHealthServiceProvider, JobsHealthServiceProvider, } from '../lib/alerts/jobs_health_service'; +import type { FieldFormatsStart } from '../../../../../src/plugins/field_formats/server'; +import type { FieldFormatsRegistryProvider } from '../../common/types/kibana'; export type SharedServices = JobServiceProvider & AnomalyDetectorsProvider & @@ -64,6 +71,7 @@ interface OkParams { scopedClient: IScopedClusterClient; mlClient: MlClient; jobSavedObjectService: JobSavedObjectService; + getFieldsFormatRegistry: FieldFormatsRegistryProvider; } type OkCallback = (okParams: OkParams) => any; @@ -76,6 +84,8 @@ export function createSharedServices( resolveMlCapabilities: ResolveMlCapabilities, getClusterClient: () => IClusterClient | null, getInternalSavedObjectsClient: () => SavedObjectsClientContract | null, + getUiSettings: () => UiSettingsServiceStart | null, + getFieldsFormat: () => FieldFormatsStart | null, isMlReady: () => Promise ): { sharedServicesProviders: SharedServices; @@ -97,12 +107,18 @@ export function createSharedServices( internalSavedObjectsClient, authorization, getSpaces !== undefined, - isMlReady + isMlReady, + getUiSettings, + getFieldsFormat ); - const { hasMlCapabilities, scopedClient, mlClient, jobSavedObjectService } = getRequestItems( - request - ); + const { + hasMlCapabilities, + scopedClient, + mlClient, + jobSavedObjectService, + getFieldsFormatRegistry, + } = getRequestItems(request); const asyncGuards: Array> = []; const guards: Guards = { @@ -120,7 +136,7 @@ export function createSharedServices( }, async ok(callback: OkCallback) { await Promise.all(asyncGuards); - return callback({ scopedClient, mlClient, jobSavedObjectService }); + return callback({ scopedClient, mlClient, jobSavedObjectService, getFieldsFormatRegistry }); }, }; return guards; @@ -154,7 +170,9 @@ function getRequestItemsProvider( internalSavedObjectsClient: SavedObjectsClientContract, authorization: SecurityPluginSetup['authz'] | undefined, spaceEnabled: boolean, - isMlReady: () => Promise + isMlReady: () => Promise, + getUiSettings: () => UiSettingsServiceStart | null, + getFieldsFormat: () => FieldFormatsStart | null ) { return (request: KibanaRequest) => { const getHasMlCapabilities = hasMlCapabilitiesProvider(resolveMlCapabilities); @@ -173,6 +191,10 @@ function getRequestItemsProvider( isMlReady ); + const uiSettingsClient = getUiSettings()!.asScopedToClient(savedObjectsClient); + const getFieldsFormatRegistry = () => + getFieldsFormat()!.fieldFormatServiceFactory(uiSettingsClient); + if (clusterClient === null) { throw new MLClusterClientUninitialized(`ML's cluster client has not been initialized`); } @@ -190,6 +212,12 @@ function getRequestItemsProvider( }; mlClient = getMlClient(scopedClient, jobSavedObjectService); } - return { hasMlCapabilities, scopedClient, mlClient, jobSavedObjectService }; + return { + hasMlCapabilities, + scopedClient, + mlClient, + jobSavedObjectService, + getFieldsFormatRegistry, + }; }; } diff --git a/x-pack/plugins/ml/server/types.ts b/x-pack/plugins/ml/server/types.ts index b04b8d8601772..da83b03766af4 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 type { + FieldFormatsSetup, + FieldFormatsStart, +} from '../../../../src/plugins/field_formats/server'; export interface LicenseCheckResult { isAvailable: boolean; @@ -47,6 +51,7 @@ export interface SavedObjectsRouteDeps { export interface PluginsSetup { cloud: CloudSetup; data: DataPluginSetup; + fieldFormats: FieldFormatsSetup; features: FeaturesPluginSetup; home: HomeServerPluginSetup; licensing: LicensingPluginSetup; @@ -59,6 +64,7 @@ export interface PluginsSetup { export interface PluginsStart { data: DataPluginStart; + fieldFormats: FieldFormatsStart; spaces?: SpacesPluginStart; } From b5f2b311303fd19d9edbfc3e49bfaaa93f1eea35 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Wed, 18 Aug 2021 12:23:13 +0200 Subject: [PATCH 05/12] [ML] update types --- .../ml/server/lib/alerts/jobs_health_service.ts | 12 +++++++----- .../alerts/register_jobs_monitoring_rule_type.ts | 13 +++++++++---- 2 files changed, 16 insertions(+), 9 deletions(-) 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 27e57c3bd7807..21fc82e538367 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 @@ -18,6 +18,7 @@ import { GetGuards } from '../../shared_services/shared_services'; import { AnomalyDetectionJobsHealthAlertContext, DelayedDataResponse, + JobsErrorsResponse, JobsHealthExecutorOptions, MmlTestResponse, NotStartedDatafeedResponse, @@ -226,13 +227,13 @@ export function jobsHealthServiceProvider( const getFormattedDate = await getDateFormatter(); - const annotations: DelayedDataResponse[] = ( + return ( await annotationService.getDelayedDataAnnotations({ jobIds: resultJobIds, earliestMs, }) ) - .map((v) => { + .map((v) => { const match = v.annotation.match(/Datafeed has missed (\d+)\s/); const missedDocsCount = match ? parseInt(match[1], 10) : 0; return { @@ -265,8 +266,6 @@ export function jobsHealthServiceProvider( end_timestamp: getFormattedDate(v.end_timestamp), }; }); - - return annotations; }, /** * Retrieves a list of the latest errors per jobs. @@ -274,7 +273,10 @@ export function jobsHealthServiceProvider( * @param previousStartedAt Time of the previous rule execution. As we intend to notify * about an error only once, limit the scope of the errors search. */ - async getErrorsReport(jobIds: string[], previousStartedAt: Date) { + async getErrorsReport( + jobIds: string[], + previousStartedAt: Date + ): Promise { const getFormattedDate = await getDateFormatter(); return ( 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 d814c2cc3dea4..4844bf1a94707 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 @@ -22,8 +22,8 @@ import { AlertInstanceState, AlertTypeState, } from '../../../../alerting/common'; -import { JobsErrorsResponse } from '../../models/job_audit_messages/job_audit_messages'; -import { AlertExecutorOptions } from '../../../../alerting/server'; +import type { AlertExecutorOptions } from '../../../../alerting/server'; +import type { JobMessage } from '../../../common/types/audit_message'; type ModelSizeStats = MlJobStats['model_size_stats']; @@ -51,14 +51,19 @@ export interface DelayedDataResponse { /** Number of missed documents */ missed_docs_count: number; /** Timestamp of the latest finalized bucket with missing docs */ - end_timestamp: number; + end_timestamp: string; +} + +export interface JobsErrorsResponse { + job_id: string; + errors: Array & { timestamp: string }>; } export type AnomalyDetectionJobHealthResult = | MmlTestResponse | NotStartedDatafeedResponse | DelayedDataResponse - | JobsErrorsResponse[number]; + | JobsErrorsResponse; export type AnomalyDetectionJobsHealthAlertContext = { results: AnomalyDetectionJobHealthResult[]; From 32cc1cff12b5540f2adc7ce73ac636b842da61d1 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Wed, 18 Aug 2021 13:07:49 +0200 Subject: [PATCH 06/12] [ML] update unit tests --- .../lib/alerts/jobs_health_service.test.ts | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) 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 d02f64e6e3fff..7192b9a919379 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 @@ -150,7 +150,7 @@ describe('JobsHealthService', () => { const jobAuditMessagesService = ({ getJobsErrorMessages: jest.fn().mockImplementation((jobIds: string) => { - return Promise.resolve({}); + return Promise.resolve([]); }), } as unknown) as jest.Mocked; @@ -161,7 +161,15 @@ describe('JobsHealthService', () => { } as unknown) as jest.Mocked; const getFieldsFormatRegistry = jest.fn().mockImplementation(() => { - return Promise.resolve({}); + return Promise.resolve({ + deserialize: jest.fn().mockImplementation(() => { + return { + convert: jest.fn().mockImplementation((v) => { + return new Date(v).toUTCString(); + }), + }; + }), + }); }) as jest.Mocked; const jobHealthService: JobsHealthService = jobsHealthServiceProvider( @@ -281,7 +289,7 @@ describe('JobsHealthService', () => { job_id: 'test_job_01', annotation: 'Datafeed has missed 11 documents due to ingest latency, latest bucket with missing data is [2021-07-30T13:50:00.000Z]. Consider increasing query_delay', - end_timestamp: 1627653300000, + end_timestamp: 'Fri, 30 Jul 2021 13:55:00 GMT', missed_docs_count: 11, }, ], @@ -348,7 +356,7 @@ describe('JobsHealthService', () => { results: [ { job_id: 'test_job_01', - log_time: 1626935914540, + log_time: 'Thu, 22 Jul 2021 06:38:34 GMT', memory_status: 'hard_limit', }, ], @@ -364,14 +372,14 @@ describe('JobsHealthService', () => { job_id: 'test_job_01', annotation: 'Datafeed has missed 11 documents due to ingest latency, latest bucket with missing data is [2021-07-30T13:50:00.000Z]. Consider increasing query_delay', - end_timestamp: 1627653300000, + end_timestamp: 'Fri, 30 Jul 2021 13:55:00 GMT', missed_docs_count: 11, }, { job_id: 'test_job_02', annotation: 'Datafeed has missed 8 documents due to ingest latency, latest bucket with missing data is [2021-07-30T13:50:00.000Z]. Consider increasing query_delay', - end_timestamp: 1627653300000, + end_timestamp: 'Fri, 30 Jul 2021 13:55:00 GMT', missed_docs_count: 8, }, ], From 23b5355ebd3679e42878098ebabbcd9d19fbae73 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Wed, 18 Aug 2021 14:51:34 +0200 Subject: [PATCH 07/12] [ML] guards for uiSettings and fieldRegistry, add custom errors --- .../ml/server/shared_services/errors.ts | 16 +++++--- .../server/shared_services/shared_services.ts | 37 ++++++++++++++++--- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/ml/server/shared_services/errors.ts b/x-pack/plugins/ml/server/shared_services/errors.ts index 4b8e6625c5aef..eb9bbbb40fbd7 100644 --- a/x-pack/plugins/ml/server/shared_services/errors.ts +++ b/x-pack/plugins/ml/server/shared_services/errors.ts @@ -5,9 +5,13 @@ * 2.0. */ -export class MLClusterClientUninitialized extends Error { - constructor(message?: string) { - super(message); - Object.setPrototypeOf(this, new.target.prototype); - } -} +export const getCustomError = (className: string, errorMessage: string) => { + const C = class extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + } + }; + Object.defineProperty(C, 'name', { value: className }); + return new C(errorMessage); +}; 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 4c1f7e491c7f7..a9f92514d27c1 100644 --- a/x-pack/plugins/ml/server/shared_services/shared_services.ts +++ b/x-pack/plugins/ml/server/shared_services/shared_services.ts @@ -28,7 +28,7 @@ import { } from './providers/anomaly_detectors'; import { ResolveMlCapabilities, MlCapabilitiesKey } from '../../common/types/capabilities'; import { hasMlCapabilitiesProvider, HasMlCapabilities } from '../lib/capabilities'; -import { MLClusterClientUninitialized } from './errors'; +import { getCustomError } from './errors'; import { MlClient, getMlClient } from '../lib/ml_client'; import { jobSavedObjectServiceFactory, JobSavedObjectService } from '../saved_objects'; import { @@ -191,14 +191,39 @@ function getRequestItemsProvider( isMlReady ); - const uiSettingsClient = getUiSettings()!.asScopedToClient(savedObjectsClient); - const getFieldsFormatRegistry = () => - getFieldsFormat()!.fieldFormatServiceFactory(uiSettingsClient); - if (clusterClient === null) { - throw new MLClusterClientUninitialized(`ML's cluster client has not been initialized`); + throw getCustomError( + 'MLClusterClientUninitialized', + `ML's cluster client has not been initialized` + ); } + const uiSettingsClient = getUiSettings()!.asScopedToClient(savedObjectsClient); + if (uiSettingsClient === null) { + throw getCustomError( + 'MLUISettingsClientUninitialized', + `ML's UI settings client has not been initialized` + ); + } + + const getFieldsFormatRegistry = async () => { + let fieldFormatRegistry; + try { + fieldFormatRegistry = await getFieldsFormat()!.fieldFormatServiceFactory(uiSettingsClient); + } catch (e) { + // throw an custom error during the fieldFormatRegistry check + } + + if (!fieldFormatRegistry) { + throw getCustomError( + 'MLFieldFormatRegistryUninitialized', + `ML's field format registry has not been initialized` + ); + } + + return fieldFormatRegistry; + }; + if (request instanceof KibanaRequest) { hasMlCapabilities = getHasMlCapabilities(request); scopedClient = clusterClient.asScoped(request); From c263baf35044c7c3f9d69c517ebdbc67403ba0c8 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Wed, 18 Aug 2021 14:53:31 +0200 Subject: [PATCH 08/12] [ML] add conditional call for uiSettings --- x-pack/plugins/ml/server/shared_services/shared_services.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 a9f92514d27c1..879497bdbbd11 100644 --- a/x-pack/plugins/ml/server/shared_services/shared_services.ts +++ b/x-pack/plugins/ml/server/shared_services/shared_services.ts @@ -198,7 +198,7 @@ function getRequestItemsProvider( ); } - const uiSettingsClient = getUiSettings()!.asScopedToClient(savedObjectsClient); + const uiSettingsClient = getUiSettings()?.asScopedToClient(savedObjectsClient); if (uiSettingsClient === null) { throw getCustomError( 'MLUISettingsClientUninitialized', @@ -209,7 +209,7 @@ function getRequestItemsProvider( const getFieldsFormatRegistry = async () => { let fieldFormatRegistry; try { - fieldFormatRegistry = await getFieldsFormat()!.fieldFormatServiceFactory(uiSettingsClient); + fieldFormatRegistry = await getFieldsFormat()!.fieldFormatServiceFactory(uiSettingsClient!); } catch (e) { // throw an custom error during the fieldFormatRegistry check } From 124768fd36fcd8fb2f6575a40018c4634c12a394 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Wed, 18 Aug 2021 16:51:52 +0200 Subject: [PATCH 09/12] [ML] fix i18n --- .../server/lib/alerts/jobs_health_service.ts | 70 +++++++++++++------ .../server/shared_services/shared_services.ts | 2 +- 2 files changed, 49 insertions(+), 23 deletions(-) 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 21fc82e538367..0a0acfb371211 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 @@ -133,8 +133,10 @@ export function jobsHealthServiceProvider( ); /** Gets values for translation string */ - const getJobsAlertingMessageValues = >(results: T) => { - const jobIds = results.map((v) => v.job_id); + const getJobsAlertingMessageValues = >( + results: T + ) => { + const jobIds = (results || []).filter(isDefined).map((v) => v.job_id); return { count: jobIds.length, jobsString: jobIds.join(', '), @@ -320,6 +322,7 @@ export function jobsHealthServiceProvider( if (config.datafeed.enabled) { const response = await this.getNotStartedDatafeeds(jobIds); if (response && response.length > 0) { + const { count, jobsString } = getJobsAlertingMessageValues(response); results.push({ name: HEALTH_CHECK_NAMES.datafeed.name, context: { @@ -329,7 +332,7 @@ export function jobsHealthServiceProvider( { defaultMessage: 'Datafeed is not started for {count, plural, one {job} other {jobs}} {jobsString}', - values: getJobsAlertingMessageValues(response), + values: { count, jobsString }, } ), }, @@ -345,28 +348,49 @@ export function jobsHealthServiceProvider( 'memory_status' ); + const { + count: hardLimitCount, + jobsString: hardLimitJobsString, + } = getJobsAlertingMessageValues(hardLimitJobs); + const { + count: softLimitCount, + jobsString: softLimitJobsString, + } = getJobsAlertingMessageValues(softLimitJobs); + + let message = ''; + + if (hardLimitCount > 0) { + message = i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.mmlMessage', { + defaultMessage: `{count, plural, one {Job} other {Jobs}} {jobsString} reached the hard model memory limit. Assign the job more memory and restore from a snapshot from prior to reaching the hard limit.`, + values: { + count: hardLimitCount, + jobsString: hardLimitJobsString, + }, + }); + } + + if (softLimitCount > 0) { + if (message.length > 0) { + message += '\n'; + } + message += i18n.translate( + 'xpack.ml.alertTypes.jobsHealthAlertingRule.mmlSoftLimitMessage', + { + defaultMessage: + '{count, plural, one {Job} other {Jobs}} {jobsString} reached the soft model memory limit. Assign the job more memory or edit the datafeed filter to limit scope of analysis.', + values: { + count: softLimitCount, + jobsString: softLimitJobsString, + }, + } + ); + } + results.push({ name: HEALTH_CHECK_NAMES.mml.name, context: { results: response, - message: - hardLimitJobs.length > 0 - ? i18n.translate( - 'xpack.ml.alertTypes.jobsHealthAlertingRule.mmlHardLimitMessage', - { - defaultMessage: - '{count, plural, one {Job} other {Jobs}} {jobsString} reached the hard model memory limit. Assign the job more memory and restore from a snapshot from prior to reaching the hard limit.', - values: getJobsAlertingMessageValues(hardLimitJobs), - } - ) - : i18n.translate( - 'xpack.ml.alertTypes.jobsHealthAlertingRule.mmlSoftLimitMessage', - { - defaultMessage: - '{count, plural, one {Job} other {Jobs}} {jobsString} reached the soft model memory limit. Assign the job more memory or edit the datafeed filter to limit scope of analysis.', - values: getJobsAlertingMessageValues(softLimitJobs), - } - ), + message, }, }); } @@ -379,6 +403,8 @@ export function jobsHealthServiceProvider( config.delayedData.docsCount ); + const { count, jobsString } = getJobsAlertingMessageValues(response); + if (response.length > 0) { results.push({ name: HEALTH_CHECK_NAMES.delayedData.name, @@ -389,7 +415,7 @@ export function jobsHealthServiceProvider( { defaultMessage: '{count, plural, one {Job} other {Jobs}} {jobsString} {count, plural, one {is} other {are}} suffering from delayed data.', - values: getJobsAlertingMessageValues(response), + values: { count, jobsString }, } ), }, 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 879497bdbbd11..dd7ff9271e683 100644 --- a/x-pack/plugins/ml/server/shared_services/shared_services.ts +++ b/x-pack/plugins/ml/server/shared_services/shared_services.ts @@ -199,7 +199,7 @@ function getRequestItemsProvider( } const uiSettingsClient = getUiSettings()?.asScopedToClient(savedObjectsClient); - if (uiSettingsClient === null) { + if (!uiSettingsClient) { throw getCustomError( 'MLUISettingsClientUninitialized', `ML's UI settings client has not been initialized` From db6192d7fa92581d4fa6dcbf97c19d3cdf58b010 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Wed, 18 Aug 2021 18:07:51 +0200 Subject: [PATCH 10/12] [ML] tests for getCustomError --- .../ml/server/shared_services/errors.test.ts | 18 ++++++++++++++++++ .../ml/server/shared_services/errors.ts | 9 ++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/ml/server/shared_services/errors.test.ts diff --git a/x-pack/plugins/ml/server/shared_services/errors.test.ts b/x-pack/plugins/ml/server/shared_services/errors.test.ts new file mode 100644 index 0000000000000..9b5492bd7c6b6 --- /dev/null +++ b/x-pack/plugins/ml/server/shared_services/errors.test.ts @@ -0,0 +1,18 @@ +/* + * 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 { getCustomError } from './errors'; + +describe('getCustomError', () => { + test('creates a custom error instance', () => { + const error = getCustomError('MLCustomError', 'farequote is not defined'); + expect(error.message).toBe('farequote is not defined'); + expect(error.name).toBe('MLCustomError'); + // make sure that custom class extends Error + expect(error).toBeInstanceOf(Error); + }); +}); diff --git a/x-pack/plugins/ml/server/shared_services/errors.ts b/x-pack/plugins/ml/server/shared_services/errors.ts index eb9bbbb40fbd7..7d74c7090674d 100644 --- a/x-pack/plugins/ml/server/shared_services/errors.ts +++ b/x-pack/plugins/ml/server/shared_services/errors.ts @@ -6,12 +6,15 @@ */ export const getCustomError = (className: string, errorMessage: string) => { - const C = class extends Error { + const CustomError = class extends Error { constructor(message?: string) { super(message); Object.setPrototypeOf(this, new.target.prototype); + // Override the error instance name + Object.defineProperty(this, 'name', { value: className }); } }; - Object.defineProperty(C, 'name', { value: className }); - return new C(errorMessage); + // set class name dynamically + Object.defineProperty(CustomError, 'name', { value: className }); + return new CustomError(errorMessage); }; From 4601d31f65e028bee243f478573ce301174f42b4 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Wed, 18 Aug 2021 18:11:39 +0200 Subject: [PATCH 11/12] [ML] fix i18n check --- x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 0a0acfb371211..ca63031f02e27 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 @@ -426,6 +426,7 @@ export function jobsHealthServiceProvider( if (config.errorMessages.enabled && previousStartedAt) { const response = await this.getErrorsReport(jobIds, previousStartedAt); if (response.length > 0) { + const { count, jobsString } = getJobsAlertingMessageValues(response); results.push({ name: HEALTH_CHECK_NAMES.errorMessages.name, context: { @@ -435,7 +436,7 @@ export function jobsHealthServiceProvider( { defaultMessage: '{count, plural, one {Job} other {Jobs}} {jobsString} {count, plural, one {contains} other {contain}} errors in the messages.', - values: getJobsAlertingMessageValues(response), + values: { count, jobsString }, } ), }, From f37aa84e63345660e31f601c81a3035438cc5733 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 19 Aug 2021 11:22:14 +0200 Subject: [PATCH 12/12] [ML] create classes for custom errors --- .../ml/server/shared_services/errors.test.ts | 42 ++++++++++++++++--- .../ml/server/shared_services/errors.ts | 14 ++++++- .../server/shared_services/shared_services.ts | 19 ++++----- 3 files changed, 56 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/ml/server/shared_services/errors.test.ts b/x-pack/plugins/ml/server/shared_services/errors.test.ts index 9b5492bd7c6b6..727012595dff3 100644 --- a/x-pack/plugins/ml/server/shared_services/errors.test.ts +++ b/x-pack/plugins/ml/server/shared_services/errors.test.ts @@ -5,14 +5,44 @@ * 2.0. */ -import { getCustomError } from './errors'; +import { + getCustomErrorClass, + MLClusterClientUninitialized, + MLUISettingsClientUninitialized, + MLFieldFormatRegistryUninitialized, +} from './errors'; -describe('getCustomError', () => { +describe('Custom errors', () => { test('creates a custom error instance', () => { - const error = getCustomError('MLCustomError', 'farequote is not defined'); - expect(error.message).toBe('farequote is not defined'); - expect(error.name).toBe('MLCustomError'); + const MLCustomError = getCustomErrorClass('MLCustomError'); + const errorInstance = new MLCustomError('farequote is not defined'); + expect(errorInstance.message).toBe('farequote is not defined'); + expect(errorInstance.name).toBe('MLCustomError'); + expect(errorInstance).toBeInstanceOf(MLCustomError); // make sure that custom class extends Error - expect(error).toBeInstanceOf(Error); + expect(errorInstance).toBeInstanceOf(Error); + }); + + test('MLClusterClientUninitialized', () => { + const errorInstance = new MLClusterClientUninitialized('cluster client is not initialized'); + expect(errorInstance.message).toBe('cluster client is not initialized'); + expect(errorInstance.name).toBe('MLClusterClientUninitialized'); + expect(errorInstance).toBeInstanceOf(MLClusterClientUninitialized); + }); + + test('MLUISettingsClientUninitialized', () => { + const errorInstance = new MLUISettingsClientUninitialized('cluster client is not initialized'); + expect(errorInstance.message).toBe('cluster client is not initialized'); + expect(errorInstance.name).toBe('MLUISettingsClientUninitialized'); + expect(errorInstance).toBeInstanceOf(MLUISettingsClientUninitialized); + }); + + test('MLFieldFormatRegistryUninitialized', () => { + const errorInstance = new MLFieldFormatRegistryUninitialized( + 'cluster client is not initialized' + ); + expect(errorInstance.message).toBe('cluster client is not initialized'); + expect(errorInstance.name).toBe('MLFieldFormatRegistryUninitialized'); + expect(errorInstance).toBeInstanceOf(MLFieldFormatRegistryUninitialized); }); }); diff --git a/x-pack/plugins/ml/server/shared_services/errors.ts b/x-pack/plugins/ml/server/shared_services/errors.ts index 7d74c7090674d..39c629ad50f5f 100644 --- a/x-pack/plugins/ml/server/shared_services/errors.ts +++ b/x-pack/plugins/ml/server/shared_services/errors.ts @@ -5,7 +5,7 @@ * 2.0. */ -export const getCustomError = (className: string, errorMessage: string) => { +export const getCustomErrorClass = (className: string) => { const CustomError = class extends Error { constructor(message?: string) { super(message); @@ -16,5 +16,15 @@ export const getCustomError = (className: string, errorMessage: string) => { }; // set class name dynamically Object.defineProperty(CustomError, 'name', { value: className }); - return new CustomError(errorMessage); + return CustomError; }; + +export const MLClusterClientUninitialized = getCustomErrorClass('MLClusterClientUninitialized'); + +export const MLUISettingsClientUninitialized = getCustomErrorClass( + 'MLUISettingsClientUninitialized' +); + +export const MLFieldFormatRegistryUninitialized = getCustomErrorClass( + 'MLFieldFormatRegistryUninitialized' +); 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 dd7ff9271e683..5c8bbffe10aed 100644 --- a/x-pack/plugins/ml/server/shared_services/shared_services.ts +++ b/x-pack/plugins/ml/server/shared_services/shared_services.ts @@ -28,7 +28,11 @@ import { } from './providers/anomaly_detectors'; import { ResolveMlCapabilities, MlCapabilitiesKey } from '../../common/types/capabilities'; import { hasMlCapabilitiesProvider, HasMlCapabilities } from '../lib/capabilities'; -import { getCustomError } from './errors'; +import { + MLClusterClientUninitialized, + MLFieldFormatRegistryUninitialized, + MLUISettingsClientUninitialized, +} from './errors'; import { MlClient, getMlClient } from '../lib/ml_client'; import { jobSavedObjectServiceFactory, JobSavedObjectService } from '../saved_objects'; import { @@ -192,18 +196,12 @@ function getRequestItemsProvider( ); if (clusterClient === null) { - throw getCustomError( - 'MLClusterClientUninitialized', - `ML's cluster client has not been initialized` - ); + throw new MLClusterClientUninitialized(`ML's cluster client has not been initialized`); } const uiSettingsClient = getUiSettings()?.asScopedToClient(savedObjectsClient); if (!uiSettingsClient) { - throw getCustomError( - 'MLUISettingsClientUninitialized', - `ML's UI settings client has not been initialized` - ); + throw new MLUISettingsClientUninitialized(`ML's UI settings client has not been initialized`); } const getFieldsFormatRegistry = async () => { @@ -215,8 +213,7 @@ function getRequestItemsProvider( } if (!fieldFormatRegistry) { - throw getCustomError( - 'MLFieldFormatRegistryUninitialized', + throw new MLFieldFormatRegistryUninitialized( `ML's field format registry has not been initialized` ); }