diff --git a/public/app/features/templating/template_srv.mock.ts b/public/app/features/templating/template_srv.mock.ts new file mode 100644 index 00000000000..394223a75c0 --- /dev/null +++ b/public/app/features/templating/template_srv.mock.ts @@ -0,0 +1,48 @@ +import { ScopedVars, VariableModel } from '@grafana/data'; +import { variableRegex } from '../variables/utils'; +import { TemplateSrv } from '@grafana/runtime'; + +/** + * Mock for TemplateSrv where you can just supply map of key and values and it will do the interpolation based on that. + * For simple tests whether you your data source for example calls correct replacing code. + * + * This is implementing TemplateSrv interface but that is not enough in most cases. Datasources require some additional + * methods and usually require TemplateSrv class directly instead of just the interface which probably should be fixed + * later on. + */ +export class TemplateSrvMock implements TemplateSrv { + private regex = variableRegex; + constructor(private variables: Record) {} + + getVariables(): VariableModel[] { + return Object.keys(this.variables).map((key) => { + return { + type: 'custom', + name: key, + label: key, + }; + }); + } + + replace(target?: string, scopedVars?: ScopedVars, format?: string | Function): string { + if (!target) { + return target ?? ''; + } + + this.regex.lastIndex = 0; + + return target.replace(this.regex, (match, var1, var2, fmt2, var3, fieldPath, fmt3) => { + const variableName = var1 || var2 || var3; + return this.variables[variableName]; + }); + } + + getVariableName(expression: string) { + this.regex.lastIndex = 0; + const match = this.regex.exec(expression); + if (!match) { + return null; + } + return match.slice(1).find((match) => match !== undefined); + } +} diff --git a/public/app/plugins/datasource/cloudwatch/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/datasource.test.ts index b9b3293fc77..24a38628cd9 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.test.ts @@ -2,22 +2,16 @@ import { lastValueFrom, of } from 'rxjs'; import { setBackendSrv, setDataSourceSrv } from '@grafana/runtime'; import { ArrayVector, DataFrame, dataFrameToJSON, dateTime, Field, MutableDataFrame } from '@grafana/data'; -import { TemplateSrv } from '../../../features/templating/template_srv'; import { CloudWatchDatasource } from './datasource'; import { toArray } from 'rxjs/operators'; import { CloudWatchLogsQueryStatus } from './types'; +import { TemplateSrvMock } from '../../../features/templating/template_srv.mock'; describe('datasource', () => { describe('query', () => { it('should return error if log query and log groups is not specified', async () => { const { datasource } = setup(); - const observable = datasource.query({ - targets: [ - { - queryMode: 'Logs' as 'Logs', - }, - ], - } as any); + const observable = datasource.query({ targets: [{ queryMode: 'Logs' as 'Logs' }] } as any); await expect(observable).toEmitValuesWith((received) => { const response = received[0]; @@ -27,14 +21,7 @@ describe('datasource', () => { it('should return empty response if queries are hidden', async () => { const { datasource } = setup(); - const observable = datasource.query({ - targets: [ - { - queryMode: 'Logs' as 'Logs', - hide: true, - }, - ], - } as any); + const observable = datasource.query({ targets: [{ queryMode: 'Logs' as 'Logs', hide: true }] } as any); await expect(observable).toEmitValuesWith((received) => { const response = received[0]; @@ -42,6 +29,25 @@ describe('datasource', () => { }); }); + it('should interpolate variables in the query', async () => { + const { datasource, fetchMock } = setup(); + datasource.query({ + targets: [ + { + queryMode: 'Logs' as 'Logs', + region: '$region', + expression: 'fields $fields', + logGroupNames: ['/some/$group'], + }, + ], + } as any); + expect(fetchMock.mock.calls[0][0].data.queries[0]).toMatchObject({ + queryString: 'fields templatedField', + logGroupNames: ['/some/templatedGroup'], + region: 'templatedRegion', + }); + }); + it('should add links to log queries', async () => { const { datasource } = setupForLogs(); const observable = datasource.query({ @@ -121,7 +127,7 @@ describe('datasource', () => { function setup({ data = [] }: { data?: any } = {}) { const datasource = new CloudWatchDatasource( { jsonData: { defaultRegion: 'us-west-1', tracingDatasourceUid: 'xray' } } as any, - new TemplateSrv(), + new TemplateSrvMock({ region: 'templatedRegion', fields: 'templatedField', group: 'templatedGroup' }) as any, { timeRange() { const time = dateTime('2021-01-01T01:00:00Z'); diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 9ccffb46800..32f9e6fcedf 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -44,6 +44,7 @@ import { LogAction, MetricQuery, MetricRequest, + StartQueryRequest, TSDBResponse, } from './types'; import { CloudWatchLanguageProvider } from './language_provider'; @@ -163,6 +164,7 @@ export class CloudWatchDatasource // This first starts the query which returns queryId which can be used to retrieve results. return this.makeLogActionRequest('StartQuery', queryParams, { + makeReplacements: true, scopedVars: options.scopedVars, skipCache: true, }).pipe( @@ -518,7 +520,7 @@ export class CloudWatchDatasource makeLogActionRequest( subtype: LogAction, - queryParams: any[], + queryParams: Array, options: { scopedVars?: ScopedVars; makeReplacements?: boolean; @@ -546,18 +548,23 @@ export class CloudWatchDatasource if (options.makeReplacements) { requestParams.queries.forEach((query) => { - if (query.hasOwnProperty('queryString')) { - query.queryString = this.replace(query.queryString, options.scopedVars, true); + const fieldsToReplace: Array< + keyof (GetLogEventsRequest & StartQueryRequest & DescribeLogGroupsRequest & GetLogGroupFieldsRequest) + > = ['queryString', 'logGroupNames', 'logGroupName', 'logGroupNamePrefix']; + + for (const fieldName of fieldsToReplace) { + if (query.hasOwnProperty(fieldName)) { + if (Array.isArray(query[fieldName])) { + query[fieldName] = query[fieldName].map((val: string) => + this.replace(val, options.scopedVars, true, fieldName) + ); + } else { + query[fieldName] = this.replace(query[fieldName], options.scopedVars, true, fieldName); + } + } } query.region = this.replace(query.region, options.scopedVars, true, 'region'); query.region = this.getActualRegion(query.region); - - // interpolate log groups - if (query.logGroupNames) { - query.logGroupNames = query.logGroupNames.map((logGroup: string) => - this.replace(logGroup, options.scopedVars, true, 'log groups') - ); - } }); } diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts index 2a863d609ce..862684cb4f7 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts @@ -290,22 +290,6 @@ describe('CloudWatchDatasource', () => { }); expect(i).toBe(3); }); - - it('should call the replace method on provided log groups', () => { - const { ds } = getTestContext(); - const replaceSpy = jest.spyOn(ds, 'replace').mockImplementation((target?: string) => target ?? ''); - ds.makeLogActionRequest('StartQuery', [ - { - queryString: 'test query string', - region: 'default', - logGroupNames: ['log-group', '${my_var}Variable', 'Cool${other_var}'], - }, - ]); - - expect(replaceSpy).toBeCalledWith('log-group', undefined, true, 'log groups'); - expect(replaceSpy).toBeCalledWith('${my_var}Variable', undefined, true, 'log groups'); - expect(replaceSpy).toBeCalledWith('Cool${other_var}', undefined, true, 'log groups'); - }); }); describe('When performing CloudWatch metrics query', () => {