[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) => {
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
});
@ -185,7 +185,7 @@ export function getColumns(
}),
render: (typical, item) => {
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
});

View file

@ -152,7 +152,7 @@ function getDetailsItems(anomaly, examples, filter) {
title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.actualTitle', {
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', {
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 moment from 'moment';
import moment from 'moment-timezone';
import { formatValue } from '../format_value';
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
// not the exact value as this will be timezone specific.
const timeOfDayRecord = {
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', () => {
const formattedValue = formatValue(1483228800, 'time_of_week');
const result = moment(formattedValue, 'ddd hh:mm', true).isValid();
expect(result).to.be(true);
expect(formatValue(359739, 'time_of_week', undefined, timeOfWeekRecord)).to.be('Wed 23:55');
});
it('correctly formats time_of_day value from numeric input', () => {
const formattedValue = formatValue(1483228800, 'time_of_day');
const result = moment(formattedValue, 'hh:mm', true).isValid();
expect(result).to.be(true);
expect(formatValue(73781, 'time_of_day', undefined, timeOfDayRecord)).to.be('15:29');
});
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', () => {
const formattedValue = formatValue([1483228800], 'time_of_week');
const result = moment(formattedValue, 'ddd hh:mm', true).isValid();
expect(result).to.be(true);
expect(formatValue([359739], 'time_of_week', undefined, timeOfWeekRecord)).to.be('Wed 23:55');
});
it('correctly formats time_of_day value from array input', () => {
const formattedValue = formatValue([1483228800], 'time_of_day');
const result = moment(formattedValue, 'hh:mm', true).isValid();
expect(result).to.be(true);
expect(formatValue([73781], 'time_of_day', undefined, timeOfDayRecord)).to.be('15:29');
});
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.
// 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'.
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.
// Unless the array is multi-valued (as it will be for multi-variate analyses such as lat_long),
// simply return the formatted single value.
if (Array.isArray(value)) {
if (value.length === 1) {
return formatSingleValue(value[0], mlFunction, fieldFormat);
return formatSingleValue(value[0], mlFunction, fieldFormat, record);
} else {
// 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}]`;
}
} else {
return formatSingleValue(value, mlFunction, fieldFormat);
return formatSingleValue(value, mlFunction, fieldFormat, record);
}
}
// Formats a single value according to the specified ML function.
// If a Kibana fieldFormat is not supplied, will fall back to default
// 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) {
return '';
}
// 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') {
const d = new Date();
const d = ((record !== undefined && record.timestamp !== undefined) ? new Date(record.timestamp) : new Date());
const i = parseInt(value);
d.setTime(i * 1000);
return moment(d).format('ddd hh:mm');
const utcMoment = moment.utc(d).startOf('week').add(i, 's');
return moment(utcMoment.valueOf()).format('ddd HH:mm');
} 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);
d.setTime(i * 1000);
return moment(d).format('hh:mm');
const utcMoment = moment.utc(d).startOf('day').add(i, 's');
return moment(utcMoment.valueOf()).format('HH:mm');
} else {
if (fieldFormat !== undefined) {
return fieldFormat.convert(value, 'text');