mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
CloudWatch: Refactoring - decouple logs and metrics in datasource file (#55079)
* break out query execution related logic * betterer fixes * remove unused * cleanup * remove unused variables * remove not used file * fix broken test * pr feedback * add comments
This commit is contained in:
parent
1a0cbdeabe
commit
7bca193ecd
@ -5895,14 +5895,6 @@ exports[`better eslint`] = {
|
|||||||
"public/app/plugins/datasource/cloud-monitoring/types.ts:5381": [
|
"public/app/plugins/datasource/cloud-monitoring/types.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
"public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts:5381": [
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
|
|
||||||
],
|
|
||||||
"public/app/plugins/datasource/cloudwatch/__mocks__/monarch/Monaco.ts:5381": [
|
"public/app/plugins/datasource/cloudwatch/__mocks__/monarch/Monaco.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
@ -5947,10 +5939,7 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||||
],
|
],
|
||||||
"public/app/plugins/datasource/cloudwatch/datasource.d.ts:5381": [
|
"public/app/plugins/datasource/cloudwatch/datasource.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
|
||||||
],
|
|
||||||
"public/app/plugins/datasource/cloudwatch/datasource.test.ts:5381": [
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||||
@ -5959,42 +5948,7 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "8"]
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "12"]
|
|
||||||
],
|
|
||||||
"public/app/plugins/datasource/cloudwatch/datasource.ts:5381": [
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "4"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "6"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "9"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "14"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "17"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "18"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "19"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "20"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "21"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "22"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "23"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "24"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "25"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "26"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "27"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "28"]
|
|
||||||
],
|
],
|
||||||
"public/app/plugins/datasource/cloudwatch/guards.ts:5381": [
|
"public/app/plugins/datasource/cloudwatch/guards.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
@ -6021,23 +5975,9 @@ exports[`better eslint`] = {
|
|||||||
"public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.test.ts:5381": [
|
"public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.test.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
"public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts:5381": [
|
"public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "15"]
|
|
||||||
],
|
],
|
||||||
"public/app/plugins/datasource/cloudwatch/types.ts:5381": [
|
"public/app/plugins/datasource/cloudwatch/types.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
|
import { CustomVariableModel, DataQueryRequest } from '@grafana/data';
|
||||||
|
import { getBackendSrv, setBackendSrv } from '@grafana/runtime';
|
||||||
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
|
||||||
|
import { CloudWatchAnnotationQueryRunner } from '../query-runner/CloudWatchAnnotationQueryRunner';
|
||||||
|
import { CloudWatchQuery } from '../types';
|
||||||
|
|
||||||
|
import { CloudWatchSettings, setupMockedTemplateService } from './CloudWatchDataSource';
|
||||||
|
import { timeRange } from './timeRange';
|
||||||
|
|
||||||
|
export function setupMockedAnnotationQueryRunner({ variables }: { variables?: CustomVariableModel[] }) {
|
||||||
|
let templateService = new TemplateSrv();
|
||||||
|
if (variables) {
|
||||||
|
templateService = setupMockedTemplateService(variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
const runner = new CloudWatchAnnotationQueryRunner(CloudWatchSettings, templateService);
|
||||||
|
const fetchMock = jest.fn().mockReturnValue(of({}));
|
||||||
|
|
||||||
|
setBackendSrv({
|
||||||
|
...getBackendSrv(),
|
||||||
|
fetch: fetchMock,
|
||||||
|
});
|
||||||
|
|
||||||
|
const request: DataQueryRequest<CloudWatchQuery> = {
|
||||||
|
range: timeRange,
|
||||||
|
rangeRaw: { from: '1483228800', to: '1483232400' },
|
||||||
|
targets: [],
|
||||||
|
requestId: '',
|
||||||
|
interval: '',
|
||||||
|
intervalMs: 0,
|
||||||
|
scopedVars: {},
|
||||||
|
timezone: '',
|
||||||
|
app: '',
|
||||||
|
startTime: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { runner, fetchMock, templateService, request, timeRange };
|
||||||
|
}
|
@ -1,62 +1,92 @@
|
|||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
import { dateTime } from '@grafana/data';
|
import {
|
||||||
import { setBackendSrv } from '@grafana/runtime';
|
DataSourceInstanceSettings,
|
||||||
|
DataSourcePluginMeta,
|
||||||
|
PluginMetaInfo,
|
||||||
|
PluginType,
|
||||||
|
VariableHide,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { getBackendSrv, setBackendSrv } from '@grafana/runtime';
|
||||||
|
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
import { initialCustomVariableModelState } from 'app/features/variables/custom/reducer';
|
import { initialCustomVariableModelState } from 'app/features/variables/custom/reducer';
|
||||||
import { CustomVariableModel } from 'app/features/variables/types';
|
import { CustomVariableModel } from 'app/features/variables/types';
|
||||||
|
|
||||||
import { TemplateSrvMock } from '../../../../features/templating/template_srv.mock';
|
|
||||||
import { CloudWatchDatasource } from '../datasource';
|
import { CloudWatchDatasource } from '../datasource';
|
||||||
|
import { CloudWatchJsonData } from '../types';
|
||||||
|
|
||||||
|
export function setupMockedTemplateService(variables: CustomVariableModel[]) {
|
||||||
|
const templateService = new TemplateSrv();
|
||||||
|
templateService.init(variables);
|
||||||
|
templateService.getVariables = jest.fn().mockReturnValue(variables);
|
||||||
|
return templateService;
|
||||||
|
}
|
||||||
|
|
||||||
|
const info: PluginMetaInfo = {
|
||||||
|
author: {
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
description: '',
|
||||||
|
links: [],
|
||||||
|
logos: {
|
||||||
|
large: '',
|
||||||
|
small: '',
|
||||||
|
},
|
||||||
|
screenshots: [],
|
||||||
|
updated: '',
|
||||||
|
version: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const meta: DataSourcePluginMeta<CloudWatchJsonData> = {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
type: PluginType.datasource,
|
||||||
|
info,
|
||||||
|
module: '',
|
||||||
|
baseUrl: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CloudWatchSettings: DataSourceInstanceSettings<CloudWatchJsonData> = {
|
||||||
|
jsonData: { defaultRegion: 'us-west-1', tracingDatasourceUid: 'xray' },
|
||||||
|
id: 0,
|
||||||
|
uid: '',
|
||||||
|
type: '',
|
||||||
|
name: 'CloudWatch Test Datasource',
|
||||||
|
meta,
|
||||||
|
readOnly: false,
|
||||||
|
access: 'direct',
|
||||||
|
};
|
||||||
|
|
||||||
export function setupMockedDataSource({
|
export function setupMockedDataSource({
|
||||||
data = [],
|
|
||||||
variables,
|
variables,
|
||||||
mockGetVariableName = true,
|
mockGetVariableName = true,
|
||||||
}: { data?: any; variables?: any; mockGetVariableName?: boolean } = {}) {
|
}: {
|
||||||
let templateService = new TemplateSrvMock({
|
variables?: CustomVariableModel[];
|
||||||
region: 'templatedRegion',
|
mockGetVariableName?: boolean;
|
||||||
fields: 'templatedField',
|
} = {}) {
|
||||||
group: 'templatedGroup',
|
let templateService = new TemplateSrv();
|
||||||
}) as any;
|
|
||||||
if (variables) {
|
if (variables) {
|
||||||
templateService = new TemplateSrv();
|
templateService = setupMockedTemplateService(variables);
|
||||||
templateService.init(variables);
|
|
||||||
templateService.getVariables = jest.fn().mockReturnValue(variables);
|
|
||||||
if (mockGetVariableName) {
|
if (mockGetVariableName) {
|
||||||
templateService.getVariableName = (name: string) => name;
|
templateService.getVariableName = (name: string) => name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const datasource = new CloudWatchDatasource(
|
const timeSrv = getTimeSrv();
|
||||||
{
|
const datasource = new CloudWatchDatasource(CloudWatchSettings, templateService, timeSrv);
|
||||||
jsonData: { defaultRegion: 'us-west-1', tracingDatasourceUid: 'xray' },
|
|
||||||
} as any,
|
|
||||||
templateService,
|
|
||||||
{
|
|
||||||
timeRange() {
|
|
||||||
const time = dateTime('2021-01-01T01:00:00Z');
|
|
||||||
const range = {
|
|
||||||
from: time.subtract(6, 'hour'),
|
|
||||||
to: time,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
...range,
|
|
||||||
raw: range,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
} as any
|
|
||||||
);
|
|
||||||
datasource.getVariables = () => ['test'];
|
datasource.getVariables = () => ['test'];
|
||||||
|
|
||||||
datasource.getNamespaces = jest.fn().mockResolvedValue([]);
|
datasource.getNamespaces = jest.fn().mockResolvedValue([]);
|
||||||
datasource.getRegions = jest.fn().mockResolvedValue([]);
|
datasource.getRegions = jest.fn().mockResolvedValue([]);
|
||||||
datasource.defaultLogGroups = [];
|
datasource.logsQueryRunner.defaultLogGroups = [];
|
||||||
const fetchMock = jest.fn().mockReturnValue(of({ data }));
|
const fetchMock = jest.fn().mockReturnValue(of({}));
|
||||||
setBackendSrv({ fetch: fetchMock } as any);
|
setBackendSrv({
|
||||||
|
...getBackendSrv(),
|
||||||
|
fetch: fetchMock,
|
||||||
|
});
|
||||||
|
|
||||||
return { datasource, fetchMock, templateService };
|
return { datasource, fetchMock, templateService, timeSrv };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const metricVariable: CustomVariableModel = {
|
export const metricVariable: CustomVariableModel = {
|
||||||
@ -180,7 +210,7 @@ export const regionVariable: CustomVariableModel = {
|
|||||||
multi: false,
|
multi: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const expressionVariable: CustomVariableModel = {
|
export const fieldsVariable: CustomVariableModel = {
|
||||||
...initialCustomVariableModelState,
|
...initialCustomVariableModelState,
|
||||||
id: 'fields',
|
id: 'fields',
|
||||||
name: 'fields',
|
name: 'fields',
|
||||||
@ -192,3 +222,17 @@ export const expressionVariable: CustomVariableModel = {
|
|||||||
options: [{ value: 'templatedField', text: 'templatedField', selected: true }],
|
options: [{ value: 'templatedField', text: 'templatedField', selected: true }],
|
||||||
multi: false,
|
multi: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const periodIntervalVariable: CustomVariableModel = {
|
||||||
|
...initialCustomVariableModelState,
|
||||||
|
id: 'period',
|
||||||
|
name: 'period',
|
||||||
|
index: 0,
|
||||||
|
current: { value: '10m', text: '10m', selected: true },
|
||||||
|
options: [{ value: '10m', text: '10m', selected: true }],
|
||||||
|
multi: false,
|
||||||
|
includeAll: false,
|
||||||
|
query: '',
|
||||||
|
hide: VariableHide.dontHide,
|
||||||
|
type: 'custom',
|
||||||
|
};
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
|
import { DataFrame } from '@grafana/data';
|
||||||
|
import { BackendDataSourceResponse, getBackendSrv, setBackendSrv } from '@grafana/runtime';
|
||||||
|
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
|
||||||
|
import { CloudWatchLogsQueryRunner } from '../query-runner/CloudWatchLogsQueryRunner';
|
||||||
|
import { CloudWatchLogsQueryStatus } from '../types';
|
||||||
|
|
||||||
|
import { CloudWatchSettings, setupMockedTemplateService } from './CloudWatchDataSource';
|
||||||
|
|
||||||
|
export function setupMockedLogsQueryRunner({
|
||||||
|
data = {
|
||||||
|
results: {},
|
||||||
|
},
|
||||||
|
variables,
|
||||||
|
mockGetVariableName = true,
|
||||||
|
}: { data?: BackendDataSourceResponse; variables?: any; mockGetVariableName?: boolean } = {}) {
|
||||||
|
let templateService = new TemplateSrv();
|
||||||
|
if (variables) {
|
||||||
|
templateService = setupMockedTemplateService(variables);
|
||||||
|
if (mockGetVariableName) {
|
||||||
|
templateService.getVariableName = (name: string) => name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const runner = new CloudWatchLogsQueryRunner(CloudWatchSettings, templateService, getTimeSrv());
|
||||||
|
const fetchMock = jest.fn().mockReturnValue(of({ data }));
|
||||||
|
setBackendSrv({
|
||||||
|
...getBackendSrv(),
|
||||||
|
fetch: fetchMock,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { runner, fetchMock, templateService };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genMockFrames(numResponses: number): DataFrame[] {
|
||||||
|
const recordIncrement = 50;
|
||||||
|
const mockFrames: DataFrame[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < numResponses; i++) {
|
||||||
|
mockFrames.push({
|
||||||
|
fields: [],
|
||||||
|
meta: {
|
||||||
|
custom: {
|
||||||
|
Status: i === numResponses - 1 ? CloudWatchLogsQueryStatus.Complete : CloudWatchLogsQueryStatus.Running,
|
||||||
|
},
|
||||||
|
stats: [
|
||||||
|
{
|
||||||
|
displayName: 'Records scanned',
|
||||||
|
value: (i + 1) * recordIncrement,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
refId: 'A',
|
||||||
|
length: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return mockFrames;
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
import { of, throwError } from 'rxjs';
|
||||||
|
|
||||||
|
import { CustomVariableModel, DataQueryError, DataQueryRequest, DataSourceInstanceSettings } from '@grafana/data';
|
||||||
|
import { BackendDataSourceResponse, getBackendSrv, setBackendSrv } from '@grafana/runtime';
|
||||||
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
|
||||||
|
import { CloudWatchMetricsQueryRunner } from '../query-runner/CloudWatchMetricsQueryRunner';
|
||||||
|
import { CloudWatchJsonData, CloudWatchQuery } from '../types';
|
||||||
|
|
||||||
|
import { CloudWatchSettings, setupMockedTemplateService } from './CloudWatchDataSource';
|
||||||
|
import { timeRange } from './timeRange';
|
||||||
|
|
||||||
|
export function setupMockedMetricsQueryRunner({
|
||||||
|
data = {
|
||||||
|
results: {},
|
||||||
|
},
|
||||||
|
variables,
|
||||||
|
mockGetVariableName = true,
|
||||||
|
throws = false,
|
||||||
|
instanceSettings = CloudWatchSettings,
|
||||||
|
}: {
|
||||||
|
data?: BackendDataSourceResponse | DataQueryError;
|
||||||
|
variables?: CustomVariableModel[];
|
||||||
|
mockGetVariableName?: boolean;
|
||||||
|
throws?: boolean;
|
||||||
|
instanceSettings?: DataSourceInstanceSettings<CloudWatchJsonData>;
|
||||||
|
} = {}) {
|
||||||
|
let templateService = new TemplateSrv();
|
||||||
|
if (variables) {
|
||||||
|
templateService = setupMockedTemplateService(variables);
|
||||||
|
if (mockGetVariableName) {
|
||||||
|
templateService.getVariableName = (name: string) => name.replace('$', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const runner = new CloudWatchMetricsQueryRunner(instanceSettings, templateService);
|
||||||
|
const fetchMock = throws
|
||||||
|
? jest.fn().mockImplementation(() => throwError(data))
|
||||||
|
: jest.fn().mockReturnValue(of({ data }));
|
||||||
|
|
||||||
|
setBackendSrv({
|
||||||
|
...getBackendSrv(),
|
||||||
|
fetch: fetchMock,
|
||||||
|
});
|
||||||
|
|
||||||
|
const request: DataQueryRequest<CloudWatchQuery> = {
|
||||||
|
range: timeRange,
|
||||||
|
rangeRaw: { from: '1483228800', to: '1483232400' },
|
||||||
|
targets: [],
|
||||||
|
requestId: '',
|
||||||
|
interval: '',
|
||||||
|
intervalMs: 0,
|
||||||
|
scopedVars: {},
|
||||||
|
timezone: '',
|
||||||
|
app: '',
|
||||||
|
startTime: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { runner, fetchMock, templateService, instanceSettings, request, timeRange };
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DataFrame,
|
||||||
|
dataFrameToJSON,
|
||||||
|
MutableDataFrame,
|
||||||
|
ArrayVector,
|
||||||
|
DataSourceInstanceSettings,
|
||||||
|
DataSourceJsonData,
|
||||||
|
DataSourceRef,
|
||||||
|
ScopedVars,
|
||||||
|
DataSourceApi,
|
||||||
|
DataQuery,
|
||||||
|
DataQueryRequest,
|
||||||
|
DataQueryResponse,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { GetDataSourceListFilters, setDataSourceSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { CloudWatchDatasource } from '../datasource';
|
||||||
|
import { CloudWatchLogsQueryStatus } from '../types';
|
||||||
|
|
||||||
|
import { meta, setupMockedDataSource } from './CloudWatchDataSource';
|
||||||
|
|
||||||
|
export function setupForLogs() {
|
||||||
|
function envelope(frame: DataFrame) {
|
||||||
|
return { data: { results: { a: { refId: 'a', frames: [dataFrameToJSON(frame)] } } } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { datasource, fetchMock, timeSrv } = setupMockedDataSource();
|
||||||
|
|
||||||
|
const startQueryFrame = new MutableDataFrame({ fields: [{ name: 'queryId', values: ['queryid'] }] });
|
||||||
|
fetchMock.mockReturnValueOnce(of(envelope(startQueryFrame)));
|
||||||
|
|
||||||
|
const logsFrame = new MutableDataFrame({
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: '@message',
|
||||||
|
values: new ArrayVector(['something']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '@timestamp',
|
||||||
|
values: new ArrayVector([1]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '@xrayTraceId',
|
||||||
|
values: new ArrayVector(['1-613f0d6b-3e7cb34375b60662359611bd']),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
meta: { custom: { Status: CloudWatchLogsQueryStatus.Complete } },
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchMock.mockReturnValueOnce(of(envelope(logsFrame)));
|
||||||
|
|
||||||
|
setDataSourceSrv({
|
||||||
|
async get() {
|
||||||
|
const ds: DataSourceApi = {
|
||||||
|
name: 'Xray',
|
||||||
|
id: 0,
|
||||||
|
type: '',
|
||||||
|
uid: '',
|
||||||
|
query: function (
|
||||||
|
request: DataQueryRequest<DataQuery>
|
||||||
|
): Observable<DataQueryResponse> | Promise<DataQueryResponse> {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
},
|
||||||
|
testDatasource: function (): Promise<CloudWatchDatasource> {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
},
|
||||||
|
meta: meta,
|
||||||
|
getRef: function (): DataSourceRef {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return ds;
|
||||||
|
},
|
||||||
|
getList: function (
|
||||||
|
filters?: GetDataSourceListFilters | undefined
|
||||||
|
): Array<DataSourceInstanceSettings<DataSourceJsonData>> {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
},
|
||||||
|
getInstanceSettings: function (
|
||||||
|
ref?: string | DataSourceRef | null | undefined,
|
||||||
|
scopedVars?: ScopedVars | undefined
|
||||||
|
): DataSourceInstanceSettings<DataSourceJsonData> | undefined {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
},
|
||||||
|
reload: function (): void {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { datasource, fetchMock, timeSrv };
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
import { dateTime, TimeRange } from '@grafana/data';
|
||||||
|
|
||||||
|
const start = 1483196400 * 1000;
|
||||||
|
const from = dateTime(start);
|
||||||
|
const to = dateTime(start + 3600 * 1000);
|
||||||
|
export const timeRange: TimeRange = { from, to, raw: { from, to } };
|
@ -9,8 +9,6 @@ import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource';
|
|||||||
|
|
||||||
import { ConfigEditor, Props } from './ConfigEditor';
|
import { ConfigEditor, Props } from './ConfigEditor';
|
||||||
|
|
||||||
const ds = setupMockedDataSource();
|
|
||||||
|
|
||||||
jest.mock('app/features/plugins/datasource_srv', () => ({
|
jest.mock('app/features/plugins/datasource_srv', () => ({
|
||||||
getDatasourceSrv: () => ({
|
getDatasourceSrv: () => ({
|
||||||
loadDatasource: jest.fn().mockResolvedValue({
|
loadDatasource: jest.fn().mockResolvedValue({
|
||||||
@ -20,9 +18,11 @@ jest.mock('app/features/plugins/datasource_srv', () => ({
|
|||||||
value: 'ap-east-1',
|
value: 'ap-east-1',
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
describeLogGroups: jest.fn().mockResolvedValue(['logGroup-foo', 'logGroup-bar']),
|
|
||||||
getActualRegion: jest.fn().mockReturnValue('ap-east-1'),
|
getActualRegion: jest.fn().mockReturnValue('ap-east-1'),
|
||||||
getVariables: jest.fn().mockReturnValue([]),
|
getVariables: jest.fn().mockReturnValue([]),
|
||||||
|
logsQueryRunner: {
|
||||||
|
describeLogGroups: jest.fn().mockResolvedValue(['logGroup-foo', 'logGroup-bar']),
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
@ -31,10 +31,11 @@ jest.mock('./XrayLinkConfig', () => ({
|
|||||||
XrayLinkConfig: () => <></>,
|
XrayLinkConfig: () => <></>,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const putMock = jest.fn();
|
||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
...jest.requireActual('@grafana/runtime'),
|
...jest.requireActual('@grafana/runtime'),
|
||||||
getBackendSrv: () => ({
|
getBackendSrv: () => ({
|
||||||
put: jest.fn().mockResolvedValue({ datasource: ds.datasource }),
|
put: putMock,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -86,6 +87,7 @@ const setup = (propOverrides?: object) => {
|
|||||||
describe('Render', () => {
|
describe('Render', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
|
putMock.mockImplementation(async () => ({ datasource: setupMockedDataSource().datasource }));
|
||||||
});
|
});
|
||||||
it('should render component', () => {
|
it('should render component', () => {
|
||||||
const wrapper = setup();
|
const wrapper = setup();
|
||||||
|
@ -25,13 +25,15 @@ describe('LogGroupSelector', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('updates upstream query log groups on region change', async () => {
|
it('updates upstream query log groups on region change', async () => {
|
||||||
ds.datasource.describeLogGroups = jest.fn().mockImplementation(async (params: DescribeLogGroupsRequest) => {
|
ds.datasource.logsQueryRunner.describeLogGroups = jest
|
||||||
if (params.region === 'region1') {
|
.fn()
|
||||||
return Promise.resolve(['log_group_1']);
|
.mockImplementation(async (params: DescribeLogGroupsRequest) => {
|
||||||
} else {
|
if (params.region === 'region1') {
|
||||||
return Promise.resolve(['log_group_2']);
|
return Promise.resolve(['log_group_1']);
|
||||||
}
|
} else {
|
||||||
});
|
return Promise.resolve(['log_group_2']);
|
||||||
|
}
|
||||||
|
});
|
||||||
const props = {
|
const props = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
selectedLogGroups: ['log_group_1'],
|
selectedLogGroups: ['log_group_1'],
|
||||||
@ -48,13 +50,15 @@ describe('LogGroupSelector', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('does not update upstream query log groups if saved is false', async () => {
|
it('does not update upstream query log groups if saved is false', async () => {
|
||||||
ds.datasource.describeLogGroups = jest.fn().mockImplementation(async (params: DescribeLogGroupsRequest) => {
|
ds.datasource.logsQueryRunner.describeLogGroups = jest
|
||||||
if (params.region === 'region1') {
|
.fn()
|
||||||
return Promise.resolve(['log_group_1']);
|
.mockImplementation(async (params: DescribeLogGroupsRequest) => {
|
||||||
} else {
|
if (params.region === 'region1') {
|
||||||
return Promise.resolve(['log_group_2']);
|
return Promise.resolve(['log_group_1']);
|
||||||
}
|
} else {
|
||||||
});
|
return Promise.resolve(['log_group_2']);
|
||||||
|
}
|
||||||
|
});
|
||||||
const props = {
|
const props = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
selectedLogGroups: ['log_group_1'],
|
selectedLogGroups: ['log_group_1'],
|
||||||
@ -94,12 +98,14 @@ describe('LogGroupSelector', () => {
|
|||||||
];
|
];
|
||||||
const testLimit = 10;
|
const testLimit = 10;
|
||||||
|
|
||||||
ds.datasource.describeLogGroups = jest.fn().mockImplementation(async (params: DescribeLogGroupsRequest) => {
|
ds.datasource.logsQueryRunner.describeLogGroups = jest
|
||||||
const theLogGroups = allLogGroups
|
.fn()
|
||||||
.filter((logGroupName) => logGroupName.startsWith(params.logGroupNamePrefix ?? ''))
|
.mockImplementation(async (params: DescribeLogGroupsRequest) => {
|
||||||
.slice(0, Math.max(params.limit ?? testLimit, testLimit));
|
const theLogGroups = allLogGroups
|
||||||
return Promise.resolve(theLogGroups);
|
.filter((logGroupName) => logGroupName.startsWith(params.logGroupNamePrefix ?? ''))
|
||||||
});
|
.slice(0, Math.max(params.limit ?? testLimit, testLimit));
|
||||||
|
return Promise.resolve(theLogGroups);
|
||||||
|
});
|
||||||
const props = {
|
const props = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
};
|
};
|
||||||
@ -123,7 +129,7 @@ describe('LogGroupSelector', () => {
|
|||||||
|
|
||||||
it('should render template variables a selectable option', async () => {
|
it('should render template variables a selectable option', async () => {
|
||||||
lodash.debounce = jest.fn().mockImplementation((fn) => fn);
|
lodash.debounce = jest.fn().mockImplementation((fn) => fn);
|
||||||
ds.datasource.describeLogGroups = jest.fn().mockResolvedValue([]);
|
ds.datasource.logsQueryRunner.describeLogGroups = jest.fn().mockResolvedValue([]);
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
const props = {
|
const props = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
|
@ -52,7 +52,7 @@ export const LogGroupSelector: React.FC<LogGroupSelectorProps> = ({
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const logGroups: string[] = await datasource.describeLogGroups({
|
const logGroups: string[] = await datasource.logsQueryRunner.describeLogGroups({
|
||||||
refId,
|
refId,
|
||||||
region,
|
region,
|
||||||
logGroupNamePrefix,
|
logGroupNamePrefix,
|
||||||
|
@ -38,7 +38,7 @@ describe('CloudWatchLogsQueryField', () => {
|
|||||||
it('loads defaultLogGroups', async () => {
|
it('loads defaultLogGroups', async () => {
|
||||||
const onRunQuery = jest.fn();
|
const onRunQuery = jest.fn();
|
||||||
const ds = setupMockedDataSource();
|
const ds = setupMockedDataSource();
|
||||||
ds.datasource.defaultLogGroups = ['foo'];
|
ds.datasource.logsQueryRunner.defaultLogGroups = ['foo'];
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<CloudWatchLogsQueryField
|
<CloudWatchLogsQueryField
|
||||||
|
@ -70,7 +70,7 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
|||||||
const { query, datasource, onChange } = this.props;
|
const { query, datasource, onChange } = this.props;
|
||||||
|
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
onChange({ ...query, logGroupNames: query.logGroupNames ?? datasource.defaultLogGroups });
|
onChange({ ...query, logGroupNames: query.logGroupNames ?? datasource.logsQueryRunner.defaultLogGroups });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -136,7 +136,7 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
|||||||
inputEl={
|
inputEl={
|
||||||
<LogGroupSelector
|
<LogGroupSelector
|
||||||
region={region}
|
region={region}
|
||||||
selectedLogGroups={logGroupNames ?? datasource.defaultLogGroups}
|
selectedLogGroups={logGroupNames ?? datasource.logsQueryRunner.defaultLogGroups}
|
||||||
datasource={datasource}
|
datasource={datasource}
|
||||||
onChange={function (logGroups: string[]): void {
|
onChange={function (logGroups: string[]): void {
|
||||||
onChange({ ...query, logGroupNames: logGroups });
|
onChange({ ...query, logGroupNames: logGroups });
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
declare let CloudWatchDatasource: any;
|
|
||||||
export default CloudWatchDatasource;
|
|
@ -1,56 +1,37 @@
|
|||||||
import { lastValueFrom, of } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
import { toArray } from 'rxjs/operators';
|
import { toArray } from 'rxjs/operators';
|
||||||
|
|
||||||
import {
|
import { dateTime, Field } from '@grafana/data';
|
||||||
ArrayVector,
|
|
||||||
DataFrame,
|
|
||||||
dataFrameToJSON,
|
|
||||||
DataQueryRequest,
|
|
||||||
dateTime,
|
|
||||||
Field,
|
|
||||||
FieldType,
|
|
||||||
LogLevel,
|
|
||||||
LogRowModel,
|
|
||||||
MutableDataFrame,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import { setDataSourceSrv } from '@grafana/runtime';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
dimensionVariable,
|
fieldsVariable,
|
||||||
expressionVariable,
|
|
||||||
labelsVariable,
|
|
||||||
limitVariable,
|
|
||||||
logGroupNamesVariable,
|
logGroupNamesVariable,
|
||||||
metricVariable,
|
|
||||||
namespaceVariable,
|
|
||||||
setupMockedDataSource,
|
setupMockedDataSource,
|
||||||
regionVariable,
|
regionVariable,
|
||||||
} from './__mocks__/CloudWatchDataSource';
|
} from './__mocks__/CloudWatchDataSource';
|
||||||
|
import { setupForLogs } from './__mocks__/logsTestContext';
|
||||||
import { validLogsQuery, validMetricsQuery } from './__mocks__/queries';
|
import { validLogsQuery, validMetricsQuery } from './__mocks__/queries';
|
||||||
import { LOGSTREAM_IDENTIFIER_INTERNAL, LOG_IDENTIFIER_INTERNAL } from './datasource';
|
import { timeRange } from './__mocks__/timeRange';
|
||||||
import {
|
import { CloudWatchLogsQuery, CloudWatchMetricsQuery, CloudWatchQuery } from './types';
|
||||||
CloudWatchAnnotationQuery,
|
|
||||||
CloudWatchLogsQueryStatus,
|
|
||||||
CloudWatchMetricsQuery,
|
|
||||||
CloudWatchQuery,
|
|
||||||
MetricEditorMode,
|
|
||||||
MetricQueryType,
|
|
||||||
} from './types';
|
|
||||||
|
|
||||||
const mockTimeRange = {
|
|
||||||
from: dateTime(1546372800000),
|
|
||||||
to: dateTime(1546380000000),
|
|
||||||
raw: {
|
|
||||||
from: dateTime(1546372800000),
|
|
||||||
to: dateTime(1546380000000),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('datasource', () => {
|
describe('datasource', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
describe('query', () => {
|
describe('query', () => {
|
||||||
it('should return error if log query and log groups is not specified', async () => {
|
it('should return error if log query and log groups is not specified', async () => {
|
||||||
const { datasource } = setupMockedDataSource();
|
const { datasource } = setupMockedDataSource();
|
||||||
const observable = datasource.query({ targets: [{ queryMode: 'Logs' as 'Logs' }] } as any);
|
const observable = datasource.query({
|
||||||
|
targets: [{ queryMode: 'Logs', id: '', refId: '', region: '' }],
|
||||||
|
requestId: '',
|
||||||
|
interval: '',
|
||||||
|
intervalMs: 0,
|
||||||
|
range: timeRange,
|
||||||
|
scopedVars: {},
|
||||||
|
timezone: '',
|
||||||
|
app: '',
|
||||||
|
startTime: 0,
|
||||||
|
});
|
||||||
|
|
||||||
await expect(observable).toEmitValuesWith((received) => {
|
await expect(observable).toEmitValuesWith((received) => {
|
||||||
const response = received[0];
|
const response = received[0];
|
||||||
@ -60,7 +41,17 @@ describe('datasource', () => {
|
|||||||
|
|
||||||
it('should return empty response if queries are hidden', async () => {
|
it('should return empty response if queries are hidden', async () => {
|
||||||
const { datasource } = setupMockedDataSource();
|
const { datasource } = setupMockedDataSource();
|
||||||
const observable = datasource.query({ targets: [{ queryMode: 'Logs' as 'Logs', hide: true }] } as any);
|
const observable = datasource.query({
|
||||||
|
targets: [{ queryMode: 'Logs', hide: true, id: '', refId: '', region: '' }],
|
||||||
|
requestId: '',
|
||||||
|
interval: '',
|
||||||
|
intervalMs: 0,
|
||||||
|
range: timeRange,
|
||||||
|
scopedVars: {},
|
||||||
|
timezone: '',
|
||||||
|
app: '',
|
||||||
|
startTime: 0,
|
||||||
|
});
|
||||||
|
|
||||||
await expect(observable).toEmitValuesWith((received) => {
|
await expect(observable).toEmitValuesWith((received) => {
|
||||||
const response = received[0];
|
const response = received[0];
|
||||||
@ -82,31 +73,43 @@ describe('datasource', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should interpolate variables in the query', async () => {
|
it('should interpolate variables in the query', async () => {
|
||||||
const { datasource, fetchMock } = setupMockedDataSource();
|
const { datasource, fetchMock } = setupMockedDataSource({
|
||||||
|
variables: [fieldsVariable, regionVariable],
|
||||||
|
});
|
||||||
await lastValueFrom(
|
await lastValueFrom(
|
||||||
datasource
|
datasource
|
||||||
.query({
|
.query({
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
|
id: '',
|
||||||
|
refId: '',
|
||||||
queryMode: 'Logs',
|
queryMode: 'Logs',
|
||||||
region: '$region',
|
region: '$region',
|
||||||
expression: 'fields $fields',
|
expression: 'fields $fields',
|
||||||
logGroupNames: ['/some/$group'],
|
logGroupNames: ['/some/group'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as any)
|
requestId: '',
|
||||||
|
interval: '',
|
||||||
|
intervalMs: 0,
|
||||||
|
range: timeRange,
|
||||||
|
scopedVars: {},
|
||||||
|
timezone: '',
|
||||||
|
app: '',
|
||||||
|
startTime: 0,
|
||||||
|
})
|
||||||
.pipe(toArray())
|
.pipe(toArray())
|
||||||
);
|
);
|
||||||
expect(fetchMock.mock.calls[0][0].data.queries[0]).toMatchObject({
|
expect(fetchMock.mock.calls[0][0].data.queries[0]).toMatchObject({
|
||||||
queryString: 'fields templatedField',
|
queryString: 'fields templatedField',
|
||||||
logGroupNames: ['/some/templatedGroup'],
|
logGroupNames: ['/some/group'],
|
||||||
region: 'templatedRegion',
|
region: 'templatedRegion',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should interpolate multi-value template variable for log group names in the query', async () => {
|
it('should interpolate multi-value template variable for log group names in the query', async () => {
|
||||||
const { datasource, fetchMock } = setupMockedDataSource({
|
const { datasource, fetchMock } = setupMockedDataSource({
|
||||||
variables: [expressionVariable, logGroupNamesVariable, regionVariable],
|
variables: [fieldsVariable, logGroupNamesVariable, regionVariable],
|
||||||
mockGetVariableName: false,
|
mockGetVariableName: false,
|
||||||
});
|
});
|
||||||
await lastValueFrom(
|
await lastValueFrom(
|
||||||
@ -114,13 +117,23 @@ describe('datasource', () => {
|
|||||||
.query({
|
.query({
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
|
id: '',
|
||||||
|
refId: '',
|
||||||
queryMode: 'Logs',
|
queryMode: 'Logs',
|
||||||
region: '$region',
|
region: '$region',
|
||||||
expression: 'fields $fields',
|
expression: 'fields $fields',
|
||||||
logGroupNames: ['$groups'],
|
logGroupNames: ['$groups'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as any)
|
requestId: '',
|
||||||
|
interval: '',
|
||||||
|
intervalMs: 0,
|
||||||
|
range: timeRange,
|
||||||
|
scopedVars: {},
|
||||||
|
timezone: '',
|
||||||
|
app: '',
|
||||||
|
startTime: 0,
|
||||||
|
})
|
||||||
.pipe(toArray())
|
.pipe(toArray())
|
||||||
);
|
);
|
||||||
expect(fetchMock.mock.calls[0][0].data.queries[0]).toMatchObject({
|
expect(fetchMock.mock.calls[0][0].data.queries[0]).toMatchObject({
|
||||||
@ -131,16 +144,39 @@ describe('datasource', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should add links to log queries', async () => {
|
it('should add links to log queries', async () => {
|
||||||
const { datasource } = setupForLogs();
|
const { datasource, timeSrv } = setupForLogs();
|
||||||
|
timeSrv.timeRange = () => {
|
||||||
|
const time = dateTime('2021-01-01T01:00:00Z');
|
||||||
|
const range = {
|
||||||
|
from: time.subtract(6, 'hour'),
|
||||||
|
to: time,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...range,
|
||||||
|
raw: range,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const observable = datasource.query({
|
const observable = datasource.query({
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
|
id: '',
|
||||||
|
region: '',
|
||||||
queryMode: 'Logs',
|
queryMode: 'Logs',
|
||||||
logGroupNames: ['test'],
|
logGroupNames: ['test'],
|
||||||
refId: 'a',
|
refId: 'a',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as any);
|
requestId: '',
|
||||||
|
interval: '',
|
||||||
|
intervalMs: 0,
|
||||||
|
range: timeRange,
|
||||||
|
scopedVars: {},
|
||||||
|
timezone: '',
|
||||||
|
app: '',
|
||||||
|
startTime: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const emits = await lastValueFrom(observable.pipe(toArray()));
|
const emits = await lastValueFrom(observable.pipe(toArray()));
|
||||||
expect(emits).toHaveLength(1);
|
expect(emits).toHaveLength(1);
|
||||||
@ -163,209 +199,6 @@ describe('datasource', () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('debouncedCustomAlert', () => {
|
|
||||||
const debouncedAlert = jest.fn();
|
|
||||||
beforeEach(() => {
|
|
||||||
const { datasource } = setupMockedDataSource({
|
|
||||||
variables: [
|
|
||||||
{ ...namespaceVariable, multi: true },
|
|
||||||
{ ...metricVariable, multi: true },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
datasource.debouncedCustomAlert = debouncedAlert;
|
|
||||||
datasource.performTimeSeriesQuery = jest.fn().mockResolvedValue([]);
|
|
||||||
datasource.query({
|
|
||||||
targets: [
|
|
||||||
{
|
|
||||||
queryMode: 'Metrics',
|
|
||||||
id: '',
|
|
||||||
region: 'us-east-2',
|
|
||||||
namespace: namespaceVariable.id,
|
|
||||||
metricName: metricVariable.id,
|
|
||||||
period: '',
|
|
||||||
alias: '',
|
|
||||||
dimensions: {},
|
|
||||||
matchExact: true,
|
|
||||||
statistic: '',
|
|
||||||
refId: '',
|
|
||||||
expression: 'x * 2',
|
|
||||||
metricQueryType: MetricQueryType.Search,
|
|
||||||
metricEditorMode: MetricEditorMode.Code,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} as any);
|
|
||||||
});
|
|
||||||
it('should show debounced alert for namespace and metric name', async () => {
|
|
||||||
expect(debouncedAlert).toHaveBeenCalledWith(
|
|
||||||
'CloudWatch templating error',
|
|
||||||
'Multi template variables are not supported for namespace'
|
|
||||||
);
|
|
||||||
expect(debouncedAlert).toHaveBeenCalledWith(
|
|
||||||
'CloudWatch templating error',
|
|
||||||
'Multi template variables are not supported for metric name'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not show debounced alert for region', async () => {
|
|
||||||
expect(debouncedAlert).not.toHaveBeenCalledWith(
|
|
||||||
'CloudWatch templating error',
|
|
||||||
'Multi template variables are not supported for region'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('filterMetricsQuery', () => {
|
|
||||||
const datasource = setupMockedDataSource().datasource;
|
|
||||||
let baseQuery: CloudWatchMetricsQuery;
|
|
||||||
beforeEach(() => {
|
|
||||||
baseQuery = {
|
|
||||||
id: '',
|
|
||||||
region: 'us-east-2',
|
|
||||||
namespace: '',
|
|
||||||
period: '',
|
|
||||||
alias: '',
|
|
||||||
metricName: '',
|
|
||||||
dimensions: {},
|
|
||||||
matchExact: true,
|
|
||||||
statistic: '',
|
|
||||||
expression: '',
|
|
||||||
refId: '',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should error if invalid mode', async () => {
|
|
||||||
expect(() => datasource.filterMetricQuery(baseQuery)).toThrowError('invalid metric editor mode');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('metric search queries', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
baseQuery = {
|
|
||||||
...baseQuery,
|
|
||||||
namespace: 'AWS/EC2',
|
|
||||||
metricName: 'CPUUtilization',
|
|
||||||
statistic: 'Average',
|
|
||||||
metricQueryType: MetricQueryType.Search,
|
|
||||||
metricEditorMode: MetricEditorMode.Builder,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not allow builder queries that dont have namespace, metric or statistic', async () => {
|
|
||||||
expect(datasource.filterMetricQuery({ ...baseQuery, statistic: undefined })).toBeFalsy();
|
|
||||||
expect(datasource.filterMetricQuery({ ...baseQuery, metricName: undefined })).toBeFalsy();
|
|
||||||
expect(datasource.filterMetricQuery({ ...baseQuery, namespace: '' })).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow builder queries that have namespace, metric or statistic', async () => {
|
|
||||||
expect(datasource.filterMetricQuery(baseQuery)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not allow code queries that dont have an expression', async () => {
|
|
||||||
expect(
|
|
||||||
datasource.filterMetricQuery({
|
|
||||||
...baseQuery,
|
|
||||||
expression: undefined,
|
|
||||||
metricEditorMode: MetricEditorMode.Code,
|
|
||||||
})
|
|
||||||
).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow code queries that have an expression', async () => {
|
|
||||||
expect(
|
|
||||||
datasource.filterMetricQuery({ ...baseQuery, expression: 'x * 2', metricEditorMode: MetricEditorMode.Code })
|
|
||||||
).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('metric search expression queries', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
baseQuery = {
|
|
||||||
...baseQuery,
|
|
||||||
metricQueryType: MetricQueryType.Search,
|
|
||||||
metricEditorMode: MetricEditorMode.Code,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not allow queries that dont have an expression', async () => {
|
|
||||||
const valid = datasource.filterMetricQuery(baseQuery);
|
|
||||||
expect(valid).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow queries that have an expression', async () => {
|
|
||||||
baseQuery.expression = 'SUM([a,x])';
|
|
||||||
const valid = datasource.filterMetricQuery(baseQuery);
|
|
||||||
expect(valid).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('metric query queries', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
baseQuery = {
|
|
||||||
...baseQuery,
|
|
||||||
metricQueryType: MetricQueryType.Query,
|
|
||||||
metricEditorMode: MetricEditorMode.Code,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not allow queries that dont have a sql expresssion', async () => {
|
|
||||||
const valid = datasource.filterMetricQuery(baseQuery);
|
|
||||||
expect(valid).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow queries that have a sql expresssion', async () => {
|
|
||||||
baseQuery.sqlExpression = 'select SUM(CPUUtilization) from "AWS/EC2"';
|
|
||||||
const valid = datasource.filterMetricQuery(baseQuery);
|
|
||||||
expect(valid).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('annotation query', () => {
|
|
||||||
const query: DataQueryRequest<CloudWatchAnnotationQuery> = {
|
|
||||||
range: mockTimeRange,
|
|
||||||
rangeRaw: mockTimeRange.raw,
|
|
||||||
targets: [
|
|
||||||
{
|
|
||||||
actionPrefix: '',
|
|
||||||
alarmNamePrefix: '',
|
|
||||||
datasource: { type: 'cloudwatch' },
|
|
||||||
dimensions: { InstanceId: 'i-12345678' },
|
|
||||||
matchExact: true,
|
|
||||||
metricName: 'CPUUtilization',
|
|
||||||
period: '300',
|
|
||||||
prefixMatching: false,
|
|
||||||
queryMode: 'Annotations',
|
|
||||||
refId: 'Anno',
|
|
||||||
namespace: `$${namespaceVariable.name}`,
|
|
||||||
region: `$${regionVariable.name}`,
|
|
||||||
statistic: 'Average',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
requestId: '',
|
|
||||||
interval: '',
|
|
||||||
intervalMs: 0,
|
|
||||||
scopedVars: {},
|
|
||||||
timezone: '',
|
|
||||||
app: '',
|
|
||||||
startTime: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
it('should issue the correct query', async () => {
|
|
||||||
const { datasource, fetchMock } = setupMockedDataSource({ variables: [namespaceVariable, regionVariable] });
|
|
||||||
await expect(datasource.query(query)).toEmitValuesWith(() => {
|
|
||||||
expect(fetchMock.mock.calls[0][0].data.queries[0]).toMatchObject(
|
|
||||||
expect.objectContaining({
|
|
||||||
region: regionVariable.current.value,
|
|
||||||
namespace: namespaceVariable.current.value,
|
|
||||||
metricName: query.targets[0].metricName,
|
|
||||||
dimensions: { InstanceId: ['i-12345678'] },
|
|
||||||
statistic: query.targets[0].statistic,
|
|
||||||
period: query.targets[0].period,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('resource requests', () => {
|
describe('resource requests', () => {
|
||||||
@ -389,298 +222,62 @@ describe('datasource', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('performTimeSeriesQuery', () => {
|
describe('when interpolating variables', () => {
|
||||||
it('should return the same length of data as result', async () => {
|
it('should return an empty array if no queries are provided', () => {
|
||||||
const { datasource } = setupMockedDataSource({
|
const { datasource } = setupMockedDataSource();
|
||||||
data: {
|
|
||||||
results: {
|
|
||||||
a: { refId: 'a', series: [{ name: 'cpu', points: [1, 1] }], meta: {} },
|
|
||||||
b: { refId: 'b', series: [{ name: 'memory', points: [2, 2] }], meta: {} },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const observable = datasource.performTimeSeriesQuery(
|
expect(datasource.interpolateVariablesInQueries([], {})).toHaveLength(0);
|
||||||
{
|
|
||||||
queries: [
|
|
||||||
{ datasourceId: 1, refId: 'a' },
|
|
||||||
{ datasourceId: 1, refId: 'b' },
|
|
||||||
],
|
|
||||||
} as any,
|
|
||||||
{ from: dateTime(), to: dateTime() } as any
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(observable).toEmitValuesWith((received) => {
|
|
||||||
const response = received[0];
|
|
||||||
expect(response.data.length).toEqual(2);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets fields.config.interval based on period', async () => {
|
it('should replace correct variables in CloudWatchLogsQuery', () => {
|
||||||
const { datasource } = setupMockedDataSource({
|
const { datasource, templateService } = setupMockedDataSource();
|
||||||
data: {
|
templateService.replace = jest.fn();
|
||||||
results: {
|
const variableName = 'someVar';
|
||||||
a: {
|
const logQuery: CloudWatchLogsQuery = {
|
||||||
refId: 'a',
|
queryMode: 'Logs',
|
||||||
series: [{ name: 'cpu', points: [1, 2], meta: { custom: { period: 60 } } }],
|
expression: `$${variableName}`,
|
||||||
},
|
region: `$${variableName}`,
|
||||||
b: {
|
id: '',
|
||||||
refId: 'b',
|
refId: '',
|
||||||
series: [{ name: 'cpu', points: [1, 2], meta: { custom: { period: 120 } } }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const observable = datasource.performTimeSeriesQuery(
|
|
||||||
{
|
|
||||||
queries: [{ datasourceId: 1, refId: 'a' }],
|
|
||||||
} as any,
|
|
||||||
{ from: dateTime(), to: dateTime() } as any
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(observable).toEmitValuesWith((received) => {
|
|
||||||
const response = received[0];
|
|
||||||
expect(response.data[0].fields[0].config.interval).toEqual(60000);
|
|
||||||
expect(response.data[1].fields[0].config.interval).toEqual(120000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('describeLogGroup', () => {
|
|
||||||
it('replaces region correctly in the query', async () => {
|
|
||||||
const { datasource, fetchMock } = setupMockedDataSource();
|
|
||||||
await datasource.describeLogGroups({ region: 'default' });
|
|
||||||
expect(fetchMock.mock.calls[0][0].data.queries[0].region).toBe('us-west-1');
|
|
||||||
|
|
||||||
await datasource.describeLogGroups({ region: 'eu-east' });
|
|
||||||
expect(fetchMock.mock.calls[1][0].data.queries[0].region).toBe('eu-east');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getLogRowContext', () => {
|
|
||||||
it('replaces parameters correctly in the query', async () => {
|
|
||||||
const { datasource, fetchMock } = setupMockedDataSource();
|
|
||||||
const row: LogRowModel = {
|
|
||||||
entryFieldIndex: 0,
|
|
||||||
rowIndex: 0,
|
|
||||||
dataFrame: new MutableDataFrame({
|
|
||||||
refId: 'B',
|
|
||||||
fields: [
|
|
||||||
{ name: 'ts', type: FieldType.time, values: [1] },
|
|
||||||
{ name: LOG_IDENTIFIER_INTERNAL, type: FieldType.string, values: ['foo'], labels: {} },
|
|
||||||
{ name: LOGSTREAM_IDENTIFIER_INTERNAL, type: FieldType.string, values: ['bar'], labels: {} },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
entry: '4',
|
|
||||||
labels: {},
|
|
||||||
hasAnsi: false,
|
|
||||||
hasUnescapedContent: false,
|
|
||||||
raw: '4',
|
|
||||||
logLevel: LogLevel.info,
|
|
||||||
timeEpochMs: 4,
|
|
||||||
timeEpochNs: '4000000',
|
|
||||||
timeFromNow: '',
|
|
||||||
timeLocal: '',
|
|
||||||
timeUtc: '',
|
|
||||||
uid: '1',
|
|
||||||
};
|
};
|
||||||
await datasource.getLogRowContext(row);
|
|
||||||
expect(fetchMock.mock.calls[0][0].data.queries[0].endTime).toBe(4);
|
|
||||||
expect(fetchMock.mock.calls[0][0].data.queries[0].region).toBe(undefined);
|
|
||||||
|
|
||||||
await datasource.getLogRowContext(row, { direction: 'FORWARD' }, { ...validLogsQuery, region: 'eu-east' });
|
datasource.interpolateVariablesInQueries([logQuery], {});
|
||||||
expect(fetchMock.mock.calls[1][0].data.queries[0].startTime).toBe(4);
|
|
||||||
expect(fetchMock.mock.calls[1][0].data.queries[0].region).toBe('eu-east');
|
expect(templateService.replace).toHaveBeenCalledWith(`$${variableName}`, {});
|
||||||
|
expect(templateService.replace).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('template variable interpolation', () => {
|
it('should replace correct variables in CloudWatchMetricsQuery', () => {
|
||||||
it('interpolates variables correctly', async () => {
|
const { datasource, templateService } = setupMockedDataSource();
|
||||||
const { datasource, fetchMock } = setupMockedDataSource({
|
templateService.replace = jest.fn();
|
||||||
variables: [namespaceVariable, metricVariable, labelsVariable, limitVariable],
|
templateService.getVariableName = jest.fn();
|
||||||
});
|
const variableName = 'someVar';
|
||||||
datasource.handleMetricQueries(
|
const metricsQuery: CloudWatchMetricsQuery = {
|
||||||
[
|
queryMode: 'Metrics',
|
||||||
{
|
id: 'someId',
|
||||||
id: '',
|
refId: 'someRefId',
|
||||||
refId: 'a',
|
expression: `$${variableName}`,
|
||||||
region: 'us-east-2',
|
region: `$${variableName}`,
|
||||||
namespace: '',
|
period: `$${variableName}`,
|
||||||
period: '',
|
alias: `$${variableName}`,
|
||||||
alias: '',
|
metricName: `$${variableName}`,
|
||||||
metricName: '',
|
namespace: `$${variableName}`,
|
||||||
dimensions: {},
|
dimensions: {
|
||||||
matchExact: true,
|
[`$${variableName}`]: `$${variableName}`,
|
||||||
statistic: '',
|
},
|
||||||
expression: '',
|
matchExact: false,
|
||||||
metricQueryType: MetricQueryType.Query,
|
statistic: '',
|
||||||
metricEditorMode: MetricEditorMode.Code,
|
sqlExpression: `$${variableName}`,
|
||||||
sqlExpression: 'SELECT SUM($metric) FROM "$namespace" GROUP BY ${labels:raw} LIMIT $limit',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{ range: { from: dateTime(), to: dateTime() } } as any
|
|
||||||
);
|
|
||||||
expect(fetchMock).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
data: expect.objectContaining({
|
|
||||||
queries: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
sqlExpression: `SELECT SUM(CPUUtilization) FROM "AWS/EC2" GROUP BY InstanceId,InstanceType LIMIT 100`,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('timezoneUTCOffset', () => {
|
|
||||||
const testQuery = {
|
|
||||||
id: '',
|
|
||||||
refId: 'a',
|
|
||||||
region: 'us-east-2',
|
|
||||||
namespace: '',
|
|
||||||
period: '',
|
|
||||||
label: '${MAX_TIME_RELATIVE}',
|
|
||||||
metricName: '',
|
|
||||||
dimensions: {},
|
|
||||||
matchExact: true,
|
|
||||||
statistic: '',
|
|
||||||
expression: '',
|
|
||||||
metricQueryType: MetricQueryType.Query,
|
|
||||||
metricEditorMode: MetricEditorMode.Code,
|
|
||||||
sqlExpression: 'SELECT SUM($metric) FROM "$namespace" GROUP BY ${labels:raw} LIMIT $limit',
|
|
||||||
};
|
|
||||||
const testTable = [
|
|
||||||
['Europe/Stockholm', '+0200'],
|
|
||||||
['America/New_York', '-0400'],
|
|
||||||
['Asia/Tokyo', '+0900'],
|
|
||||||
['UTC', '+0000'],
|
|
||||||
];
|
|
||||||
describe.each(testTable)('should use the right time zone offset', (ianaTimezone, expectedOffset) => {
|
|
||||||
const { datasource, fetchMock } = setupMockedDataSource();
|
|
||||||
datasource.handleMetricQueries([testQuery], {
|
|
||||||
range: { from: dateTime(), to: dateTime() },
|
|
||||||
timezone: ianaTimezone,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
data: expect.objectContaining({
|
|
||||||
queries: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
timezoneUTCOffset: expectedOffset,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('interpolateMetricsQueryVariables', () => {
|
|
||||||
it('interpolates dimensions correctly', () => {
|
|
||||||
const testQuery = {
|
|
||||||
id: 'a',
|
|
||||||
refId: 'a',
|
|
||||||
region: 'us-east-2',
|
|
||||||
namespace: '',
|
|
||||||
dimensions: { InstanceId: '$dimension' },
|
|
||||||
};
|
};
|
||||||
const ds = setupMockedDataSource({ variables: [dimensionVariable], mockGetVariableName: false });
|
|
||||||
const result = ds.datasource.interpolateMetricsQueryVariables(testQuery, {
|
|
||||||
dimension: { text: 'foo', value: 'foo' },
|
|
||||||
});
|
|
||||||
expect(result).toStrictEqual({
|
|
||||||
alias: '',
|
|
||||||
metricName: '',
|
|
||||||
namespace: '',
|
|
||||||
period: '',
|
|
||||||
sqlExpression: '',
|
|
||||||
dimensions: { InstanceId: ['foo'] },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('convertMultiFiltersFormat', () => {
|
datasource.interpolateVariablesInQueries([metricsQuery], {});
|
||||||
const ds = setupMockedDataSource({ variables: [labelsVariable, dimensionVariable], mockGetVariableName: false });
|
|
||||||
it('converts keys and values correctly', () => {
|
|
||||||
const filters = { $dimension: ['b'], a: ['$labels', 'bar'] };
|
|
||||||
const result = ds.datasource.convertMultiFilterFormat(filters);
|
|
||||||
expect(result).toStrictEqual({
|
|
||||||
env: ['b'],
|
|
||||||
a: ['InstanceId', 'InstanceType', 'bar'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getLogGroupFields', () => {
|
// We interpolate `expression`, `region`, `period`, `alias`, `metricName`, and `nameSpace` in CloudWatchMetricsQuery
|
||||||
it('passes region correctly', async () => {
|
expect(templateService.replace).toHaveBeenCalledWith(`$${variableName}`, {});
|
||||||
const { datasource, fetchMock } = setupMockedDataSource();
|
expect(templateService.replace).toHaveBeenCalledTimes(7);
|
||||||
fetchMock.mockReturnValueOnce(
|
|
||||||
of({
|
expect(templateService.getVariableName).toHaveBeenCalledWith(`$${variableName}`);
|
||||||
data: {
|
expect(templateService.getVariableName).toHaveBeenCalledTimes(1);
|
||||||
results: {
|
|
||||||
A: {
|
|
||||||
frames: [
|
|
||||||
dataFrameToJSON(
|
|
||||||
new MutableDataFrame({
|
|
||||||
fields: [
|
|
||||||
{ name: 'key', values: [] },
|
|
||||||
{ name: 'val', values: [] },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
await datasource.getLogGroupFields({ region: 'us-west-1', logGroupName: 'test' });
|
|
||||||
expect(fetchMock.mock.calls[0][0].data.queries[0].region).toBe('us-west-1');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function setupForLogs() {
|
|
||||||
function envelope(frame: DataFrame) {
|
|
||||||
return { data: { results: { a: { refId: 'a', frames: [dataFrameToJSON(frame)] } } } };
|
|
||||||
}
|
|
||||||
|
|
||||||
const { datasource, fetchMock } = setupMockedDataSource();
|
|
||||||
|
|
||||||
const startQueryFrame = new MutableDataFrame({ fields: [{ name: 'queryId', values: ['queryid'] }] });
|
|
||||||
fetchMock.mockReturnValueOnce(of(envelope(startQueryFrame)));
|
|
||||||
|
|
||||||
const logsFrame = new MutableDataFrame({
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: '@message',
|
|
||||||
values: new ArrayVector(['something']),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '@timestamp',
|
|
||||||
values: new ArrayVector([1]),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '@xrayTraceId',
|
|
||||||
values: new ArrayVector(['1-613f0d6b-3e7cb34375b60662359611bd']),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
meta: { custom: { Status: CloudWatchLogsQueryStatus.Complete } },
|
|
||||||
});
|
|
||||||
|
|
||||||
fetchMock.mockReturnValueOnce(of(envelope(logsFrame)));
|
|
||||||
|
|
||||||
setDataSourceSrv({
|
|
||||||
async get() {
|
|
||||||
return {
|
|
||||||
name: 'Xray',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
return { datasource, fetchMock };
|
|
||||||
}
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -109,8 +109,10 @@ async function runSuggestionTest(query: string, expectedItems: string[][]) {
|
|||||||
|
|
||||||
function makeDatasource(): CloudWatchDatasource {
|
function makeDatasource(): CloudWatchDatasource {
|
||||||
return {
|
return {
|
||||||
getLogGroupFields(): Promise<GetLogGroupFieldsResponse> {
|
logsQueryRunner: {
|
||||||
return Promise.resolve({ logGroupFields: [{ name: 'field1' }, { name: '@message' }] });
|
getLogGroupFields(): Promise<GetLogGroupFieldsResponse> {
|
||||||
|
return Promise.resolve({ logGroupFields: [{ name: 'field1' }, { name: '@message' }] });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as any;
|
} as any;
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ export class CloudWatchLanguageProvider extends LanguageProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
request = (url: string, params?: any): Promise<TSDBResponse> => {
|
request = (url: string, params?: any): Promise<TSDBResponse> => {
|
||||||
return lastValueFrom(this.datasource.awsRequest(url, params));
|
return lastValueFrom(this.datasource.logsQueryRunner.awsRequest(url, params));
|
||||||
};
|
};
|
||||||
|
|
||||||
start = () => {
|
start = () => {
|
||||||
@ -145,7 +145,7 @@ export class CloudWatchLanguageProvider extends LanguageProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
logGroups.map((logGroup) => this.datasource.getLogGroupFields({ logGroupName: logGroup, region }))
|
logGroups.map((logGroup) => this.datasource.logsQueryRunner.getLogGroupFields({ logGroupName: logGroup, region }))
|
||||||
);
|
);
|
||||||
|
|
||||||
const fields = [
|
const fields = [
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
import { setupMockedAnnotationQueryRunner } from '../__mocks__/AnnotationQueryRunner';
|
||||||
|
import { namespaceVariable, regionVariable } from '../__mocks__/CloudWatchDataSource';
|
||||||
|
import { CloudWatchAnnotationQuery } from '../types';
|
||||||
|
|
||||||
|
describe('CloudWatchAnnotationQueryRunner', () => {
|
||||||
|
const queries: CloudWatchAnnotationQuery[] = [
|
||||||
|
{
|
||||||
|
actionPrefix: '',
|
||||||
|
alarmNamePrefix: '',
|
||||||
|
datasource: { type: 'cloudwatch' },
|
||||||
|
dimensions: { InstanceId: 'i-12345678' },
|
||||||
|
matchExact: true,
|
||||||
|
metricName: 'CPUUtilization',
|
||||||
|
period: '300',
|
||||||
|
prefixMatching: false,
|
||||||
|
queryMode: 'Annotations',
|
||||||
|
refId: 'Anno',
|
||||||
|
namespace: `$${namespaceVariable.name}`,
|
||||||
|
region: `$${regionVariable.name}`,
|
||||||
|
statistic: 'Average',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
it('should issue the correct query', async () => {
|
||||||
|
const { runner, fetchMock, request } = setupMockedAnnotationQueryRunner({
|
||||||
|
variables: [namespaceVariable, regionVariable],
|
||||||
|
});
|
||||||
|
await expect(runner.handleAnnotationQuery(queries, request)).toEmitValuesWith(() => {
|
||||||
|
expect(fetchMock.mock.calls[0][0].data.queries[0]).toMatchObject(
|
||||||
|
expect.objectContaining({
|
||||||
|
region: regionVariable.current.value,
|
||||||
|
namespace: namespaceVariable.current.value,
|
||||||
|
metricName: queries[0].metricName,
|
||||||
|
dimensions: { InstanceId: ['i-12345678'] },
|
||||||
|
statistic: queries[0].statistic,
|
||||||
|
period: queries[0].period,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,44 @@
|
|||||||
|
import { map, Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { DataQueryRequest, DataQueryResponse, DataSourceInstanceSettings } from '@grafana/data';
|
||||||
|
import { toDataQueryResponse } from '@grafana/runtime';
|
||||||
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
|
||||||
|
import { CloudWatchAnnotationQuery, CloudWatchJsonData, CloudWatchQuery } from '../types';
|
||||||
|
|
||||||
|
import { CloudWatchQueryRunner } from './CloudWatchQueryRunner';
|
||||||
|
|
||||||
|
// This class handles execution of CloudWatch annotation queries
|
||||||
|
export class CloudWatchAnnotationQueryRunner extends CloudWatchQueryRunner {
|
||||||
|
constructor(instanceSettings: DataSourceInstanceSettings<CloudWatchJsonData>, templateSrv: TemplateSrv) {
|
||||||
|
super(instanceSettings, templateSrv);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAnnotationQuery(
|
||||||
|
queries: CloudWatchAnnotationQuery[],
|
||||||
|
options: DataQueryRequest<CloudWatchQuery>
|
||||||
|
): Observable<DataQueryResponse> {
|
||||||
|
return this.awsRequest(this.dsQueryEndpoint, {
|
||||||
|
from: options.range.from.valueOf().toString(),
|
||||||
|
to: options.range.to.valueOf().toString(),
|
||||||
|
queries: queries.map((query) => ({
|
||||||
|
...query,
|
||||||
|
statistic: this.templateSrv.replace(query.statistic),
|
||||||
|
region: this.templateSrv.replace(this.getActualRegion(query.region)),
|
||||||
|
namespace: this.templateSrv.replace(query.namespace),
|
||||||
|
metricName: this.templateSrv.replace(query.metricName),
|
||||||
|
dimensions: this.convertDimensionFormat(query.dimensions ?? {}, {}),
|
||||||
|
period: query.period ?? '',
|
||||||
|
actionPrefix: query.actionPrefix ?? '',
|
||||||
|
alarmNamePrefix: query.alarmNamePrefix ?? '',
|
||||||
|
type: 'annotationQuery',
|
||||||
|
datasource: this.ref,
|
||||||
|
})),
|
||||||
|
}).pipe(
|
||||||
|
map((r) => {
|
||||||
|
const frames = toDataQueryResponse({ data: r }).data;
|
||||||
|
return { data: frames };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,325 @@
|
|||||||
|
import { interval, lastValueFrom, of } from 'rxjs';
|
||||||
|
|
||||||
|
import { LogRowModel, MutableDataFrame, FieldType, LogLevel, dataFrameToJSON, DataQueryErrorType } from '@grafana/data';
|
||||||
|
import { BackendDataSourceResponse } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { genMockFrames, setupMockedLogsQueryRunner } from '../__mocks__/LogsQueryRunner';
|
||||||
|
import { validLogsQuery } from '../__mocks__/queries';
|
||||||
|
import { LogAction } from '../types';
|
||||||
|
import * as rxjsUtils from '../utils/rxjs/increasingInterval';
|
||||||
|
|
||||||
|
import { LOG_IDENTIFIER_INTERNAL, LOGSTREAM_IDENTIFIER_INTERNAL } from './CloudWatchLogsQueryRunner';
|
||||||
|
|
||||||
|
describe('CloudWatchLogsQueryRunner', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
describe('describeLogGroup', () => {
|
||||||
|
it('replaces region correctly in the query', async () => {
|
||||||
|
const { runner, fetchMock } = setupMockedLogsQueryRunner();
|
||||||
|
await runner.describeLogGroups({ region: 'default' });
|
||||||
|
expect(fetchMock.mock.calls[0][0].data.queries[0].region).toBe('us-west-1');
|
||||||
|
|
||||||
|
await runner.describeLogGroups({ region: 'eu-east' });
|
||||||
|
expect(fetchMock.mock.calls[1][0].data.queries[0].region).toBe('eu-east');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return log groups as an array of strings', async () => {
|
||||||
|
const data: BackendDataSourceResponse = {
|
||||||
|
results: {
|
||||||
|
A: {
|
||||||
|
frames: [
|
||||||
|
{
|
||||||
|
schema: {
|
||||||
|
name: 'logGroups',
|
||||||
|
refId: 'A',
|
||||||
|
fields: [{ name: 'logGroupName', type: FieldType.string }],
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
values: [
|
||||||
|
[
|
||||||
|
'/aws/containerinsights/dev303-workshop/application',
|
||||||
|
'/aws/containerinsights/dev303-workshop/dataplane',
|
||||||
|
'/aws/containerinsights/dev303-workshop/flowlogs',
|
||||||
|
'/aws/containerinsights/dev303-workshop/host',
|
||||||
|
'/aws/containerinsights/dev303-workshop/performance',
|
||||||
|
'/aws/containerinsights/dev303-workshop/prometheus',
|
||||||
|
'/aws/containerinsights/ecommerce-sockshop/application',
|
||||||
|
'/aws/containerinsights/ecommerce-sockshop/dataplane',
|
||||||
|
'/aws/containerinsights/ecommerce-sockshop/host',
|
||||||
|
'/aws/containerinsights/ecommerce-sockshop/performance',
|
||||||
|
'/aws/containerinsights/watchdemo-perf/application',
|
||||||
|
'/aws/containerinsights/watchdemo-perf/dataplane',
|
||||||
|
'/aws/containerinsights/watchdemo-perf/host',
|
||||||
|
'/aws/containerinsights/watchdemo-perf/performance',
|
||||||
|
'/aws/containerinsights/watchdemo-perf/prometheus',
|
||||||
|
'/aws/containerinsights/watchdemo-prod-us-east-1/performance',
|
||||||
|
'/aws/containerinsights/watchdemo-staging/application',
|
||||||
|
'/aws/containerinsights/watchdemo-staging/dataplane',
|
||||||
|
'/aws/containerinsights/watchdemo-staging/host',
|
||||||
|
'/aws/containerinsights/watchdemo-staging/performance',
|
||||||
|
'/aws/ecs/containerinsights/bugbash-ec2/performance',
|
||||||
|
'/aws/ecs/containerinsights/ecs-demoworkshop/performance',
|
||||||
|
'/aws/ecs/containerinsights/ecs-workshop-dev/performance',
|
||||||
|
'/aws/eks/dev303-workshop/cluster',
|
||||||
|
'/aws/events/cloudtrail',
|
||||||
|
'/aws/events/ecs',
|
||||||
|
'/aws/lambda/cwsyn-mycanary-fac97ded-f134-499a-9d71-4c3be1f63182',
|
||||||
|
'/aws/lambda/cwsyn-watch-linkchecks-ef7ef273-5da2-4663-af54-d2f52d55b060',
|
||||||
|
'/ecs/ecs-cwagent-daemon-service',
|
||||||
|
'/ecs/ecs-demo-limitTask',
|
||||||
|
'CloudTrail/DefaultLogGroup',
|
||||||
|
'container-insights-prometheus-beta',
|
||||||
|
'container-insights-prometheus-demo',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { runner } = setupMockedLogsQueryRunner({ data });
|
||||||
|
const expectedLogGroups = [
|
||||||
|
'/aws/containerinsights/dev303-workshop/application',
|
||||||
|
'/aws/containerinsights/dev303-workshop/dataplane',
|
||||||
|
'/aws/containerinsights/dev303-workshop/flowlogs',
|
||||||
|
'/aws/containerinsights/dev303-workshop/host',
|
||||||
|
'/aws/containerinsights/dev303-workshop/performance',
|
||||||
|
'/aws/containerinsights/dev303-workshop/prometheus',
|
||||||
|
'/aws/containerinsights/ecommerce-sockshop/application',
|
||||||
|
'/aws/containerinsights/ecommerce-sockshop/dataplane',
|
||||||
|
'/aws/containerinsights/ecommerce-sockshop/host',
|
||||||
|
'/aws/containerinsights/ecommerce-sockshop/performance',
|
||||||
|
'/aws/containerinsights/watchdemo-perf/application',
|
||||||
|
'/aws/containerinsights/watchdemo-perf/dataplane',
|
||||||
|
'/aws/containerinsights/watchdemo-perf/host',
|
||||||
|
'/aws/containerinsights/watchdemo-perf/performance',
|
||||||
|
'/aws/containerinsights/watchdemo-perf/prometheus',
|
||||||
|
'/aws/containerinsights/watchdemo-prod-us-east-1/performance',
|
||||||
|
'/aws/containerinsights/watchdemo-staging/application',
|
||||||
|
'/aws/containerinsights/watchdemo-staging/dataplane',
|
||||||
|
'/aws/containerinsights/watchdemo-staging/host',
|
||||||
|
'/aws/containerinsights/watchdemo-staging/performance',
|
||||||
|
'/aws/ecs/containerinsights/bugbash-ec2/performance',
|
||||||
|
'/aws/ecs/containerinsights/ecs-demoworkshop/performance',
|
||||||
|
'/aws/ecs/containerinsights/ecs-workshop-dev/performance',
|
||||||
|
'/aws/eks/dev303-workshop/cluster',
|
||||||
|
'/aws/events/cloudtrail',
|
||||||
|
'/aws/events/ecs',
|
||||||
|
'/aws/lambda/cwsyn-mycanary-fac97ded-f134-499a-9d71-4c3be1f63182',
|
||||||
|
'/aws/lambda/cwsyn-watch-linkchecks-ef7ef273-5da2-4663-af54-d2f52d55b060',
|
||||||
|
'/ecs/ecs-cwagent-daemon-service',
|
||||||
|
'/ecs/ecs-demo-limitTask',
|
||||||
|
'CloudTrail/DefaultLogGroup',
|
||||||
|
'container-insights-prometheus-beta',
|
||||||
|
'container-insights-prometheus-demo',
|
||||||
|
];
|
||||||
|
|
||||||
|
const logGroups = await runner.describeLogGroups({ region: 'default' });
|
||||||
|
|
||||||
|
expect(logGroups).toEqual(expectedLogGroups);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLogRowContext', () => {
|
||||||
|
it('replaces parameters correctly in the query', async () => {
|
||||||
|
const { runner, fetchMock } = setupMockedLogsQueryRunner();
|
||||||
|
const row: LogRowModel = {
|
||||||
|
entryFieldIndex: 0,
|
||||||
|
rowIndex: 0,
|
||||||
|
dataFrame: new MutableDataFrame({
|
||||||
|
refId: 'B',
|
||||||
|
fields: [
|
||||||
|
{ name: 'ts', type: FieldType.time, values: [1] },
|
||||||
|
{ name: LOG_IDENTIFIER_INTERNAL, type: FieldType.string, values: ['foo'], labels: {} },
|
||||||
|
{ name: LOGSTREAM_IDENTIFIER_INTERNAL, type: FieldType.string, values: ['bar'], labels: {} },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
entry: '4',
|
||||||
|
labels: {},
|
||||||
|
hasAnsi: false,
|
||||||
|
hasUnescapedContent: false,
|
||||||
|
raw: '4',
|
||||||
|
logLevel: LogLevel.info,
|
||||||
|
timeEpochMs: 4,
|
||||||
|
timeEpochNs: '4000000',
|
||||||
|
timeFromNow: '',
|
||||||
|
timeLocal: '',
|
||||||
|
timeUtc: '',
|
||||||
|
uid: '1',
|
||||||
|
};
|
||||||
|
await runner.getLogRowContext(row);
|
||||||
|
expect(fetchMock.mock.calls[0][0].data.queries[0].endTime).toBe(4);
|
||||||
|
expect(fetchMock.mock.calls[0][0].data.queries[0].region).toBe(undefined);
|
||||||
|
|
||||||
|
await runner.getLogRowContext(row, { direction: 'FORWARD' }, { ...validLogsQuery, region: 'eu-east' });
|
||||||
|
expect(fetchMock.mock.calls[1][0].data.queries[0].startTime).toBe(4);
|
||||||
|
expect(fetchMock.mock.calls[1][0].data.queries[0].region).toBe('eu-east');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLogGroupFields', () => {
|
||||||
|
it('passes region correctly', async () => {
|
||||||
|
const { runner, fetchMock } = setupMockedLogsQueryRunner();
|
||||||
|
fetchMock.mockReturnValueOnce(
|
||||||
|
of({
|
||||||
|
data: {
|
||||||
|
results: {
|
||||||
|
A: {
|
||||||
|
frames: [
|
||||||
|
dataFrameToJSON(
|
||||||
|
new MutableDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'key', values: [] },
|
||||||
|
{ name: 'val', values: [] },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await runner.getLogGroupFields({ region: 'us-west-1', logGroupName: 'test' });
|
||||||
|
expect(fetchMock.mock.calls[0][0].data.queries[0].region).toBe('us-west-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('logs query', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(rxjsUtils, 'increasingInterval').mockImplementation(() => interval(100));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should stop querying when timed out', async () => {
|
||||||
|
const { runner } = setupMockedLogsQueryRunner();
|
||||||
|
const fakeFrames = genMockFrames(20);
|
||||||
|
const initialRecordsMatched = fakeFrames[0].meta!.stats!.find((stat) => stat.displayName === 'Records scanned')!
|
||||||
|
.value!;
|
||||||
|
for (let i = 1; i < 4; i++) {
|
||||||
|
fakeFrames[i].meta!.stats = [
|
||||||
|
{
|
||||||
|
displayName: 'Records scanned',
|
||||||
|
value: initialRecordsMatched,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalRecordsMatched = fakeFrames[9].meta!.stats!.find((stat) => stat.displayName === 'Records scanned')!
|
||||||
|
.value!;
|
||||||
|
for (let i = 10; i < fakeFrames.length; i++) {
|
||||||
|
fakeFrames[i].meta!.stats = [
|
||||||
|
{
|
||||||
|
displayName: 'Records scanned',
|
||||||
|
value: finalRecordsMatched,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
jest.spyOn(runner, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => {
|
||||||
|
if (subtype === 'GetQueryResults') {
|
||||||
|
const mockObservable = of([fakeFrames[i]]);
|
||||||
|
i++;
|
||||||
|
return mockObservable;
|
||||||
|
} else {
|
||||||
|
return of([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const iterations = 15;
|
||||||
|
// Times out after 15 passes for consistent testing
|
||||||
|
const timeoutFunc = () => {
|
||||||
|
return i >= iterations;
|
||||||
|
};
|
||||||
|
const myResponse = await lastValueFrom(
|
||||||
|
runner.logsQuery([{ queryId: 'fake-query-id', region: 'default', refId: 'A' }], timeoutFunc)
|
||||||
|
);
|
||||||
|
|
||||||
|
const expectedData = [
|
||||||
|
{
|
||||||
|
...fakeFrames[14],
|
||||||
|
meta: {
|
||||||
|
custom: {
|
||||||
|
Status: 'Cancelled',
|
||||||
|
},
|
||||||
|
stats: fakeFrames[14].meta!.stats,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(myResponse).toEqual({
|
||||||
|
data: expectedData,
|
||||||
|
key: 'test-key',
|
||||||
|
state: 'Done',
|
||||||
|
error: {
|
||||||
|
type: DataQueryErrorType.Timeout,
|
||||||
|
message: `error: query timed out after 5 attempts`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(i).toBe(iterations);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should continue querying as long as new data is being received', async () => {
|
||||||
|
const { runner } = setupMockedLogsQueryRunner();
|
||||||
|
const fakeFrames = genMockFrames(15);
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
jest.spyOn(runner, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => {
|
||||||
|
if (subtype === 'GetQueryResults') {
|
||||||
|
const mockObservable = of([fakeFrames[i]]);
|
||||||
|
i++;
|
||||||
|
return mockObservable;
|
||||||
|
} else {
|
||||||
|
return of([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const startTime = new Date();
|
||||||
|
const timeoutFunc = () => {
|
||||||
|
return Date.now() >= startTime.valueOf() + 6000;
|
||||||
|
};
|
||||||
|
const myResponse = await lastValueFrom(
|
||||||
|
runner.logsQuery([{ queryId: 'fake-query-id', region: 'default', refId: 'A' }], timeoutFunc)
|
||||||
|
);
|
||||||
|
expect(myResponse).toEqual({
|
||||||
|
data: [fakeFrames[fakeFrames.length - 1]],
|
||||||
|
key: 'test-key',
|
||||||
|
state: 'Done',
|
||||||
|
});
|
||||||
|
expect(i).toBe(15);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should stop querying when results come back with status "Complete"', async () => {
|
||||||
|
const { runner } = setupMockedLogsQueryRunner();
|
||||||
|
const fakeFrames = genMockFrames(3);
|
||||||
|
let i = 0;
|
||||||
|
jest.spyOn(runner, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => {
|
||||||
|
if (subtype === 'GetQueryResults') {
|
||||||
|
const mockObservable = of([fakeFrames[i]]);
|
||||||
|
i++;
|
||||||
|
return mockObservable;
|
||||||
|
} else {
|
||||||
|
return of([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const startTime = new Date();
|
||||||
|
const timeoutFunc = () => {
|
||||||
|
return Date.now() >= startTime.valueOf() + 6000;
|
||||||
|
};
|
||||||
|
const myResponse = await lastValueFrom(
|
||||||
|
runner.logsQuery([{ queryId: 'fake-query-id', region: 'default', refId: 'A' }], timeoutFunc)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(myResponse).toEqual({
|
||||||
|
data: [fakeFrames[2]],
|
||||||
|
key: 'test-key',
|
||||||
|
state: 'Done',
|
||||||
|
});
|
||||||
|
expect(i).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,473 @@
|
|||||||
|
import { isEmpty, set } from 'lodash';
|
||||||
|
import {
|
||||||
|
Observable,
|
||||||
|
of,
|
||||||
|
mergeMap,
|
||||||
|
map,
|
||||||
|
from,
|
||||||
|
concatMap,
|
||||||
|
finalize,
|
||||||
|
repeat,
|
||||||
|
scan,
|
||||||
|
share,
|
||||||
|
takeWhile,
|
||||||
|
tap,
|
||||||
|
zip,
|
||||||
|
catchError,
|
||||||
|
lastValueFrom,
|
||||||
|
} from 'rxjs';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DataFrame,
|
||||||
|
DataQueryError,
|
||||||
|
DataQueryErrorType,
|
||||||
|
DataQueryRequest,
|
||||||
|
DataQueryResponse,
|
||||||
|
DataSourceInstanceSettings,
|
||||||
|
LoadingState,
|
||||||
|
LogRowModel,
|
||||||
|
rangeUtil,
|
||||||
|
ScopedVars,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { BackendDataSourceResponse, config, FetchError, FetchResponse, toDataQueryResponse } from '@grafana/runtime';
|
||||||
|
import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider';
|
||||||
|
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CloudWatchJsonData,
|
||||||
|
CloudWatchLogsQuery,
|
||||||
|
CloudWatchLogsQueryStatus,
|
||||||
|
CloudWatchLogsRequest,
|
||||||
|
CloudWatchQuery,
|
||||||
|
DescribeLogGroupsRequest,
|
||||||
|
GetLogEventsRequest,
|
||||||
|
GetLogGroupFieldsRequest,
|
||||||
|
GetLogGroupFieldsResponse,
|
||||||
|
LogAction,
|
||||||
|
StartQueryRequest,
|
||||||
|
} from '../types';
|
||||||
|
import { addDataLinksToLogsResponse } from '../utils/datalinks';
|
||||||
|
import { runWithRetry } from '../utils/logsRetry';
|
||||||
|
import { increasingInterval } from '../utils/rxjs/increasingInterval';
|
||||||
|
|
||||||
|
import { CloudWatchQueryRunner } from './CloudWatchQueryRunner';
|
||||||
|
|
||||||
|
export const LOG_IDENTIFIER_INTERNAL = '__log__grafana_internal__';
|
||||||
|
export const LOGSTREAM_IDENTIFIER_INTERNAL = '__logstream__grafana_internal__';
|
||||||
|
|
||||||
|
// This class handles execution of CloudWatch logs query data queries
|
||||||
|
export class CloudWatchLogsQueryRunner extends CloudWatchQueryRunner {
|
||||||
|
logsTimeout: string;
|
||||||
|
defaultLogGroups: string[];
|
||||||
|
logQueries: Record<string, { id: string; region: string; statsQuery: boolean }> = {};
|
||||||
|
tracingDataSourceUid?: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
instanceSettings: DataSourceInstanceSettings<CloudWatchJsonData>,
|
||||||
|
templateSrv: TemplateSrv,
|
||||||
|
private readonly timeSrv: TimeSrv
|
||||||
|
) {
|
||||||
|
super(instanceSettings, templateSrv);
|
||||||
|
|
||||||
|
this.tracingDataSourceUid = instanceSettings.jsonData.tracingDatasourceUid;
|
||||||
|
this.logsTimeout = instanceSettings.jsonData.logsTimeout || '15m';
|
||||||
|
this.defaultLogGroups = instanceSettings.jsonData.defaultLogGroups || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle log query. The log query works by starting the query on the CloudWatch and then periodically polling for
|
||||||
|
* results.
|
||||||
|
* @param logQueries
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
handleLogQueries = (
|
||||||
|
logQueries: CloudWatchLogsQuery[],
|
||||||
|
options: DataQueryRequest<CloudWatchQuery>
|
||||||
|
): Observable<DataQueryResponse> => {
|
||||||
|
const queryParams = logQueries.map((target: CloudWatchLogsQuery) => ({
|
||||||
|
queryString: target.expression || '',
|
||||||
|
refId: target.refId,
|
||||||
|
logGroupNames: target.logGroupNames || this.defaultLogGroups,
|
||||||
|
region: super.replaceVariableAndDisplayWarningIfMulti(
|
||||||
|
this.getActualRegion(target.region),
|
||||||
|
options.scopedVars,
|
||||||
|
true,
|
||||||
|
'region'
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const validLogQueries = queryParams.filter((item) => item.logGroupNames?.length);
|
||||||
|
if (logQueries.length > validLogQueries.length) {
|
||||||
|
return of({ data: [], error: { message: 'Log group is required' } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// No valid targets, return the empty result to save a round trip.
|
||||||
|
if (isEmpty(validLogQueries)) {
|
||||||
|
return of({ data: [], state: LoadingState.Done });
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = new Date();
|
||||||
|
const timeoutFunc = () => {
|
||||||
|
return Date.now() >= startTime.valueOf() + rangeUtil.intervalToMs(this.logsTimeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
return runWithRetry(
|
||||||
|
(targets: StartQueryRequest[]) => {
|
||||||
|
return this.makeLogActionRequest('StartQuery', targets, {
|
||||||
|
makeReplacements: true,
|
||||||
|
scopedVars: options.scopedVars,
|
||||||
|
skipCache: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
queryParams,
|
||||||
|
timeoutFunc
|
||||||
|
).pipe(
|
||||||
|
mergeMap(({ frames, error }: { frames: DataFrame[]; error?: DataQueryError }) =>
|
||||||
|
// This queries for the results
|
||||||
|
this.logsQuery(
|
||||||
|
frames.map((dataFrame) => ({
|
||||||
|
queryId: dataFrame.fields[0].values.get(0),
|
||||||
|
region: dataFrame.meta?.custom?.['Region'] ?? 'default',
|
||||||
|
refId: dataFrame.refId!,
|
||||||
|
statsGroups: logQueries.find((target) => target.refId === dataFrame.refId)?.statsGroups,
|
||||||
|
})),
|
||||||
|
timeoutFunc
|
||||||
|
).pipe(
|
||||||
|
map((response: DataQueryResponse) => {
|
||||||
|
if (!response.error && error) {
|
||||||
|
response.error = error;
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
mergeMap((dataQueryResponse) => {
|
||||||
|
return from(
|
||||||
|
(async () => {
|
||||||
|
await addDataLinksToLogsResponse(
|
||||||
|
dataQueryResponse,
|
||||||
|
options,
|
||||||
|
this.timeSrv.timeRange(),
|
||||||
|
this.replaceVariableAndDisplayWarningIfMulti.bind(this),
|
||||||
|
this.expandVariableToArray.bind(this),
|
||||||
|
this.getActualRegion.bind(this),
|
||||||
|
this.tracingDataSourceUid
|
||||||
|
);
|
||||||
|
|
||||||
|
return dataQueryResponse;
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks progress and polls data of a started logs query with some retry logic.
|
||||||
|
* @param queryParams
|
||||||
|
*/
|
||||||
|
logsQuery(
|
||||||
|
queryParams: Array<{
|
||||||
|
queryId: string;
|
||||||
|
refId: string;
|
||||||
|
limit?: number;
|
||||||
|
region: string;
|
||||||
|
statsGroups?: string[];
|
||||||
|
}>,
|
||||||
|
timeoutFunc: () => boolean
|
||||||
|
): Observable<DataQueryResponse> {
|
||||||
|
this.logQueries = {};
|
||||||
|
queryParams.forEach((param) => {
|
||||||
|
this.logQueries[param.refId] = {
|
||||||
|
id: param.queryId,
|
||||||
|
region: param.region,
|
||||||
|
statsQuery: (param.statsGroups?.length ?? 0) > 0 ?? false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataFrames = increasingInterval({ startPeriod: 100, endPeriod: 1000, step: 300 }).pipe(
|
||||||
|
concatMap((_) => this.makeLogActionRequest('GetQueryResults', queryParams, { skipCache: true })),
|
||||||
|
repeat(),
|
||||||
|
share()
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialValue: { failures: number; prevRecordsMatched: Record<string, number> } = {
|
||||||
|
failures: 0,
|
||||||
|
prevRecordsMatched: {},
|
||||||
|
};
|
||||||
|
const consecutiveFailedAttempts = dataFrames.pipe(
|
||||||
|
scan(({ failures, prevRecordsMatched }, frames) => {
|
||||||
|
failures++;
|
||||||
|
for (const frame of frames) {
|
||||||
|
const recordsMatched = frame.meta?.stats?.find((stat) => stat.displayName === 'Records scanned')?.value!;
|
||||||
|
if (recordsMatched > (prevRecordsMatched[frame.refId!] ?? 0)) {
|
||||||
|
failures = 0;
|
||||||
|
}
|
||||||
|
prevRecordsMatched[frame.refId!] = recordsMatched;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { failures, prevRecordsMatched };
|
||||||
|
}, initialValue),
|
||||||
|
map(({ failures }) => failures),
|
||||||
|
share()
|
||||||
|
);
|
||||||
|
|
||||||
|
const queryResponse: Observable<DataQueryResponse> = zip(dataFrames, consecutiveFailedAttempts).pipe(
|
||||||
|
tap(([dataFrames]) => {
|
||||||
|
for (const frame of dataFrames) {
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
CloudWatchLogsQueryStatus.Complete,
|
||||||
|
CloudWatchLogsQueryStatus.Cancelled,
|
||||||
|
CloudWatchLogsQueryStatus.Failed,
|
||||||
|
].includes(frame.meta?.custom?.['Status']) &&
|
||||||
|
this.logQueries.hasOwnProperty(frame.refId!)
|
||||||
|
) {
|
||||||
|
delete this.logQueries[frame.refId!];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
map(([dataFrames, failedAttempts]) => {
|
||||||
|
if (timeoutFunc()) {
|
||||||
|
for (const frame of dataFrames) {
|
||||||
|
set(frame, 'meta.custom.Status', CloudWatchLogsQueryStatus.Cancelled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: dataFrames,
|
||||||
|
key: 'test-key',
|
||||||
|
state: dataFrames.every((dataFrame) =>
|
||||||
|
[
|
||||||
|
CloudWatchLogsQueryStatus.Complete,
|
||||||
|
CloudWatchLogsQueryStatus.Cancelled,
|
||||||
|
CloudWatchLogsQueryStatus.Failed,
|
||||||
|
].includes(dataFrame.meta?.custom?.['Status'])
|
||||||
|
)
|
||||||
|
? LoadingState.Done
|
||||||
|
: LoadingState.Loading,
|
||||||
|
error: timeoutFunc()
|
||||||
|
? {
|
||||||
|
message: `error: query timed out after ${failedAttempts} attempts`,
|
||||||
|
type: DataQueryErrorType.Timeout,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
takeWhile(({ state }) => state !== LoadingState.Error && state !== LoadingState.Done, true)
|
||||||
|
);
|
||||||
|
|
||||||
|
return withTeardown(queryResponse, () => this.stopQueries());
|
||||||
|
}
|
||||||
|
|
||||||
|
stopQueries() {
|
||||||
|
if (Object.keys(this.logQueries).length > 0) {
|
||||||
|
this.makeLogActionRequest(
|
||||||
|
'StopQuery',
|
||||||
|
Object.values(this.logQueries).map((logQuery) => ({ queryId: logQuery.id, region: logQuery.region })),
|
||||||
|
{
|
||||||
|
makeReplacements: false,
|
||||||
|
skipCache: true,
|
||||||
|
}
|
||||||
|
).pipe(
|
||||||
|
finalize(() => {
|
||||||
|
this.logQueries = {};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
makeLogActionRequest(
|
||||||
|
subtype: LogAction,
|
||||||
|
queryParams: CloudWatchLogsRequest[],
|
||||||
|
options: {
|
||||||
|
scopedVars?: ScopedVars;
|
||||||
|
makeReplacements?: boolean;
|
||||||
|
skipCache?: boolean;
|
||||||
|
} = {
|
||||||
|
makeReplacements: true,
|
||||||
|
skipCache: false,
|
||||||
|
}
|
||||||
|
): Observable<DataFrame[]> {
|
||||||
|
const range = this.timeSrv.timeRange();
|
||||||
|
|
||||||
|
const requestParams = {
|
||||||
|
from: range.from.valueOf().toString(),
|
||||||
|
to: range.to.valueOf().toString(),
|
||||||
|
queries: queryParams.map((param: CloudWatchLogsRequest) => ({
|
||||||
|
// eslint-ignore-next-line
|
||||||
|
refId: (param as StartQueryRequest).refId || 'A',
|
||||||
|
intervalMs: 1, // dummy
|
||||||
|
maxDataPoints: 1, // dummy
|
||||||
|
datasource: this.ref,
|
||||||
|
type: 'logAction',
|
||||||
|
subtype: subtype,
|
||||||
|
...param,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.makeReplacements) {
|
||||||
|
requestParams.queries.forEach((query: CloudWatchLogsRequest) => {
|
||||||
|
const fieldsToReplace: Array<
|
||||||
|
keyof (GetLogEventsRequest & StartQueryRequest & DescribeLogGroupsRequest & GetLogGroupFieldsRequest)
|
||||||
|
> = ['queryString', 'logGroupNames', 'logGroupName', 'logGroupNamePrefix'];
|
||||||
|
|
||||||
|
// eslint-ignore-next-line
|
||||||
|
const anyQuery: any = query;
|
||||||
|
for (const fieldName of fieldsToReplace) {
|
||||||
|
if (query.hasOwnProperty(fieldName)) {
|
||||||
|
if (Array.isArray(anyQuery[fieldName])) {
|
||||||
|
anyQuery[fieldName] = anyQuery[fieldName].flatMap((val: string) => {
|
||||||
|
if (fieldName === 'logGroupNames') {
|
||||||
|
return this.expandVariableToArray(val, options.scopedVars || {});
|
||||||
|
}
|
||||||
|
return this.replaceVariableAndDisplayWarningIfMulti(val, options.scopedVars, true, fieldName);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
anyQuery[fieldName] = this.replaceVariableAndDisplayWarningIfMulti(
|
||||||
|
anyQuery[fieldName],
|
||||||
|
options.scopedVars,
|
||||||
|
true,
|
||||||
|
fieldName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyQuery.region) {
|
||||||
|
anyQuery.region = this.replaceVariableAndDisplayWarningIfMulti(
|
||||||
|
anyQuery.region,
|
||||||
|
options.scopedVars,
|
||||||
|
true,
|
||||||
|
'region'
|
||||||
|
);
|
||||||
|
anyQuery.region = this.getActualRegion(anyQuery.region);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultsToDataFrames = (
|
||||||
|
val:
|
||||||
|
| { data: BackendDataSourceResponse | undefined }
|
||||||
|
| FetchResponse<BackendDataSourceResponse | undefined>
|
||||||
|
| DataQueryError
|
||||||
|
): DataFrame[] => toDataQueryResponse(val).data || [];
|
||||||
|
let headers = {};
|
||||||
|
if (options.skipCache) {
|
||||||
|
headers = {
|
||||||
|
'X-Cache-Skip': true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.awsRequest(this.dsQueryEndpoint, requestParams, headers).pipe(
|
||||||
|
map((response) => resultsToDataFrames({ data: response })),
|
||||||
|
catchError((err: FetchError) => {
|
||||||
|
if (config.featureToggles.datasourceQueryMultiStatus && err.status === 207) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.status === 400) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.data?.error) {
|
||||||
|
throw err.data.error;
|
||||||
|
} else if (err.data?.message) {
|
||||||
|
// In PROD we do not supply .error
|
||||||
|
throw err.data.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLogRowContext = async (
|
||||||
|
row: LogRowModel,
|
||||||
|
{ limit = 10, direction = 'BACKWARD' }: RowContextOptions = {},
|
||||||
|
query?: CloudWatchLogsQuery
|
||||||
|
): Promise<{ data: DataFrame[] }> => {
|
||||||
|
let logStreamField = null;
|
||||||
|
let logField = null;
|
||||||
|
|
||||||
|
for (const field of row.dataFrame.fields) {
|
||||||
|
if (field.name === LOGSTREAM_IDENTIFIER_INTERNAL) {
|
||||||
|
logStreamField = field;
|
||||||
|
if (logField !== null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (field.name === LOG_IDENTIFIER_INTERNAL) {
|
||||||
|
logField = field;
|
||||||
|
if (logStreamField !== null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestParams: GetLogEventsRequest = {
|
||||||
|
limit,
|
||||||
|
startFromHead: direction !== 'BACKWARD',
|
||||||
|
region: query?.region,
|
||||||
|
logGroupName: parseLogGroupName(logField!.values.get(row.rowIndex)),
|
||||||
|
logStreamName: logStreamField!.values.get(row.rowIndex),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (direction === 'BACKWARD') {
|
||||||
|
requestParams.endTime = row.timeEpochMs;
|
||||||
|
} else {
|
||||||
|
requestParams.startTime = row.timeEpochMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataFrames = await lastValueFrom(this.makeLogActionRequest('GetLogEvents', [requestParams]));
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: dataFrames,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
async describeLogGroups(params: DescribeLogGroupsRequest): Promise<string[]> {
|
||||||
|
const dataFrames = await lastValueFrom(this.makeLogActionRequest('DescribeLogGroups', [params]));
|
||||||
|
|
||||||
|
const logGroupNames = dataFrames[0]?.fields[0]?.values.toArray() ?? [];
|
||||||
|
return logGroupNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
async describeAllLogGroups(params: DescribeLogGroupsRequest): Promise<string[]> {
|
||||||
|
const dataFrames = await lastValueFrom(this.makeLogActionRequest('DescribeAllLogGroups', [params]));
|
||||||
|
|
||||||
|
const logGroupNames = dataFrames[0]?.fields[0]?.values.toArray() ?? [];
|
||||||
|
return logGroupNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLogGroupFields(params: GetLogGroupFieldsRequest): Promise<GetLogGroupFieldsResponse> {
|
||||||
|
const dataFrames = await lastValueFrom(this.makeLogActionRequest('GetLogGroupFields', [params]));
|
||||||
|
|
||||||
|
const fieldNames = dataFrames[0].fields[0].values.toArray();
|
||||||
|
const fieldPercentages = dataFrames[0].fields[1].values.toArray();
|
||||||
|
const getLogGroupFieldsResponse = {
|
||||||
|
logGroupFields: fieldNames.map((val, i) => ({ name: val, percent: fieldPercentages[i] })) ?? [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return getLogGroupFieldsResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function withTeardown<T = DataQueryResponse>(observable: Observable<T>, onUnsubscribe: () => void): Observable<T> {
|
||||||
|
return new Observable<T>((subscriber) => {
|
||||||
|
const innerSub = observable.subscribe({
|
||||||
|
next: (val) => subscriber.next(val),
|
||||||
|
error: (err) => subscriber.next(err),
|
||||||
|
complete: () => subscriber.complete(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
innerSub.unsubscribe();
|
||||||
|
onUnsubscribe();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLogGroupName(logIdentifier: string): string {
|
||||||
|
const colonIndex = logIdentifier.lastIndexOf(':');
|
||||||
|
return logIdentifier.slice(colonIndex + 1);
|
||||||
|
}
|
@ -0,0 +1,862 @@
|
|||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
|
import { CustomVariableModel, getFrameDisplayName, VariableHide } from '@grafana/data';
|
||||||
|
import { dateTime } from '@grafana/data/src/datetime/moment_wrapper';
|
||||||
|
import { BackendDataSourceResponse } from '@grafana/runtime';
|
||||||
|
import { initialVariableModelState } from 'app/features/variables/types';
|
||||||
|
import * as redux from 'app/store/store';
|
||||||
|
|
||||||
|
import {
|
||||||
|
namespaceVariable,
|
||||||
|
metricVariable,
|
||||||
|
labelsVariable,
|
||||||
|
limitVariable,
|
||||||
|
dimensionVariable,
|
||||||
|
periodIntervalVariable,
|
||||||
|
} from '../__mocks__/CloudWatchDataSource';
|
||||||
|
import { setupMockedMetricsQueryRunner } from '../__mocks__/MetricsQueryRunner';
|
||||||
|
import { MetricQueryType, MetricEditorMode, CloudWatchMetricsQuery, DataQueryError } from '../types';
|
||||||
|
|
||||||
|
describe('CloudWatchMetricsQueryRunner', () => {
|
||||||
|
describe('performTimeSeriesQuery', () => {
|
||||||
|
it('should return the same length of data as result', async () => {
|
||||||
|
const { runner, timeRange } = setupMockedMetricsQueryRunner({
|
||||||
|
data: {
|
||||||
|
results: {
|
||||||
|
a: { refId: 'a', series: [{ target: 'cpu', datapoints: [[1, 1]] }] },
|
||||||
|
b: { refId: 'b', series: [{ target: 'memory', datapoints: [[2, 2]] }] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const observable = runner.performTimeSeriesQuery(
|
||||||
|
{
|
||||||
|
queries: [
|
||||||
|
{ datasourceId: 1, refId: 'a' },
|
||||||
|
{ datasourceId: 1, refId: 'b' },
|
||||||
|
],
|
||||||
|
from: '',
|
||||||
|
to: '',
|
||||||
|
},
|
||||||
|
timeRange
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(observable).toEmitValuesWith((received) => {
|
||||||
|
const response = received[0];
|
||||||
|
expect(response.data.length).toEqual(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets fields.config.interval based on period', async () => {
|
||||||
|
const { runner, timeRange } = setupMockedMetricsQueryRunner({
|
||||||
|
data: {
|
||||||
|
results: {
|
||||||
|
a: {
|
||||||
|
refId: 'a',
|
||||||
|
series: [{ target: 'cpu', datapoints: [[1, 2]], meta: { custom: { period: 60 } } }],
|
||||||
|
},
|
||||||
|
b: {
|
||||||
|
refId: 'b',
|
||||||
|
series: [{ target: 'cpu', datapoints: [[1, 2]], meta: { custom: { period: 120 } } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const observable = runner.performTimeSeriesQuery(
|
||||||
|
{
|
||||||
|
queries: [{ datasourceId: 1, refId: 'a' }],
|
||||||
|
from: '',
|
||||||
|
to: '',
|
||||||
|
},
|
||||||
|
timeRange
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(observable).toEmitValuesWith((received) => {
|
||||||
|
const response = received[0];
|
||||||
|
expect(response.data[0].fields[0].config.interval).toEqual(60000);
|
||||||
|
expect(response.data[1].fields[0].config.interval).toEqual(120000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When performing CloudWatch metrics query', () => {
|
||||||
|
const queries: CloudWatchMetricsQuery[] = [
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
metricQueryType: MetricQueryType.Search,
|
||||||
|
metricEditorMode: MetricEditorMode.Builder,
|
||||||
|
queryMode: 'Metrics',
|
||||||
|
expression: '',
|
||||||
|
refId: 'A',
|
||||||
|
region: 'us-east-1',
|
||||||
|
namespace: 'AWS/EC2',
|
||||||
|
metricName: 'CPUUtilization',
|
||||||
|
dimensions: {
|
||||||
|
InstanceId: 'i-12345678',
|
||||||
|
},
|
||||||
|
statistic: 'Average',
|
||||||
|
period: '300',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const data: BackendDataSourceResponse = {
|
||||||
|
results: {
|
||||||
|
A: {
|
||||||
|
tables: [],
|
||||||
|
error: '',
|
||||||
|
refId: 'A',
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
target: 'CPUUtilization_Average',
|
||||||
|
datapoints: [
|
||||||
|
[1, 1483228800000],
|
||||||
|
[2, 1483229100000],
|
||||||
|
[5, 1483229700000],
|
||||||
|
],
|
||||||
|
tags: {
|
||||||
|
InstanceId: 'i-12345678',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should generate the correct query', async () => {
|
||||||
|
const { runner, fetchMock, request } = setupMockedMetricsQueryRunner({ data });
|
||||||
|
|
||||||
|
await expect(runner.handleMetricQueries(queries, request)).toEmitValuesWith(() => {
|
||||||
|
expect(fetchMock.mock.calls[0][0].data.queries).toMatchObject(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
namespace: queries[0].namespace,
|
||||||
|
metricName: queries[0].metricName,
|
||||||
|
dimensions: { InstanceId: ['i-12345678'] },
|
||||||
|
statistic: queries[0].statistic,
|
||||||
|
period: queries[0].period,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate the correct query with interval variable', async () => {
|
||||||
|
const queries: CloudWatchMetricsQuery[] = [
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
metricQueryType: MetricQueryType.Search,
|
||||||
|
metricEditorMode: MetricEditorMode.Builder,
|
||||||
|
queryMode: 'Metrics',
|
||||||
|
refId: 'A',
|
||||||
|
region: 'us-east-1',
|
||||||
|
namespace: 'AWS/EC2',
|
||||||
|
metricName: 'CPUUtilization',
|
||||||
|
dimensions: {
|
||||||
|
InstanceId: 'i-12345678',
|
||||||
|
},
|
||||||
|
statistic: 'Average',
|
||||||
|
period: '[[period]]',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const { runner, fetchMock, request } = setupMockedMetricsQueryRunner({
|
||||||
|
data,
|
||||||
|
variables: [periodIntervalVariable],
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(runner.handleMetricQueries(queries, request)).toEmitValuesWith(() => {
|
||||||
|
expect(fetchMock.mock.calls[0][0].data.queries[0].period).toEqual('600');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return series list', async () => {
|
||||||
|
const { runner, request } = setupMockedMetricsQueryRunner({ data });
|
||||||
|
|
||||||
|
await expect(runner.handleMetricQueries(queries, request)).toEmitValuesWith((received) => {
|
||||||
|
const result = received[0];
|
||||||
|
expect(getFrameDisplayName(result.data[0])).toBe(
|
||||||
|
data.results.A.series?.length && data.results.A.series[0].target
|
||||||
|
);
|
||||||
|
expect(result.data[0].fields[1].values.buffer[0]).toBe(
|
||||||
|
data.results.A.series?.length && data.results.A.series[0].datapoints[0][0]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and throttling exception is thrown', () => {
|
||||||
|
const partialQuery: CloudWatchMetricsQuery = {
|
||||||
|
metricQueryType: MetricQueryType.Search,
|
||||||
|
metricEditorMode: MetricEditorMode.Builder,
|
||||||
|
queryMode: 'Metrics',
|
||||||
|
namespace: 'AWS/EC2',
|
||||||
|
metricName: 'CPUUtilization',
|
||||||
|
dimensions: {
|
||||||
|
InstanceId: 'i-12345678',
|
||||||
|
},
|
||||||
|
statistic: 'Average',
|
||||||
|
period: '300',
|
||||||
|
expression: '',
|
||||||
|
id: '',
|
||||||
|
region: '',
|
||||||
|
refId: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const queries: CloudWatchMetricsQuery[] = [
|
||||||
|
{ ...partialQuery, refId: 'A', region: 'us-east-1' },
|
||||||
|
{ ...partialQuery, refId: 'B', region: 'us-east-2' },
|
||||||
|
{ ...partialQuery, refId: 'C', region: 'us-east-1' },
|
||||||
|
{ ...partialQuery, refId: 'D', region: 'us-east-2' },
|
||||||
|
{ ...partialQuery, refId: 'E', region: 'eu-north-1' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const backendErrorResponse: DataQueryError<CloudWatchMetricsQuery> = {
|
||||||
|
data: {
|
||||||
|
message: 'Throttling: exception',
|
||||||
|
results: {
|
||||||
|
A: {
|
||||||
|
frames: [],
|
||||||
|
series: [],
|
||||||
|
tables: [],
|
||||||
|
error: 'Throttling: exception',
|
||||||
|
refId: 'A',
|
||||||
|
meta: {},
|
||||||
|
},
|
||||||
|
B: {
|
||||||
|
frames: [],
|
||||||
|
series: [],
|
||||||
|
tables: [],
|
||||||
|
error: 'Throttling: exception',
|
||||||
|
refId: 'B',
|
||||||
|
meta: {},
|
||||||
|
},
|
||||||
|
C: {
|
||||||
|
frames: [],
|
||||||
|
series: [],
|
||||||
|
tables: [],
|
||||||
|
error: 'Throttling: exception',
|
||||||
|
refId: 'C',
|
||||||
|
meta: {},
|
||||||
|
},
|
||||||
|
D: {
|
||||||
|
frames: [],
|
||||||
|
series: [],
|
||||||
|
tables: [],
|
||||||
|
error: 'Throttling: exception',
|
||||||
|
refId: 'D',
|
||||||
|
meta: {},
|
||||||
|
},
|
||||||
|
E: {
|
||||||
|
frames: [],
|
||||||
|
series: [],
|
||||||
|
tables: [],
|
||||||
|
error: 'Throttling: exception',
|
||||||
|
refId: 'E',
|
||||||
|
meta: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
redux.setStore({
|
||||||
|
...redux.store,
|
||||||
|
dispatch: jest.fn(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display one alert error message per region+datasource combination', async () => {
|
||||||
|
const { runner, request } = setupMockedMetricsQueryRunner({ data: backendErrorResponse, throws: true });
|
||||||
|
const memoizedDebounceSpy = jest.spyOn(runner, 'debouncedAlert');
|
||||||
|
|
||||||
|
await expect(runner.handleMetricQueries(queries, request)).toEmitValuesWith(() => {
|
||||||
|
expect(memoizedDebounceSpy).toHaveBeenCalledWith('CloudWatch Test Datasource', 'us-east-1');
|
||||||
|
expect(memoizedDebounceSpy).toHaveBeenCalledWith('CloudWatch Test Datasource', 'us-east-2');
|
||||||
|
expect(memoizedDebounceSpy).toHaveBeenCalledWith('CloudWatch Test Datasource', 'eu-north-1');
|
||||||
|
expect(memoizedDebounceSpy).toBeCalledTimes(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleMetricQueries ', () => {
|
||||||
|
const queries: CloudWatchMetricsQuery[] = [
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
metricQueryType: MetricQueryType.Search,
|
||||||
|
metricEditorMode: MetricEditorMode.Builder,
|
||||||
|
queryMode: 'Metrics',
|
||||||
|
refId: 'A',
|
||||||
|
region: 'us-east-1',
|
||||||
|
namespace: 'AWS/ApplicationELB',
|
||||||
|
metricName: 'TargetResponseTime',
|
||||||
|
dimensions: {
|
||||||
|
LoadBalancer: 'lb',
|
||||||
|
TargetGroup: 'tg',
|
||||||
|
},
|
||||||
|
statistic: 'p90.00',
|
||||||
|
period: '300s',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const data: BackendDataSourceResponse = {
|
||||||
|
results: {
|
||||||
|
A: {
|
||||||
|
tables: [],
|
||||||
|
error: '',
|
||||||
|
refId: 'A',
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
target: 'TargetResponseTime_p90.00',
|
||||||
|
datapoints: [
|
||||||
|
[1, 1483228800000],
|
||||||
|
[2, 1483229100000],
|
||||||
|
[5, 1483229700000],
|
||||||
|
],
|
||||||
|
tags: {
|
||||||
|
LoadBalancer: 'lb',
|
||||||
|
TargetGroup: 'tg',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should return series list', async () => {
|
||||||
|
const { runner, request } = setupMockedMetricsQueryRunner({ data });
|
||||||
|
|
||||||
|
await expect(runner.handleMetricQueries(queries, request)).toEmitValuesWith((received) => {
|
||||||
|
const result = received[0];
|
||||||
|
expect(getFrameDisplayName(result.data[0])).toBe(
|
||||||
|
data.results.A.series?.length && data.results.A.series[0].target
|
||||||
|
);
|
||||||
|
expect(result.data[0].fields[1].values.buffer[0]).toBe(
|
||||||
|
data.results.A.series?.length && data.results.A.series[0].datapoints[0][0]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('template variable interpolation', () => {
|
||||||
|
it('interpolates variables correctly', async () => {
|
||||||
|
const { runner, fetchMock, request } = setupMockedMetricsQueryRunner({
|
||||||
|
variables: [namespaceVariable, metricVariable, labelsVariable, limitVariable],
|
||||||
|
});
|
||||||
|
runner.handleMetricQueries(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
refId: 'a',
|
||||||
|
region: 'us-east-2',
|
||||||
|
namespace: '',
|
||||||
|
period: '',
|
||||||
|
alias: '',
|
||||||
|
metricName: '',
|
||||||
|
dimensions: {},
|
||||||
|
matchExact: true,
|
||||||
|
statistic: '',
|
||||||
|
expression: '',
|
||||||
|
metricQueryType: MetricQueryType.Query,
|
||||||
|
metricEditorMode: MetricEditorMode.Code,
|
||||||
|
sqlExpression: 'SELECT SUM($metric) FROM "$namespace" GROUP BY ${labels:raw} LIMIT $limit',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
request
|
||||||
|
);
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: expect.objectContaining({
|
||||||
|
queries: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
sqlExpression: `SELECT SUM(CPUUtilization) FROM "AWS/EC2" GROUP BY InstanceId,InstanceType LIMIT 100`,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
describe('When performing CloudWatch query with template variables', () => {
|
||||||
|
const key = 'key';
|
||||||
|
const var1: CustomVariableModel = {
|
||||||
|
...initialVariableModelState,
|
||||||
|
id: 'var1',
|
||||||
|
rootStateKey: key,
|
||||||
|
name: 'var1',
|
||||||
|
index: 0,
|
||||||
|
current: { value: 'var1-foo', text: 'var1-foo', selected: true },
|
||||||
|
options: [{ value: 'var1-foo', text: 'var1-foo', selected: true }],
|
||||||
|
multi: false,
|
||||||
|
includeAll: false,
|
||||||
|
query: '',
|
||||||
|
hide: VariableHide.dontHide,
|
||||||
|
type: 'custom',
|
||||||
|
};
|
||||||
|
const var2: CustomVariableModel = {
|
||||||
|
...initialVariableModelState,
|
||||||
|
id: 'var2',
|
||||||
|
rootStateKey: key,
|
||||||
|
name: 'var2',
|
||||||
|
index: 1,
|
||||||
|
current: { value: 'var2-foo', text: 'var2-foo', selected: true },
|
||||||
|
options: [{ value: 'var2-foo', text: 'var2-foo', selected: true }],
|
||||||
|
multi: false,
|
||||||
|
includeAll: false,
|
||||||
|
query: '',
|
||||||
|
hide: VariableHide.dontHide,
|
||||||
|
type: 'custom',
|
||||||
|
};
|
||||||
|
const var3: CustomVariableModel = {
|
||||||
|
...initialVariableModelState,
|
||||||
|
id: 'var3',
|
||||||
|
rootStateKey: key,
|
||||||
|
name: 'var3',
|
||||||
|
index: 2,
|
||||||
|
current: { value: ['var3-foo', 'var3-baz'], text: 'var3-foo + var3-baz', selected: true },
|
||||||
|
options: [
|
||||||
|
{ selected: true, value: 'var3-foo', text: 'var3-foo' },
|
||||||
|
{ selected: false, value: 'var3-bar', text: 'var3-bar' },
|
||||||
|
{ selected: true, value: 'var3-baz', text: 'var3-baz' },
|
||||||
|
],
|
||||||
|
multi: true,
|
||||||
|
includeAll: false,
|
||||||
|
query: '',
|
||||||
|
hide: VariableHide.dontHide,
|
||||||
|
type: 'custom',
|
||||||
|
};
|
||||||
|
const var4: CustomVariableModel = {
|
||||||
|
...initialVariableModelState,
|
||||||
|
id: 'var4',
|
||||||
|
rootStateKey: key,
|
||||||
|
name: 'var4',
|
||||||
|
index: 3,
|
||||||
|
options: [
|
||||||
|
{ selected: true, value: 'var4-foo', text: 'var4-foo' },
|
||||||
|
{ selected: false, value: 'var4-bar', text: 'var4-bar' },
|
||||||
|
{ selected: true, value: 'var4-baz', text: 'var4-baz' },
|
||||||
|
],
|
||||||
|
current: { value: ['var4-foo', 'var4-baz'], text: 'var4-foo + var4-baz', selected: true },
|
||||||
|
multi: true,
|
||||||
|
includeAll: false,
|
||||||
|
query: '',
|
||||||
|
hide: VariableHide.dontHide,
|
||||||
|
type: 'custom',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should generate the correct query for single template variable', async () => {
|
||||||
|
const { runner, fetchMock, request } = setupMockedMetricsQueryRunner({ variables: [var1, var2, var3, var4] });
|
||||||
|
const queries: CloudWatchMetricsQuery[] = [
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
metricQueryType: MetricQueryType.Search,
|
||||||
|
metricEditorMode: MetricEditorMode.Builder,
|
||||||
|
queryMode: 'Metrics',
|
||||||
|
refId: 'A',
|
||||||
|
region: 'us-east-1',
|
||||||
|
namespace: 'TestNamespace',
|
||||||
|
metricName: 'TestMetricName',
|
||||||
|
dimensions: {
|
||||||
|
dim2: '$var2',
|
||||||
|
},
|
||||||
|
statistic: 'Average',
|
||||||
|
period: '300s',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
await expect(runner.handleMetricQueries(queries, request)).toEmitValuesWith(() => {
|
||||||
|
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate the correct query in the case of one multilple template variables', async () => {
|
||||||
|
const { runner, fetchMock, request } = setupMockedMetricsQueryRunner({ variables: [var1, var2, var3, var4] });
|
||||||
|
const queries: CloudWatchMetricsQuery[] = [
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
metricQueryType: MetricQueryType.Search,
|
||||||
|
metricEditorMode: MetricEditorMode.Builder,
|
||||||
|
queryMode: 'Metrics',
|
||||||
|
refId: 'A',
|
||||||
|
region: 'us-east-1',
|
||||||
|
namespace: 'TestNamespace',
|
||||||
|
metricName: 'TestMetricName',
|
||||||
|
dimensions: {
|
||||||
|
dim1: '$var1',
|
||||||
|
dim2: '$var2',
|
||||||
|
dim3: '$var3',
|
||||||
|
},
|
||||||
|
statistic: 'Average',
|
||||||
|
period: '300s',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
runner.handleMetricQueries(queries, {
|
||||||
|
...request,
|
||||||
|
scopedVars: {
|
||||||
|
var1: { selected: true, value: 'var1-foo', text: '' },
|
||||||
|
var2: { selected: true, value: 'var2-foo', text: '' },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toEmitValuesWith(() => {
|
||||||
|
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']);
|
||||||
|
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']);
|
||||||
|
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate the correct query in the case of multilple multi template variables', async () => {
|
||||||
|
const { runner, fetchMock, request } = setupMockedMetricsQueryRunner({ variables: [var1, var2, var3, var4] });
|
||||||
|
const queries: CloudWatchMetricsQuery[] = [
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
metricQueryType: MetricQueryType.Search,
|
||||||
|
metricEditorMode: MetricEditorMode.Builder,
|
||||||
|
queryMode: 'Metrics',
|
||||||
|
refId: 'A',
|
||||||
|
region: 'us-east-1',
|
||||||
|
namespace: 'TestNamespace',
|
||||||
|
metricName: 'TestMetricName',
|
||||||
|
dimensions: {
|
||||||
|
dim1: '$var1',
|
||||||
|
dim3: '$var3',
|
||||||
|
dim4: '$var4',
|
||||||
|
},
|
||||||
|
statistic: 'Average',
|
||||||
|
period: '300s',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await expect(runner.handleMetricQueries(queries, request)).toEmitValuesWith(() => {
|
||||||
|
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']);
|
||||||
|
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']);
|
||||||
|
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim4']).toStrictEqual(['var4-foo', 'var4-baz']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate the correct query for multilple template variables, lack scopedVars', async () => {
|
||||||
|
const { runner, fetchMock, request } = setupMockedMetricsQueryRunner({ variables: [var1, var2, var3, var4] });
|
||||||
|
const queries: CloudWatchMetricsQuery[] = [
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
metricQueryType: MetricQueryType.Search,
|
||||||
|
metricEditorMode: MetricEditorMode.Builder,
|
||||||
|
queryMode: 'Metrics',
|
||||||
|
refId: 'A',
|
||||||
|
region: 'us-east-1',
|
||||||
|
namespace: 'TestNamespace',
|
||||||
|
metricName: 'TestMetricName',
|
||||||
|
dimensions: {
|
||||||
|
dim1: '$var1',
|
||||||
|
dim2: '$var2',
|
||||||
|
dim3: '$var3',
|
||||||
|
},
|
||||||
|
statistic: 'Average',
|
||||||
|
period: '300',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
runner.handleMetricQueries(queries, {
|
||||||
|
...request,
|
||||||
|
scopedVars: {
|
||||||
|
var1: { selected: true, value: 'var1-foo', text: '' },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toEmitValuesWith(() => {
|
||||||
|
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']);
|
||||||
|
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']);
|
||||||
|
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('timezoneUTCOffset', () => {
|
||||||
|
const testQuery = {
|
||||||
|
id: '',
|
||||||
|
refId: 'a',
|
||||||
|
region: 'us-east-2',
|
||||||
|
namespace: '',
|
||||||
|
period: '',
|
||||||
|
label: '${MAX_TIME_RELATIVE}',
|
||||||
|
metricName: '',
|
||||||
|
dimensions: {},
|
||||||
|
matchExact: true,
|
||||||
|
statistic: '',
|
||||||
|
expression: '',
|
||||||
|
metricQueryType: MetricQueryType.Query,
|
||||||
|
metricEditorMode: MetricEditorMode.Code,
|
||||||
|
sqlExpression: 'SELECT SUM($metric) FROM "$namespace" GROUP BY ${labels:raw} LIMIT $limit',
|
||||||
|
};
|
||||||
|
const testTable = [
|
||||||
|
['Europe/Stockholm', '+0200'],
|
||||||
|
['America/New_York', '-0400'],
|
||||||
|
['Asia/Tokyo', '+0900'],
|
||||||
|
['UTC', '+0000'],
|
||||||
|
];
|
||||||
|
describe.each(testTable)('should use the right time zone offset', (ianaTimezone, expectedOffset) => {
|
||||||
|
const { runner, fetchMock, request } = setupMockedMetricsQueryRunner();
|
||||||
|
runner.handleMetricQueries([testQuery], {
|
||||||
|
...request,
|
||||||
|
range: { ...request.range, from: dateTime(), to: dateTime() },
|
||||||
|
timezone: ianaTimezone,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: expect.objectContaining({
|
||||||
|
queries: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
timezoneUTCOffset: expectedOffset,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('debouncedCustomAlert', () => {
|
||||||
|
const debouncedAlert = jest.fn();
|
||||||
|
beforeEach(() => {
|
||||||
|
const { runner, request } = setupMockedMetricsQueryRunner({
|
||||||
|
variables: [
|
||||||
|
{ ...namespaceVariable, multi: true },
|
||||||
|
{ ...metricVariable, multi: true },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
runner.debouncedCustomAlert = debouncedAlert;
|
||||||
|
runner.performTimeSeriesQuery = jest.fn().mockResolvedValue([]);
|
||||||
|
runner.handleMetricQueries(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
queryMode: 'Metrics',
|
||||||
|
id: '',
|
||||||
|
region: 'us-east-2',
|
||||||
|
namespace: namespaceVariable.id,
|
||||||
|
metricName: metricVariable.id,
|
||||||
|
period: '',
|
||||||
|
alias: '',
|
||||||
|
dimensions: {},
|
||||||
|
matchExact: true,
|
||||||
|
statistic: '',
|
||||||
|
refId: '',
|
||||||
|
expression: 'x * 2',
|
||||||
|
metricQueryType: MetricQueryType.Search,
|
||||||
|
metricEditorMode: MetricEditorMode.Code,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
request
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('should show debounced alert for namespace and metric name', async () => {
|
||||||
|
expect(debouncedAlert).toHaveBeenCalledWith(
|
||||||
|
'CloudWatch templating error',
|
||||||
|
'Multi template variables are not supported for namespace'
|
||||||
|
);
|
||||||
|
expect(debouncedAlert).toHaveBeenCalledWith(
|
||||||
|
'CloudWatch templating error',
|
||||||
|
'Multi template variables are not supported for metric name'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show debounced alert for region', async () => {
|
||||||
|
expect(debouncedAlert).not.toHaveBeenCalledWith(
|
||||||
|
'CloudWatch templating error',
|
||||||
|
'Multi template variables are not supported for region'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('interpolateMetricsQueryVariables', () => {
|
||||||
|
it('interpolates dimensions correctly', () => {
|
||||||
|
const testQuery = {
|
||||||
|
id: 'a',
|
||||||
|
refId: 'a',
|
||||||
|
region: 'us-east-2',
|
||||||
|
namespace: '',
|
||||||
|
dimensions: { InstanceId: '$dimension' },
|
||||||
|
};
|
||||||
|
const { runner } = setupMockedMetricsQueryRunner({ variables: [dimensionVariable], mockGetVariableName: false });
|
||||||
|
const result = runner.interpolateMetricsQueryVariables(testQuery, {
|
||||||
|
dimension: { text: 'foo', value: 'foo' },
|
||||||
|
});
|
||||||
|
expect(result).toStrictEqual({
|
||||||
|
alias: '',
|
||||||
|
metricName: '',
|
||||||
|
namespace: '',
|
||||||
|
period: '',
|
||||||
|
sqlExpression: '',
|
||||||
|
dimensions: { InstanceId: ['foo'] },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('convertMultiFiltersFormat', () => {
|
||||||
|
const { runner } = setupMockedMetricsQueryRunner({
|
||||||
|
variables: [labelsVariable, dimensionVariable],
|
||||||
|
mockGetVariableName: false,
|
||||||
|
});
|
||||||
|
it('converts keys and values correctly', () => {
|
||||||
|
const filters = { $dimension: ['b'], a: ['$labels', 'bar'] };
|
||||||
|
const result = runner.convertMultiFilterFormat(filters);
|
||||||
|
expect(result).toStrictEqual({
|
||||||
|
env: ['b'],
|
||||||
|
a: ['InstanceId', 'InstanceType', 'bar'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filterMetricsQuery', () => {
|
||||||
|
const runner = setupMockedMetricsQueryRunner().runner;
|
||||||
|
let baseQuery: CloudWatchMetricsQuery;
|
||||||
|
beforeEach(() => {
|
||||||
|
baseQuery = {
|
||||||
|
id: '',
|
||||||
|
region: 'us-east-2',
|
||||||
|
namespace: '',
|
||||||
|
period: '',
|
||||||
|
alias: '',
|
||||||
|
metricName: '',
|
||||||
|
dimensions: {},
|
||||||
|
matchExact: true,
|
||||||
|
statistic: '',
|
||||||
|
expression: '',
|
||||||
|
refId: '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error if invalid mode', async () => {
|
||||||
|
expect(() => runner.filterMetricQuery(baseQuery)).toThrowError('invalid metric editor mode');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('metric search queries', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
baseQuery = {
|
||||||
|
...baseQuery,
|
||||||
|
namespace: 'AWS/EC2',
|
||||||
|
metricName: 'CPUUtilization',
|
||||||
|
statistic: 'Average',
|
||||||
|
metricQueryType: MetricQueryType.Search,
|
||||||
|
metricEditorMode: MetricEditorMode.Builder,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow builder queries that dont have namespace, metric or statistic', async () => {
|
||||||
|
expect(runner.filterMetricQuery({ ...baseQuery, statistic: undefined })).toBeFalsy();
|
||||||
|
expect(runner.filterMetricQuery({ ...baseQuery, metricName: undefined })).toBeFalsy();
|
||||||
|
expect(runner.filterMetricQuery({ ...baseQuery, namespace: '' })).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow builder queries that have namespace, metric or statistic', async () => {
|
||||||
|
expect(runner.filterMetricQuery(baseQuery)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow code queries that dont have an expression', async () => {
|
||||||
|
expect(
|
||||||
|
runner.filterMetricQuery({
|
||||||
|
...baseQuery,
|
||||||
|
expression: undefined,
|
||||||
|
metricEditorMode: MetricEditorMode.Code,
|
||||||
|
})
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow code queries that have an expression', async () => {
|
||||||
|
expect(
|
||||||
|
runner.filterMetricQuery({ ...baseQuery, expression: 'x * 2', metricEditorMode: MetricEditorMode.Code })
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('metric search expression queries', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
baseQuery = {
|
||||||
|
...baseQuery,
|
||||||
|
metricQueryType: MetricQueryType.Search,
|
||||||
|
metricEditorMode: MetricEditorMode.Code,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow queries that dont have an expression', async () => {
|
||||||
|
const valid = runner.filterMetricQuery(baseQuery);
|
||||||
|
expect(valid).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow queries that have an expression', async () => {
|
||||||
|
baseQuery.expression = 'SUM([a,x])';
|
||||||
|
const valid = runner.filterMetricQuery(baseQuery);
|
||||||
|
expect(valid).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('metric query queries', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
baseQuery = {
|
||||||
|
...baseQuery,
|
||||||
|
metricQueryType: MetricQueryType.Query,
|
||||||
|
metricEditorMode: MetricEditorMode.Code,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow queries that dont have a sql expresssion', async () => {
|
||||||
|
const valid = runner.filterMetricQuery(baseQuery);
|
||||||
|
expect(valid).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow queries that have a sql expresssion', async () => {
|
||||||
|
baseQuery.sqlExpression = 'select SUM(CPUUtilization) from "AWS/EC2"';
|
||||||
|
const valid = runner.filterMetricQuery(baseQuery);
|
||||||
|
expect(valid).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When query region is "default"', () => {
|
||||||
|
it('should return the datasource region if empty or "default"', () => {
|
||||||
|
const { runner, instanceSettings } = setupMockedMetricsQueryRunner();
|
||||||
|
const defaultRegion = instanceSettings.jsonData.defaultRegion;
|
||||||
|
|
||||||
|
expect(runner.getActualRegion()).toBe(defaultRegion);
|
||||||
|
expect(runner.getActualRegion('')).toBe(defaultRegion);
|
||||||
|
expect(runner.getActualRegion('default')).toBe(defaultRegion);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the specified region if specified', () => {
|
||||||
|
const { runner } = setupMockedMetricsQueryRunner();
|
||||||
|
|
||||||
|
expect(runner.getActualRegion('some-fake-region-1')).toBe('some-fake-region-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should query for the datasource region if empty or "default"', async () => {
|
||||||
|
const { runner, instanceSettings, request } = setupMockedMetricsQueryRunner();
|
||||||
|
const performTimeSeriesQueryMock = jest
|
||||||
|
.spyOn(runner, 'performTimeSeriesQuery')
|
||||||
|
.mockReturnValue(of({ data: [], error: undefined }));
|
||||||
|
|
||||||
|
const queries: CloudWatchMetricsQuery[] = [
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
metricQueryType: MetricQueryType.Search,
|
||||||
|
metricEditorMode: MetricEditorMode.Builder,
|
||||||
|
queryMode: 'Metrics',
|
||||||
|
refId: 'A',
|
||||||
|
region: 'default',
|
||||||
|
namespace: 'AWS/EC2',
|
||||||
|
metricName: 'CPUUtilization',
|
||||||
|
dimensions: {
|
||||||
|
InstanceId: 'i-12345678',
|
||||||
|
},
|
||||||
|
statistic: 'Average',
|
||||||
|
period: '300s',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await expect(runner.handleMetricQueries(queries, request)).toEmitValuesWith(() => {
|
||||||
|
expect(performTimeSeriesQueryMock.mock.calls[0][0].queries[0].region).toBe(
|
||||||
|
instanceSettings.jsonData.defaultRegion
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,239 @@
|
|||||||
|
import { findLast, isEmpty } from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
|
import { catchError, map, Observable, of, throwError } from 'rxjs';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DataFrame,
|
||||||
|
DataQueryRequest,
|
||||||
|
DataQueryResponse,
|
||||||
|
DataSourceInstanceSettings,
|
||||||
|
dateTimeFormat,
|
||||||
|
FieldType,
|
||||||
|
rangeUtil,
|
||||||
|
ScopedVars,
|
||||||
|
TimeRange,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { toDataQueryResponse } from '@grafana/runtime';
|
||||||
|
import { notifyApp } from 'app/core/actions';
|
||||||
|
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||||
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
import { store } from 'app/store/store';
|
||||||
|
import { AppNotificationTimeout } from 'app/types';
|
||||||
|
|
||||||
|
import { ThrottlingErrorMessage } from '../components/ThrottlingErrorMessage';
|
||||||
|
import memoizedDebounce from '../memoizedDebounce';
|
||||||
|
import { migrateMetricQuery } from '../migrations/metricQueryMigrations';
|
||||||
|
import {
|
||||||
|
CloudWatchJsonData,
|
||||||
|
CloudWatchMetricsQuery,
|
||||||
|
CloudWatchQuery,
|
||||||
|
DataQueryError,
|
||||||
|
MetricEditorMode,
|
||||||
|
MetricQuery,
|
||||||
|
MetricQueryType,
|
||||||
|
MetricRequest,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
import { CloudWatchQueryRunner } from './CloudWatchQueryRunner';
|
||||||
|
|
||||||
|
const displayAlert = (datasourceName: string, region: string) =>
|
||||||
|
store.dispatch(
|
||||||
|
notifyApp(
|
||||||
|
createErrorNotification(
|
||||||
|
`CloudWatch request limit reached in ${region} for data source ${datasourceName}`,
|
||||||
|
'',
|
||||||
|
undefined,
|
||||||
|
React.createElement(ThrottlingErrorMessage, { region }, null)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// This class handles execution of CloudWatch metrics query data queries
|
||||||
|
export class CloudWatchMetricsQueryRunner extends CloudWatchQueryRunner {
|
||||||
|
debouncedAlert: (datasourceName: string, region: string) => void = memoizedDebounce(
|
||||||
|
displayAlert,
|
||||||
|
AppNotificationTimeout.Error
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor(instanceSettings: DataSourceInstanceSettings<CloudWatchJsonData>, templateSrv: TemplateSrv) {
|
||||||
|
super(instanceSettings, templateSrv);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMetricQueries = (
|
||||||
|
metricQueries: CloudWatchMetricsQuery[],
|
||||||
|
options: DataQueryRequest<CloudWatchQuery>
|
||||||
|
): Observable<DataQueryResponse> => {
|
||||||
|
const timezoneUTCOffset = dateTimeFormat(Date.now(), {
|
||||||
|
timeZone: options.timezone,
|
||||||
|
format: 'Z',
|
||||||
|
}).replace(':', '');
|
||||||
|
|
||||||
|
const validMetricsQueries = metricQueries
|
||||||
|
.filter(this.filterMetricQuery)
|
||||||
|
.map((q: CloudWatchMetricsQuery): MetricQuery => {
|
||||||
|
const migratedQuery = migrateMetricQuery(q);
|
||||||
|
const migratedAndIterpolatedQuery = this.replaceMetricQueryVars(migratedQuery, options);
|
||||||
|
|
||||||
|
return {
|
||||||
|
timezoneUTCOffset,
|
||||||
|
intervalMs: options.intervalMs,
|
||||||
|
maxDataPoints: options.maxDataPoints,
|
||||||
|
...migratedAndIterpolatedQuery,
|
||||||
|
type: 'timeSeriesQuery',
|
||||||
|
datasource: this.ref,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// No valid targets, return the empty result to save a round trip.
|
||||||
|
if (isEmpty(validMetricsQueries)) {
|
||||||
|
return of({ data: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
from: options?.range?.from.valueOf().toString(),
|
||||||
|
to: options?.range?.to.valueOf().toString(),
|
||||||
|
queries: validMetricsQueries,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.performTimeSeriesQuery(request, options.range);
|
||||||
|
};
|
||||||
|
|
||||||
|
interpolateMetricsQueryVariables(
|
||||||
|
query: CloudWatchMetricsQuery,
|
||||||
|
scopedVars: ScopedVars
|
||||||
|
): Pick<CloudWatchMetricsQuery, 'alias' | 'metricName' | 'namespace' | 'period' | 'dimensions' | 'sqlExpression'> {
|
||||||
|
return {
|
||||||
|
alias: this.replaceVariableAndDisplayWarningIfMulti(query.alias, scopedVars),
|
||||||
|
metricName: this.replaceVariableAndDisplayWarningIfMulti(query.metricName, scopedVars),
|
||||||
|
namespace: this.replaceVariableAndDisplayWarningIfMulti(query.namespace, scopedVars),
|
||||||
|
period: this.replaceVariableAndDisplayWarningIfMulti(query.period, scopedVars),
|
||||||
|
sqlExpression: this.replaceVariableAndDisplayWarningIfMulti(query.sqlExpression, scopedVars),
|
||||||
|
dimensions: this.convertDimensionFormat(query.dimensions ?? {}, scopedVars),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
performTimeSeriesQuery(request: MetricRequest, { from, to }: TimeRange): Observable<DataQueryResponse> {
|
||||||
|
return this.awsRequest(this.dsQueryEndpoint, request).pipe(
|
||||||
|
map((res) => {
|
||||||
|
const dataframes: DataFrame[] = toDataQueryResponse({ data: res }).data;
|
||||||
|
if (!dataframes || dataframes.length <= 0) {
|
||||||
|
return { data: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastError = findLast(res.results, (v) => !!v.error);
|
||||||
|
|
||||||
|
dataframes.forEach((frame) => {
|
||||||
|
frame.fields.forEach((field) => {
|
||||||
|
if (field.type === FieldType.time) {
|
||||||
|
// field.config.interval is populated in order for Grafana to fill in null values at frame intervals
|
||||||
|
field.config.interval = frame.meta?.custom?.period * 1000;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: dataframes,
|
||||||
|
error: lastError ? { message: lastError.error } : undefined,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
catchError((err: DataQueryError<CloudWatchMetricsQuery>) => {
|
||||||
|
const isFrameError = err.data?.results;
|
||||||
|
|
||||||
|
// Error is not frame specific
|
||||||
|
if (!isFrameError && err.data && err.data.message === 'Metric request error' && err.data.error) {
|
||||||
|
err.message = err.data.error;
|
||||||
|
return throwError(() => err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The error is either for a specific frame or for all the frames
|
||||||
|
const results: Array<{ error?: string }> = Object.values(err.data?.results ?? {});
|
||||||
|
const firstErrorResult = results.find((r) => r.error);
|
||||||
|
if (firstErrorResult) {
|
||||||
|
err.message = firstErrorResult.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.some((r) => r.error && /^Throttling:.*/.test(r.error))) {
|
||||||
|
const failedRedIds = Object.keys(err.data?.results ?? {});
|
||||||
|
const regionsAffected = Object.values(request.queries).reduce(
|
||||||
|
(res: string[], { refId, region }) =>
|
||||||
|
(refId && !failedRedIds.includes(refId)) || res.includes(region) ? res : [...res, region],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
regionsAffected.forEach((region) => {
|
||||||
|
const actualRegion = this.getActualRegion(region);
|
||||||
|
if (actualRegion) {
|
||||||
|
this.debouncedAlert(this.instanceSettings.name, actualRegion);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return throwError(() => err);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
filterMetricQuery(query: CloudWatchMetricsQuery): boolean {
|
||||||
|
const { region, metricQueryType, metricEditorMode, expression, metricName, namespace, sqlExpression, statistic } =
|
||||||
|
query;
|
||||||
|
if (!region) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (metricQueryType === MetricQueryType.Search && metricEditorMode === MetricEditorMode.Builder) {
|
||||||
|
return !!namespace && !!metricName && !!statistic;
|
||||||
|
} else if (metricQueryType === MetricQueryType.Search && metricEditorMode === MetricEditorMode.Code) {
|
||||||
|
return !!expression;
|
||||||
|
} else if (metricQueryType === MetricQueryType.Query) {
|
||||||
|
// still TBD how to validate the visual query builder for SQL
|
||||||
|
return !!sqlExpression;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('invalid metric editor mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceMetricQueryVars(
|
||||||
|
query: CloudWatchMetricsQuery,
|
||||||
|
options: DataQueryRequest<CloudWatchQuery>
|
||||||
|
): CloudWatchMetricsQuery {
|
||||||
|
query.region = this.templateSrv.replace(this.getActualRegion(query.region), options.scopedVars);
|
||||||
|
query.namespace = this.replaceVariableAndDisplayWarningIfMulti(
|
||||||
|
query.namespace,
|
||||||
|
options.scopedVars,
|
||||||
|
true,
|
||||||
|
'namespace'
|
||||||
|
);
|
||||||
|
query.metricName = this.replaceVariableAndDisplayWarningIfMulti(
|
||||||
|
query.metricName,
|
||||||
|
options.scopedVars,
|
||||||
|
true,
|
||||||
|
'metric name'
|
||||||
|
);
|
||||||
|
query.dimensions = this.convertDimensionFormat(query.dimensions ?? {}, options.scopedVars);
|
||||||
|
query.statistic = this.templateSrv.replace(query.statistic, options.scopedVars);
|
||||||
|
query.period = String(this.getPeriod(query, options)); // use string format for period in graph query, and alerting
|
||||||
|
query.id = this.templateSrv.replace(query.id, options.scopedVars);
|
||||||
|
query.expression = this.templateSrv.replace(query.expression, options.scopedVars);
|
||||||
|
query.sqlExpression = this.templateSrv.replace(query.sqlExpression, options.scopedVars, 'raw');
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPeriod(target: CloudWatchMetricsQuery, options: DataQueryRequest<CloudWatchQuery>) {
|
||||||
|
let period = this.templateSrv.replace(target.period, options.scopedVars);
|
||||||
|
if (period && period.toLowerCase() !== 'auto') {
|
||||||
|
let p: number;
|
||||||
|
if (/^\d+$/.test(period)) {
|
||||||
|
p = parseInt(period, 10);
|
||||||
|
} else {
|
||||||
|
p = rangeUtil.intervalToSeconds(period);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p < 1) {
|
||||||
|
p = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
return period;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,122 @@
|
|||||||
|
import { Observable, map } from 'rxjs';
|
||||||
|
|
||||||
|
import { DataSourceInstanceSettings, DataSourceRef, getDataSourceRef, ScopedVars } from '@grafana/data';
|
||||||
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
|
import { notifyApp } from 'app/core/actions';
|
||||||
|
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||||
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
import { store } from 'app/store/store';
|
||||||
|
import { AppNotificationTimeout } from 'app/types';
|
||||||
|
|
||||||
|
import memoizedDebounce from '../memoizedDebounce';
|
||||||
|
import { CloudWatchJsonData, Dimensions, MetricRequest, MultiFilters, TSDBResponse } from '../types';
|
||||||
|
|
||||||
|
export abstract class CloudWatchQueryRunner {
|
||||||
|
templateSrv: TemplateSrv;
|
||||||
|
ref: DataSourceRef;
|
||||||
|
dsQueryEndpoint = '/api/ds/query';
|
||||||
|
debouncedCustomAlert: (title: string, message: string) => void = memoizedDebounce(
|
||||||
|
displayCustomError,
|
||||||
|
AppNotificationTimeout.Error
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor(public instanceSettings: DataSourceInstanceSettings<CloudWatchJsonData>, templateSrv: TemplateSrv) {
|
||||||
|
this.templateSrv = templateSrv;
|
||||||
|
this.ref = getDataSourceRef(instanceSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
awsRequest(url: string, data: MetricRequest, headers: Record<string, string> = {}): Observable<TSDBResponse> {
|
||||||
|
const options = {
|
||||||
|
method: 'POST',
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
return getBackendSrv()
|
||||||
|
.fetch<TSDBResponse>(options)
|
||||||
|
.pipe(map((result) => result.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
convertDimensionFormat(dimensions: Dimensions, scopedVars: ScopedVars): Dimensions {
|
||||||
|
return Object.entries(dimensions).reduce((result, [key, value]) => {
|
||||||
|
key = this.replaceVariableAndDisplayWarningIfMulti(key, scopedVars, true, 'dimension keys');
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return { ...result, [key]: value };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return { ...result, [key]: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const newValues = this.expandVariableToArray(value, scopedVars);
|
||||||
|
return { ...result, [key]: newValues };
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the value for a given template variable
|
||||||
|
expandVariableToArray(value: string, scopedVars: ScopedVars): string[] {
|
||||||
|
const variableName = this.templateSrv.getVariableName(value);
|
||||||
|
const valueVar = this.templateSrv.getVariables().find(({ name }) => {
|
||||||
|
return name === variableName;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (variableName && valueVar) {
|
||||||
|
const isMultiVariable =
|
||||||
|
valueVar?.type === 'custom' || valueVar?.type === 'query' || valueVar?.type === 'datasource';
|
||||||
|
if (isMultiVariable && valueVar.multi) {
|
||||||
|
return this.templateSrv.replace(value, scopedVars, 'pipe').split('|');
|
||||||
|
}
|
||||||
|
return [this.templateSrv.replace(value, scopedVars)];
|
||||||
|
}
|
||||||
|
return [value];
|
||||||
|
}
|
||||||
|
|
||||||
|
convertMultiFilterFormat(multiFilters: MultiFilters, fieldName?: string) {
|
||||||
|
return Object.entries(multiFilters).reduce((result, [key, values]) => {
|
||||||
|
const interpolatedKey = this.replaceVariableAndDisplayWarningIfMulti(key, {}, true, fieldName);
|
||||||
|
if (!values) {
|
||||||
|
return { ...result, [interpolatedKey]: null };
|
||||||
|
}
|
||||||
|
const initialVal: string[] = [];
|
||||||
|
const newValues = values.reduce((result, value) => {
|
||||||
|
const vals = this.expandVariableToArray(value, {});
|
||||||
|
return [...result, ...vals];
|
||||||
|
}, initialVal);
|
||||||
|
return { ...result, [interpolatedKey]: newValues };
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceVariableAndDisplayWarningIfMulti(
|
||||||
|
target?: string,
|
||||||
|
scopedVars?: ScopedVars,
|
||||||
|
displayErrorIfIsMultiTemplateVariable?: boolean,
|
||||||
|
fieldName?: string
|
||||||
|
) {
|
||||||
|
if (displayErrorIfIsMultiTemplateVariable && !!target) {
|
||||||
|
const variables = this.templateSrv.getVariables();
|
||||||
|
const variable = variables.find(({ name }) => name === this.templateSrv.getVariableName(target));
|
||||||
|
const isMultiVariable =
|
||||||
|
variable?.type === 'custom' || variable?.type === 'query' || variable?.type === 'datasource';
|
||||||
|
if (isMultiVariable && variable.multi) {
|
||||||
|
this.debouncedCustomAlert(
|
||||||
|
'CloudWatch templating error',
|
||||||
|
`Multi template variables are not supported for ${fieldName || target}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.templateSrv.replace(target, scopedVars);
|
||||||
|
}
|
||||||
|
|
||||||
|
getActualRegion(region?: string) {
|
||||||
|
if (region === 'default' || region === undefined || region === '') {
|
||||||
|
return this.instanceSettings.jsonData.defaultRegion ?? '';
|
||||||
|
}
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayCustomError = (title: string, message: string) =>
|
||||||
|
store.dispatch(notifyApp(createErrorNotification(title, message)));
|
@ -1,914 +0,0 @@
|
|||||||
import { interval, lastValueFrom, of, throwError } from 'rxjs';
|
|
||||||
import { createFetchResponse } from 'test/helpers/createFetchResponse';
|
|
||||||
import { getTemplateSrvDependencies } from 'test/helpers/getTemplateSrvDependencies';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DataFrame,
|
|
||||||
DataQueryErrorType,
|
|
||||||
DataSourceInstanceSettings,
|
|
||||||
dateMath,
|
|
||||||
getFrameDisplayName,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
|
|
||||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
|
||||||
import * as redux from 'app/store/store';
|
|
||||||
|
|
||||||
import { convertToStoreState } from '../../../../../test/helpers/convertToStoreState';
|
|
||||||
import { CustomVariableModel, initialVariableModelState, VariableHide } from '../../../../features/variables/types';
|
|
||||||
import { CloudWatchDatasource } from '../datasource';
|
|
||||||
import {
|
|
||||||
CloudWatchJsonData,
|
|
||||||
CloudWatchLogsQuery,
|
|
||||||
CloudWatchLogsQueryStatus,
|
|
||||||
CloudWatchMetricsQuery,
|
|
||||||
LogAction,
|
|
||||||
MetricEditorMode,
|
|
||||||
MetricQueryType,
|
|
||||||
} from '../types';
|
|
||||||
import * as rxjsUtils from '../utils/rxjs/increasingInterval';
|
|
||||||
|
|
||||||
jest.mock('@grafana/runtime', () => ({
|
|
||||||
...(jest.requireActual('@grafana/runtime') as unknown as object),
|
|
||||||
getBackendSrv: () => backendSrv,
|
|
||||||
}));
|
|
||||||
|
|
||||||
type Args = { response?: any; throws?: boolean; templateSrv?: TemplateSrv };
|
|
||||||
|
|
||||||
function getTestContext({ response = {}, throws = false, templateSrv = new TemplateSrv() }: Args = {}) {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
|
|
||||||
const fetchMock = jest.spyOn(backendSrv, 'fetch');
|
|
||||||
|
|
||||||
throws
|
|
||||||
? fetchMock.mockImplementation(() => throwError(response))
|
|
||||||
: fetchMock.mockImplementation(() => of(createFetchResponse(response)));
|
|
||||||
|
|
||||||
const instanceSettings = {
|
|
||||||
jsonData: { defaultRegion: 'us-east-1' },
|
|
||||||
name: 'TestDatasource',
|
|
||||||
} as DataSourceInstanceSettings<CloudWatchJsonData>;
|
|
||||||
|
|
||||||
const timeSrv = {
|
|
||||||
time: { from: '2016-12-31 15:00:00Z', to: '2016-12-31 16:00:00Z' },
|
|
||||||
timeRange: () => {
|
|
||||||
return {
|
|
||||||
from: dateMath.parse(timeSrv.time.from, false),
|
|
||||||
to: dateMath.parse(timeSrv.time.to, true),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
} as TimeSrv;
|
|
||||||
|
|
||||||
const ds = new CloudWatchDatasource(instanceSettings, templateSrv, timeSrv);
|
|
||||||
|
|
||||||
return { ds, fetchMock, instanceSettings };
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('CloudWatchDatasource', () => {
|
|
||||||
const start = 1483196400 * 1000;
|
|
||||||
const defaultTimeRange = { from: new Date(start), to: new Date(start + 3600 * 1000) };
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('When getting log groups', () => {
|
|
||||||
it('should return log groups as an array of strings', async () => {
|
|
||||||
const response = {
|
|
||||||
results: {
|
|
||||||
A: {
|
|
||||||
frames: [
|
|
||||||
{
|
|
||||||
schema: {
|
|
||||||
name: 'logGroups',
|
|
||||||
refId: 'A',
|
|
||||||
fields: [{ name: 'logGroupName', type: 'string', typeInfo: { frame: 'string', nullable: true } }],
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
values: [
|
|
||||||
[
|
|
||||||
'/aws/containerinsights/dev303-workshop/application',
|
|
||||||
'/aws/containerinsights/dev303-workshop/dataplane',
|
|
||||||
'/aws/containerinsights/dev303-workshop/flowlogs',
|
|
||||||
'/aws/containerinsights/dev303-workshop/host',
|
|
||||||
'/aws/containerinsights/dev303-workshop/performance',
|
|
||||||
'/aws/containerinsights/dev303-workshop/prometheus',
|
|
||||||
'/aws/containerinsights/ecommerce-sockshop/application',
|
|
||||||
'/aws/containerinsights/ecommerce-sockshop/dataplane',
|
|
||||||
'/aws/containerinsights/ecommerce-sockshop/host',
|
|
||||||
'/aws/containerinsights/ecommerce-sockshop/performance',
|
|
||||||
'/aws/containerinsights/watchdemo-perf/application',
|
|
||||||
'/aws/containerinsights/watchdemo-perf/dataplane',
|
|
||||||
'/aws/containerinsights/watchdemo-perf/host',
|
|
||||||
'/aws/containerinsights/watchdemo-perf/performance',
|
|
||||||
'/aws/containerinsights/watchdemo-perf/prometheus',
|
|
||||||
'/aws/containerinsights/watchdemo-prod-us-east-1/performance',
|
|
||||||
'/aws/containerinsights/watchdemo-staging/application',
|
|
||||||
'/aws/containerinsights/watchdemo-staging/dataplane',
|
|
||||||
'/aws/containerinsights/watchdemo-staging/host',
|
|
||||||
'/aws/containerinsights/watchdemo-staging/performance',
|
|
||||||
'/aws/ecs/containerinsights/bugbash-ec2/performance',
|
|
||||||
'/aws/ecs/containerinsights/ecs-demoworkshop/performance',
|
|
||||||
'/aws/ecs/containerinsights/ecs-workshop-dev/performance',
|
|
||||||
'/aws/eks/dev303-workshop/cluster',
|
|
||||||
'/aws/events/cloudtrail',
|
|
||||||
'/aws/events/ecs',
|
|
||||||
'/aws/lambda/cwsyn-mycanary-fac97ded-f134-499a-9d71-4c3be1f63182',
|
|
||||||
'/aws/lambda/cwsyn-watch-linkchecks-ef7ef273-5da2-4663-af54-d2f52d55b060',
|
|
||||||
'/ecs/ecs-cwagent-daemon-service',
|
|
||||||
'/ecs/ecs-demo-limitTask',
|
|
||||||
'CloudTrail/DefaultLogGroup',
|
|
||||||
'container-insights-prometheus-beta',
|
|
||||||
'container-insights-prometheus-demo',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const { ds } = getTestContext({ response });
|
|
||||||
const expectedLogGroups = [
|
|
||||||
'/aws/containerinsights/dev303-workshop/application',
|
|
||||||
'/aws/containerinsights/dev303-workshop/dataplane',
|
|
||||||
'/aws/containerinsights/dev303-workshop/flowlogs',
|
|
||||||
'/aws/containerinsights/dev303-workshop/host',
|
|
||||||
'/aws/containerinsights/dev303-workshop/performance',
|
|
||||||
'/aws/containerinsights/dev303-workshop/prometheus',
|
|
||||||
'/aws/containerinsights/ecommerce-sockshop/application',
|
|
||||||
'/aws/containerinsights/ecommerce-sockshop/dataplane',
|
|
||||||
'/aws/containerinsights/ecommerce-sockshop/host',
|
|
||||||
'/aws/containerinsights/ecommerce-sockshop/performance',
|
|
||||||
'/aws/containerinsights/watchdemo-perf/application',
|
|
||||||
'/aws/containerinsights/watchdemo-perf/dataplane',
|
|
||||||
'/aws/containerinsights/watchdemo-perf/host',
|
|
||||||
'/aws/containerinsights/watchdemo-perf/performance',
|
|
||||||
'/aws/containerinsights/watchdemo-perf/prometheus',
|
|
||||||
'/aws/containerinsights/watchdemo-prod-us-east-1/performance',
|
|
||||||
'/aws/containerinsights/watchdemo-staging/application',
|
|
||||||
'/aws/containerinsights/watchdemo-staging/dataplane',
|
|
||||||
'/aws/containerinsights/watchdemo-staging/host',
|
|
||||||
'/aws/containerinsights/watchdemo-staging/performance',
|
|
||||||
'/aws/ecs/containerinsights/bugbash-ec2/performance',
|
|
||||||
'/aws/ecs/containerinsights/ecs-demoworkshop/performance',
|
|
||||||
'/aws/ecs/containerinsights/ecs-workshop-dev/performance',
|
|
||||||
'/aws/eks/dev303-workshop/cluster',
|
|
||||||
'/aws/events/cloudtrail',
|
|
||||||
'/aws/events/ecs',
|
|
||||||
'/aws/lambda/cwsyn-mycanary-fac97ded-f134-499a-9d71-4c3be1f63182',
|
|
||||||
'/aws/lambda/cwsyn-watch-linkchecks-ef7ef273-5da2-4663-af54-d2f52d55b060',
|
|
||||||
'/ecs/ecs-cwagent-daemon-service',
|
|
||||||
'/ecs/ecs-demo-limitTask',
|
|
||||||
'CloudTrail/DefaultLogGroup',
|
|
||||||
'container-insights-prometheus-beta',
|
|
||||||
'container-insights-prometheus-demo',
|
|
||||||
];
|
|
||||||
|
|
||||||
const logGroups = await ds.describeLogGroups({ region: 'default' });
|
|
||||||
|
|
||||||
expect(logGroups).toEqual(expectedLogGroups);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('When performing CloudWatch logs query', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.spyOn(rxjsUtils, 'increasingInterval').mockImplementation(() => interval(100));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should stop querying when timed out', async () => {
|
|
||||||
const { ds } = getTestContext();
|
|
||||||
const fakeFrames = genMockFrames(20);
|
|
||||||
const initialRecordsMatched = fakeFrames[0].meta!.stats!.find((stat) => stat.displayName === 'Records scanned')!
|
|
||||||
.value!;
|
|
||||||
for (let i = 1; i < 4; i++) {
|
|
||||||
fakeFrames[i].meta!.stats = [
|
|
||||||
{
|
|
||||||
displayName: 'Records scanned',
|
|
||||||
value: initialRecordsMatched,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalRecordsMatched = fakeFrames[9].meta!.stats!.find((stat) => stat.displayName === 'Records scanned')!
|
|
||||||
.value!;
|
|
||||||
for (let i = 10; i < fakeFrames.length; i++) {
|
|
||||||
fakeFrames[i].meta!.stats = [
|
|
||||||
{
|
|
||||||
displayName: 'Records scanned',
|
|
||||||
value: finalRecordsMatched,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
jest.spyOn(ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => {
|
|
||||||
if (subtype === 'GetQueryResults') {
|
|
||||||
const mockObservable = of([fakeFrames[i]]);
|
|
||||||
i++;
|
|
||||||
return mockObservable;
|
|
||||||
} else {
|
|
||||||
return of([]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const iterations = 15;
|
|
||||||
// Times out after 15 passes for consistent testing
|
|
||||||
const timeoutFunc = () => {
|
|
||||||
return i >= iterations;
|
|
||||||
};
|
|
||||||
const myResponse = await lastValueFrom(
|
|
||||||
ds.logsQuery([{ queryId: 'fake-query-id', region: 'default', refId: 'A' }], timeoutFunc)
|
|
||||||
);
|
|
||||||
|
|
||||||
const expectedData = [
|
|
||||||
{
|
|
||||||
...fakeFrames[14],
|
|
||||||
meta: {
|
|
||||||
custom: {
|
|
||||||
Status: 'Cancelled',
|
|
||||||
},
|
|
||||||
stats: fakeFrames[14].meta!.stats,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(myResponse).toEqual({
|
|
||||||
data: expectedData,
|
|
||||||
key: 'test-key',
|
|
||||||
state: 'Done',
|
|
||||||
error: {
|
|
||||||
type: DataQueryErrorType.Timeout,
|
|
||||||
message: `error: query timed out after 5 attempts`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(i).toBe(iterations);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should continue querying as long as new data is being received', async () => {
|
|
||||||
const { ds } = getTestContext();
|
|
||||||
const fakeFrames = genMockFrames(15);
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
jest.spyOn(ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => {
|
|
||||||
if (subtype === 'GetQueryResults') {
|
|
||||||
const mockObservable = of([fakeFrames[i]]);
|
|
||||||
i++;
|
|
||||||
return mockObservable;
|
|
||||||
} else {
|
|
||||||
return of([]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTime = new Date();
|
|
||||||
const timeoutFunc = () => {
|
|
||||||
return Date.now() >= startTime.valueOf() + 6000;
|
|
||||||
};
|
|
||||||
const myResponse = await lastValueFrom(
|
|
||||||
ds.logsQuery([{ queryId: 'fake-query-id', region: 'default', refId: 'A' }], timeoutFunc)
|
|
||||||
);
|
|
||||||
expect(myResponse).toEqual({
|
|
||||||
data: [fakeFrames[fakeFrames.length - 1]],
|
|
||||||
key: 'test-key',
|
|
||||||
state: 'Done',
|
|
||||||
});
|
|
||||||
expect(i).toBe(15);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should stop querying when results come back with status "Complete"', async () => {
|
|
||||||
const { ds } = getTestContext();
|
|
||||||
const fakeFrames = genMockFrames(3);
|
|
||||||
let i = 0;
|
|
||||||
jest.spyOn(ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => {
|
|
||||||
if (subtype === 'GetQueryResults') {
|
|
||||||
const mockObservable = of([fakeFrames[i]]);
|
|
||||||
i++;
|
|
||||||
return mockObservable;
|
|
||||||
} else {
|
|
||||||
return of([]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTime = new Date();
|
|
||||||
const timeoutFunc = () => {
|
|
||||||
return Date.now() >= startTime.valueOf() + 6000;
|
|
||||||
};
|
|
||||||
const myResponse = await lastValueFrom(
|
|
||||||
ds.logsQuery([{ queryId: 'fake-query-id', region: 'default', refId: 'A' }], timeoutFunc)
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(myResponse).toEqual({
|
|
||||||
data: [fakeFrames[2]],
|
|
||||||
key: 'test-key',
|
|
||||||
state: 'Done',
|
|
||||||
});
|
|
||||||
expect(i).toBe(3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('When performing CloudWatch metrics query', () => {
|
|
||||||
const query: any = {
|
|
||||||
range: defaultTimeRange,
|
|
||||||
rangeRaw: { from: 1483228800, to: 1483232400 },
|
|
||||||
targets: [
|
|
||||||
{
|
|
||||||
metricQueryType: MetricQueryType.Search,
|
|
||||||
metricEditorMode: MetricEditorMode.Builder,
|
|
||||||
type: 'Metrics',
|
|
||||||
expression: '',
|
|
||||||
refId: 'A',
|
|
||||||
region: 'us-east-1',
|
|
||||||
namespace: 'AWS/EC2',
|
|
||||||
metricName: 'CPUUtilization',
|
|
||||||
dimensions: {
|
|
||||||
InstanceId: 'i-12345678',
|
|
||||||
},
|
|
||||||
statistic: 'Average',
|
|
||||||
period: '300',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const response: any = {
|
|
||||||
timings: [null],
|
|
||||||
results: {
|
|
||||||
A: {
|
|
||||||
type: 'Metrics',
|
|
||||||
error: '',
|
|
||||||
refId: 'A',
|
|
||||||
meta: {},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: 'CPUUtilization_Average',
|
|
||||||
points: [
|
|
||||||
[1, 1483228800000],
|
|
||||||
[2, 1483229100000],
|
|
||||||
[5, 1483229700000],
|
|
||||||
],
|
|
||||||
tags: {
|
|
||||||
InstanceId: 'i-12345678',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
it('should generate the correct query', async () => {
|
|
||||||
const { ds, fetchMock } = getTestContext({ response });
|
|
||||||
|
|
||||||
await expect(ds.query(query)).toEmitValuesWith(() => {
|
|
||||||
expect(fetchMock.mock.calls[0][0].data.queries).toMatchObject(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
namespace: query.targets[0].namespace,
|
|
||||||
metricName: query.targets[0].metricName,
|
|
||||||
dimensions: { InstanceId: ['i-12345678'] },
|
|
||||||
statistic: query.targets[0].statistic,
|
|
||||||
period: query.targets[0].period,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should generate the correct query with interval variable', async () => {
|
|
||||||
const period: CustomVariableModel = {
|
|
||||||
...initialVariableModelState,
|
|
||||||
id: 'period',
|
|
||||||
name: 'period',
|
|
||||||
index: 0,
|
|
||||||
current: { value: '10m', text: '10m', selected: true },
|
|
||||||
options: [{ value: '10m', text: '10m', selected: true }],
|
|
||||||
multi: false,
|
|
||||||
includeAll: false,
|
|
||||||
query: '',
|
|
||||||
hide: VariableHide.dontHide,
|
|
||||||
type: 'custom',
|
|
||||||
};
|
|
||||||
const templateSrv = new TemplateSrv();
|
|
||||||
templateSrv.init([period]);
|
|
||||||
|
|
||||||
const query: any = {
|
|
||||||
range: defaultTimeRange,
|
|
||||||
rangeRaw: { from: 1483228800, to: 1483232400 },
|
|
||||||
targets: [
|
|
||||||
{
|
|
||||||
metricQueryType: MetricQueryType.Search,
|
|
||||||
metricEditorMode: MetricEditorMode.Builder,
|
|
||||||
type: 'Metrics',
|
|
||||||
refId: 'A',
|
|
||||||
region: 'us-east-1',
|
|
||||||
namespace: 'AWS/EC2',
|
|
||||||
metricName: 'CPUUtilization',
|
|
||||||
dimensions: {
|
|
||||||
InstanceId: 'i-12345678',
|
|
||||||
},
|
|
||||||
statistic: 'Average',
|
|
||||||
period: '[[period]]',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const { ds, fetchMock } = getTestContext({ response, templateSrv });
|
|
||||||
|
|
||||||
await expect(ds.query(query)).toEmitValuesWith(() => {
|
|
||||||
expect(fetchMock.mock.calls[0][0].data.queries[0].period).toEqual('600');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return series list', async () => {
|
|
||||||
const { ds } = getTestContext({ response });
|
|
||||||
|
|
||||||
await expect(ds.query(query)).toEmitValuesWith((received) => {
|
|
||||||
const result = received[0];
|
|
||||||
expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name);
|
|
||||||
expect(result.data[0].fields[1].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('and throttling exception is thrown', () => {
|
|
||||||
const partialQuery = {
|
|
||||||
metricQueryType: MetricQueryType.Search,
|
|
||||||
metricEditorMode: MetricEditorMode.Builder,
|
|
||||||
type: 'Metrics',
|
|
||||||
namespace: 'AWS/EC2',
|
|
||||||
metricName: 'CPUUtilization',
|
|
||||||
dimensions: {
|
|
||||||
InstanceId: 'i-12345678',
|
|
||||||
},
|
|
||||||
statistic: 'Average',
|
|
||||||
period: '300',
|
|
||||||
expression: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
const query: any = {
|
|
||||||
range: defaultTimeRange,
|
|
||||||
rangeRaw: { from: 1483228800, to: 1483232400 },
|
|
||||||
targets: [
|
|
||||||
{ ...partialQuery, refId: 'A', region: 'us-east-1' },
|
|
||||||
{ ...partialQuery, refId: 'B', region: 'us-east-2' },
|
|
||||||
{ ...partialQuery, refId: 'C', region: 'us-east-1' },
|
|
||||||
{ ...partialQuery, refId: 'D', region: 'us-east-2' },
|
|
||||||
{ ...partialQuery, refId: 'E', region: 'eu-north-1' },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const backendErrorResponse = {
|
|
||||||
data: {
|
|
||||||
message: 'Throttling: exception',
|
|
||||||
results: {
|
|
||||||
A: {
|
|
||||||
error: 'Throttling: exception',
|
|
||||||
refId: 'A',
|
|
||||||
meta: {},
|
|
||||||
},
|
|
||||||
B: {
|
|
||||||
error: 'Throttling: exception',
|
|
||||||
refId: 'B',
|
|
||||||
meta: {},
|
|
||||||
},
|
|
||||||
C: {
|
|
||||||
error: 'Throttling: exception',
|
|
||||||
refId: 'C',
|
|
||||||
meta: {},
|
|
||||||
},
|
|
||||||
D: {
|
|
||||||
error: 'Throttling: exception',
|
|
||||||
refId: 'D',
|
|
||||||
meta: {},
|
|
||||||
},
|
|
||||||
E: {
|
|
||||||
error: 'Throttling: exception',
|
|
||||||
refId: 'E',
|
|
||||||
meta: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
redux.setStore({
|
|
||||||
dispatch: jest.fn(),
|
|
||||||
} as any);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should display one alert error message per region+datasource combination', async () => {
|
|
||||||
const { ds } = getTestContext({ response: backendErrorResponse, throws: true });
|
|
||||||
const memoizedDebounceSpy = jest.spyOn(ds, 'debouncedAlert');
|
|
||||||
|
|
||||||
await expect(ds.query(query)).toEmitValuesWith((received) => {
|
|
||||||
expect(memoizedDebounceSpy).toHaveBeenCalledWith('TestDatasource', 'us-east-1');
|
|
||||||
expect(memoizedDebounceSpy).toHaveBeenCalledWith('TestDatasource', 'us-east-2');
|
|
||||||
expect(memoizedDebounceSpy).toHaveBeenCalledWith('TestDatasource', 'eu-north-1');
|
|
||||||
expect(memoizedDebounceSpy).toBeCalledTimes(3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('When query region is "default"', () => {
|
|
||||||
it('should return the datasource region if empty or "default"', () => {
|
|
||||||
const { ds, instanceSettings } = getTestContext();
|
|
||||||
const defaultRegion = instanceSettings.jsonData.defaultRegion;
|
|
||||||
|
|
||||||
expect(ds.getActualRegion()).toBe(defaultRegion);
|
|
||||||
expect(ds.getActualRegion('')).toBe(defaultRegion);
|
|
||||||
expect(ds.getActualRegion('default')).toBe(defaultRegion);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the specified region if specified', () => {
|
|
||||||
const { ds } = getTestContext();
|
|
||||||
|
|
||||||
expect(ds.getActualRegion('some-fake-region-1')).toBe('some-fake-region-1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should query for the datasource region if empty or "default"', async () => {
|
|
||||||
const { ds, instanceSettings } = getTestContext();
|
|
||||||
const performTimeSeriesQueryMock = jest.spyOn(ds, 'performTimeSeriesQuery').mockReturnValue(of({}));
|
|
||||||
|
|
||||||
const query: any = {
|
|
||||||
range: defaultTimeRange,
|
|
||||||
rangeRaw: { from: 1483228800, to: 1483232400 },
|
|
||||||
targets: [
|
|
||||||
{
|
|
||||||
metricQueryType: MetricQueryType.Search,
|
|
||||||
metricEditorMode: MetricEditorMode.Builder,
|
|
||||||
type: 'Metrics',
|
|
||||||
refId: 'A',
|
|
||||||
region: 'default',
|
|
||||||
namespace: 'AWS/EC2',
|
|
||||||
metricName: 'CPUUtilization',
|
|
||||||
dimensions: {
|
|
||||||
InstanceId: 'i-12345678',
|
|
||||||
},
|
|
||||||
statistic: 'Average',
|
|
||||||
period: '300s',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
await expect(ds.query(query)).toEmitValuesWith(() => {
|
|
||||||
expect(performTimeSeriesQueryMock.mock.calls[0][0].queries[0].region).toBe(
|
|
||||||
instanceSettings.jsonData.defaultRegion
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('When interpolating variables', () => {
|
|
||||||
it('should return an empty array if no queries are provided', () => {
|
|
||||||
const templateSrv: any = { replace: jest.fn(), getVariables: () => [] };
|
|
||||||
const { ds } = getTestContext({ templateSrv });
|
|
||||||
|
|
||||||
expect(ds.interpolateVariablesInQueries([], {})).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should replace correct variables in CloudWatchLogsQuery', () => {
|
|
||||||
const templateSrv: any = { replace: jest.fn(), getVariables: () => [] };
|
|
||||||
const { ds } = getTestContext({ templateSrv });
|
|
||||||
const variableName = 'someVar';
|
|
||||||
const logQuery: CloudWatchLogsQuery = {
|
|
||||||
id: 'someId',
|
|
||||||
refId: 'someRefId',
|
|
||||||
queryMode: 'Logs',
|
|
||||||
expression: `$${variableName}`,
|
|
||||||
region: `$${variableName}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
ds.interpolateVariablesInQueries([logQuery], {});
|
|
||||||
|
|
||||||
// We interpolate `region` in CloudWatchLogsQuery
|
|
||||||
expect(templateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {});
|
|
||||||
expect(templateSrv.replace).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should replace correct variables in CloudWatchMetricsQuery', () => {
|
|
||||||
const templateSrv: any = {
|
|
||||||
replace: jest.fn(),
|
|
||||||
getVariables: () => [],
|
|
||||||
getVariableName: jest.fn((name: string) => name),
|
|
||||||
};
|
|
||||||
const { ds } = getTestContext({ templateSrv });
|
|
||||||
const variableName = 'someVar';
|
|
||||||
const logQuery: CloudWatchMetricsQuery = {
|
|
||||||
queryMode: 'Metrics',
|
|
||||||
id: 'someId',
|
|
||||||
refId: 'someRefId',
|
|
||||||
expression: `$${variableName}`,
|
|
||||||
region: `$${variableName}`,
|
|
||||||
period: `$${variableName}`,
|
|
||||||
alias: `$${variableName}`,
|
|
||||||
metricName: `$${variableName}`,
|
|
||||||
namespace: `$${variableName}`,
|
|
||||||
dimensions: {
|
|
||||||
[`$${variableName}`]: `$${variableName}`,
|
|
||||||
},
|
|
||||||
matchExact: false,
|
|
||||||
statistic: '',
|
|
||||||
sqlExpression: `$${variableName}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
ds.interpolateVariablesInQueries([logQuery], {});
|
|
||||||
|
|
||||||
// We interpolate `expression`, `region`, `period`, `alias`, `metricName`, and `nameSpace` in CloudWatchMetricsQuery
|
|
||||||
expect(templateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {});
|
|
||||||
expect(templateSrv.replace).toHaveBeenCalledTimes(7);
|
|
||||||
|
|
||||||
expect(templateSrv.getVariableName).toHaveBeenCalledWith(`$${variableName}`);
|
|
||||||
expect(templateSrv.getVariableName).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('When performing CloudWatch query for extended statistic', () => {
|
|
||||||
const query: any = {
|
|
||||||
range: defaultTimeRange,
|
|
||||||
rangeRaw: { from: 1483228800, to: 1483232400 },
|
|
||||||
targets: [
|
|
||||||
{
|
|
||||||
metricQueryType: MetricQueryType.Search,
|
|
||||||
metricEditorMode: MetricEditorMode.Builder,
|
|
||||||
type: 'Metrics',
|
|
||||||
refId: 'A',
|
|
||||||
region: 'us-east-1',
|
|
||||||
namespace: 'AWS/ApplicationELB',
|
|
||||||
metricName: 'TargetResponseTime',
|
|
||||||
dimensions: {
|
|
||||||
LoadBalancer: 'lb',
|
|
||||||
TargetGroup: 'tg',
|
|
||||||
},
|
|
||||||
statistic: 'p90.00',
|
|
||||||
period: '300s',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const response: any = {
|
|
||||||
timings: [null],
|
|
||||||
results: {
|
|
||||||
A: {
|
|
||||||
error: '',
|
|
||||||
refId: 'A',
|
|
||||||
meta: {},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: 'TargetResponseTime_p90.00',
|
|
||||||
points: [
|
|
||||||
[1, 1483228800000],
|
|
||||||
[2, 1483229100000],
|
|
||||||
[5, 1483229700000],
|
|
||||||
],
|
|
||||||
tags: {
|
|
||||||
LoadBalancer: 'lb',
|
|
||||||
TargetGroup: 'tg',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
it('should return series list', async () => {
|
|
||||||
const { ds } = getTestContext({ response });
|
|
||||||
|
|
||||||
await expect(ds.query(query)).toEmitValuesWith((received) => {
|
|
||||||
const result = received[0];
|
|
||||||
expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name);
|
|
||||||
expect(result.data[0].fields[1].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('When performing CloudWatch query with template variables', () => {
|
|
||||||
let templateSrv: TemplateSrv;
|
|
||||||
beforeEach(() => {
|
|
||||||
const key = 'key';
|
|
||||||
const var1: CustomVariableModel = {
|
|
||||||
...initialVariableModelState,
|
|
||||||
id: 'var1',
|
|
||||||
rootStateKey: key,
|
|
||||||
name: 'var1',
|
|
||||||
index: 0,
|
|
||||||
current: { value: 'var1-foo', text: 'var1-foo', selected: true },
|
|
||||||
options: [{ value: 'var1-foo', text: 'var1-foo', selected: true }],
|
|
||||||
multi: false,
|
|
||||||
includeAll: false,
|
|
||||||
query: '',
|
|
||||||
hide: VariableHide.dontHide,
|
|
||||||
type: 'custom',
|
|
||||||
};
|
|
||||||
const var2: CustomVariableModel = {
|
|
||||||
...initialVariableModelState,
|
|
||||||
id: 'var2',
|
|
||||||
rootStateKey: key,
|
|
||||||
name: 'var2',
|
|
||||||
index: 1,
|
|
||||||
current: { value: 'var2-foo', text: 'var2-foo', selected: true },
|
|
||||||
options: [{ value: 'var2-foo', text: 'var2-foo', selected: true }],
|
|
||||||
multi: false,
|
|
||||||
includeAll: false,
|
|
||||||
query: '',
|
|
||||||
hide: VariableHide.dontHide,
|
|
||||||
type: 'custom',
|
|
||||||
};
|
|
||||||
const var3: CustomVariableModel = {
|
|
||||||
...initialVariableModelState,
|
|
||||||
id: 'var3',
|
|
||||||
rootStateKey: key,
|
|
||||||
name: 'var3',
|
|
||||||
index: 2,
|
|
||||||
current: { value: ['var3-foo', 'var3-baz'], text: 'var3-foo + var3-baz', selected: true },
|
|
||||||
options: [
|
|
||||||
{ selected: true, value: 'var3-foo', text: 'var3-foo' },
|
|
||||||
{ selected: false, value: 'var3-bar', text: 'var3-bar' },
|
|
||||||
{ selected: true, value: 'var3-baz', text: 'var3-baz' },
|
|
||||||
],
|
|
||||||
multi: true,
|
|
||||||
includeAll: false,
|
|
||||||
query: '',
|
|
||||||
hide: VariableHide.dontHide,
|
|
||||||
type: 'custom',
|
|
||||||
};
|
|
||||||
const var4: CustomVariableModel = {
|
|
||||||
...initialVariableModelState,
|
|
||||||
id: 'var4',
|
|
||||||
rootStateKey: key,
|
|
||||||
name: 'var4',
|
|
||||||
index: 3,
|
|
||||||
options: [
|
|
||||||
{ selected: true, value: 'var4-foo', text: 'var4-foo' },
|
|
||||||
{ selected: false, value: 'var4-bar', text: 'var4-bar' },
|
|
||||||
{ selected: true, value: 'var4-baz', text: 'var4-baz' },
|
|
||||||
],
|
|
||||||
current: { value: ['var4-foo', 'var4-baz'], text: 'var4-foo + var4-baz', selected: true },
|
|
||||||
multi: true,
|
|
||||||
includeAll: false,
|
|
||||||
query: '',
|
|
||||||
hide: VariableHide.dontHide,
|
|
||||||
type: 'custom',
|
|
||||||
};
|
|
||||||
const variables = [var1, var2, var3, var4];
|
|
||||||
const state = convertToStoreState(key, variables);
|
|
||||||
templateSrv = new TemplateSrv(getTemplateSrvDependencies(state));
|
|
||||||
templateSrv.init(variables);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should generate the correct query for single template variable', async () => {
|
|
||||||
const { ds, fetchMock } = getTestContext({ templateSrv });
|
|
||||||
const query: any = {
|
|
||||||
range: defaultTimeRange,
|
|
||||||
rangeRaw: { from: 1483228800, to: 1483232400 },
|
|
||||||
targets: [
|
|
||||||
{
|
|
||||||
metricQueryType: MetricQueryType.Search,
|
|
||||||
metricEditorMode: MetricEditorMode.Builder,
|
|
||||||
type: 'Metrics',
|
|
||||||
refId: 'A',
|
|
||||||
region: 'us-east-1',
|
|
||||||
namespace: 'TestNamespace',
|
|
||||||
metricName: 'TestMetricName',
|
|
||||||
dimensions: {
|
|
||||||
dim2: '[[var2]]',
|
|
||||||
},
|
|
||||||
statistic: 'Average',
|
|
||||||
period: '300s',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
await expect(ds.query(query)).toEmitValuesWith(() => {
|
|
||||||
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should generate the correct query in the case of one multilple template variables', async () => {
|
|
||||||
const { ds, fetchMock } = getTestContext({ templateSrv });
|
|
||||||
const query: any = {
|
|
||||||
range: defaultTimeRange,
|
|
||||||
rangeRaw: { from: 1483228800, to: 1483232400 },
|
|
||||||
targets: [
|
|
||||||
{
|
|
||||||
metricQueryType: MetricQueryType.Search,
|
|
||||||
metricEditorMode: MetricEditorMode.Builder,
|
|
||||||
type: 'Metrics',
|
|
||||||
refId: 'A',
|
|
||||||
region: 'us-east-1',
|
|
||||||
namespace: 'TestNamespace',
|
|
||||||
metricName: 'TestMetricName',
|
|
||||||
dimensions: {
|
|
||||||
dim1: '[[var1]]',
|
|
||||||
dim2: '[[var2]]',
|
|
||||||
dim3: '[[var3]]',
|
|
||||||
},
|
|
||||||
statistic: 'Average',
|
|
||||||
period: '300s',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
scopedVars: {
|
|
||||||
var1: { selected: true, value: 'var1-foo' },
|
|
||||||
var2: { selected: true, value: 'var2-foo' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await expect(ds.query(query)).toEmitValuesWith(() => {
|
|
||||||
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']);
|
|
||||||
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']);
|
|
||||||
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should generate the correct query in the case of multilple multi template variables', async () => {
|
|
||||||
const { ds, fetchMock } = getTestContext({ templateSrv });
|
|
||||||
const query: any = {
|
|
||||||
range: defaultTimeRange,
|
|
||||||
rangeRaw: { from: 1483228800, to: 1483232400 },
|
|
||||||
targets: [
|
|
||||||
{
|
|
||||||
metricQueryType: MetricQueryType.Search,
|
|
||||||
metricEditorMode: MetricEditorMode.Builder,
|
|
||||||
type: 'Metrics',
|
|
||||||
refId: 'A',
|
|
||||||
region: 'us-east-1',
|
|
||||||
namespace: 'TestNamespace',
|
|
||||||
metricName: 'TestMetricName',
|
|
||||||
dimensions: {
|
|
||||||
dim1: '[[var1]]',
|
|
||||||
dim3: '[[var3]]',
|
|
||||||
dim4: '[[var4]]',
|
|
||||||
},
|
|
||||||
statistic: 'Average',
|
|
||||||
period: '300s',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
await expect(ds.query(query)).toEmitValuesWith(() => {
|
|
||||||
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']);
|
|
||||||
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']);
|
|
||||||
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim4']).toStrictEqual(['var4-foo', 'var4-baz']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should generate the correct query for multilple template variables, lack scopedVars', async () => {
|
|
||||||
const { ds, fetchMock } = getTestContext({ templateSrv });
|
|
||||||
const query: any = {
|
|
||||||
range: defaultTimeRange,
|
|
||||||
rangeRaw: { from: 1483228800, to: 1483232400 },
|
|
||||||
targets: [
|
|
||||||
{
|
|
||||||
metricQueryType: MetricQueryType.Search,
|
|
||||||
metricEditorMode: MetricEditorMode.Builder,
|
|
||||||
type: 'Metrics',
|
|
||||||
refId: 'A',
|
|
||||||
region: 'us-east-1',
|
|
||||||
namespace: 'TestNamespace',
|
|
||||||
metricName: 'TestMetricName',
|
|
||||||
dimensions: {
|
|
||||||
dim1: '[[var1]]',
|
|
||||||
dim2: '[[var2]]',
|
|
||||||
dim3: '[[var3]]',
|
|
||||||
},
|
|
||||||
statistic: 'Average',
|
|
||||||
period: '300',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
scopedVars: {
|
|
||||||
var1: { selected: true, value: 'var1-foo' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await expect(ds.query(query)).toEmitValuesWith(() => {
|
|
||||||
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']);
|
|
||||||
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']);
|
|
||||||
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function genMockFrames(numResponses: number): DataFrame[] {
|
|
||||||
const recordIncrement = 50;
|
|
||||||
const mockFrames: DataFrame[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < numResponses; i++) {
|
|
||||||
mockFrames.push({
|
|
||||||
fields: [],
|
|
||||||
meta: {
|
|
||||||
custom: {
|
|
||||||
Status: i === numResponses - 1 ? CloudWatchLogsQueryStatus.Complete : CloudWatchLogsQueryStatus.Running,
|
|
||||||
},
|
|
||||||
stats: [
|
|
||||||
{
|
|
||||||
displayName: 'Records scanned',
|
|
||||||
value: (i + 1) * recordIncrement,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
refId: 'A',
|
|
||||||
length: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return mockFrames;
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
import { AwsAuthDataSourceJsonData, AwsAuthDataSourceSecureJsonData } from '@grafana/aws-sdk';
|
import { AwsAuthDataSourceJsonData, AwsAuthDataSourceSecureJsonData } from '@grafana/aws-sdk';
|
||||||
import { DataQuery, DataSourceRef, SelectableValue } from '@grafana/data';
|
import { DataFrame, DataQuery, DataSourceRef, SelectableValue } from '@grafana/data';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
QueryEditorArrayExpression,
|
QueryEditorArrayExpression,
|
||||||
@ -244,6 +244,7 @@ export interface TSDBQueryResult<T = any> {
|
|||||||
refId: string;
|
refId: string;
|
||||||
series: TSDBTimeSeries[];
|
series: TSDBTimeSeries[];
|
||||||
tables: Array<TSDBTable<T>>;
|
tables: Array<TSDBTable<T>>;
|
||||||
|
frames: DataFrame[];
|
||||||
|
|
||||||
error?: string;
|
error?: string;
|
||||||
meta?: any;
|
meta?: any;
|
||||||
@ -254,6 +255,15 @@ export interface TSDBTable<T = any> {
|
|||||||
rows: T[];
|
rows: T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DataQueryError<CloudWatchMetricsQuery> {
|
||||||
|
data?: {
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
results: Record<string, TSDBQueryResult<CloudWatchMetricsQuery>>;
|
||||||
|
};
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TSDBTimeSeries {
|
export interface TSDBTimeSeries {
|
||||||
name: string;
|
name: string;
|
||||||
points: TSDBTimePoint[];
|
points: TSDBTimePoint[];
|
||||||
|
@ -19,7 +19,7 @@ ds.datasource.getRegions = jest.fn().mockResolvedValue([{ label: 'a', value: 'a'
|
|||||||
ds.datasource.getNamespaces = jest.fn().mockResolvedValue([{ label: 'b', value: 'b' }]);
|
ds.datasource.getNamespaces = jest.fn().mockResolvedValue([{ label: 'b', value: 'b' }]);
|
||||||
ds.datasource.getMetrics = jest.fn().mockResolvedValue([{ label: 'c', value: 'c' }]);
|
ds.datasource.getMetrics = jest.fn().mockResolvedValue([{ label: 'c', value: 'c' }]);
|
||||||
ds.datasource.getDimensionKeys = jest.fn().mockResolvedValue([{ label: 'd', value: 'd' }]);
|
ds.datasource.getDimensionKeys = jest.fn().mockResolvedValue([{ label: 'd', value: 'd' }]);
|
||||||
ds.datasource.describeAllLogGroups = jest.fn().mockResolvedValue(['a', 'b']);
|
ds.datasource.logsQueryRunner.describeAllLogGroups = jest.fn().mockResolvedValue(['a', 'b']);
|
||||||
const getDimensionValues = jest.fn().mockResolvedValue([{ label: 'e', value: 'e' }]);
|
const getDimensionValues = jest.fn().mockResolvedValue([{ label: 'e', value: 'e' }]);
|
||||||
const getEbsVolumeIds = jest.fn().mockResolvedValue([{ label: 'f', value: 'f' }]);
|
const getEbsVolumeIds = jest.fn().mockResolvedValue([{ label: 'f', value: 'f' }]);
|
||||||
const getEc2InstanceAttribute = jest.fn().mockResolvedValue([{ label: 'g', value: 'g' }]);
|
const getEc2InstanceAttribute = jest.fn().mockResolvedValue([{ label: 'g', value: 'g' }]);
|
||||||
|
@ -55,7 +55,10 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleLogGroupsQuery({ region, logGroupPrefix }: VariableQuery) {
|
async handleLogGroupsQuery({ region, logGroupPrefix }: VariableQuery) {
|
||||||
const logGroups = await this.datasource.describeAllLogGroups({ region, logGroupNamePrefix: logGroupPrefix });
|
const logGroups = await this.datasource.logsQueryRunner.describeAllLogGroups({
|
||||||
|
region,
|
||||||
|
logGroupNamePrefix: logGroupPrefix,
|
||||||
|
});
|
||||||
return logGroups.map((s) => ({
|
return logGroups.map((s) => ({
|
||||||
text: s,
|
text: s,
|
||||||
value: s,
|
value: s,
|
||||||
|
Loading…
Reference in New Issue
Block a user