Cloudwatch: Set time zone offset in GMD request (#48772)

* set timezone offset in case feature is enabled

* add unit tests

* add unit tests

* remove unused import
This commit is contained in:
Erik Sundell 2022-05-16 12:15:54 +02:00 committed by GitHub
parent fa37c6c9d3
commit 3106af9eec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 144 additions and 36 deletions

View File

@ -9,23 +9,24 @@ import (
) )
type cloudWatchQuery struct { type cloudWatchQuery struct {
RefId string RefId string
Region string Region string
Id string Id string
Namespace string Namespace string
MetricName string MetricName string
Statistic string Statistic string
Expression string Expression string
SqlExpression string SqlExpression string
ReturnData bool ReturnData bool
Dimensions map[string][]string Dimensions map[string][]string
Period int Period int
Alias string Alias string
Label string Label string
MatchExact bool MatchExact bool
UsedExpression string UsedExpression string
MetricQueryType metricQueryType TimezoneUTCOffset string
MetricEditorMode metricEditorMode MetricQueryType metricQueryType
MetricEditorMode metricEditorMode
} }
func (q *cloudWatchQuery) getGMDAPIMode() gmdApiMode { func (q *cloudWatchQuery) getGMDAPIMode() gmdApiMode {

View File

@ -5,6 +5,7 @@ import (
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/grafana/grafana/pkg/services/featuremgmt"
) )
func (e *cloudWatchExecutor) buildMetricDataInput(startTime time.Time, endTime time.Time, func (e *cloudWatchExecutor) buildMetricDataInput(startTime time.Time, endTime time.Time,
@ -14,6 +15,15 @@ func (e *cloudWatchExecutor) buildMetricDataInput(startTime time.Time, endTime t
EndTime: aws.Time(endTime), EndTime: aws.Time(endTime),
ScanBy: aws.String("TimestampAscending"), ScanBy: aws.String("TimestampAscending"),
} }
shouldSetLabelOptions := e.features.IsEnabled(featuremgmt.FlagCloudWatchDynamicLabels) && len(queries) > 0 && len(queries[0].TimezoneUTCOffset) > 0
if shouldSetLabelOptions {
metricDataInput.LabelOptions = &cloudwatch.LabelOptions{
Timezone: aws.String(queries[0].TimezoneUTCOffset),
}
}
for _, query := range queries { for _, query := range queries {
metricDataQuery, err := e.buildMetricDataQuery(query) metricDataQuery, err := e.buildMetricDataQuery(query)
if err != nil { if err != nil {

View File

@ -0,0 +1,44 @@
package cloudwatch
import (
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMetricDataInputBuilder(t *testing.T) {
now := time.Now()
tests := []struct {
name string
timezoneUTCOffset string
expectedLabelOptions *cloudwatch.LabelOptions
featureEnabled bool
}{
{name: "when timezoneUTCOffset is provided and feature is enabled", timezoneUTCOffset: "+1234", expectedLabelOptions: &cloudwatch.LabelOptions{Timezone: aws.String("+1234")}, featureEnabled: true},
{name: "when timezoneUTCOffset is not provided and feature is enabled", timezoneUTCOffset: "", expectedLabelOptions: nil, featureEnabled: true},
{name: "when timezoneUTCOffset is provided and feature is disabled", timezoneUTCOffset: "+1234", expectedLabelOptions: nil, featureEnabled: false},
{name: "when timezoneUTCOffset is not provided and feature is disabled", timezoneUTCOffset: "", expectedLabelOptions: nil, featureEnabled: false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
executor := newExecutor(nil, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures(featuremgmt.FlagCloudWatchDynamicLabels, tc.featureEnabled))
query := getBaseQuery()
query.TimezoneUTCOffset = tc.timezoneUTCOffset
from := now.Add(time.Hour * -2)
to := now.Add(time.Hour * -1)
mdi, err := executor.buildMetricDataInput(from, to, []*cloudWatchQuery{query})
assert.NoError(t, err)
require.NotNil(t, mdi)
assert.Equal(t, tc.expectedLabelOptions, mdi.LabelOptions)
})
}
}

View File

@ -201,6 +201,8 @@ func parseRequestQuery(model *simplejson.Json, refId string, startTime time.Time
label := model.Get("label").MustString() label := model.Get("label").MustString()
returnData := !model.Get("hide").MustBool(false) returnData := !model.Get("hide").MustBool(false)
queryType := model.Get("type").MustString() queryType := model.Get("type").MustString()
timezoneUTCOffset := model.Get("timezoneUTCOffset").MustString("")
if queryType == "" { if queryType == "" {
// If no type is provided we assume we are called by alerting service, which requires to return data! // If no type is provided we assume we are called by alerting service, which requires to return data!
// Note, this is sort of a hack, but the official Grafana interfaces do not carry the information // Note, this is sort of a hack, but the official Grafana interfaces do not carry the information
@ -221,23 +223,24 @@ func parseRequestQuery(model *simplejson.Json, refId string, startTime time.Time
} }
return &cloudWatchQuery{ return &cloudWatchQuery{
RefId: refId, RefId: refId,
Region: region, Region: region,
Id: id, Id: id,
Namespace: namespace, Namespace: namespace,
MetricName: metricName, MetricName: metricName,
Statistic: statistic, Statistic: statistic,
Expression: expression, Expression: expression,
ReturnData: returnData, ReturnData: returnData,
Dimensions: dimensions, Dimensions: dimensions,
Period: period, Period: period,
Alias: alias, Alias: alias,
Label: label, Label: label,
MatchExact: matchExact, MatchExact: matchExact,
UsedExpression: "", UsedExpression: "",
MetricQueryType: metricQueryType, MetricQueryType: metricQueryType,
MetricEditorMode: metricEditorModeValue, MetricEditorMode: metricEditorModeValue,
SqlExpression: sqlExpression, SqlExpression: sqlExpression,
TimezoneUTCOffset: timezoneUTCOffset,
}, nil }, nil
} }

View File

@ -399,6 +399,50 @@ describe('datasource', () => {
}); });
}); });
describe('timezoneUTCOffset', () => {
const testQuery = {
id: '',
refId: 'a',
region: 'us-east-2',
namespace: '',
period: '',
label: '${MAX_TIME_RELATIVE}',
metricName: '',
dimensions: {},
matchExact: true,
statistic: '',
expression: '',
metricQueryType: MetricQueryType.Query,
metricEditorMode: MetricEditorMode.Code,
sqlExpression: 'SELECT SUM($metric) FROM "$namespace" GROUP BY ${labels:raw} LIMIT $limit',
};
const testTable = [
['Europe/Stockholm', '+0200'],
['America/New_York', '-0400'],
['Asia/Tokyo', '+0900'],
['UTC', '+0000'],
];
describe.each(testTable)('should use the right time zone offset', (ianaTimezone, expectedOffset) => {
const { datasource, fetchMock } = setupMockedDataSource();
datasource.handleMetricQueries([testQuery], {
range: { from: dateTime(), to: dateTime() },
timezone: ianaTimezone,
} as any);
expect(fetchMock).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
queries: expect.arrayContaining([
expect.objectContaining({
timezoneUTCOffset: expectedOffset,
}),
]),
}),
})
);
});
});
describe('convertMultiFiltersFormat', () => { describe('convertMultiFiltersFormat', () => {
const ds = setupMockedDataSource({ variables: [labelsVariable, dimensionVariable], mockGetVariableName: false }); const ds = setupMockedDataSource({ variables: [labelsVariable, dimensionVariable], mockGetVariableName: false });
it('converts keys and values correctly', () => { it('converts keys and values correctly', () => {

View File

@ -12,6 +12,7 @@ import {
DataSourceInstanceSettings, DataSourceInstanceSettings,
DataSourceWithLogsContextSupport, DataSourceWithLogsContextSupport,
dateMath, dateMath,
dateTimeFormat,
FieldType, FieldType,
LoadingState, LoadingState,
LogRowModel, LogRowModel,
@ -22,6 +23,7 @@ import {
import { DataSourceWithBackend, FetchError, getBackendSrv, toDataQueryResponse } from '@grafana/runtime'; import { DataSourceWithBackend, FetchError, getBackendSrv, toDataQueryResponse } from '@grafana/runtime';
import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider'; import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider';
import { notifyApp } from 'app/core/actions'; import { notifyApp } from 'app/core/actions';
import { config } from 'app/core/config';
import { createErrorNotification } from 'app/core/copy/appNotification'; import { createErrorNotification } from 'app/core/copy/appNotification';
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv'; import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
@ -29,8 +31,6 @@ import { VariableWithMultiSupport } from 'app/features/variables/types';
import { store } from 'app/store/store'; import { store } from 'app/store/store';
import { AppNotificationTimeout } from 'app/types'; import { AppNotificationTimeout } from 'app/types';
import config from '../../../core/config';
import { CloudWatchAnnotationSupport } from './annotationSupport'; import { CloudWatchAnnotationSupport } from './annotationSupport';
import { SQLCompletionItemProvider } from './cloudwatch-sql/completion/CompletionItemProvider'; import { SQLCompletionItemProvider } from './cloudwatch-sql/completion/CompletionItemProvider';
import { ThrottlingErrorMessage } from './components/ThrottlingErrorMessage'; import { ThrottlingErrorMessage } from './components/ThrottlingErrorMessage';
@ -290,11 +290,17 @@ export class CloudWatchDatasource
metricQueries: CloudWatchMetricsQuery[], metricQueries: CloudWatchMetricsQuery[],
options: DataQueryRequest<CloudWatchQuery> options: DataQueryRequest<CloudWatchQuery>
): Observable<DataQueryResponse> => { ): Observable<DataQueryResponse> => {
const timezoneUTCOffset = dateTimeFormat(Date.now(), {
timeZone: options.timezone,
format: 'Z',
}).replace(':', '');
const validMetricsQueries = metricQueries.filter(this.filterQuery).map((q: CloudWatchMetricsQuery): MetricQuery => { const validMetricsQueries = metricQueries.filter(this.filterQuery).map((q: CloudWatchMetricsQuery): MetricQuery => {
const migratedQuery = migrateMetricQuery(q); const migratedQuery = migrateMetricQuery(q);
const migratedAndIterpolatedQuery = this.replaceMetricQueryVars(migratedQuery, options); const migratedAndIterpolatedQuery = this.replaceMetricQueryVars(migratedQuery, options);
return { return {
timezoneUTCOffset,
intervalMs: options.intervalMs, intervalMs: options.intervalMs,
maxDataPoints: options.maxDataPoints, maxDataPoints: options.maxDataPoints,
...migratedAndIterpolatedQuery, ...migratedAndIterpolatedQuery,