CloudWatch: Fix variable interpolation for Logs queries (#104041)

pull/104781/head
Ida Štambuk 2 weeks ago committed by GitHub
parent c7f55ea5c6
commit ddf33bcb66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 23
      public/app/plugins/datasource/cloudwatch/datasource.test.ts
  2. 1
      public/app/plugins/datasource/cloudwatch/datasource.ts
  3. 106
      public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.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', () => {

@ -156,6 +156,7 @@ export class CloudWatchDatasource
),
...(isCloudWatchMetricsQuery(query) &&
this.metricsQueryRunner.interpolateMetricsQueryVariables(query, scopedVars)),
...(isCloudWatchLogsQuery(query) && this.logsQueryRunner.interpolateLogsQueryVariables(query, scopedVars)),
}));
}

@ -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<CustomFormatterVariable>) => {
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<CloudWatchLogsQuery, 'expression' | 'logGroups' | 'logGroupNames'> {
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<CustomFormatterVariable>) => {
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.
*/

Loading…
Cancel
Save