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 {
RefId string
Region string
Id string
Namespace string
MetricName string
Statistic string
Expression string
SqlExpression string
ReturnData bool
Dimensions map[string][]string
Period int
Alias string
Label string
MatchExact bool
UsedExpression string
MetricQueryType metricQueryType
MetricEditorMode metricEditorMode
RefId string
Region string
Id string
Namespace string
MetricName string
Statistic string
Expression string
SqlExpression string
ReturnData bool
Dimensions map[string][]string
Period int
Alias string
Label string
MatchExact bool
UsedExpression string
TimezoneUTCOffset string
MetricQueryType metricQueryType
MetricEditorMode metricEditorMode
}
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/service/cloudwatch"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
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),
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 {
metricDataQuery, err := e.buildMetricDataQuery(query)
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()
returnData := !model.Get("hide").MustBool(false)
queryType := model.Get("type").MustString()
timezoneUTCOffset := model.Get("timezoneUTCOffset").MustString("")
if queryType == "" {
// 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
@ -221,23 +223,24 @@ func parseRequestQuery(model *simplejson.Json, refId string, startTime time.Time
}
return &cloudWatchQuery{
RefId: refId,
Region: region,
Id: id,
Namespace: namespace,
MetricName: metricName,
Statistic: statistic,
Expression: expression,
ReturnData: returnData,
Dimensions: dimensions,
Period: period,
Alias: alias,
Label: label,
MatchExact: matchExact,
UsedExpression: "",
MetricQueryType: metricQueryType,
MetricEditorMode: metricEditorModeValue,
SqlExpression: sqlExpression,
RefId: refId,
Region: region,
Id: id,
Namespace: namespace,
MetricName: metricName,
Statistic: statistic,
Expression: expression,
ReturnData: returnData,
Dimensions: dimensions,
Period: period,
Alias: alias,
Label: label,
MatchExact: matchExact,
UsedExpression: "",
MetricQueryType: metricQueryType,
MetricEditorMode: metricEditorModeValue,
SqlExpression: sqlExpression,
TimezoneUTCOffset: timezoneUTCOffset,
}, 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', () => {
const ds = setupMockedDataSource({ variables: [labelsVariable, dimensionVariable], mockGetVariableName: false });
it('converts keys and values correctly', () => {

View File

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