[ML] Fix creation of alert instances for Anomaly detection alert type (#93605)

* [ML] use static alert instance key, remove logic for handling duplicates

* [ML] use job id as an alert instance id

* [ML] beta label

* [ML] round time interval

* [ML] fix preview button

* [ML] improve the default template

* [ML] remove redundant kibanaBaseUrl
This commit is contained in:
Dima Arnautov 2021-03-05 18:26:58 +01:00 committed by GitHub
parent 5755d7907c
commit 15cda17f2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 41 additions and 98 deletions

View file

@ -6,7 +6,7 @@
*/
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { EuiSpacer, EuiForm } from '@elastic/eui';
import { EuiSpacer, EuiForm, EuiBetaBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import useMount from 'react-use/lib/useMount';
import { i18n } from '@kbn/i18n';
import { JobSelectorControl } from './job_selector';
@ -116,6 +116,22 @@ const MlAnomalyAlertTrigger: FC<MlAnomalyAlertTriggerProps> = ({
return (
<EuiForm data-test-subj={'mlAnomalyAlertForm'}>
<EuiFlexGroup gutterSize={'none'} justifyContent={'flexEnd'}>
<EuiFlexItem grow={false}>
<EuiBetaBadge
label={i18n.translate('xpack.ml.anomalyDetectionAlert.betaBadgeLabel', {
defaultMessage: 'Beta',
})}
tooltipContent={i18n.translate(
'xpack.ml.anomalyDetectionAlert.betaBadgeTooltipContent',
{
defaultMessage: `Anomaly detection alerts are a beta feature. We'd love to hear your feedback.`,
}
)}
/>
</EuiFlexItem>
</EuiFlexGroup>
<JobSelectorControl
jobsAndGroupIds={jobsAndGroupIds}
adJobsApiService={adJobsApiService}

View file

@ -152,7 +152,7 @@ export const PreviewAlertCondition: FC<PreviewAlertConditionProps> = ({
(alertParams.jobSelection?.jobIds?.length! > 0 ||
alertParams.jobSelection?.groupIds?.length! > 0) &&
!!alertParams.resultType &&
!!alertParams.severity &&
alertParams.severity !== undefined &&
validationErrors === null;
const isInvalid = lookBehindInterval !== undefined && !!validationErrors;

View file

@ -65,25 +65,28 @@ export function registerMlAlerts(triggersActionsUi: TriggersAndActionsUIPublicPl
'xpack.ml.alertTypes.anomalyDetection.defaultActionMessage',
{
defaultMessage: `Elastic Stack Machine Learning Alert:
- Job IDs: \\{\\{#context.jobIds\\}\\}\\{\\{context.jobIds\\}\\} - \\{\\{/context.jobIds\\}\\}
- Job IDs: \\{\\{context.jobIds\\}\\}
- Time: \\{\\{context.timestampIso8601\\}\\}
- Anomaly score: \\{\\{context.score\\}\\}
Alerts are raised based on real-time scores. Remember that scores may be adjusted over time as data continues to be analyzed.
\\{\\{! Section might be not relevant if selected jobs don't contain influencer configuration \\}\\}
Top influencers:
\\{\\{#context.topInfluencers\\}\\}
\\{\\{influencer_field_name\\}\\} = \\{\\{influencer_field_value\\}\\} [\\{\\{score\\}\\}]
\\{\\{/context.topInfluencers\\}\\}
\\{\\{#context.topInfluencers.length\\}\\}
Top influencers:
\\{\\{#context.topInfluencers\\}\\}
\\{\\{influencer_field_name\\}\\} = \\{\\{influencer_field_value\\}\\} [\\{\\{score\\}\\}]
\\{\\{/context.topInfluencers\\}\\}
\\{\\{/context.topInfluencers.length\\}\\}
Top records:
\\{\\{#context.topRecords\\}\\}
\\{\\{function\\}\\}(\\{\\{field_name\\}\\}) \\{\\{by_field_value\\}\\} \\{\\{over_field_value\\}\\} \\{\\{partition_field_value\\}\\} [\\{\\{score\\}\\}]
\\{\\{/context.topRecords\\}\\}
\\{\\{#context.topRecords.length\\}\\}
Top records:
\\{\\{#context.topRecords\\}\\}
\\{\\{function\\}\\}(\\{\\{field_name\\}\\}) \\{\\{by_field_value\\}\\} \\{\\{over_field_value\\}\\} \\{\\{partition_field_value\\}\\} [\\{\\{score\\}\\}]
\\{\\{/context.topRecords\\}\\}
\\{\\{/context.topRecords.length\\}\\}
\\{\\{! Replace kibanaBaseUrl if not configured in Kibana \\}\\}
[Open in Anomaly Explorer](\\{\\{\\{context.kibanaBaseUrl\\}\\}\\}\\{\\{\\{context.anomalyExplorerUrl\\}\\}\\})
[Open in Anomaly Explorer](\\{\\{\\{kibanaBaseUrl\\}\\}\\}\\{\\{\\{context.anomalyExplorerUrl\\}\\}\\})
`,
}
),

View file

@ -16,7 +16,7 @@ import {
MlAnomalyDetectionAlertPreviewRequest,
} from '../../routes/schemas/alerting_schema';
import { ANOMALY_RESULT_TYPE } from '../../../common/constants/anomalies';
import { AnomalyResultType } from '../../../common/types/anomalies';
import { AnomalyRecordDoc, AnomalyResultType } from '../../../common/types/anomalies';
import {
AlertExecutionResult,
InfluencerAnomalyAlertDoc,
@ -27,8 +27,6 @@ import {
} from '../../../common/types/alerts';
import { AnomalyDetectionAlertContext } from './register_anomaly_detection_alert_type';
import { MlJobsResponse } from '../../../common/types/job_service';
import { ANOMALY_SCORE_MATCH_GROUP_ID } from '../../../common/constants/alerts';
import { getEntityFieldName, getEntityFieldValue } from '../../../common/util/anomaly_utils';
import { resolveBucketSpanInSeconds } from '../../../common/util/job_utils';
/**
@ -249,20 +247,10 @@ export function alertingServiceProvider(mlClient: MlClient, esClient: Elasticsea
};
/**
* Provides unique key for the anomaly result.
* Provides a key for alert instance.
*/
const getAlertInstanceKey = (source: any): string => {
let alertInstanceKey = `${source.job_id}_${source.timestamp}`;
if (source.result_type === ANOMALY_RESULT_TYPE.INFLUENCER) {
alertInstanceKey += `_${source.influencer_field_name}_${source.influencer_field_value}`;
} else if (source.result_type === ANOMALY_RESULT_TYPE.RECORD) {
const fieldName = getEntityFieldName(source);
const fieldValue = getEntityFieldValue(source);
const entity =
fieldName !== undefined && fieldValue !== undefined ? `_${fieldName}_${fieldValue}` : '';
alertInstanceKey += `_${source.detector_index}_${source.function}${entity}`;
}
return alertInstanceKey;
const getAlertInstanceKey = (source: AnomalyRecordDoc): string => {
return source.job_id;
};
/**
@ -297,8 +285,10 @@ export function alertingServiceProvider(mlClient: MlClient, esClient: Elasticsea
*/
const lookBackTimeInterval = `${Math.max(
// Double the max bucket span
resolveBucketSpanInSeconds(jobsResponse.map((v) => v.analysis_config.bucket_span)) * 2,
checkIntervalGap ? checkIntervalGap.asSeconds() : 0
Math.round(
resolveBucketSpanInSeconds(jobsResponse.map((v) => v.analysis_config.bucket_span)) * 2
),
checkIntervalGap ? Math.round(checkIntervalGap.asSeconds()) : 0
)}s`;
const jobIds = jobsResponse.map((v) => v.job_id);
@ -499,15 +489,11 @@ export function alertingServiceProvider(mlClient: MlClient, esClient: Elasticsea
* Return the result of an alert condition execution.
*
* @param params - Alert params
* @param publicBaseUrl
* @param alertId - Alert ID
* @param startedAt
* @param previousStartedAt
*/
execute: async (
params: MlAnomalyDetectionAlertParams,
publicBaseUrl: string | undefined,
alertId: string,
startedAt: Date,
previousStartedAt: Date | null
): Promise<AnomalyDetectionAlertContext | undefined> => {
@ -530,60 +516,8 @@ export function alertingServiceProvider(mlClient: MlClient, esClient: Elasticsea
...result,
name: result.alertInstanceKey,
anomalyExplorerUrl,
kibanaBaseUrl: publicBaseUrl!,
};
let kibanaEventLogCount = 0;
try {
// Check kibana-event-logs for presence of this alert instance
const kibanaLogResults = await esClient.count({
index: '.kibana-event-log-*',
body: {
query: {
bool: {
must: [
{
term: {
'kibana.alerting.action_group_id': {
value: ANOMALY_SCORE_MATCH_GROUP_ID,
},
},
},
{
term: {
'kibana.alerting.instance_id': {
value: executionResult.name,
},
},
},
{
nested: {
path: 'kibana.saved_objects',
query: {
term: {
'kibana.saved_objects.id': {
value: alertId,
},
},
},
},
},
],
},
},
},
});
kibanaEventLogCount = kibanaLogResults.body.count;
} catch (e) {
// eslint-disable-next-line no-console
console.log('Unable to check kibana event logs', e);
}
if (kibanaEventLogCount > 0) {
return;
}
return executionResult;
},
/**

View file

@ -38,13 +38,11 @@ export type AnomalyDetectionAlertContext = {
topRecords: RecordAnomalyAlertDoc[];
topInfluencers?: InfluencerAnomalyAlertDoc[];
anomalyExplorerUrl: string;
kibanaBaseUrl: string;
} & AlertInstanceContext;
export function registerAnomalyDetectionAlertType({
alerts,
mlSharedServices,
publicBaseUrl,
}: RegisterAlertParams) {
alerts.registerType<
MlAnomalyDetectionAlertParams,
@ -129,13 +127,7 @@ export function registerAnomalyDetectionAlertType({
services.savedObjectsClient,
fakeRequest
);
const executionResult = await execute(
params,
publicBaseUrl,
alertId,
startedAt,
previousStartedAt
);
const executionResult = await execute(params, startedAt, previousStartedAt);
if (executionResult) {
const alertInstanceName = executionResult.name;

View file

@ -14,7 +14,6 @@ export interface RegisterAlertParams {
alerts: AlertingPlugin['setup'];
logger: Logger;
mlSharedServices: SharedServices;
publicBaseUrl: string | undefined;
}
export function registerMlAlerts(params: RegisterAlertParams) {

View file

@ -213,7 +213,6 @@ export class MlServerPlugin
alerts: plugins.alerts,
logger: this.log,
mlSharedServices: sharedServices,
publicBaseUrl: coreSetup.http.basePath.publicBaseUrl,
});
}