diff --git a/public/app/plugins/datasource/cloud-monitoring/datasource.ts b/public/app/plugins/datasource/cloud-monitoring/datasource.ts index eb3bc48d876..c81e954335a 100644 --- a/public/app/plugins/datasource/cloud-monitoring/datasource.ts +++ b/public/app/plugins/datasource/cloud-monitoring/datasource.ts @@ -137,7 +137,8 @@ export default class CloudMonitoringDatasource extends DataSourceApi this.prepareTimeSeriesQuery(q, options)); + .map(q => this.prepareTimeSeriesQuery(q, options.scopedVars)) + .map(q => ({ ...q, intervalMs: options.intervalMs, type: 'timeSeriesQuery' })); if (queries.length > 0) { const { data } = await this.api.post({ @@ -309,13 +310,13 @@ export default class CloudMonitoringDatasource extends DataSourceApi>(object: T, scopedVars: ScopedVars = {}): T { return Object.entries(object).reduce((acc, [key, value]) => { return { ...acc, [key]: value && _.isString(value) ? this.templateSrv.replace(value, scopedVars) : value, }; - }, {}); + }, {} as T); } shouldRunQuery(query: CloudMonitoringQuery): boolean { @@ -335,14 +336,12 @@ export default class CloudMonitoringDatasource extends DataSourceApi - ) { + scopedVars: ScopedVars + ): CloudMonitoringQuery { return { datasourceId: this.id, refId, queryType, - intervalMs: intervalMs, - type: 'timeSeriesQuery', metricQuery: { ...this.interpolateProps(metricQuery, scopedVars), projectName: this.templateSrv.replace( @@ -352,10 +351,14 @@ export default class CloudMonitoringDatasource extends DataSourceApi this.prepareTimeSeriesQuery(query, scopedVars)); + } + interpolateFilters(filters: string[], scopedVars: ScopedVars) { const completeFilter = _.chunk(filters, 4) .map(([key, operator, value, condition]) => ({ diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index dac87fb9c4a..b06b910248c 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -39,6 +39,7 @@ import { MetricQuery, MetricRequest, TSDBResponse, + isCloudWatchLogsQuery, } from './types'; import { from, Observable, of, merge, zip } from 'rxjs'; import { catchError, finalize, map, mergeMap, tap, concatMap, scan, share, repeat, takeWhile } from 'rxjs/operators'; @@ -927,12 +928,12 @@ export class CloudWatchDatasource extends DataSourceApi name === this.templateSrv.getVariableName(target)); @@ -973,6 +974,39 @@ export class CloudWatchDatasource extends DataSourceApi ({ + ...query, + region: this.getActualRegion(this.replace(query.region, scopedVars)), + expression: this.replace(query.expression, scopedVars), + + ...(!isCloudWatchLogsQuery(query) && this.interpolateMetricsQueryVariables(query, scopedVars)), + })); + } + + interpolateMetricsQueryVariables( + query: CloudWatchMetricsQuery, + scopedVars: ScopedVars + ): Pick { + return { + alias: this.replace(query.alias, scopedVars), + metricName: this.replace(query.metricName, scopedVars), + namespace: this.replace(query.namespace, scopedVars), + period: this.replace(query.period, scopedVars), + dimensions: Object.entries(query.dimensions).reduce((prev, [key, value]) => { + if (Array.isArray(value)) { + return { ...prev, [key]: value }; + } + + return { ...prev, [this.replace(key, scopedVars)]: this.replace(value, scopedVars) }; + }, {}), + }; + } } function withTeardown(observable: Observable, onUnsubscribe: () => void): Observable { diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts index 6cae3561f08..ea65a7c5a40 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts @@ -10,13 +10,20 @@ import { DataQueryErrorType, } from '@grafana/data'; import { TemplateSrv } from 'app/features/templating/template_srv'; -import { CloudWatchLogsQueryStatus, CloudWatchMetricsQuery, CloudWatchQuery, LogAction } from '../types'; +import { + CloudWatchLogsQueryStatus, + CloudWatchMetricsQuery, + CloudWatchQuery, + LogAction, + CloudWatchLogsQuery, +} from '../types'; import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { convertToStoreState } from '../../../../../test/helpers/convertToStoreState'; import { getTemplateSrvDependencies } from 'test/helpers/getTemplateSrvDependencies'; import { of, interval } from 'rxjs'; import { CustomVariableModel, VariableHide } from '../../../../features/variables/types'; +import { TimeSrvStub } from '../../../../../test/specs/helpers'; import * as rxjsUtils from '../utils/rxjs/increasingInterval'; @@ -677,6 +684,69 @@ describe('CloudWatchDatasource', () => { }); }); + describe('When interpolating variables', () => { + beforeEach(() => { + jest.clearAllMocks(); + + ctx.mockedTemplateSrv = { + replace: jest.fn(), + }; + + ctx.ds = new CloudWatchDatasource( + instanceSettings, + ctx.mockedTemplateSrv, + (new TimeSrvStub() as unknown) as TimeSrv + ); + }); + + it('should return an empty array if no queries are provided', () => { + expect(ctx.ds.interpolateVariablesInQueries([], {})).toHaveLength(0); + }); + + it('should replace correct variables in CloudWatchLogsQuery', () => { + const variableName = 'someVar'; + const logQuery: CloudWatchLogsQuery = { + id: 'someId', + refId: 'someRefId', + queryMode: 'Logs', + expression: `$${variableName}`, + region: `$${variableName}`, + }; + + ctx.ds.interpolateVariablesInQueries([logQuery], {}); + + // We interpolate `expression` and `region` in CloudWatchLogsQuery + expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {}); + expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledTimes(2); + }); + + it('should replace correct variables in CloudWatchMetricsQuery', () => { + const variableName = 'someVar'; + const logQuery: CloudWatchMetricsQuery = { + id: 'someId', + refId: 'someRefId', + queryMode: 'Metrics', + expression: `$${variableName}`, + region: `$${variableName}`, + period: `$${variableName}`, + alias: `$${variableName}`, + metricName: `$${variableName}`, + namespace: `$${variableName}`, + dimensions: { + [`$${variableName}`]: `$${variableName}`, + }, + matchExact: false, + statistics: [], + }; + + ctx.ds.interpolateVariablesInQueries([logQuery], {}); + + // We interpolate `expression`, `region`, `period`, `alias`, `metricName`, `nameSpace` and `dimensions` in CloudWatchMetricsQuery + expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {}); + expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledTimes(8); + }); + }); + describe('When performing CloudWatch query for extended statistics', () => { const query = { range: defaultTimeRange, diff --git a/public/app/plugins/datasource/cloudwatch/types.ts b/public/app/plugins/datasource/cloudwatch/types.ts index 1bc34f0b174..14217d36b8f 100644 --- a/public/app/plugins/datasource/cloudwatch/types.ts +++ b/public/app/plugins/datasource/cloudwatch/types.ts @@ -1,7 +1,7 @@ import { DataQuery, SelectableValue, DataSourceJsonData } from '@grafana/data'; export interface CloudWatchMetricsQuery extends DataQuery { - queryMode: 'Metrics'; + queryMode?: 'Metrics'; id: string; region: string; @@ -44,6 +44,9 @@ export interface CloudWatchLogsQuery extends DataQuery { export type CloudWatchQuery = CloudWatchMetricsQuery | CloudWatchLogsQuery; +export const isCloudWatchLogsQuery = (cloudwatchQuery: CloudWatchQuery): cloudwatchQuery is CloudWatchLogsQuery => + (cloudwatchQuery as CloudWatchLogsQuery).queryMode === 'Logs'; + export interface AnnotationQuery extends CloudWatchMetricsQuery { prefixMatching: boolean; actionPrefix: string; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts index 1bcccf14c80..1941fe94aa1 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts @@ -9,6 +9,7 @@ import { DataSourceInstanceSettings, DataQueryResponseData, LoadingState, + ScopedVars, } from '@grafana/data'; import { Observable, of, from } from 'rxjs'; import { DataSourceWithBackend } from '@grafana/runtime'; @@ -255,4 +256,10 @@ export default class Datasource extends DataSourceApi this.pseudoDatasource[query.queryType].applyTemplateVariables(query, scopedVars) as AzureMonitorQuery + ); + } } diff --git a/public/app/plugins/datasource/opentsdb/datasource.ts b/public/app/plugins/datasource/opentsdb/datasource.ts index a76aa425ab0..34fcac4b434 100644 --- a/public/app/plugins/datasource/opentsdb/datasource.ts +++ b/public/app/plugins/datasource/opentsdb/datasource.ts @@ -1,6 +1,6 @@ import angular from 'angular'; import _ from 'lodash'; -import { dateMath, DataQueryRequest, DataSourceApi } from '@grafana/data'; +import { dateMath, DataQueryRequest, DataSourceApi, ScopedVars } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { OpenTsdbOptions, OpenTsdbQuery } from './types'; @@ -493,6 +493,17 @@ export default class OpenTsDatasource extends DataSourceApi ({ + ...query, + metric: this.templateSrv.replace(query.metric, scopedVars), + })); + } + convertToTSDBTime(date: any, roundUp: any, timezone: any) { if (date === 'now') { return null; diff --git a/public/app/plugins/datasource/opentsdb/specs/datasource.test.ts b/public/app/plugins/datasource/opentsdb/specs/datasource.test.ts index aeb43bd6a3b..571b23dec7f 100644 --- a/public/app/plugins/datasource/opentsdb/specs/datasource.test.ts +++ b/public/app/plugins/datasource/opentsdb/specs/datasource.test.ts @@ -1,5 +1,6 @@ import OpenTsDatasource from '../datasource'; import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ +import { OpenTsdbQuery } from '../types'; jest.mock('@grafana/runtime', () => ({ ...((jest.requireActual('@grafana/runtime') as unknown) as object), @@ -108,4 +109,33 @@ describe('opentsdb', () => { expect(requestOptions.params.q).toBe('bar'); }); }); + + describe('When interpolating variables', () => { + beforeEach(() => { + jest.clearAllMocks(); + + ctx.mockedTemplateSrv = { + replace: jest.fn(), + }; + + ctx.ds = new OpenTsDatasource(instanceSettings, ctx.mockedTemplateSrv); + }); + + it('should return an empty array if no queries are provided', () => { + expect(ctx.ds.interpolateVariablesInQueries([], {})).toHaveLength(0); + }); + + it('should replace correct variables', () => { + const variableName = 'someVar'; + const logQuery: OpenTsdbQuery = { + refId: 'someRefId', + metric: `$${variableName}`, + }; + + ctx.ds.interpolateVariablesInQueries([logQuery], {}); + + expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {}); + expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledTimes(1); + }); + }); });