Elasticsearch: Add time zone setting to Date Histogram aggregation (#40882)
* TimeZonePicker: Allow specifying internal timezones to display * Elasticsearch: Add time zone setting to Date Histogram aggregation * rename time_zone to timeZone * Add tests * fix typo * Update packages/grafana-data/src/datetime/timezones.ts Co-authored-by: Chris Cowan <chris@chriscowan.us> Co-authored-by: Chris Cowan <chris@chriscowan.us>
This commit is contained in:
co-authored by
Chris Cowan
parent
de83e5702c
commit
7b7c193551
@@ -48,11 +48,13 @@ export const getTimeZoneInfo = (zone: string, timestamp: number): TimeZoneInfo |
|
||||
return mapToInfo(zone, timestamp);
|
||||
};
|
||||
|
||||
export const getTimeZones = memoize((includeInternal = false): TimeZone[] => {
|
||||
export const getTimeZones = memoize((includeInternal: boolean | InternalTimeZones[] = false): TimeZone[] => {
|
||||
const initial: TimeZone[] = [];
|
||||
|
||||
if (includeInternal) {
|
||||
initial.push.apply(initial, [InternalTimeZones.default, InternalTimeZones.localBrowserTime, InternalTimeZones.utc]);
|
||||
if (includeInternal === true) {
|
||||
initial.push(InternalTimeZones.default, InternalTimeZones.localBrowserTime, InternalTimeZones.utc);
|
||||
} else if (includeInternal) {
|
||||
initial.push(...includeInternal);
|
||||
}
|
||||
|
||||
return moment.tz.names().reduce((zones: TimeZone[], zone: string) => {
|
||||
@@ -67,32 +69,34 @@ export const getTimeZones = memoize((includeInternal = false): TimeZone[] => {
|
||||
}, initial);
|
||||
});
|
||||
|
||||
export const getTimeZoneGroups = memoize((includeInternal = false): GroupedTimeZones[] => {
|
||||
const timeZones = getTimeZones(includeInternal);
|
||||
export const getTimeZoneGroups = memoize(
|
||||
(includeInternal: boolean | InternalTimeZones[] = false): GroupedTimeZones[] => {
|
||||
const timeZones = getTimeZones(includeInternal);
|
||||
|
||||
const groups = timeZones.reduce((groups: Record<string, TimeZone[]>, zone: TimeZone) => {
|
||||
const delimiter = zone.indexOf('/');
|
||||
const groups = timeZones.reduce((groups: Record<string, TimeZone[]>, zone: TimeZone) => {
|
||||
const delimiter = zone.indexOf('/');
|
||||
|
||||
if (delimiter === -1) {
|
||||
const group = '';
|
||||
if (delimiter === -1) {
|
||||
const group = '';
|
||||
groups[group] = groups[group] ?? [];
|
||||
groups[group].push(zone);
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
const group = zone.substr(0, delimiter);
|
||||
groups[group] = groups[group] ?? [];
|
||||
groups[group].push(zone);
|
||||
|
||||
return groups;
|
||||
}
|
||||
}, {});
|
||||
|
||||
const group = zone.substr(0, delimiter);
|
||||
groups[group] = groups[group] ?? [];
|
||||
groups[group].push(zone);
|
||||
|
||||
return groups;
|
||||
}, {});
|
||||
|
||||
return Object.keys(groups).map((name) => ({
|
||||
name,
|
||||
zones: groups[name],
|
||||
}));
|
||||
});
|
||||
return Object.keys(groups).map((name) => ({
|
||||
name,
|
||||
zones: groups[name],
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
const mapInternal = (zone: string, timestamp: number): TimeZoneInfo | undefined => {
|
||||
switch (zone) {
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface Props {
|
||||
width?: number;
|
||||
autoFocus?: boolean;
|
||||
onBlur?: () => void;
|
||||
includeInternal?: boolean;
|
||||
includeInternal?: boolean | InternalTimeZones[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ interface SelectableZoneGroup extends SelectableValue<string> {
|
||||
options: SelectableZone[];
|
||||
}
|
||||
|
||||
const useTimeZones = (includeInternal: boolean): SelectableZoneGroup[] => {
|
||||
const useTimeZones = (includeInternal: boolean | InternalTimeZones[]): SelectableZoneGroup[] => {
|
||||
const now = Date.now();
|
||||
|
||||
const timeZoneGroups = getTimeZoneGroups(includeInternal).map((group: GroupedTimeZones) => {
|
||||
|
||||
@@ -244,6 +244,7 @@ type DateHistogramAgg struct {
|
||||
ExtendedBounds *ExtendedBounds `json:"extended_bounds"`
|
||||
Format string `json:"format"`
|
||||
Offset string `json:"offset,omitempty"`
|
||||
TimeZone string `json:"time_zone,omitempty"`
|
||||
}
|
||||
|
||||
// FiltersAggregation represents a filters aggregation
|
||||
|
||||
@@ -264,6 +264,12 @@ func addDateHistogramAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg, timeFro
|
||||
a.Missing = &missing
|
||||
}
|
||||
|
||||
if timezone, err := bucketAgg.Settings.Get("timeZone").String(); err == nil {
|
||||
if timezone != "utc" {
|
||||
a.TimeZone = timezone
|
||||
}
|
||||
}
|
||||
|
||||
aggBuilder = b
|
||||
})
|
||||
|
||||
|
||||
@@ -402,6 +402,52 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
|
||||
require.Equal(t, hAgg.Field, "@timestamp")
|
||||
require.Equal(t, hAgg.Interval, "$__interval")
|
||||
require.Equal(t, hAgg.MinDocCount, 2)
|
||||
|
||||
t.Run("Should not include time_zone when timeZone is utc", func(t *testing.T) {
|
||||
c := newFakeClient("7.0.0")
|
||||
_, err := executeTsdbQuery(c, `{
|
||||
"timeField": "@timestamp",
|
||||
"bucketAggs": [
|
||||
{
|
||||
"id": "2",
|
||||
"type": "date_histogram",
|
||||
"field": "@timestamp",
|
||||
"settings": {
|
||||
"timeZone": "utc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"metrics": [{"type": "count", "id": "1" }]
|
||||
}`, from, to, 15*time.Second)
|
||||
require.NoError(t, err)
|
||||
sr := c.multisearchRequests[0].Requests[0]
|
||||
|
||||
dateHistogram := sr.Aggs[0].Aggregation.Aggregation.(*es.DateHistogramAgg)
|
||||
require.Empty(t, dateHistogram.TimeZone)
|
||||
})
|
||||
|
||||
t.Run("Should include time_zone when timeZone is not utc", func(t *testing.T) {
|
||||
c := newFakeClient("7.0.0")
|
||||
_, err := executeTsdbQuery(c, `{
|
||||
"timeField": "@timestamp",
|
||||
"bucketAggs": [
|
||||
{
|
||||
"id": "2",
|
||||
"type": "date_histogram",
|
||||
"field": "@timestamp",
|
||||
"settings": {
|
||||
"timeZone": "America/Los_Angeles"
|
||||
}
|
||||
}
|
||||
],
|
||||
"metrics": [{"type": "count", "id": "1" }]
|
||||
}`, from, to, 15*time.Second)
|
||||
require.NoError(t, err)
|
||||
sr := c.multisearchRequests[0].Requests[0]
|
||||
|
||||
deteHistogram := sr.Aggs[0].Aggregation.Aggregation.(*es.DateHistogramAgg)
|
||||
require.Equal(t, deteHistogram.TimeZone, "America/Los_Angeles")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("With histogram agg", func(t *testing.T) {
|
||||
@@ -1163,7 +1209,7 @@ func TestTimeSeriesQueryParser(t *testing.T) {
|
||||
"timeField": "@timestamp",
|
||||
"query": "@metric:cpu",
|
||||
"alias": "{{@hostname}} {{metric}}",
|
||||
"interval": "10m",
|
||||
"interval": "10m",
|
||||
"metrics": [
|
||||
{
|
||||
"field": "@value",
|
||||
|
||||
+12
-2
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { InlineField, Input, Select } from '@grafana/ui';
|
||||
import { InlineField, Input, Select, TimeZonePicker } from '@grafana/ui';
|
||||
import { DateHistogram } from '../aggregations';
|
||||
import { bucketAggregationConfig } from '../utils';
|
||||
import { useDispatch } from '../../../../hooks/useStatelessReducer';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { InternalTimeZones, SelectableValue } from '@grafana/data';
|
||||
import { changeBucketAggregationSetting } from '../state/actions';
|
||||
import { inlineFieldProps } from '.';
|
||||
import { uniqueId } from 'lodash';
|
||||
@@ -99,6 +99,16 @@ export const DateHistogramSettingsEditor = ({ bucketAgg }: Props) => {
|
||||
defaultValue={bucketAgg.settings?.offset || bucketAggregationConfig.date_histogram.defaultSettings?.offset}
|
||||
/>
|
||||
</InlineField>
|
||||
|
||||
<InlineField label="Timezone" {...inlineFieldProps}>
|
||||
<TimeZonePicker
|
||||
value={bucketAgg.settings?.timeZone || bucketAggregationConfig.date_histogram.defaultSettings?.timeZone}
|
||||
includeInternal={[InternalTimeZones.utc]}
|
||||
onChange={(timeZone) => {
|
||||
dispatch(changeBucketAggregationSetting({ bucketAgg, settingName: 'timeZone', newValue: timeZone }));
|
||||
}}
|
||||
/>
|
||||
</InlineField>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ export interface DateHistogram extends BucketAggregationWithField {
|
||||
min_doc_count?: string;
|
||||
trimEdges?: string;
|
||||
offset?: string;
|
||||
timeZone?: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
import { BucketsConfiguration } from '../../../types';
|
||||
import { defaultFilter } from './SettingsEditor/FiltersSettingsEditor/utils';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { InternalTimeZones, SelectableValue } from '@grafana/data';
|
||||
|
||||
export const bucketAggregationConfig: BucketsConfiguration = {
|
||||
terms: {
|
||||
@@ -34,6 +34,7 @@ export const bucketAggregationConfig: BucketsConfiguration = {
|
||||
interval: 'auto',
|
||||
min_doc_count: '0',
|
||||
trimEdges: '0',
|
||||
timeZone: InternalTimeZones.utc,
|
||||
},
|
||||
},
|
||||
histogram: {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { InternalTimeZones } from '@grafana/data';
|
||||
import { gte, lt } from 'semver';
|
||||
import {
|
||||
Filters,
|
||||
@@ -99,6 +100,9 @@ export class ElasticQueryBuilder {
|
||||
esAgg.min_doc_count = settings.min_doc_count || 0;
|
||||
esAgg.extended_bounds = { min: '$timeFrom', max: '$timeTo' };
|
||||
esAgg.format = 'epoch_millis';
|
||||
if (settings.timeZone && settings.timeZone !== InternalTimeZones.utc) {
|
||||
esAgg.time_zone = settings.timeZone;
|
||||
}
|
||||
|
||||
if (settings.offset !== '') {
|
||||
esAgg.offset = settings.offset;
|
||||
|
||||
@@ -841,5 +841,50 @@ describe('ElasticQueryBuilder', () => {
|
||||
|
||||
expect(serialDiff.lag).toBe(1);
|
||||
});
|
||||
|
||||
describe('date_histogram', () => {
|
||||
it('should not include time_zone if not present in the query model', () => {
|
||||
const query = builder.build({
|
||||
refId: 'A',
|
||||
metrics: [{ type: 'count', id: '1' }],
|
||||
timeField: '@timestamp',
|
||||
bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '2', settings: { min_doc_count: '1' } }],
|
||||
});
|
||||
|
||||
expect(query.aggs['2'].date_histogram.time_zone).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('should not include time_zone if "utc" in the query model', () => {
|
||||
const query = builder.build({
|
||||
refId: 'A',
|
||||
metrics: [{ type: 'count', id: '1' }],
|
||||
timeField: '@timestamp',
|
||||
bucketAggs: [
|
||||
{ type: 'date_histogram', field: '@timestamp', id: '2', settings: { min_doc_count: '1', timeZone: 'utc' } },
|
||||
],
|
||||
});
|
||||
|
||||
expect(query.aggs['2'].date_histogram.time_zone).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('should include time_zone if not "utc" in the query model', () => {
|
||||
const expectedTimezone = 'America/Los_angeles';
|
||||
const query = builder.build({
|
||||
refId: 'A',
|
||||
metrics: [{ type: 'count', id: '1' }],
|
||||
timeField: '@timestamp',
|
||||
bucketAggs: [
|
||||
{
|
||||
type: 'date_histogram',
|
||||
field: '@timestamp',
|
||||
id: '2',
|
||||
settings: { min_doc_count: '1', timeZone: expectedTimezone },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(query.aggs['2'].date_histogram.time_zone).toBe(expectedTimezone);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user