From 9595fd6b6697f7f0e60ddb2f9ead7c43d5affcd0 Mon Sep 17 00:00:00 2001 From: Dan Keder Date: Wed, 29 Jun 2022 07:39:50 +0200 Subject: [PATCH] Prometheus: Support 1ms resolution intervals (#44707) * Prometheus: Support 1ms resolution in time ranges * UI: Support 1ms resolution in time ranges --- .../grafana-data/src/datetime/formats.test.ts | 29 ++++- packages/grafana-data/src/datetime/formats.ts | 18 ++- .../src/datetime/rangeutil.test.ts | 122 ++++++++++++++++++ .../grafana-data/src/datetime/rangeutil.ts | 5 +- .../grafana-ui/src/components/Graph/utils.ts | 3 + .../uPlot/config/UPlotAxisBuilder.ts | 2 +- pkg/setting/date_formats.go | 14 +- .../cloudmonitoring/time_series_query_test.go | 2 +- pkg/tsdb/intervalv2/intervalv2.go | 3 + pkg/tsdb/intervalv2/intervalv2_test.go | 2 + .../prometheus/buffered/time_series_query.go | 4 +- 11 files changed, 188 insertions(+), 16 deletions(-) diff --git a/packages/grafana-data/src/datetime/formats.test.ts b/packages/grafana-data/src/datetime/formats.test.ts index f99fac43802..f21e55e9bdc 100644 --- a/packages/grafana-data/src/datetime/formats.test.ts +++ b/packages/grafana-data/src/datetime/formats.test.ts @@ -1,4 +1,4 @@ -import { localTimeFormat } from './formats'; +import { localTimeFormat, systemDateFormats } from './formats'; describe('Date Formats', () => { it('localTimeFormat', () => { @@ -36,3 +36,30 @@ describe('Date Formats without hour12', () => { expect(format).toBe('MM/DD/YYYY, HH:mm:ss'); }); }); + +describe('systemDateFormats', () => { + it('contains correct date formats', () => { + expect(systemDateFormats.fullDate).toBe('YYYY-MM-DD HH:mm:ss'); + expect(systemDateFormats.fullDateMS).toBe('YYYY-MM-DD HH:mm:ss.SSS'); + expect(systemDateFormats.interval.millisecond).toBe('HH:mm:ss.SSS'); + expect(systemDateFormats.interval.second).toBe('HH:mm:ss'); + expect(systemDateFormats.interval.minute).toBe('HH:mm'); + expect(systemDateFormats.interval.hour).toBe('MM/DD HH:mm'); + expect(systemDateFormats.interval.day).toBe('MM/DD'); + expect(systemDateFormats.interval.month).toBe('YYYY-MM'); + expect(systemDateFormats.interval.year).toBe('YYYY'); + }); + + it('contains correct browser-localized date formats', () => { + systemDateFormats.useBrowserLocale(); + expect(systemDateFormats.fullDate).toBe('MM/DD/YYYY, hh:mm:ss A'); + expect(systemDateFormats.fullDateMS).toBe('MM/DD/YYYY, hh:mm:ss.SSS A'); + expect(systemDateFormats.interval.millisecond).toBe('HH:mm:ss.SSS'); + expect(systemDateFormats.interval.second).toBe('HH:mm:ss'); + expect(systemDateFormats.interval.minute).toBe('HH:mm'); + expect(systemDateFormats.interval.hour).toBe('MM/DD, HH:mm'); + expect(systemDateFormats.interval.day).toBe('MM/DD'); + expect(systemDateFormats.interval.month).toBe('MM/YYYY'); + expect(systemDateFormats.interval.year).toBe('YYYY'); + }); +}); diff --git a/packages/grafana-data/src/datetime/formats.ts b/packages/grafana-data/src/datetime/formats.ts index df3711aabd0..92b9a99db62 100644 --- a/packages/grafana-data/src/datetime/formats.ts +++ b/packages/grafana-data/src/datetime/formats.ts @@ -1,6 +1,7 @@ export interface SystemDateFormatSettings { fullDate: string; interval: { + millisecond: string; second: string; minute: string; hour: string; @@ -12,10 +13,13 @@ export interface SystemDateFormatSettings { } const DEFAULT_SYSTEM_DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss'; +const DEFAULT_SYSTEM_DATE_MS_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSS'; export class SystemDateFormatsState { fullDate = DEFAULT_SYSTEM_DATE_FORMAT; + fullDateMS = DEFAULT_SYSTEM_DATE_MS_FORMAT; interval = { + millisecond: 'HH:mm:ss.SSS', second: 'HH:mm:ss', minute: 'HH:mm', hour: 'MM/DD HH:mm', @@ -33,11 +37,6 @@ export class SystemDateFormatsState { } } - get fullDateMS() { - // Add millisecond to seconds part - return this.fullDate.replace('ss', 'ss.SSS'); - } - useBrowserLocale() { this.fullDate = localTimeFormat({ year: 'numeric', @@ -48,6 +47,15 @@ export class SystemDateFormatsState { second: '2-digit', }); + // ES5 doesn't support `DateTimeFormatOptions.fractionalSecondDigits` so we have to use + // a hack with string replacement. + this.fullDateMS = this.fullDate.replace('ss', 'ss.SSS'); + + this.interval.millisecond = localTimeFormat( + { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }, + null, + this.interval.second + ).replace('ss', 'ss.SSS'); this.interval.second = localTimeFormat( { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }, null, diff --git a/packages/grafana-data/src/datetime/rangeutil.test.ts b/packages/grafana-data/src/datetime/rangeutil.test.ts index d3f376faefb..7f48135d928 100644 --- a/packages/grafana-data/src/datetime/rangeutil.test.ts +++ b/packages/grafana-data/src/datetime/rangeutil.test.ts @@ -53,6 +53,128 @@ describe('Range Utils', () => { }); }); + describe('roundInterval', () => { + it('rounds 9ms to 1ms', () => { + expect(rangeUtil.roundInterval(9)).toEqual(1); + }); + + it('rounds 14ms to 10ms', () => { + expect(rangeUtil.roundInterval(9)).toEqual(1); + }); + + it('rounds 34ms to 20ms', () => { + expect(rangeUtil.roundInterval(34)).toEqual(20); + }); + + it('rounds 74ms to 50ms', () => { + expect(rangeUtil.roundInterval(74)).toEqual(50); + }); + + it('rounds 149ms to 100ms', () => { + expect(rangeUtil.roundInterval(149)).toEqual(100); + }); + + it('rounds 349ms to 200ms', () => { + expect(rangeUtil.roundInterval(349)).toEqual(200); + }); + + it('rounds 749ms to 500ms', () => { + expect(rangeUtil.roundInterval(749)).toEqual(500); + }); + + it('rounds 1.5s to 1s', () => { + expect(rangeUtil.roundInterval(1499)).toEqual(1000); + }); + + it('rounds 3.5s to 2s', () => { + expect(rangeUtil.roundInterval(3499)).toEqual(2000); + }); + + it('rounds 7.5s to 5s', () => { + expect(rangeUtil.roundInterval(7499)).toEqual(5000); + }); + + it('rounds 12.5s to 10s', () => { + expect(rangeUtil.roundInterval(12499)).toEqual(10000); + }); + + it('rounds 17.5s to 15s', () => { + expect(rangeUtil.roundInterval(17499)).toEqual(15000); + }); + + it('rounds 25s to 20s', () => { + expect(rangeUtil.roundInterval(24999)).toEqual(20000); + }); + + it('rounds 45s to 30s', () => { + expect(rangeUtil.roundInterval(44999)).toEqual(30000); + }); + + it('rounds 1m30s to 1m', () => { + expect(rangeUtil.roundInterval(89999)).toEqual(60000); + }); + + it('rounds 3m30s to 2m', () => { + expect(rangeUtil.roundInterval(209999)).toEqual(120000); + }); + + it('rounds 7m30s to 5m', () => { + expect(rangeUtil.roundInterval(449999)).toEqual(300000); + }); + + it('rounds 12m30s to 10m', () => { + expect(rangeUtil.roundInterval(749999)).toEqual(600000); + }); + + it('rounds 17m30s to 15m', () => { + expect(rangeUtil.roundInterval(1049999)).toEqual(900000); + }); + + it('rounds 25m to 20m', () => { + expect(rangeUtil.roundInterval(1499999)).toEqual(1200000); + }); + + it('rounds 45m to 30m', () => { + expect(rangeUtil.roundInterval(2699999)).toEqual(1800000); + }); + + it('rounds 1h30m to 1h', () => { + expect(rangeUtil.roundInterval(5399999)).toEqual(3600000); + }); + + it('rounds 2h30m to 2h', () => { + expect(rangeUtil.roundInterval(8999999)).toEqual(7200000); + }); + + it('rounds 4h30m to 3h', () => { + expect(rangeUtil.roundInterval(16199999)).toEqual(10800000); + }); + + it('rounds 9h to 6h', () => { + expect(rangeUtil.roundInterval(32399999)).toEqual(21600000); + }); + + it('rounds 1d to 12h', () => { + expect(rangeUtil.roundInterval(86399999)).toEqual(43200000); + }); + + it('rounds 1w to 1d', () => { + expect(rangeUtil.roundInterval(604799999)).toEqual(86400000); + }); + + it('rounds 3w to 1w', () => { + expect(rangeUtil.roundInterval(1814399999)).toEqual(604800000); + }); + + it('rounds 6w to 30d', () => { + expect(rangeUtil.roundInterval(3628799999)).toEqual(2592000000); + }); + + it('rounds >6w to 1y', () => { + expect(rangeUtil.roundInterval(3628800000)).toEqual(31536000000); + }); + }); + describe('relativeToTimeRange', () => { it('should convert seconds to timeRange', () => { const relativeTimeRange = { from: 600, to: 300 }; diff --git a/packages/grafana-data/src/datetime/rangeutil.ts b/packages/grafana-data/src/datetime/rangeutil.ts index ec0dded1a86..4c65cc1da8b 100644 --- a/packages/grafana-data/src/datetime/rangeutil.ts +++ b/packages/grafana-data/src/datetime/rangeutil.ts @@ -341,6 +341,9 @@ export function intervalToMs(str: string): number { export function roundInterval(interval: number) { switch (true) { + // 0.01s + case interval < 10: + return 1; // 0.001s // 0.015s case interval < 15: return 10; // 0.01s @@ -392,7 +395,7 @@ export function roundInterval(interval: number) { // 12.5m case interval < 750000: return 600000; // 10m - // 12.5m + // 17.5m case interval < 1050000: return 900000; // 15m // 25m diff --git a/packages/grafana-ui/src/components/Graph/utils.ts b/packages/grafana-ui/src/components/Graph/utils.ts index 51cc5b1e632..afd71cccac1 100644 --- a/packages/grafana-ui/src/components/Graph/utils.ts +++ b/packages/grafana-ui/src/components/Graph/utils.ts @@ -118,6 +118,9 @@ export const graphTimeFormat = (ticks: number | null, min: number | null, max: n const oneDay = 86400010; const oneYear = 31536000000; + if (secPerTick <= 10) { + return systemDateFormats.interval.millisecond; + } if (secPerTick <= 45) { return systemDateFormats.interval.second; } diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts index 59b4e50b66a..3eafa453f10 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts @@ -204,7 +204,7 @@ export function formatTime( let format = systemDateFormats.interval.year; if (foundIncr < timeUnitSize.second) { - format = systemDateFormats.interval.second.replace('ss', 'ss.SS'); + format = systemDateFormats.interval.millisecond; } else if (foundIncr <= timeUnitSize.minute) { format = systemDateFormats.interval.second; } else if (range <= timeUnitSize.day) { diff --git a/pkg/setting/date_formats.go b/pkg/setting/date_formats.go index a23606278ae..93568aa2fd3 100644 --- a/pkg/setting/date_formats.go +++ b/pkg/setting/date_formats.go @@ -15,12 +15,13 @@ type DateFormats struct { } type DateFormatIntervals struct { - Second string `json:"second"` - Minute string `json:"minute"` - Hour string `json:"hour"` - Day string `json:"day"` - Month string `json:"month"` - Year string `json:"year"` + Millisecond string `json:"millisecond"` + Second string `json:"second"` + Minute string `json:"minute"` + Hour string `json:"hour"` + Day string `json:"day"` + Month string `json:"month"` + Year string `json:"year"` } const localBrowser = "browser" @@ -42,6 +43,7 @@ func valueAsTimezone(section *ini.Section, keyName string) (string, error) { func (cfg *Cfg) readDateFormats() { dateFormats := cfg.Raw.Section("date_formats") cfg.DateFormats.FullDate = valueAsString(dateFormats, "full_date", "YYYY-MM-DD HH:mm:ss") + cfg.DateFormats.Interval.Millisecond = valueAsString(dateFormats, "interval_millisecond", "HH:mm:ss.SSS") cfg.DateFormats.Interval.Second = valueAsString(dateFormats, "interval_second", "HH:mm:ss") cfg.DateFormats.Interval.Minute = valueAsString(dateFormats, "interval_minute", "HH:mm") cfg.DateFormats.Interval.Hour = valueAsString(dateFormats, "interval_hour", "MM-DD HH:mm") diff --git a/pkg/tsdb/cloudmonitoring/time_series_query_test.go b/pkg/tsdb/cloudmonitoring/time_series_query_test.go index 33912e9bf48..df56700724a 100644 --- a/pkg/tsdb/cloudmonitoring/time_series_query_test.go +++ b/pkg/tsdb/cloudmonitoring/time_series_query_test.go @@ -104,7 +104,7 @@ func TestTimeSeriesQuery(t *testing.T) { t.Run("appends graph_period to the query", func(t *testing.T) { query := &cloudMonitoringTimeSeriesQuery{} - assert.Equal(t, query.appendGraphPeriod(&backend.QueryDataRequest{Queries: []backend.DataQuery{{}}}), " | graph_period 10ms") + assert.Equal(t, query.appendGraphPeriod(&backend.QueryDataRequest{Queries: []backend.DataQuery{{}}}), " | graph_period 1ms") }) t.Run("skips graph_period if disabled", func(t *testing.T) { diff --git a/pkg/tsdb/intervalv2/intervalv2.go b/pkg/tsdb/intervalv2/intervalv2.go index 781d78cdc6a..77a66f591d8 100644 --- a/pkg/tsdb/intervalv2/intervalv2.go +++ b/pkg/tsdb/intervalv2/intervalv2.go @@ -159,6 +159,9 @@ func FormatDuration(inter time.Duration) string { //nolint: gocyclo func roundInterval(interval time.Duration) time.Duration { switch { + // 0.01s + case interval <= 10*time.Millisecond: + return time.Millisecond * 1 // 0.001s // 0.015s case interval <= 15*time.Millisecond: return time.Millisecond * 10 // 0.01s diff --git a/pkg/tsdb/intervalv2/intervalv2_test.go b/pkg/tsdb/intervalv2/intervalv2_test.go index e5d06854d5a..d7adff32928 100644 --- a/pkg/tsdb/intervalv2/intervalv2_test.go +++ b/pkg/tsdb/intervalv2/intervalv2_test.go @@ -70,6 +70,8 @@ func TestRoundInterval(t *testing.T) { interval time.Duration expected time.Duration }{ + {"10ms", time.Millisecond * 10, time.Millisecond * 1}, + {"15ms", time.Millisecond * 15, time.Millisecond * 10}, {"30ms", time.Millisecond * 30, time.Millisecond * 20}, {"45ms", time.Millisecond * 45, time.Millisecond * 50}, } diff --git a/pkg/tsdb/prometheus/buffered/time_series_query.go b/pkg/tsdb/prometheus/buffered/time_series_query.go index 8d2634c35d1..ad184e91d69 100644 --- a/pkg/tsdb/prometheus/buffered/time_series_query.go +++ b/pkg/tsdb/prometheus/buffered/time_series_query.go @@ -630,7 +630,9 @@ func newDataFrame(name string, typ string, fields ...*data.Field) *data.Frame { } func alignTimeRange(t time.Time, step time.Duration, offset int64) time.Time { - return time.Unix(int64(math.Floor((float64(t.Unix()+offset)/step.Seconds()))*step.Seconds()-float64(offset)), 0) + offsetNano := float64(offset * 1e9) + stepNano := float64(step.Nanoseconds()) + return time.Unix(0, int64(math.Floor((float64(t.UnixNano())+offsetNano)/stepNano)*stepNano-offsetNano)) } func isVariableInterval(interval string) bool {