diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index 8842c3e12ee8..78d20575c349 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -15,7 +15,7 @@ import _ from 'lodash'; import $ from 'jquery'; import DragSelect from 'dragselect'; -import moment from 'moment'; +import moment from 'moment-timezone'; import 'plugins/ml/components/anomalies_table'; import 'plugins/ml/components/controls'; @@ -76,6 +76,7 @@ module.controller('MlExplorerController', function ( $timeout, AppState, Private, + config, mlCheckboxShowChartsService, mlSelectLimitService, mlSelectIntervalService, @@ -87,6 +88,10 @@ module.controller('MlExplorerController', function ( timefilter.enableTimeRangeSelector(); timefilter.enableAutoRefreshSelector(); + // Pass the timezone to the server for use when aggregating anomalies (by day / hour) for the table. + const tzConfig = config.get('dateFormat:tz'); + const dateFormatTz = (tzConfig !== 'Browser') ? tzConfig : moment.tz.guess(); + const TimeBuckets = Private(IntervalHelperProvider); const queryFilter = Private(FilterBarQueryFilterProvider); const mlJobSelectService = Private(JobSelectServiceProvider); @@ -946,6 +951,7 @@ module.controller('MlExplorerController', function ( mlSelectSeverityService.state.get('threshold').val, timeRange.earliestMs, timeRange.latestMs, + dateFormatTz, 500, MAX_CATEGORY_EXAMPLES ).then((resp) => { diff --git a/x-pack/plugins/ml/public/services/ml_api_service/results.js b/x-pack/plugins/ml/public/services/ml_api_service/results.js index b4254fa7b251..f89edfb32d2a 100644 --- a/x-pack/plugins/ml/public/services/ml_api_service/results.js +++ b/x-pack/plugins/ml/public/services/ml_api_service/results.js @@ -21,6 +21,7 @@ export const results = { threshold, earliestMs, latestMs, + dateFormatTz, maxRecords, maxExamples) { @@ -35,6 +36,7 @@ export const results = { threshold, earliestMs, latestMs, + dateFormatTz, maxRecords, maxExamples } diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_controller.js b/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_controller.js index 6fed683f8981..44b7d59fe504 100644 --- a/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_controller.js +++ b/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_controller.js @@ -13,7 +13,7 @@ */ import _ from 'lodash'; -import moment from 'moment'; +import moment from 'moment-timezone'; import 'plugins/ml/components/anomalies_table'; import 'plugins/ml/components/controls'; @@ -71,6 +71,7 @@ module.controller('MlTimeSeriesExplorerController', function ( $timeout, Private, AppState, + config, mlSelectIntervalService, mlSelectSeverityService) { @@ -98,6 +99,10 @@ module.controller('MlTimeSeriesExplorerController', function ( $scope.showForecast = true; // Toggles display of forecast data in the focus chart $scope.showForecastCheckbox = false; + // Pass the timezone to the server for use when aggregating anomalies (by day / hour) for the table. + const tzConfig = config.get('dateFormat:tz'); + const dateFormatTz = (tzConfig !== 'Browser') ? tzConfig : moment.tz.guess(); + $scope.permissions = { canForecastJob: checkPermission('canForecastJob') }; @@ -682,6 +687,7 @@ module.controller('MlTimeSeriesExplorerController', function ( } function loadAnomaliesTableData(earliestMs, latestMs) { + ml.results.getAnomaliesTableData( [$scope.selectedJob.job_id], $scope.criteriaFields, @@ -690,6 +696,7 @@ module.controller('MlTimeSeriesExplorerController', function ( mlSelectSeverityService.state.get('threshold').val, earliestMs, latestMs, + dateFormatTz, ANOMALIES_MAX_RESULTS ).then((resp) => { const anomalies = resp.anomalies; diff --git a/x-pack/plugins/ml/server/models/results_service/build_anomaly_table_items.js b/x-pack/plugins/ml/server/models/results_service/build_anomaly_table_items.js index e1755f136dec..583c806137af 100644 --- a/x-pack/plugins/ml/server/models/results_service/build_anomaly_table_items.js +++ b/x-pack/plugins/ml/server/models/results_service/build_anomaly_table_items.js @@ -6,7 +6,7 @@ import _ from 'lodash'; -import moment from 'moment'; +import moment from 'moment-timezone'; import { getEntityFieldName, @@ -17,13 +17,15 @@ import { // Builds the items for display in the anomalies table from the supplied list of anomaly records. -export function buildAnomalyTableItems(anomalyRecords, aggregationInterval) { +// Provide the timezone to use for aggregating anomalies (by day or hour) as set in the +// Kibana dateFormat:tz setting. +export function buildAnomalyTableItems(anomalyRecords, aggregationInterval, dateFormatTz) { // Aggregate the anomaly records if necessary, and create skeleton display records with // time, detector (description) and source record properties set. let displayRecords = []; if (aggregationInterval !== 'second') { - displayRecords = aggregateAnomalies(anomalyRecords, aggregationInterval); + displayRecords = aggregateAnomalies(anomalyRecords, aggregationInterval, dateFormatTz); } else { // Show all anomaly records. displayRecords = anomalyRecords.map((record) => { @@ -115,7 +117,7 @@ export function buildAnomalyTableItems(anomalyRecords, aggregationInterval) { }); } -function aggregateAnomalies(anomalyRecords, interval) { +function aggregateAnomalies(anomalyRecords, interval, dateFormatTz) { // Aggregate the anomaly records by time, jobId, detectorIndex, and entity (by/over/partition). // anomalyRecords assumed to be supplied in ascending time order. if (anomalyRecords.length === 0) { @@ -125,7 +127,9 @@ function aggregateAnomalies(anomalyRecords, interval) { const aggregatedData = {}; anomalyRecords.forEach((record) => { // Use moment.js to get start of interval. - const roundedTime = moment(record.timestamp).startOf(interval).valueOf(); + const roundedTime = (dateFormatTz !== undefined) ? + moment(record.timestamp).tz(dateFormatTz).startOf(interval).valueOf() : + moment(record.timestamp).startOf(interval).valueOf(); if (aggregatedData[roundedTime] === undefined) { aggregatedData[roundedTime] = {}; } diff --git a/x-pack/plugins/ml/server/models/results_service/results_service.js b/x-pack/plugins/ml/server/models/results_service/results_service.js index 7b569c26009a..34191dd2aa67 100644 --- a/x-pack/plugins/ml/server/models/results_service/results_service.js +++ b/x-pack/plugins/ml/server/models/results_service/results_service.js @@ -35,6 +35,7 @@ export function resultsServiceProvider(callWithRequest) { threshold, earliestMs, latestMs, + dateFormatTz, maxRecords = DEFAULT_QUERY_SIZE, maxExamples = DEFAULT_MAX_EXAMPLES) { @@ -163,7 +164,7 @@ export function resultsServiceProvider(callWithRequest) { tableData.interval = (daysDiff < 2 ? 'hour' : 'day'); } - tableData.anomalies = buildAnomalyTableItems(records, tableData.interval); + tableData.anomalies = buildAnomalyTableItems(records, tableData.interval, dateFormatTz); // Load examples for any categorization anomalies. const categoryAnomalies = tableData.anomalies.filter(item => item.entityName === 'mlcategory'); diff --git a/x-pack/plugins/ml/server/routes/results_service.js b/x-pack/plugins/ml/server/routes/results_service.js index 145563d85f89..4213d35fcdd2 100644 --- a/x-pack/plugins/ml/server/routes/results_service.js +++ b/x-pack/plugins/ml/server/routes/results_service.js @@ -21,6 +21,7 @@ function getAnomaliesTableData(callWithRequest, payload) { threshold, earliestMs, latestMs, + dateFormatTz, maxRecords, maxExamples } = payload; return rs.getAnomaliesTableData( @@ -31,6 +32,7 @@ function getAnomaliesTableData(callWithRequest, payload) { threshold, earliestMs, latestMs, + dateFormatTz, maxRecords, maxExamples); }