From 6c8c0d4dcac1e8c1ca7d39922802087fa1694b6a Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Thu, 28 Feb 2019 09:32:32 +0000 Subject: [PATCH] [ML] Fix formatting of values for time of day or week anomalies (#32134) --- .../anomalies_table_columns.js | 4 +- .../anomalies_table/anomaly_details.js | 4 +- .../formatters/__tests__/format_value.js | 58 ++++++++++++++----- .../ml/public/formatters/format_value.js | 34 +++++++---- 4 files changed, 70 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/ml/public/components/anomalies_table/anomalies_table_columns.js b/x-pack/plugins/ml/public/components/anomalies_table/anomalies_table_columns.js index b45d65b2f56f6..ff2c61d3030a4 100644 --- a/x-pack/plugins/ml/public/components/anomalies_table/anomalies_table_columns.js +++ b/x-pack/plugins/ml/public/components/anomalies_table/anomalies_table_columns.js @@ -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 }); diff --git a/x-pack/plugins/ml/public/components/anomalies_table/anomaly_details.js b/x-pack/plugins/ml/public/components/anomalies_table/anomaly_details.js index 7bbc59dddad5b..cc47a83eda597 100644 --- a/x-pack/plugins/ml/public/components/anomalies_table/anomaly_details.js +++ b/x-pack/plugins/ml/public/components/anomalies_table/anomaly_details.js @@ -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) }); } diff --git a/x-pack/plugins/ml/public/formatters/__tests__/format_value.js b/x-pack/plugins/ml/public/formatters/__tests__/format_value.js index ff5548b73551e..69362e76ceced 100644 --- a/x-pack/plugins/ml/public/formatters/__tests__/format_value.js +++ b/x-pack/plugins/ml/public/formatters/__tests__/format_value.js @@ -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', () => { diff --git a/x-pack/plugins/ml/public/formatters/format_value.js b/x-pack/plugins/ml/public/formatters/format_value.js index da6eeba608b1b..800eb9dc48af6 100644 --- a/x-pack/plugins/ml/public/formatters/format_value.js +++ b/x-pack/plugins/ml/public/formatters/format_value.js @@ -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');