CloudWatch: Fix variable interpolation for Logs queries (#104041)
This commit is contained in:
@@ -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)),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
+59
-47
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user