diff --git a/public/app/plugins/datasource/cloudwatch/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/datasource.test.ts index dd3d66c62eb..4d17c9b8eca 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.test.ts @@ -233,7 +233,7 @@ describe('datasource', () => { it('should interpolate variables in the query', async () => { const { datasource, queryMock } = setupMockedDataSource({ - variables: [fieldsVariable, regionVariable], + variables: [fieldsVariable, regionVariable, logGroupNamesVariable], }); await lastValueFrom( datasource @@ -245,6 +245,7 @@ describe('datasource', () => { queryMode: 'Logs', region: '$region', expression: 'fields $fields', + logGroups: [{ name: '$groups', arn: '$groups' }], logGroupNames: ['/some/group'], }, ], @@ -261,6 +262,10 @@ describe('datasource', () => { ); expect(queryMock.mock.calls[0][0].targets[0]).toMatchObject({ queryString: 'fields templatedField', + logGroups: [ + { name: 'templatedGroup-arn-1', arn: 'templatedGroup-arn-1' }, + { name: 'templatedGroup-arn-2', arn: 'templatedGroup-arn-2' }, + ], logGroupNames: ['/some/group'], region: 'templatedRegion', }); @@ -374,21 +379,23 @@ describe('datasource', () => { }); it('should replace correct variables in CloudWatchLogsQuery', () => { - const { datasource, templateService } = setupMockedDataSource(); - templateService.replace = jest.fn(); - const variableName = 'someVar'; + const { datasource, templateService } = setupMockedDataSource({ variables: [logGroupNamesVariable] }); + templateService.replace = jest.fn().mockImplementation(() => 'resolved1|resolved2'); const logQuery: CloudWatchLogsQuery = { queryMode: 'Logs', - expression: `$${variableName}`, - region: `$${variableName}`, + expression: `$expressionVar`, + region: `$regionVar`, + logGroups: [{ name: '$groups', arn: '$groups' }], id: '', refId: '', }; datasource.interpolateVariablesInQueries([logQuery], {}); - expect(templateService.replace).toHaveBeenCalledWith(`$${variableName}`, {}); - expect(templateService.replace).toHaveBeenCalledTimes(1); + expect(templateService.replace).toHaveBeenNthCalledWith(1, '$regionVar', {}); + expect(templateService.replace).toHaveBeenNthCalledWith(2, '$groups', {}, 'pipe'); + expect(templateService.replace).toHaveBeenNthCalledWith(3, '$expressionVar', {}, undefined); + expect(templateService.replace).toHaveBeenCalledTimes(3); }); it('should replace correct variables in CloudWatchMetricsQuery', () => { diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index bb11d43809f..685dce3c15d 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -156,6 +156,7 @@ export class CloudWatchDatasource ), ...(isCloudWatchMetricsQuery(query) && this.metricsQueryRunner.interpolateMetricsQueryVariables(query, scopedVars)), + ...(isCloudWatchLogsQuery(query) && this.logsQueryRunner.interpolateLogsQueryVariables(query, scopedVars)), })); } diff --git a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts index 715f4d62d68..b86fb1ee44e 100644 --- a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts +++ b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts @@ -27,6 +27,7 @@ import { LogRowContextOptions, LogRowContextQueryDirection, LogRowModel, + ScopedVars, getDefaultTimeRange, rangeUtil, } from '@grafana/data'; @@ -92,56 +93,11 @@ export class CloudWatchLogsQueryRunner extends CloudWatchRequest { const validLogQueries = logQueries.filter(this.filterQuery); const startQueryRequests: StartQueryRequest[] = validLogQueries.map((target: CloudWatchLogsQuery) => { - const interpolatedLogGroupArns = interpolateStringArrayUsingSingleOrMultiValuedVariable( - this.templateSrv, - (target.logGroups || this.instanceSettings.jsonData.logGroups || []).map((lg) => lg.arn), - options.scopedVars - ); - - // need to support legacy format variables too - const interpolatedLogGroupNames = interpolateStringArrayUsingSingleOrMultiValuedVariable( - this.templateSrv, - target.logGroupNames || this.instanceSettings.jsonData.defaultLogGroups || [], - options.scopedVars, - 'text' - ); - - // if a log group template variable expands to log group that has already been selected in the log group picker, we need to remove duplicates. - // Otherwise the StartLogQuery API will return a permission error - const logGroups = uniq(interpolatedLogGroupArns).map((arn) => ({ arn, name: arn })); - const logGroupNames = uniq(interpolatedLogGroupNames); - - const logsSQLCustomerFormatter = (value: unknown, model: Partial) => { - if ( - (typeof value === 'string' && value.startsWith('arn:') && value.endsWith(':*')) || - (Array.isArray(value) && - value.every((v) => typeof v === 'string' && v.startsWith('arn:') && v.endsWith(':*'))) - ) { - const varName = model.name || ''; - const variable = this.templateSrv.getVariables().find(({ name }) => name === varName); - // checks the raw query string for a log group template variable that occurs inside `logGroups(logGroupIdentifier:[ ... ])\` - // to later surround the log group names with backticks - // this assumes there's only a single template variable used inside the [ ] - const shouldSurroundInQuotes = target.expression - ?.replaceAll(/[\r\n\t\s]+/g, '') - .includes(`\`logGroups(logGroupIdentifier:[$${varName}])\``); - if (variable && 'current' in variable && 'text' in variable.current) { - if (Array.isArray(variable.current.text)) { - return variable.current.text.map((v) => (shouldSurroundInQuotes ? `'${v}'` : v)).join(','); - } - return shouldSurroundInQuotes ? `'${variable.current.text}'` : variable.current.text; - } - } - - return value; - }; - const formatter = target.queryLanguage === LogsQueryLanguage.SQL ? logsSQLCustomerFormatter : undefined; - const queryString = this.templateSrv.replace(target.expression || '', options.scopedVars, formatter); - + const { expression, logGroups, logGroupNames } = this.interpolateLogsQueryVariables(target, options.scopedVars); return { refId: target.refId, region: this.templateSrv.replace(this.getActualRegion(target.region)), - queryString, + queryString: expression ?? '', logGroups, logGroupNames, queryLanguage: target.queryLanguage, @@ -224,6 +180,62 @@ export class CloudWatchLogsQueryRunner extends CloudWatchRequest { return await lastValueFrom(this.makeLogActionRequest('GetLogEvents', [requestParams], queryFn)); }; + interpolateLogsQueryVariables( + query: CloudWatchLogsQuery, + scopedVars: ScopedVars + ): Pick { + const interpolatedLogGroupArns = interpolateStringArrayUsingSingleOrMultiValuedVariable( + this.templateSrv, + (query.logGroups || this.instanceSettings.jsonData.logGroups || []).map((lg) => lg.arn), + scopedVars + ); + + // need to support legacy format variables too + const interpolatedLogGroupNames = interpolateStringArrayUsingSingleOrMultiValuedVariable( + this.templateSrv, + query.logGroupNames || this.instanceSettings.jsonData.defaultLogGroups || [], + scopedVars, + 'text' + ); + + // if a log group template variable expands to log group that has already been selected in the log group picker, we need to remove duplicates. + // Otherwise the StartLogQuery API will return a permission error + const logGroups = uniq(interpolatedLogGroupArns).map((arn) => ({ arn, name: arn })); + const logGroupNames = uniq(interpolatedLogGroupNames); + + const logsSQLCustomerFormatter = (value: unknown, model: Partial) => { + if ( + (typeof value === 'string' && value.startsWith('arn:') && value.endsWith(':*')) || + (Array.isArray(value) && value.every((v) => typeof v === 'string' && v.startsWith('arn:') && v.endsWith(':*'))) + ) { + const varName = model.name || ''; + const variable = this.templateSrv.getVariables().find(({ name }) => name === varName); + // checks the raw query string for a log group template variable that occurs inside `logGroups(logGroupIdentifier:[ ... ])\` + // to later surround the log group names with backticks + // this assumes there's only a single template variable used inside the [ ] + const shouldSurroundInQuotes = query.expression + ?.replaceAll(/[\r\n\t\s]+/g, '') + .includes(`\`logGroups(logGroupIdentifier:[$${varName}])\``); + if (variable && 'current' in variable && 'text' in variable.current) { + if (Array.isArray(variable.current.text)) { + return variable.current.text.map((v) => (shouldSurroundInQuotes ? `'${v}'` : v)).join(','); + } + return shouldSurroundInQuotes ? `'${variable.current.text}'` : variable.current.text; + } + } + + return value; + }; + const formatter = query.queryLanguage === LogsQueryLanguage.SQL ? logsSQLCustomerFormatter : undefined; + const expression = this.templateSrv.replace(query.expression || '', scopedVars, formatter); + + return { + logGroups, + logGroupNames, + expression, + }; + } + /** * Check if an already started query is complete and returns results if it is. Otherwise it will start polling for results. */