diff --git a/x-pack/legacy/plugins/monitoring/common/formatting.js b/x-pack/legacy/plugins/monitoring/common/formatting.js index e94ed44efff05..a3b3ce07c8c76 100644 --- a/x-pack/legacy/plugins/monitoring/common/formatting.js +++ b/x-pack/legacy/plugins/monitoring/common/formatting.js @@ -17,8 +17,10 @@ export const LARGE_ABBREVIATED = '0,0.[0]a'; * @param date Either a numeric Unix timestamp or a {@code Date} object * @returns The date formatted using 'LL LTS' */ -export function formatDateTimeLocal(date) { - return moment.tz(date, moment.tz.guess()).format('LL LTS'); +export function formatDateTimeLocal(date, useUTC = false) { + return useUTC + ? moment.utc(date).format('LL LTS') + : moment.tz(date, moment.tz.guess()).format('LL LTS'); } /** diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.js b/x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.js index 9f425a81d8667..9d5ebd274ea9e 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.js +++ b/x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.js @@ -76,8 +76,8 @@ export class ChartTarget extends React.Component { .value(); } - getOptions() { - const opts = getChartOptions({ + async getOptions() { + const opts = await getChartOptions({ yaxis: { tickFormatter: this.props.tickFormatter }, xaxis: this.props.timeRange }); @@ -88,12 +88,12 @@ export class ChartTarget extends React.Component { }; } - renderChart() { + async renderChart() { const { target } = this.refs; const { series } = this.props; const data = this.filterData(series, this.props.seriesToShow); - this.plot = $.plot(target, data, this.getOptions()); + this.plot = $.plot(target, data, await this.getOptions()); this._handleResize = () => { if (!this.plot) { return; } diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/get_chart_options.js b/x-pack/legacy/plugins/monitoring/public/components/chart/get_chart_options.js index cd81aff14701a..7f54b7ec0d2a7 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/chart/get_chart_options.js +++ b/x-pack/legacy/plugins/monitoring/public/components/chart/get_chart_options.js @@ -4,17 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ +import chrome from 'ui/chrome'; import { merge } from 'lodash'; import { CHART_LINE_COLOR, CHART_TEXT_COLOR } from '../../../common/constants'; -export function getChartOptions(axisOptions) { +export async function getChartOptions(axisOptions) { + const $injector = await chrome.dangerouslyGetActiveInjector(); + const timezone = $injector.get('config').get('dateFormat:tz'); const opts = { legend: { show: false }, xaxis: { color: CHART_LINE_COLOR, - timezone: 'browser', + timezone: timezone === 'Browser' ? 'browser' : 'utc', mode: 'time', // requires `time` flot plugin font: { color: CHART_TEXT_COLOR diff --git a/x-pack/legacy/plugins/monitoring/public/components/logs/logs.js b/x-pack/legacy/plugins/monitoring/public/components/logs/logs.js index 8a5e067a8a93e..c59a3d595b14f 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/logs/logs.js +++ b/x-pack/legacy/plugins/monitoring/public/components/logs/logs.js @@ -50,7 +50,7 @@ const columns = [ field: 'timestamp', name: columnTimestampTitle, width: '12%', - render: timestamp => formatDateTimeLocal(timestamp), + render: timestamp => formatDateTimeLocal(timestamp, true), }, { field: 'level', @@ -80,7 +80,7 @@ const clusterColumns = [ field: 'timestamp', name: columnTimestampTitle, width: '12%', - render: timestamp => formatDateTimeLocal(timestamp), + render: timestamp => formatDateTimeLocal(timestamp, true), }, { field: 'level', diff --git a/x-pack/legacy/plugins/monitoring/server/lib/details/__test__/get_metrics.test.js b/x-pack/legacy/plugins/monitoring/server/lib/details/__test__/get_metrics.test.js index f7caf9de0d1fe..5af714f7b3ba2 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/details/__test__/get_metrics.test.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/details/__test__/get_metrics.test.js @@ -51,7 +51,10 @@ function getMockReq(metricsBuckets = []) { }, params: { clusterUuid: '1234xyz' - } + }, + getUiSettingsService: () => ({ + get: () => 'Browser' + }) }; } diff --git a/x-pack/legacy/plugins/monitoring/server/lib/details/get_metrics.js b/x-pack/legacy/plugins/monitoring/server/lib/details/get_metrics.js index 57c936f960212..c5d2ee2032b01 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/details/get_metrics.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/details/get_metrics.js @@ -10,8 +10,9 @@ import Promise from 'bluebird'; import { checkParam } from '../error_missing_required'; import { getSeries } from './get_series'; import { calculateTimeseriesInterval } from '../calculate_timeseries_interval'; +import { getTimezone } from '../get_timezone'; -export function getMetrics(req, indexPattern, metricSet = [], filters = []) { +export async function getMetrics(req, indexPattern, metricSet = [], filters = []) { checkParam(indexPattern, 'indexPattern in details/getMetrics'); checkParam(metricSet, 'metricSet in details/getMetrics'); @@ -21,6 +22,7 @@ export function getMetrics(req, indexPattern, metricSet = [], filters = []) { const max = moment.utc(req.payload.timeRange.max).valueOf(); const minIntervalSeconds = config.get('xpack.monitoring.min_interval_seconds'); const bucketSize = calculateTimeseriesInterval(min, max, minIntervalSeconds); + const timezone = await getTimezone(req); return Promise.map(metricSet, metric => { // metric names match the literal metric name, but they can be supplied in groups or individually @@ -33,7 +35,7 @@ export function getMetrics(req, indexPattern, metricSet = [], filters = []) { } return Promise.map(metricNames, metricName => { - return getSeries(req, indexPattern, metricName, filters, { min, max, bucketSize }); + return getSeries(req, indexPattern, metricName, filters, { min, max, bucketSize, timezone }); }); }) .then(rows => { diff --git a/x-pack/legacy/plugins/monitoring/server/lib/details/get_series.js b/x-pack/legacy/plugins/monitoring/server/lib/details/get_series.js index 306e93273c157..e66878f522ecb 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/details/get_series.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/details/get_series.js @@ -14,6 +14,7 @@ import { NORMALIZED_DERIVATIVE_UNIT, CALCULATE_DURATION_UNTIL } from '../../../common/constants'; +import { formatUTCTimestampForTimezone } from '../format_timezone'; /** * Derivative metrics for the first two agg buckets are unusable. For the first bucket, there @@ -177,7 +178,7 @@ const formatBucketSize = bucketSizeInSeconds => { return formatTimestampToDuration(timestamp, CALCULATE_DURATION_UNTIL, now); }; -function handleSeries(metric, min, max, bucketSizeInSeconds, response) { +function handleSeries(metric, min, max, bucketSizeInSeconds, timezone, response) { const { derivative, calculation: customCalculation } = metric; const buckets = get(response, 'aggregations.check.buckets', []); const firstUsableBucketIndex = findFirstUsableBucketIndex(buckets, min); @@ -193,14 +194,17 @@ function handleSeries(metric, min, max, bucketSizeInSeconds, response) { data = buckets .slice(firstUsableBucketIndex, lastUsableBucketIndex + 1) // take only the buckets we know are usable .map(bucket => ([ - bucket.key, + formatUTCTimestampForTimezone(bucket.key, timezone), calculation(bucket, key, metric, bucketSizeInSeconds) ])); // map buckets to X/Y coords for Flot charting } return { bucket_size: formatBucketSize(bucketSizeInSeconds), - timeRange: { min, max }, + timeRange: { + min: formatUTCTimestampForTimezone(min, timezone), + max: formatUTCTimestampForTimezone(max, timezone), + }, metric: metric.serialize(), data }; @@ -217,7 +221,7 @@ function handleSeries(metric, min, max, bucketSizeInSeconds, response) { * @param {Array} filters Any filters that should be applied to the query. * @return {Promise} The object response containing the {@code timeRange}, {@code metric}, and {@code data}. */ -export async function getSeries(req, indexPattern, metricName, filters, { min, max, bucketSize }) { +export async function getSeries(req, indexPattern, metricName, filters, { min, max, bucketSize, timezone }) { checkParam(indexPattern, 'indexPattern in details/getSeries'); const metric = metrics[metricName]; @@ -226,5 +230,5 @@ export async function getSeries(req, indexPattern, metricName, filters, { min, m } const response = await fetchSeries(req, indexPattern, metric, min, max, bucketSize, filters); - return handleSeries(metric, min, max, bucketSize, response); + return handleSeries(metric, min, max, bucketSize, timezone, response); } diff --git a/x-pack/legacy/plugins/monitoring/server/lib/format_timezone.js b/x-pack/legacy/plugins/monitoring/server/lib/format_timezone.js new file mode 100644 index 0000000000000..334477ac1c359 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/format_timezone.js @@ -0,0 +1,25 @@ +/* + * 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. + */ +import moment from 'moment'; + + +/** + * This function is designed to offset a UTC timestamp based on the provided timezone + * For example, EST is UTC-4h so this function will subtract (4 * 60 * 60 * 1000)ms + * from the UTC timestamp. This allows us to allow users to view monitoring data + * in various timezones without needing to not store UTC dates. + * + * @param {*} utcTimestamp UTC timestamp + * @param {*} timezone The timezone to convert into + */ +export const formatUTCTimestampForTimezone = (utcTimestamp, timezone) => { + if (timezone === 'Browser') { + return utcTimestamp; + } + const offsetInMinutes = moment.tz(timezone).utcOffset(); + const offsetTimestamp = utcTimestamp + (offsetInMinutes * 1 * 60 * 1000); + return offsetTimestamp; +}; diff --git a/x-pack/legacy/plugins/monitoring/server/lib/get_timezone.js b/x-pack/legacy/plugins/monitoring/server/lib/get_timezone.js new file mode 100644 index 0000000000000..0da15bf6b28e1 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/get_timezone.js @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export async function getTimezone(req) { + return await req.getUiSettingsService().get('dateFormat:tz'); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/logs/get_logs.js b/x-pack/legacy/plugins/monitoring/server/lib/logs/get_logs.js index 80d7f21fc45db..0d45b8c6a1c4e 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/logs/get_logs.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/logs/get_logs.js @@ -4,10 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; import { get } from 'lodash'; import { checkParam } from '../error_missing_required'; import { createTimeFilter } from '../create_query'; import { detectReason } from './detect_reason'; +import { formatUTCTimestampForTimezone } from '../format_timezone'; +import { getTimezone } from '../get_timezone'; async function handleResponse(response, req, filebeatIndexPattern, opts) { const result = { @@ -15,15 +18,17 @@ async function handleResponse(response, req, filebeatIndexPattern, opts) { logs: [] }; + const timezone = await getTimezone(req); const hits = get(response, 'hits.hits', []); if (hits.length) { result.enabled = true; result.logs = hits.map(hit => { const source = hit._source; const type = get(source, 'event.dataset').split('.')[1]; + const utcTimestamp = moment(get(source, '@timestamp')).valueOf(); return { - timestamp: get(source, '@timestamp'), + timestamp: formatUTCTimestampForTimezone(utcTimestamp, timezone), component: get(source, 'elasticsearch.component'), node: get(source, 'elasticsearch.node.name'), index: get(source, 'elasticsearch.index.name'), diff --git a/x-pack/test/api_integration/apis/monitoring/logs/fixtures/index_detail.json b/x-pack/test/api_integration/apis/monitoring/logs/fixtures/index_detail.json index abba0d12dbef5..2e9fd359962db 100644 --- a/x-pack/test/api_integration/apis/monitoring/logs/fixtures/index_detail.json +++ b/x-pack/test/api_integration/apis/monitoring/logs/fixtures/index_detail.json @@ -1,7 +1,7 @@ { "enabled": true, "logs": [{ - "timestamp": "2019-03-15T17:07:21.089Z", + "timestamp": 1552669641089, "component": "o.e.n.Node", "node": "Elastic-MBP.local", "index": ".monitoring-es", diff --git a/x-pack/test/api_integration/apis/monitoring/logs/fixtures/node_detail.json b/x-pack/test/api_integration/apis/monitoring/logs/fixtures/node_detail.json index ef197266273ec..c79fd67205751 100644 --- a/x-pack/test/api_integration/apis/monitoring/logs/fixtures/node_detail.json +++ b/x-pack/test/api_integration/apis/monitoring/logs/fixtures/node_detail.json @@ -1,21 +1,21 @@ { "enabled": true, "logs": [{ - "timestamp": "2019-03-15T17:19:07.365Z", + "timestamp": 1552670347365, "component": "o.e.d.x.m.r.a.RestMonitoringBulkAction", "node": "Elastic-MBP.local", "level": "WARN", "type": "deprecation", "message": "[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead." }, { - "timestamp": "2019-03-15T17:18:57.366Z", + "timestamp": 1552670337366, "component": "o.e.d.x.m.r.a.RestMonitoringBulkAction", "node": "Elastic-MBP.local", "level": "WARN", "type": "deprecation", "message": "[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead." }, { - "timestamp": "2019-03-15T17:18:47.400Z", + "timestamp": 1552670327400, "component": "o.e.c.m.MetaDataCreateIndexService", "node": "Elastic-MBP.local", "index": ".monitoring-beats-7-2019.03.15", @@ -23,14 +23,14 @@ "type": "server", "message": "creating index, cause [auto(bulk api)], templates [.monitoring-beats], shards [1]/[0], mappings [_doc]" }, { - "timestamp": "2019-03-15T17:18:47.387Z", + "timestamp": 1552670327387, "component": "o.e.d.x.m.r.a.RestMonitoringBulkAction", "node": "Elastic-MBP.local", "level": "WARN", "type": "deprecation", "message": "[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead." }, { - "timestamp": "2019-03-15T17:18:42.084Z", + "timestamp": 1552670322084, "component": "o.e.c.m.MetaDataMappingService", "node": "Elastic-MBP.local", "index": "filebeat-8.0.0-2019.03.15-000001", @@ -38,7 +38,7 @@ "type": "server", "message": "update_mapping [_doc]" }, { - "timestamp": "2019-03-15T17:18:41.811Z", + "timestamp": 1552670321811, "component": "o.e.c.m.MetaDataMappingService", "node": "Elastic-MBP.local", "index": "filebeat-8.0.0-2019.03.15-000001", @@ -46,7 +46,7 @@ "type": "server", "message": "update_mapping [_doc]" }, { - "timestamp": "2019-03-15T17:18:41.447Z", + "timestamp": 1552670321447, "component": "o.e.c.m.MetaDataCreateIndexService", "node": "Elastic-MBP.local", "index": "filebeat-8.0.0-2019.03.15-000001", @@ -54,21 +54,21 @@ "type": "server", "message": "creating index, cause [api], templates [filebeat-8.0.0], shards [1]/[1], mappings [_doc]" }, { - "timestamp": "2019-03-15T17:18:41.385Z", + "timestamp": 1552670321385, "component": "o.e.c.m.MetaDataIndexTemplateService", "node": "Elastic-MBP.local", "level": "INFO", "type": "server", "message": "adding template [filebeat-8.0.0] for index patterns [filebeat-8.0.0-*]" }, { - "timestamp": "2019-03-15T17:18:41.185Z", + "timestamp": 1552670321185, "component": "o.e.x.i.a.TransportPutLifecycleAction", "node": "Elastic-MBP.local", "level": "INFO", "type": "server", "message": "adding index lifecycle policy [filebeat-8.0.0]" }, { - "timestamp": "2019-03-15T17:18:36.137Z", + "timestamp": 1552670316137, "component": "o.e.c.r.a.AllocationService", "node": "Elastic-MBP.local", "level": "INFO",