208 lines
7.2 KiB
JavaScript
208 lines
7.2 KiB
JavaScript
/*
|
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
* or more contributor license agreements. Licensed under the Elastic License;
|
|
* you may not use this file except in compliance with the Elastic License.
|
|
*/
|
|
|
|
|
|
|
|
/*
|
|
* Contains functions for operations commonly performed on anomaly data
|
|
* to extract information for display in dashboards.
|
|
*/
|
|
|
|
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',
|
|
'median', 'varp', 'info_content', 'time'];
|
|
|
|
// List of function descriptions for which typical values from record level results should be displayed.
|
|
const DISPLAY_TYPICAL_FUNCTIONS = ['count', 'distinct_count', 'lat_long', 'mean', 'max', 'min', 'sum',
|
|
'median', 'varp', 'info_content', 'time'];
|
|
|
|
// Returns a severity label (one of critical, major, minor, warning or unknown)
|
|
// for the supplied normalized anomaly score (a value between 0 and 100).
|
|
export function getSeverity(normalizedScore) {
|
|
if (normalizedScore >= 75) {
|
|
return 'critical';
|
|
} else if (normalizedScore >= 50) {
|
|
return 'major';
|
|
} else if (normalizedScore >= 25) {
|
|
return 'minor';
|
|
} else if (normalizedScore >= 0) {
|
|
return 'warning';
|
|
} else {
|
|
return 'unknown';
|
|
}
|
|
}
|
|
|
|
// Returns a severity label (one of critical, major, minor, warning, low or unknown)
|
|
// for the supplied normalized anomaly score (a value between 0 and 100), where scores
|
|
// less than 3 are assigned a severity of 'low'.
|
|
export function getSeverityWithLow(normalizedScore) {
|
|
if (normalizedScore >= 75) {
|
|
return 'critical';
|
|
} else if (normalizedScore >= 50) {
|
|
return 'major';
|
|
} else if (normalizedScore >= 25) {
|
|
return 'minor';
|
|
} else if (normalizedScore >= 3) {
|
|
return 'warning';
|
|
} else if (normalizedScore >= 0) {
|
|
return 'low';
|
|
} else {
|
|
return 'unknown';
|
|
}
|
|
}
|
|
|
|
// Returns a severity RGB color (one of critical, major, minor, warning, low_warning or unknown)
|
|
// for the supplied normalized anomaly score (a value between 0 and 100).
|
|
export function getSeverityColor(normalizedScore) {
|
|
if (normalizedScore >= 75) {
|
|
return '#fe5050';
|
|
} else if (normalizedScore >= 50) {
|
|
return '#fba740';
|
|
} else if (normalizedScore >= 25) {
|
|
return '#fdec25';
|
|
} else if (normalizedScore >= 3) {
|
|
return '#8bc8fb';
|
|
} else if (normalizedScore >= 0) {
|
|
return '#d2e9f7';
|
|
} else {
|
|
return '#ffffff';
|
|
}
|
|
}
|
|
|
|
// Recurses through an object holding the list of detector descriptions against job IDs
|
|
// checking for duplicate descriptions. For any detectors with duplicate descriptions, the
|
|
// description is modified by appending the job ID in parentheses.
|
|
// Only checks for duplicates across jobs; any duplicates within a job are left as-is.
|
|
export function labelDuplicateDetectorDescriptions(detectorsByJob) {
|
|
const checkedJobIds = [];
|
|
_.each(detectorsByJob, function (detectors, jobId) {
|
|
checkedJobIds.push(jobId);
|
|
const otherJobs = _.omit(detectorsByJob, checkedJobIds);
|
|
_.each(detectors, function (description, i) {
|
|
_.each(otherJobs, function (otherJobDetectors, otherJobId) {
|
|
_.each(otherJobDetectors, function (otherDescription, j) {
|
|
if (description === otherDescription) {
|
|
detectors[i] = description + ' (' + jobId + ')';
|
|
otherJobDetectors[j] = description + ' (' + otherJobId + ')';
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
return detectorsByJob;
|
|
}
|
|
|
|
// Returns the name of the field to use as the entity name from the source record
|
|
// obtained from Elasticsearch. The function looks first for a by_field, then over_field,
|
|
// then partition_field, returning undefined if none of these fields are present.
|
|
export function getEntityFieldName(record) {
|
|
// Analyses with by and over fields, will have a top-level by_field_name, but
|
|
// the by_field_value(s) will be in the nested causes array.
|
|
if (_.has(record, 'by_field_name') && _.has(record, 'by_field_value')) {
|
|
return record.by_field_name;
|
|
}
|
|
|
|
if (_.has(record, 'over_field_name')) {
|
|
return record.over_field_name;
|
|
}
|
|
|
|
if (_.has(record, 'partition_field_name')) {
|
|
return record.partition_field_name;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
// Returns the value of the field to use as the entity value from the source record
|
|
// obtained from Elasticsearch. The function looks first for a by_field, then over_field,
|
|
// then partition_field, returning undefined if none of these fields are present.
|
|
export function getEntityFieldValue(record) {
|
|
if (_.has(record, 'by_field_value')) {
|
|
return record.by_field_value;
|
|
}
|
|
|
|
if (_.has(record, 'over_field_value')) {
|
|
return record.over_field_value;
|
|
}
|
|
|
|
if (_.has(record, 'partition_field_value')) {
|
|
return record.partition_field_value;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
// Returns whether actual values should be displayed for a record with the specified function description.
|
|
// Note that the 'function' field in a record contains what the user entered e.g. 'high_count',
|
|
// whereas the 'function_description' field holds a ML-built display hint for function e.g. 'count'.
|
|
export function showActualForFunction(functionDescription) {
|
|
return _.indexOf(DISPLAY_ACTUAL_FUNCTIONS, functionDescription) > -1;
|
|
}
|
|
|
|
// Returns whether typical values should be displayed for a record with the specified function description.
|
|
// Note that the 'function' field in a record contains what the user entered e.g. 'high_count',
|
|
// whereas the 'function_description' field holds a ML-built display hint for function e.g. 'count'.
|
|
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):
|
|
// count
|
|
// distinct_count
|
|
// rare
|
|
// info_content
|
|
// mean
|
|
// median
|
|
// min
|
|
// max
|
|
// varp
|
|
// sum
|
|
// lat_long
|
|
// time
|
|
// The input to toES and the output from toML correspond to the value of the
|
|
// function_description field of anomaly records.
|
|
export const aggregationTypeTransform = {
|
|
toES: function (oldAggType) {
|
|
let newAggType = oldAggType;
|
|
|
|
if (newAggType === 'mean') {
|
|
newAggType = 'avg';
|
|
} else if (newAggType === 'distinct_count') {
|
|
newAggType = 'cardinality';
|
|
} else if (newAggType === 'median') {
|
|
newAggType = 'percentiles';
|
|
}
|
|
|
|
return newAggType;
|
|
},
|
|
toML: function (oldAggType) {
|
|
let newAggType = oldAggType;
|
|
|
|
if (newAggType === 'avg') {
|
|
newAggType = 'mean';
|
|
} else if (newAggType === 'cardinality') {
|
|
newAggType = 'distinct_count';
|
|
} else if (newAggType === 'percentiles') {
|
|
newAggType = 'median';
|
|
}
|
|
|
|
return newAggType;
|
|
}
|
|
};
|