Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions x-pack/plugins/ml/common/constants/detector_rule.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,11 @@ export const OPERATOR = {
GREATER_THAN: 'gt',
GREATER_THAN_OR_EQUAL: 'gte',
};

// List of detector functions which don't support rules with numeric conditions.
export const CONDITIONS_NOT_SUPPORTED_FUNCTIONS = [
'freq_rare',
'lat_long',
'metric',
'rare',
];
113 changes: 113 additions & 0 deletions x-pack/plugins/ml/common/util/__tests__/anomaly_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getEntityFieldValue,
showActualForFunction,
showTypicalForFunction,
isRuleSupported,
aggregationTypeTransform
} from '../anomaly_utils';

Expand Down Expand Up @@ -81,6 +82,103 @@ describe('ML - anomaly utils', () => {
'field_name': 'responsetime'
};

const metricNoEntityRecord = {
'job_id': 'farequote_metric',
'result_type': 'record',
'probability': 0.030133495093182184,
'record_score': 0.024881740359975164,
'initial_record_score': 0.024881740359975164,
'bucket_span': 900,
'detector_index': 0,
'is_interim': false,
'timestamp': 1486845000000,
'function': 'metric',
'function_description': 'mean',
'typical': [
545.7764658569108
],
'actual': [
758.8220213274412
],
'field_name': 'responsetime',
'influencers': [
{
'influencer_field_name': 'airline',
'influencer_field_values': [
'NKS'
]
}
],
'airline': [
'NKS'
]
};

const rareEntityRecord = {
'job_id': 'gallery',
'result_type': 'record',
'probability': 0.02277014211908481,
'record_score': 4.545378107075983,
'initial_record_score': 4.545378107075983,
'bucket_span': 3600,
'detector_index': 0,
'is_interim': false,
'timestamp': 1495879200000,
'by_field_name': 'status',
'function': 'rare',
'function_description': 'rare',
'over_field_name': 'clientip',
'over_field_value': '173.252.74.112',
'causes': [
{
'probability': 0.02277014211908481,
'by_field_name': 'status',
'by_field_value': '206',
'function': 'rare',
'function_description': 'rare',
'typical': [
0.00014832458182211878
],
'actual': [
1
],
'over_field_name': 'clientip',
'over_field_value': '173.252.74.112'
}
],
'influencers': [
{
'influencer_field_name': 'uri',
'influencer_field_values': [
'/wp-content/uploads/2013/06/dune_house_oil_on_canvas_24x20-298x298.jpg',
'/wp-content/uploads/2013/10/Case-dAste-1-11-298x298.png'
]
},
{
'influencer_field_name': 'status',
'influencer_field_values': [
'206'
]
},
{
'influencer_field_name': 'clientip',
'influencer_field_values': [
'173.252.74.112'
]
}
],
'clientip': [
'173.252.74.112'
],
'uri': [
'/wp-content/uploads/2013/06/dune_house_oil_on_canvas_24x20-298x298.jpg',
'/wp-content/uploads/2013/10/Case-dAste-1-11-298x298.png'
],
'status': [
'206'
]
};

describe('getSeverity', () => {

it('returns warning for 0 <= score < 25', () => {
Expand Down Expand Up @@ -282,6 +380,21 @@ describe('ML - anomaly utils', () => {

});

describe('isRuleSupported', () => {
it('returns true for anomalies supporting rules', () => {
expect(isRuleSupported(partitionEntityRecord)).to.be(true);
expect(isRuleSupported(byEntityRecord)).to.be(true);
expect(isRuleSupported(overEntityRecord)).to.be(true);
expect(isRuleSupported(rareEntityRecord)).to.be(true);
expect(isRuleSupported(noEntityRecord)).to.be(true);
});

it('returns false for anomaly not supporting rules', () => {
expect(isRuleSupported(metricNoEntityRecord)).to.be(false);
});

});

describe('aggregationTypeTransform', () => {
it('returns correct ES aggregation type for ML function description', () => {
expect(aggregationTypeTransform.toES('count')).to.be('count');
Expand Down
9 changes: 9 additions & 0 deletions x-pack/plugins/ml/common/util/anomaly_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/

import _ from 'lodash';
import { CONDITIONS_NOT_SUPPORTED_FUNCTIONS } from '../constants/detector_rule';

// List of function descriptions for which actual values from record level results should be displayed.
const DISPLAY_ACTUAL_FUNCTIONS = ['count', 'distinct_count', 'lat_long', 'mean', 'max', 'min', 'sum',
Expand Down Expand Up @@ -152,6 +153,14 @@ export function showTypicalForFunction(functionDescription) {
return _.indexOf(DISPLAY_TYPICAL_FUNCTIONS, functionDescription) > -1;
}

// Returns whether a rule can be configured against the specified anomaly.
export function isRuleSupported(record) {
// A rule can be configured with a numeric condition if the function supports it,
// and/or with scope if there is a partitioning fields.
return (CONDITIONS_NOT_SUPPORTED_FUNCTIONS.indexOf(record.function) === -1) ||
(getEntityFieldName(record) !== undefined);
}

// Two functions for converting aggregation type names.
// ML and ES use different names for the same function.
// Possible values for ML aggregation type are (defined in lib/model/CAnomalyDetector.cc):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { checkPermission } from 'plugins/ml/privilege/check_privilege';

import { mlAnomaliesTableService } from './anomalies_table_service';
import { mlFieldFormatService } from 'plugins/ml/services/field_format_service';
import { getSeverityColor } from 'plugins/ml/../common/util/anomaly_utils';
import { getSeverityColor, isRuleSupported } from 'plugins/ml/../common/util/anomaly_utils';
import { formatValue } from 'plugins/ml/formatters/format_value';
import { RuleEditorFlyout } from 'plugins/ml/components/rule_editor';

Expand All @@ -56,8 +56,8 @@ function renderTime(date, aggregationInterval) {
}

function showLinksMenuForItem(item) {
const canUpdateJob = checkPermission('canUpdateJob');
return (canUpdateJob ||
const canConfigureRules = (isRuleSupported(item) && checkPermission('canUpdateJob'));
return (canConfigureRules ||
item.isTimeSeriesViewDetector ||
item.entityName === 'mlcategory' ||
item.customUrls !== undefined);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { toastNotifications } from 'ui/notify';

import { ES_FIELD_TYPES } from 'plugins/ml/../common/constants/field_types';
import { checkPermission } from 'plugins/ml/privilege/check_privilege';
import { isRuleSupported } from 'plugins/ml/../common/util/anomaly_utils';
import { parseInterval } from 'plugins/ml/../common/util/parse_interval';
import { getFieldTypeFromMapping } from 'plugins/ml/services/mapping_service';
import { ml } from 'plugins/ml/services/ml_api_service';
Expand Down Expand Up @@ -336,7 +337,7 @@ export class LinksMenu extends Component {

render() {
const { anomaly, showViewSeriesLink } = this.props;
const canUpdateJob = checkPermission('canUpdateJob');
const canConfigureRules = (isRuleSupported(anomaly.source) && checkPermission('canUpdateJob'));

const button = (
<EuiButtonIcon
Expand Down Expand Up @@ -387,7 +388,7 @@ export class LinksMenu extends Component {
);
}

if (canUpdateJob) {
if (canConfigureRules) {
items.push(
<EuiContextMenuItem
key="create_rule"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import {
deleteJobRule
} from './utils';

import { ACTION } from '../../../common/constants/detector_rule';
import { ACTION, CONDITIONS_NOT_SUPPORTED_FUNCTIONS } from '../../../common/constants/detector_rule';
import { getPartitioningFieldNames } from 'plugins/ml/../common/util/job_utils';
import { mlJobService } from 'plugins/ml/services/job_service';
import { ml } from 'plugins/ml/services/ml_api_service';
Expand Down Expand Up @@ -366,8 +366,6 @@ export class RuleEditorFlyout extends Component {

let flyout;

const hasPartitioningFields = (this.partitioningFieldNames && this.partitioningFieldNames.length > 0);

if (ruleIndex === -1) {
flyout = (
<EuiFlyout
Expand Down Expand Up @@ -409,9 +407,12 @@ export class RuleEditorFlyout extends Component {
</EuiFlyout>
);
} else {
const hasPartitioningFields = (this.partitioningFieldNames && this.partitioningFieldNames.length > 0);
const conditionSupported = (CONDITIONS_NOT_SUPPORTED_FUNCTIONS.indexOf(anomaly.source.function) === -1);
const conditionsText = 'Add numeric conditions to take action according ' +
'to the actual or typical values of the anomaly. Multiple conditions are ' +
'combined using AND.';

flyout = (
<EuiFlyout
className="ml-rule-editor-flyout"
Expand Down Expand Up @@ -452,14 +453,23 @@ export class RuleEditorFlyout extends Component {
<h2>Conditions</h2>
</EuiTitle>
<EuiSpacer size="s" />
<EuiCheckbox
id="enable_conditions_checkbox"
className="scope-enable-checkbox"
label={conditionsText}
checked={isConditionsEnabled}
onChange={this.onConditionsEnabledChange}
disabled={!hasPartitioningFields}
/>
{(conditionSupported === true) ?
(
<EuiCheckbox
id="enable_conditions_checkbox"
className="scope-enable-checkbox"
label={conditionsText}
checked={isConditionsEnabled}
onChange={this.onConditionsEnabledChange}
disabled={!conditionSupported || !hasPartitioningFields}
/>
) : (
<EuiCallOut
title={`Conditions are not supported for detectors using the ${anomaly.source.function} function`}
iconType="iInCircle"
/>
)
}
<EuiSpacer size="s" />
<ConditionsSection
isEnabled={isConditionsEnabled}
Expand Down