[ML] Fix formatting of values for time of day or week anomalies (#32134) (#32287)

This commit is contained in:
Pete Harverson 2019-03-01 10:42:41 +00:00 committed by GitHub
parent 06e4388b95
commit 1687a660bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 70 additions and 30 deletions

View file

@ -171,7 +171,7 @@ export function getColumns(
}), }),
render: (actual, item) => { render: (actual, item) => {
const fieldFormat = mlFieldFormatService.getFieldFormat(item.jobId, item.source.detector_index); const fieldFormat = mlFieldFormatService.getFieldFormat(item.jobId, item.source.detector_index);
return formatValue(item.actual, item.source.function, fieldFormat); return formatValue(item.actual, item.source.function, fieldFormat, item.source);
}, },
sortable: true sortable: true
}); });
@ -185,7 +185,7 @@ export function getColumns(
}), }),
render: (typical, item) => { render: (typical, item) => {
const fieldFormat = mlFieldFormatService.getFieldFormat(item.jobId, item.source.detector_index); const fieldFormat = mlFieldFormatService.getFieldFormat(item.jobId, item.source.detector_index);
return formatValue(item.typical, item.source.function, fieldFormat); return formatValue(item.typical, item.source.function, fieldFormat, item.source);
}, },
sortable: true sortable: true
}); });

View file

@ -152,7 +152,7 @@ function getDetailsItems(anomaly, examples, filter) {
title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.actualTitle', { title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.actualTitle', {
defaultMessage: 'actual', defaultMessage: 'actual',
}), }),
description: formatValue(anomaly.actual, source.function) description: formatValue(anomaly.actual, source.function, undefined, source)
}); });
} }
@ -161,7 +161,7 @@ function getDetailsItems(anomaly, examples, filter) {
title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.typicalTitle', { title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.typicalTitle', {
defaultMessage: 'typical', defaultMessage: 'typical',
}), }),
description: formatValue(anomaly.typical, source.function) description: formatValue(anomaly.typical, source.function, undefined, source)
}); });
} }

View file

