CloudWatch Logs: Fix variable interpolation in queries (#40899)

* Interpolate variables in queries

* Remove test
This commit is contained in:
Andrej Ocenas 2021-10-26 11:40:49 +02:00 committed by GitHub
parent 0808ec927e
commit 8cf7e520e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 88 additions and 43 deletions

View File

@ -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<string, string>) {}
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);
}
}

View File

@ -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');

View File

@ -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<GetLogEventsRequest | StartQueryRequest | DescribeLogGroupsRequest | GetLogGroupFieldsRequest>,
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')
);
}
});
}

View File

@ -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', () => {