@ -7,23 +7,55 @@
import expect from 'expect.js'; import expect from 'expect.js';
import moment from 'moment'; import moment from 'moment-timezone';
import { formatValue } from '../format_value'; import { formatValue } from '../format_value';
describe('ML - formatValue formatter', () => { describe('ML - formatValue formatter', () => {
const timeOfWeekRecord = {
job_id: 'gallery_time_of_week',
result_type: 'record',
probability: 0.012818,
record_score: 53.55134,
bucket_span: 900,
detector_index: 0,
timestamp: 1530155700000,
by_field_name: 'clientip',
by_field_value: '65.55.215.39',
function: 'time_of_week',
function_description: 'time'
};
// Just check the return value is in the expected format, and const timeOfDayRecord = {
// not the exact value as this will be timezone specific. job_id: 'gallery_time_of_day',
result_type: 'record',
probability: 0.012818,
record_score: 97.94245,
bucket_span: 900,
detector_index: 0,
timestamp: 1517472900000,
by_field_name: 'clientip',
by_field_value: '157.56.93.83',
function: 'time_of_day',
function_description: 'time'
};
// Set timezone to US/Eastern for time_of_day and time_of_week tests.
beforeEach(() => {
moment.tz.setDefault('US/Eastern');
});
afterEach(() => {
moment.tz.setDefault('Browser');
});
// For time_of_day and time_of_week test values which are offsets in seconds
// from UTC start of week / day are formatted correctly using the test timezone.
it('correctly formats time_of_week value from numeric input', () => { it('correctly formats time_of_week value from numeric input', () => {
const formattedValue = formatValue(1483228800, 'time_of_week'); expect(formatValue(359739, 'time_of_week', undefined, timeOfWeekRecord)).to.be('Wed 23:55');
const result = moment(formattedValue, 'ddd hh:mm', true).isValid();
expect(result).to.be(true);
}); });
it('correctly formats time_of_day value from numeric input', () => { it('correctly formats time_of_day value from numeric input', () => {
const formattedValue = formatValue(1483228800, 'time_of_day'); expect(formatValue(73781, 'time_of_day', undefined, timeOfDayRecord)).to.be('15:29');
const result = moment(formattedValue, 'hh:mm', true).isValid();
expect(result).to.be(true);
}); });
it('correctly formats number values from numeric input', () => { it('correctly formats number values from numeric input', () => {
@ -37,15 +69,11 @@ describe('ML - formatValue formatter', () => {
}); });
it('correctly formats time_of_week value from array input', () => { it('correctly formats time_of_week value from array input', () => {
const formattedValue = formatValue([1483228800], 'time_of_week'); expect(formatValue([359739], 'time_of_week', undefined, timeOfWeekRecord)).to.be('Wed 23:55');
const result = moment(formattedValue, 'ddd hh:mm', true).isValid();
expect(result).to.be(true);
}); });
it('correctly formats time_of_day value from array input', () => { it('correctly formats time_of_day value from array input', () => {
const formattedValue = formatValue([1483228800], 'time_of_day'); expect(formatValue([73781], 'time_of_day', undefined, timeOfDayRecord)).to.be('15:29');
const result = moment(formattedValue, 'hh:mm', true).isValid();
expect(result).to.be(true);
}); });
it('correctly formats number values from array input', () => { it('correctly formats number values from array input', () => {

View file

@ -24,42 +24,54 @@ const SIGFIGS_IF_ROUNDING = 3; // Number of sigfigs to use for values < 10
// Formats the value of an actual or typical field from a machine learning anomaly record. // Formats the value of an actual or typical field from a machine learning anomaly record.
// mlFunction is the 'function' field from the ML record containing what the user entered e.g. 'high_count', // mlFunction is the 'function' field from the ML record containing what the user entered e.g. 'high_count',
// (as opposed to the 'function_description' field which holds an ML-built display hint for the function e.g. 'count'. // (as opposed to the 'function_description' field which holds an ML-built display hint for the function e.g. 'count'.
export function formatValue(value, mlFunction, fieldFormat) { // If a Kibana fieldFormat is not supplied, will fall back to default
// formatting depending on the magnitude of the value.
// For time_of_day or time_of_week functions the anomaly record
// containing the timestamp of the anomaly should be supplied in
// order to correctly format the day or week offset to the time of the anomaly.
export function formatValue(value, mlFunction, fieldFormat, record) {
// actual and typical values in anomaly record results will be arrays. // actual and typical values in anomaly record results will be arrays.
// Unless the array is multi-valued (as it will be for multi-variate analyses such as lat_long), // Unless the array is multi-valued (as it will be for multi-variate analyses such as lat_long),
// simply return the formatted single value. // simply return the formatted single value.
if (Array.isArray(value)) { if (Array.isArray(value)) {
if (value.length === 1) { if (value.length === 1) {
return formatSingleValue(value[0], mlFunction, fieldFormat); return formatSingleValue(value[0], mlFunction, fieldFormat, record);
} else { } else {
// Return with array style formatting. // Return with array style formatting.
const values = value.map(val => formatSingleValue(val, mlFunction, fieldFormat)); const values = value.map(val => formatSingleValue(val, mlFunction, fieldFormat, record));
return `[${values}]`; return `[${values}]`;
} }
} else { } else {
return formatSingleValue(value, mlFunction, fieldFormat); return formatSingleValue(value, mlFunction, fieldFormat, record);
} }
} }
// Formats a single value according to the specified ML function. // Formats a single value according to the specified ML function.
// If a Kibana fieldFormat is not supplied, will fall back to default // If a Kibana fieldFormat is not supplied, will fall back to default
// formatting depending on the magnitude of the value. // formatting depending on the magnitude of the value.
function formatSingleValue(value, mlFunction, fieldFormat) { // For time_of_day or time_of_week functions the anomaly record
// containing the timestamp of the anomaly should be supplied in
// order to correctly format the day or week offset to the time of the anomaly.
function formatSingleValue(value, mlFunction, fieldFormat, record) {
if (value === undefined || value === null) { if (value === undefined || value === null) {
return ''; return '';
} }
// If the analysis function is time_of_week/day, format as day/time. // If the analysis function is time_of_week/day, format as day/time.
// For time_of_week / day, actual / typical is the UTC offset in seconds from the
// start of the week / day, so need to manipulate to UTC moment of the start of the week / day
// that the anomaly occurred using record timestamp if supplied, add on the offset, and finally
// revert back to configured timezone for formatting.
if (mlFunction === 'time_of_week') { if (mlFunction === 'time_of_week') {
const d = new Date(); const d = ((record !== undefined && record.timestamp !== undefined) ? new Date(record.timestamp) : new Date());
const i = parseInt(value); const i = parseInt(value);
d.setTime(i * 1000); const utcMoment = moment.utc(d).startOf('week').add(i, 's');
return moment(d).format('ddd hh:mm'); return moment(utcMoment.valueOf()).format('ddd HH:mm');
} else if (mlFunction === 'time_of_day') { } else if (mlFunction === 'time_of_day') {
const d = new Date(); const d = ((record !== undefined && record.timestamp !== undefined) ? new Date(record.timestamp) : new Date());
const i = parseInt(value); const i = parseInt(value);
d.setTime(i * 1000); const utcMoment = moment.utc(d).startOf('day').add(i, 's');
return moment(d).format('hh:mm'); return moment(utcMoment.valueOf()).format('HH:mm');
} else { } else {
if (fieldFormat !== undefined) { if (fieldFormat !== undefined) {
return fieldFormat.convert(value, 'text'); return fieldFormat.convert(value, 'text');