AzureMonitor: Frontend cleanup (#66871)

* Remove unused mocks

* Remove time grain converter anys

* Improve mocks

- Add context mock
- Update datasource mock
- Add util functions

* Remove anys from log_analytics_test

* Improve response typing

* Remove redundant angular code

* Remove more anys

- Add Resource type

* More type updates

* Remove unused code and update arg ds test

* Remove old annotations test

* Remove unused code and update some more types

* Fix lint

* Fix lint
pull/69122/head
Andreas Christou 2 years ago committed by GitHub
parent c9adcc1e97
commit 6b67bade55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 85
      .betterer.results
  2. 26
      public/app/plugins/datasource/azuremonitor/__mocks__/datasource.ts
  3. 92
      public/app/plugins/datasource/azuremonitor/__mocks__/instanceSettings.ts
  4. 16
      public/app/plugins/datasource/azuremonitor/__mocks__/query_ctrl.ts
  5. 3
      public/app/plugins/datasource/azuremonitor/__mocks__/sdk.ts
  6. 16
      public/app/plugins/datasource/azuremonitor/__mocks__/utils.ts
  7. 2
      public/app/plugins/datasource/azuremonitor/azure_log_analytics/__mocks__/schema.ts
  8. 151
      public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.test.ts
  9. 81
      public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.ts
  10. 303
      public/app/plugins/datasource/azuremonitor/azure_log_analytics/response_parser.ts
  11. 133
      public/app/plugins/datasource/azuremonitor/azure_log_analytics/utils.ts
  12. 6
      public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.test.ts
  13. 26
      public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts
  14. 45
      public/app/plugins/datasource/azuremonitor/azure_monitor/response_parser.ts
  15. 79
      public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.test.ts
  16. 2
      public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.ts
  17. 10
      public/app/plugins/datasource/azuremonitor/components/ConfigEditor.tsx
  18. 3
      public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/QueryField.tsx
  19. 4
      public/app/plugins/datasource/azuremonitor/datasource.ts
  20. 186
      public/app/plugins/datasource/azuremonitor/log_analytics/querystring_builder.test.ts
  21. 85
      public/app/plugins/datasource/azuremonitor/log_analytics/querystring_builder.ts
  22. 4
      public/app/plugins/datasource/azuremonitor/time_grain_converter.ts
  23. 146
      public/app/plugins/datasource/azuremonitor/types/types.ts
  24. 18
      public/app/plugins/datasource/azuremonitor/utils/common.ts

@ -3630,70 +3630,13 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"]
],
"public/app/plugins/datasource/azuremonitor/__mocks__/query_ctrl.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"]
],
"public/app/plugins/datasource/azuremonitor/azure_log_analytics/__mocks__/schema.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.test.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"]
],
"public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_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, "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"]
],
"public/app/plugins/datasource/azuremonitor/azure_log_analytics/response_parser.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"],
[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, "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, "Unexpected any. Specify a different type.", "14"]
],
"public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_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"]
],
"public/app/plugins/datasource/azuremonitor/azure_monitor/response_parser.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"]
],
"public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/QueryField.tsx: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.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"public/app/plugins/datasource/azuremonitor/components/MonitorConfig.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
@ -3701,28 +3644,6 @@ exports[`better eslint`] = {
"public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/datasource/azuremonitor/datasource.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/datasource/azuremonitor/log_analytics/querystring_builder.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/azuremonitor/time_grain_converter.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/datasource/azuremonitor/types/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/datasource/azuremonitor/utils/common.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/datasource/azuremonitor/utils/messageFromError.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],

@ -1,11 +1,31 @@
import { DataSourceInstanceSettings } from '@grafana/data';
import { ContextSrv } from 'app/core/services/context_srv';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { TemplateSrv } from 'app/features/templating/template_srv';
import Datasource from '../datasource';
import { AzureDataSourceJsonData } from '../types';
import { createMockInstanceSetttings } from './instanceSettings';
import { DeepPartial } from './utils';
export interface Context {
instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>;
templateSrv: TemplateSrv;
datasource: Datasource;
getResource: jest.Mock;
}
export function createContext(overrides?: DeepPartial<Context>): Context {
const instanceSettings = createMockInstanceSetttings(overrides?.instanceSettings);
return {
instanceSettings,
templateSrv: new TemplateSrv(),
datasource: new Datasource(instanceSettings),
getResource: jest.fn(),
};
}
type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};
const contextSrv = new ContextSrv();
const timeSrv = new TimeSrv(contextSrv);

@ -1,29 +1,71 @@
import { DataSourceInstanceSettings, DataSourcePluginMeta } from '@grafana/data';
import { DataSourceInstanceSettings, PluginType } from '@grafana/data';
import { AzureDataSourceInstanceSettings, AzureDataSourceJsonData } from '../types';
import { AzureDataSourceInstanceSettings } from '../types';
export const createMockInstanceSetttings = (
overrides?: Partial<DataSourceInstanceSettings>,
jsonDataOverrides?: Partial<AzureDataSourceJsonData>
): AzureDataSourceInstanceSettings => ({
url: '/ds/1',
id: 1,
uid: 'abc',
type: 'azuremonitor',
access: 'proxy',
meta: {} as DataSourcePluginMeta,
name: 'azure',
readOnly: false,
...overrides,
import { DeepPartial, mapPartialArrayObject } from './utils';
jsonData: {
cloudName: 'azuremonitor',
azureAuthType: 'clientsecret',
export const createMockInstanceSetttings = (
overrides?: DeepPartial<DataSourceInstanceSettings>
): AzureDataSourceInstanceSettings => {
const metaOverrides = overrides?.meta;
return {
url: '/ds/1',
id: 1,
uid: 'abc',
type: 'azuremonitor',
access: 'proxy',
name: 'azure',
readOnly: false,
...overrides,
meta: {
id: 'grafana-azure-monitor-datasource',
name: 'Azure Monitor',
type: PluginType.datasource,
module: 'path_to_module',
baseUrl: 'base_url',
...metaOverrides,
info: {
description: 'Azure Monitor',
updated: 'updated',
version: '1.0.0',
...metaOverrides?.info,
screenshots: mapPartialArrayObject(
{ name: 'Azure Screenshot', path: 'path_to_screenshot' },
metaOverrides?.info?.screenshots
),
links: mapPartialArrayObject(
{ name: 'Azure Link', url: 'link_url', target: '_blank' },
metaOverrides?.info?.links
),
author: {
name: 'test',
...metaOverrides?.info?.author,
},
logos: {
large: 'large.logo',
small: 'small.logo',
...metaOverrides?.info?.logos,
},
build: {
time: 0,
repo: 'repo',
branch: 'branch',
hash: 'hash',
number: 1,
pr: 1,
...metaOverrides?.info?.build,
},
},
},
jsonData: {
cloudName: 'azuremonitor',
azureAuthType: 'clientsecret',
// monitor
tenantId: 'abc-123',
clientId: 'def-456',
subscriptionId: 'ghi-789',
...jsonDataOverrides,
},
});
// monitor
tenantId: 'abc-123',
clientId: 'def-456',
subscriptionId: 'ghi-789',
...overrides?.jsonData,
},
};
};

@ -1,16 +0,0 @@
export class QueryCtrl {
target: any;
datasource: any;
panelCtrl: any;
panel: any;
hasRawMode = false;
error = '';
constructor(public $scope: any) {
this.panelCtrl = this.panelCtrl || { panel: {} };
this.target = this.target || { target: '' };
this.panel = this.panelCtrl.panel;
}
refresh() {}
}

@ -1,3 +0,0 @@
import { QueryCtrl } from './query_ctrl';
export { QueryCtrl };

@ -38,3 +38,19 @@ export function createTemplateVariables(templateableProps: string[], value = '')
});
return templateVariables;
}
export type DeepPartial<K> = {
[attr in keyof K]?: K[attr] extends object ? DeepPartial<K[attr]> : K[attr];
};
export function mapPartialArrayObject<T extends object>(defaultValue: T, arr?: Array<DeepPartial<T | undefined>>): T[] {
if (!arr) {
return [defaultValue];
}
return arr.map((item?: DeepPartial<T>) => {
if (!item) {
return defaultValue;
}
return { ...item, ...defaultValue };
});
}

@ -162,7 +162,7 @@ export default class FakeSchemaData {
};
}
static getlogAnalyticsFakeMetadata(): any {
static getlogAnalyticsFakeMetadata() {
return {
tables: [
{

@ -1,6 +1,6 @@
import { toUtc } from '@grafana/data';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { Context, createContext } from '../__mocks__/datasource';
import createMockQuery from '../__mocks__/query';
import { createTemplateVariables } from '../__mocks__/utils';
import { singleVariable } from '../__mocks__/variables';
@ -18,39 +18,29 @@ jest.mock('@grafana/runtime', () => ({
getTemplateSrv: () => templateSrv,
}));
const makeResourceURI = (
resourceName: string,
resourceGroup = 'test-resource-group',
subscriptionID = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
) =>
`/subscriptions/${subscriptionID}/resourceGroups/${resourceGroup}/providers/Microsoft.OperationalInsights/workspaces/${resourceName}`;
describe('AzureLogAnalyticsDatasource', () => {
const ctx: any = {};
let ctx: Context;
beforeEach(() => {
templateSrv.init([singleVariable]);
templateSrv.getVariables = jest.fn().mockReturnValue([singleVariable]);
ctx.instanceSettings = {
jsonData: { subscriptionId: 'xxx' },
url: 'http://azureloganalyticsapi',
templateSrv: templateSrv,
};
ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings);
ctx = createContext({
instanceSettings: { jsonData: { subscriptionId: 'xxx' }, url: 'http://azureloganalyticsapi' },
});
ctx.templateSrv = templateSrv;
});
describe('When performing getSchema', () => {
beforeEach(() => {
ctx.mockGetResource = jest.fn().mockImplementation((path: string) => {
ctx.getResource = jest.fn().mockImplementation((path: string) => {
expect(path).toContain('metadata');
return Promise.resolve(FakeSchemaData.getlogAnalyticsFakeMetadata());
});
ctx.ds.azureLogAnalyticsDatasource.getResource = ctx.mockGetResource;
ctx.datasource.azureLogAnalyticsDatasource.getResource = ctx.getResource;
});
it('should return a schema to use with monaco-kusto', async () => {
const result = await ctx.ds.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace');
const result = await ctx.datasource.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace');
expect(result.database.tables).toHaveLength(2);
expect(result.database.tables[0].name).toBe('Alert');
@ -81,12 +71,12 @@ describe('AzureLogAnalyticsDatasource', () => {
});
it('should interpolate variables when making a request for a schema with a uri that contains template variables', async () => {
await ctx.ds.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace/$var1');
expect(ctx.mockGetResource).lastCalledWith('loganalytics/v1myWorkspace/var1-foo/metadata');
await ctx.datasource.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace/$var1');
expect(ctx.getResource).lastCalledWith('loganalytics/v1myWorkspace/var1-foo/metadata');
});
it('should include macros as suggested functions', async () => {
const result = await ctx.ds.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace');
const result = await ctx.datasource.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace');
expect(result.database.functions.map((f: { name: string }) => f.name)).toEqual([
'Func1',
'_AzureBackup_GetVaults',
@ -99,123 +89,42 @@ describe('AzureLogAnalyticsDatasource', () => {
});
it('should include template variables as global parameters', async () => {
const result = await ctx.ds.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace');
const result = await ctx.datasource.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace');
expect(result.globalParameters.map((f: { name: string }) => f.name)).toEqual([`$${singleVariable.name}`]);
});
});
describe('When performing annotationQuery', () => {
const tableResponse = {
tables: [
{
name: 'PrimaryResult',
columns: [
{
name: 'TimeGenerated',
type: 'datetime',
},
{
name: 'Text',
type: 'string',
},
{
name: 'Tags',
type: 'string',
},
],
rows: [
['2018-06-02T20:20:00Z', 'Computer1', 'tag1,tag2'],
['2018-06-02T20:28:00Z', 'Computer2', 'tag2'],
],
},
],
};
const workspaceResponse = {
value: [
{
name: 'aworkspace',
id: makeResourceURI('a-workspace'),
properties: {
source: 'Azure',
customerId: 'abc1b44e-3e57-4410-b027-6cc0ae6dee67',
},
},
],
};
let annotationResults: any[];
beforeEach(async () => {
ctx.ds.azureLogAnalyticsDatasource.getResource = jest.fn().mockImplementation((path: string) => {
if (path.indexOf('Microsoft.OperationalInsights/workspaces') > -1) {
return Promise.resolve(workspaceResponse);
} else {
return Promise.resolve(tableResponse);
}
});
annotationResults = await ctx.ds.annotationQuery({
annotation: {
rawQuery: 'Heartbeat | where $__timeFilter()| project TimeGenerated, Text=Computer, tags="test"',
workspace: 'abc1b44e-3e57-4410-b027-6cc0ae6dee67',
},
range: {
from: toUtc('2017-08-22T20:00:00Z'),
to: toUtc('2017-08-22T23:59:00Z'),
},
rangeRaw: {
from: 'now-4h',
to: 'now',
},
});
});
it('should return a list of categories in the correct format', () => {
expect(annotationResults.length).toBe(2);
expect(annotationResults[0].time).toBe(1527970800000);
expect(annotationResults[0].text).toBe('Computer1');
expect(annotationResults[0].tags[0]).toBe('tag1');
expect(annotationResults[0].tags[1]).toBe('tag2');
expect(annotationResults[1].time).toBe(1527971280000);
expect(annotationResults[1].text).toBe('Computer2');
expect(annotationResults[1].tags[0]).toBe('tag2');
});
});
describe('When performing getWorkspaces', () => {
beforeEach(() => {
ctx.ds.azureLogAnalyticsDatasource.getWorkspaceList = jest
ctx.datasource.azureLogAnalyticsDatasource.getResource = jest
.fn()
.mockResolvedValue({ value: [{ name: 'foobar', id: 'foo', properties: { customerId: 'bar' } }] });
});
it('should return the workspace id', async () => {
const workspaces = await ctx.ds.azureLogAnalyticsDatasource.getWorkspaces('sub');
const workspaces = await ctx.datasource.azureLogAnalyticsDatasource.getWorkspaces('sub');
expect(workspaces).toEqual([{ text: 'foobar', value: 'foo' }]);
});
});
describe('When performing getFirstWorkspace', () => {
beforeEach(() => {
ctx.ds.azureLogAnalyticsDatasource.getDefaultOrFirstSubscription = jest.fn().mockResolvedValue('foo');
ctx.ds.azureLogAnalyticsDatasource.getWorkspaces = jest
ctx.datasource.azureLogAnalyticsDatasource.getDefaultOrFirstSubscription = jest.fn().mockResolvedValue('foo');
ctx.datasource.azureLogAnalyticsDatasource.getWorkspaces = jest
.fn()
.mockResolvedValue([{ text: 'foobar', value: 'foo' }]);
ctx.ds.azureLogAnalyticsDatasource.firstWorkspace = undefined;
ctx.datasource.azureLogAnalyticsDatasource.firstWorkspace = undefined;
});
it('should return the stored workspace', async () => {
ctx.ds.azureLogAnalyticsDatasource.firstWorkspace = 'bar';
const workspace = await ctx.ds.azureLogAnalyticsDatasource.getFirstWorkspace();
ctx.datasource.azureLogAnalyticsDatasource.firstWorkspace = 'bar';
const workspace = await ctx.datasource.azureLogAnalyticsDatasource.getFirstWorkspace();
expect(workspace).toEqual('bar');
expect(ctx.ds.azureLogAnalyticsDatasource.getDefaultOrFirstSubscription).not.toHaveBeenCalled();
expect(ctx.datasource.azureLogAnalyticsDatasource.getDefaultOrFirstSubscription).not.toHaveBeenCalled();
});
it('should return the first workspace', async () => {
const workspace = await ctx.ds.azureLogAnalyticsDatasource.getFirstWorkspace();
const workspace = await ctx.datasource.azureLogAnalyticsDatasource.getFirstWorkspace();
expect(workspace).toEqual('foo');
});
});
@ -252,15 +161,9 @@ describe('AzureLogAnalyticsDatasource', () => {
});
describe('When performing filterQuery', () => {
const ctx: any = {};
let laDatasource: AzureLogAnalyticsDatasource;
beforeEach(() => {
ctx.instanceSettings = {
jsonData: { subscriptionId: 'xxx' },
url: 'http://azureloganalyticsapi',
};
laDatasource = new AzureLogAnalyticsDatasource(ctx.instanceSettings);
});
@ -351,7 +254,7 @@ describe('AzureLogAnalyticsDatasource', () => {
it('should return a query unchanged if no template variables are provided', () => {
const query = createMockQuery();
query.queryType = AzureQueryType.LogAnalytics;
const templatedQuery = ctx.ds.interpolateVariablesInQueries([query], {});
const templatedQuery = ctx.datasource.interpolateVariablesInQueries([query], {});
expect(templatedQuery[0]).toEqual(query);
});
@ -369,7 +272,7 @@ describe('AzureLogAnalyticsDatasource', () => {
...query.azureLogAnalytics,
...azureLogAnalytics,
};
const templatedQuery = ctx.ds.interpolateVariablesInQueries([query], {});
const templatedQuery = ctx.datasource.interpolateVariablesInQueries([query], {});
expect(templatedQuery[0]).toHaveProperty('datasource');
expect(templatedQuery[0].azureLogAnalytics).toMatchObject({
query: templateVariables.get('query')?.templateVariable.current.value,
@ -389,7 +292,7 @@ describe('AzureLogAnalyticsDatasource', () => {
...query.azureLogAnalytics,
...azureLogAnalytics,
};
const templatedQuery = ctx.ds.interpolateVariablesInQueries([query], {});
const templatedQuery = ctx.datasource.interpolateVariablesInQueries([query], {});
expect(templatedQuery[0]).toHaveProperty('datasource');
expect(templatedQuery[0].azureLogAnalytics).toMatchObject({
resources: ['resource1', 'resource2'],
@ -413,7 +316,7 @@ describe('AzureLogAnalyticsDatasource', () => {
...azureTraces,
};
const templatedQuery = ctx.ds.interpolateVariablesInQueries([query], {});
const templatedQuery = ctx.datasource.interpolateVariablesInQueries([query], {});
expect(templatedQuery[0]).toHaveProperty('datasource');
expect(templatedQuery[0].azureTraces).toMatchObject({
query: templateVariables.get('query')?.templateVariable.current.value,
@ -441,7 +344,7 @@ describe('AzureLogAnalyticsDatasource', () => {
...query.azureTraces,
...azureTraces,
};
const templatedQuery = ctx.ds.interpolateVariablesInQueries([query], {});
const templatedQuery = ctx.datasource.interpolateVariablesInQueries([query], {});
expect(templatedQuery[0]).toHaveProperty('datasource');
expect(templatedQuery[0].azureTraces).toMatchObject({
resources: ['resource1', 'resource2'],

@ -1,28 +1,24 @@
import { map } from 'lodash';
import { DataSourceInstanceSettings, DataSourceRef, ScopedVars } from '@grafana/data';
import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
import { DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime';
import { TimeSrv, getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { isGUIDish } from '../components/ResourcePicker/utils';
import ResponseParser from '../azure_monitor/response_parser';
import { getAuthType, getAzureCloud, getAzurePortalUrl } from '../credentials';
import LogAnalyticsQuerystringBuilder from '../log_analytics/querystring_builder';
import {
AzureAPIResponse,
AzureDataSourceJsonData,
AzureLogsVariable,
AzureMonitorQuery,
AzureQueryType,
DatasourceValidationResult,
Subscription,
Workspace,
} from '../types';
import { interpolateVariable, routeNames } from '../utils/common';
import ResponseParser, { transformMetadataToKustoSchema } from './response_parser';
interface AdhocQuery {
datasource: DataSourceRef;
path: string;
resultFormat: string;
}
import { transformMetadataToKustoSchema } from './utils';
export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
AzureMonitorQuery,
@ -70,7 +66,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
}
const path = `${this.azureMonitorPath}?api-version=2019-03-01`;
return await this.getResource(path).then((result: any) => {
return await this.getResource<AzureAPIResponse<Subscription>>(path).then((result) => {
return ResponseParser.parseSubscriptions(result);
});
}
@ -79,7 +75,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
const response = await this.getWorkspaceList(subscription);
return (
map(response.value, (val: any) => {
map(response.value, (val: Workspace) => {
return {
text: val.name,
value: val.id,
@ -88,13 +84,13 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
);
}
private getWorkspaceList(subscription: string): Promise<any> {
private getWorkspaceList(subscription: string): Promise<AzureAPIResponse<Workspace>> {
const subscriptionId = getTemplateSrv().replace(subscription || this.defaultSubscriptionId);
const workspaceListUrl =
this.azureMonitorPath +
`/${subscriptionId}/providers/Microsoft.OperationalInsights/workspaces?api-version=2017-04-26-preview`;
return this.getResource(workspaceListUrl);
return this.getResource<AzureAPIResponse<Workspace>>(workspaceListUrl);
}
async getMetadata(resourceUri: string) {
@ -201,29 +197,6 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
return this.instanceSettings.jsonData.logAnalyticsDefaultWorkspace;
}
private buildQuery(query: string, options: any, workspace: string): AdhocQuery[] {
const querystringBuilder = new LogAnalyticsQuerystringBuilder(
getTemplateSrv().replace(query, {}, interpolateVariable),
options,
'TimeGenerated'
);
const querystring = querystringBuilder.generate().uriString;
const path = isGUIDish(workspace)
? `${this.resourcePath}/v1/workspaces/${workspace}/query?${querystring}`
: `${this.resourcePath}/v1${workspace}/query?${querystring}`;
const queries = [
{
datasource: this.getRef(),
path: path,
resultFormat: 'table',
},
];
return queries;
}
async getDefaultOrFirstSubscription(): Promise<string | undefined> {
if (this.defaultSubscriptionId) {
return this.defaultSubscriptionId;
@ -252,40 +225,6 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
return workspace;
}
annotationQuery(options: any) {
if (!options.annotation.rawQuery) {
return Promise.reject({
message: 'Query missing in annotation definition',
});
}
const queries = this.buildQuery(options.annotation.rawQuery, options, options.annotation.workspace);
const promises = this.doQueries(queries);
return Promise.all(promises).then((results) => {
const annotations = new ResponseParser(results).transformToAnnotations(options);
return annotations;
});
}
doQueries(queries: AdhocQuery[]) {
return map(queries, (query) => {
return this.getResource(query.path)
.then((result: any) => {
return {
result: result,
query: query,
};
})
.catch((err: any) => {
throw {
error: err,
query: query,
};
});
});
}
private validateDatasource(): DatasourceValidationResult | undefined {
const authType = getAuthType(this.instanceSettings);

@ -1,303 +0,0 @@
import { concat, find, flattenDeep, forEach, get, map } from 'lodash';
import { AnnotationEvent, dateTime, TimeSeries, VariableModel } from '@grafana/data';
import { AzureLogsTableData, AzureLogsVariable } from '../types';
import { AzureLogAnalyticsMetadata } from '../types/logAnalyticsMetadata';
export default class ResponseParser {
declare columns: string[];
constructor(private results: any) {}
parseQueryResult(): any {
let data: any[] = [];
let columns: any[] = [];
for (let i = 0; i < this.results.length; i++) {
if (this.results[i].result.tables.length === 0) {
continue;
}
columns = this.results[i].result.tables[0].columns;
const rows = this.results[i].result.tables[0].rows;
if (this.results[i].query.resultFormat === 'time_series') {
data = concat(data, this.parseTimeSeriesResult(this.results[i].query, columns, rows));
} else {
data = concat(data, this.parseTableResult(this.results[i].query, columns, rows));
}
}
return data;
}
parseTimeSeriesResult(query: { refId: string; query: any }, columns: any[], rows: any): TimeSeries[] {
const data: TimeSeries[] = [];
let timeIndex = -1;
let metricIndex = -1;
let valueIndex = -1;
for (let i = 0; i < columns.length; i++) {
if (timeIndex === -1 && columns[i].type === 'datetime') {
timeIndex = i;
}
if (metricIndex === -1 && columns[i].type === 'string') {
metricIndex = i;
}
if (valueIndex === -1 && ['int', 'long', 'real', 'double'].indexOf(columns[i].type) > -1) {
valueIndex = i;
}
}
if (timeIndex === -1) {
throw new Error('No datetime column found in the result. The Time Series format requires a time column.');
}
forEach(rows, (row) => {
const epoch = ResponseParser.dateTimeToEpoch(row[timeIndex]);
const metricName = metricIndex > -1 ? row[metricIndex] : columns[valueIndex].name;
const bucket = ResponseParser.findOrCreateBucket(data, metricName);
bucket.datapoints.push([row[valueIndex], epoch]);
bucket.refId = query.refId;
bucket.meta = {
executedQueryString: query.query,
};
});
return data;
}
parseTableResult(query: { refId: string; query: string }, columns: any[], rows: any[]): AzureLogsTableData {
const tableResult: AzureLogsTableData = {
type: 'table',
columns: map(columns, (col) => {
return { text: col.name, type: col.type };
}),
rows: rows,
refId: query.refId,
meta: {
executedQueryString: query.query,
},
};
return tableResult;
}
parseToVariables(): AzureLogsVariable[] {
const queryResult = this.parseQueryResult();
const variables: AzureLogsVariable[] = [];
forEach(queryResult, (result) => {
forEach(flattenDeep(result.rows), (row) => {
variables.push({
text: row,
value: row,
} as AzureLogsVariable);
});
});
return variables;
}
transformToAnnotations(options: any) {
const queryResult = this.parseQueryResult();
const list: AnnotationEvent[] = [];
forEach(queryResult, (result) => {
let timeIndex = -1;
let textIndex = -1;
let tagsIndex = -1;
for (let i = 0; i < result.columns.length; i++) {
if (timeIndex === -1 && result.columns[i].type === 'datetime') {
timeIndex = i;
}
if (textIndex === -1 && result.columns[i].text.toLowerCase() === 'text') {
textIndex = i;
}
if (tagsIndex === -1 && result.columns[i].text.toLowerCase() === 'tags') {
tagsIndex = i;
}
}
forEach(result.rows, (row) => {
list.push({
annotation: options.annotation,
time: Math.floor(ResponseParser.dateTimeToEpoch(row[timeIndex])),
text: row[textIndex] ? row[textIndex].toString() : '',
tags: row[tagsIndex] ? row[tagsIndex].trim().split(/\s*,\s*/) : [],
});
});
});
return list;
}
static findOrCreateBucket(data: TimeSeries[], target: any): TimeSeries {
let dataTarget: any = find(data, ['target', target]);
if (!dataTarget) {
dataTarget = { target: target, datapoints: [], refId: '', query: '' };
data.push(dataTarget);
}
return dataTarget;
}
static dateTimeToEpoch(dateTimeValue: any) {
return dateTime(dateTimeValue).valueOf();
}
static parseSubscriptions(result: any): Array<{ text: string; value: string }> {
const list: Array<{ text: string; value: string }> = [];
if (!result) {
return list;
}
const valueFieldName = 'subscriptionId';
const textFieldName = 'displayName';
for (let i = 0; i < result.value.length; i++) {
if (!find(list, ['value', get(result.value[i], valueFieldName)])) {
list.push({
text: `${get(result.value[i], textFieldName)}`,
value: get(result.value[i], valueFieldName),
});
}
}
return list;
}
}
// matches (name):(type) = (defaultValue)
// e.g. fromRangeStart:datetime = datetime(null)
// - name: fromRangeStart
// - type: datetime
// - defaultValue: datetime(null)
const METADATA_FUNCTION_PARAMS = /([\w\W]+):([\w]+)(?:\s?=\s?([\w\W]+))?/;
function transformMetadataFunction(sourceSchema: AzureLogAnalyticsMetadata) {
if (!sourceSchema.functions) {
return [];
}
return sourceSchema.functions.map((fn) => {
const params =
fn.parameters &&
fn.parameters
.split(', ')
.map((arg) => {
const match = arg.match(METADATA_FUNCTION_PARAMS);
if (!match) {
return;
}
const [, name, type, defaultValue] = match;
return {
name,
type,
defaultValue,
cslDefaultValue: defaultValue,
};
})
.filter(<T>(v: T): v is Exclude<T, undefined> => !!v);
return {
name: fn.name,
body: fn.body,
inputParameters: params || [],
};
});
}
export function transformMetadataToKustoSchema(
sourceSchema: AzureLogAnalyticsMetadata,
nameOrIdOrSomething: string,
templateVariables: VariableModel[]
) {
const database = {
name: nameOrIdOrSomething,
tables: sourceSchema.tables,
functions: transformMetadataFunction(sourceSchema),
majorVersion: 0,
minorVersion: 0,
};
// Adding macros as known functions
database.functions.push(
{
name: '$__timeFilter',
body: '{ true }',
inputParameters: [
{
name: 'timeColumn',
type: 'System.String',
defaultValue: '""',
cslDefaultValue: '""',
},
],
},
{
name: '$__timeFrom',
body: '{ datetime(2018-06-05T18:09:58.907Z) }',
inputParameters: [],
},
{
name: '$__timeTo',
body: '{ datetime(2018-06-05T20:09:58.907Z) }',
inputParameters: [],
},
{
name: '$__escapeMulti',
body: `{ @'\\grafana-vm\Network(eth0)\Total', @'\\hello!'}`,
inputParameters: [
{
name: '$myVar',
type: 'System.String',
defaultValue: '$myVar',
cslDefaultValue: '$myVar',
},
],
},
{
name: '$__contains',
body: `{ colName in ('value1','value2') }`,
inputParameters: [
{
name: 'colName',
type: 'System.String',
defaultValue: 'colName',
cslDefaultValue: 'colName',
},
{
name: '$myVar',
type: 'System.String',
defaultValue: '$myVar',
cslDefaultValue: '$myVar',
},
],
}
);
// Adding macros as global parameters
const globalParameters = templateVariables.map((v) => {
return {
name: `$${v.name}`,
type: 'dynamic',
};
});
return {
clusterType: 'Engine',
cluster: {
connectionString: nameOrIdOrSomething,
databases: [database],
},
database: database,
globalParameters,
};
}

@ -0,0 +1,133 @@
import { VariableModel } from '@grafana/data';
import { AzureLogAnalyticsMetadata } from '../types/logAnalyticsMetadata';
// matches (name):(type) = (defaultValue)
// e.g. fromRangeStart:datetime = datetime(null)
// - name: fromRangeStart
// - type: datetime
// - defaultValue: datetime(null)
const METADATA_FUNCTION_PARAMS = /([\w\W]+):([\w]+)(?:\s?=\s?([\w\W]+))?/;
function transformMetadataFunction(sourceSchema: AzureLogAnalyticsMetadata) {
if (!sourceSchema.functions) {
return [];
}
return sourceSchema.functions.map((fn) => {
const params =
fn.parameters &&
fn.parameters
.split(', ')
.map((arg) => {
const match = arg.match(METADATA_FUNCTION_PARAMS);
if (!match) {
return;
}
const [, name, type, defaultValue] = match;
return {
name,
type,
defaultValue,
cslDefaultValue: defaultValue,
};
})
.filter(<T>(v: T): v is Exclude<T, undefined> => !!v);
return {
name: fn.name,
body: fn.body,
inputParameters: params || [],
};
});
}
export function transformMetadataToKustoSchema(
sourceSchema: AzureLogAnalyticsMetadata,
nameOrIdOrSomething: string,
templateVariables: VariableModel[]
) {
const database = {
name: nameOrIdOrSomething,
tables: sourceSchema.tables,
functions: transformMetadataFunction(sourceSchema),
majorVersion: 0,
minorVersion: 0,
};
// Adding macros as known functions
database.functions.push(
{
name: '$__timeFilter',
body: '{ true }',
inputParameters: [
{
name: 'timeColumn',
type: 'System.String',
defaultValue: '""',
cslDefaultValue: '""',
},
],
},
{
name: '$__timeFrom',
body: '{ datetime(2018-06-05T18:09:58.907Z) }',
inputParameters: [],
},
{
name: '$__timeTo',
body: '{ datetime(2018-06-05T20:09:58.907Z) }',
inputParameters: [],
},
{
name: '$__escapeMulti',
body: `{ @'\\grafana-vm\Network(eth0)\Total', @'\\hello!'}`,
inputParameters: [
{
name: '$myVar',
type: 'System.String',
defaultValue: '$myVar',
cslDefaultValue: '$myVar',
},
],
},
{
name: '$__contains',
body: `{ colName in ('value1','value2') }`,
inputParameters: [
{
name: 'colName',
type: 'System.String',
defaultValue: 'colName',
cslDefaultValue: 'colName',
},
{
name: '$myVar',
type: 'System.String',
defaultValue: '$myVar',
cslDefaultValue: '$myVar',
},
],
}
);
// Adding macros as global parameters
const globalParameters = templateVariables.map((v) => {
return {
name: `$${v.name}`,
type: 'dynamic',
};
});
return {
clusterType: 'Engine',
cluster: {
connectionString: nameOrIdOrSomething,
databases: [database],
},
database: database,
globalParameters,
};
}

@ -7,7 +7,7 @@ import createMockQuery from '../__mocks__/query';
import { createTemplateVariables } from '../__mocks__/utils';
import { multiVariable, singleVariable, subscriptionsVariable } from '../__mocks__/variables';
import AzureMonitorDatasource from '../datasource';
import { AzureDataSourceJsonData, AzureMonitorLocationsResponse, AzureQueryType } from '../types';
import { AzureAPIResponse, AzureDataSourceJsonData, AzureQueryType, Location } from '../types';
const templateSrv = new TemplateSrv();
@ -477,7 +477,7 @@ describe('AzureMonitorDatasource', () => {
});
describe('When performing getLocations', () => {
const sub1Response: AzureMonitorLocationsResponse = {
const sub1Response: AzureAPIResponse<Location> = {
value: [
{
id: '/subscriptions/mock-subscription-id-1/locations/northeurope',
@ -497,7 +497,7 @@ describe('AzureMonitorDatasource', () => {
],
};
const sub2Response: AzureMonitorLocationsResponse = {
const sub2Response: AzureAPIResponse<Location> = {
value: [
{
id: '/subscriptions/mock-subscription-id-2/locations/eastus2',

@ -1,3 +1,4 @@
import { Namespace } from 'i18next';
import { find, startsWith } from 'lodash';
import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
@ -8,11 +9,8 @@ import { getAuthType, getAzureCloud, getAzurePortalUrl } from '../credentials';
import TimegrainConverter from '../time_grain_converter';
import {
AzureDataSourceJsonData,
AzureMonitorMetricNamespacesResponse,
AzureMonitorMetricNamesResponse,
AzureMonitorMetricsMetadataResponse,
AzureMonitorQuery,
AzureMonitorResourceGroupsResponse,
AzureQueryType,
DatasourceValidationResult,
GetMetricNamespacesQuery,
@ -21,8 +19,12 @@ import {
AzureMetricQuery,
AzureMonitorLocations,
AzureMonitorProvidersResponse,
AzureMonitorLocationsResponse,
AzureAPIResponse,
AzureGetResourceNamesQuery,
Subscription,
Location,
ResourceGroup,
Metric,
} from '../types';
import { routeNames } from '../utils/common';
import migrateQuery from '../utils/migrateQuery';
@ -162,7 +164,9 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
return [];
}
return this.getResource(`${this.resourcePath}/subscriptions?api-version=2019-03-01`).then((result: any) => {
return this.getResource<AzureAPIResponse<Subscription>>(
`${this.resourcePath}/subscriptions?api-version=2019-03-01`
).then((result) => {
return ResponseParser.parseSubscriptions(result);
});
}
@ -170,8 +174,8 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
getResourceGroups(subscriptionId: string) {
return this.getResource(
`${this.resourcePath}/subscriptions/${subscriptionId}/resourceGroups?api-version=${this.listByResourceGroupApiVersion}`
).then((result: AzureMonitorResourceGroupsResponse) => {
return ResponseParser.parseResponseValues(result, 'name', 'name');
).then((result: AzureAPIResponse<ResourceGroup>) => {
return ResponseParser.parseResponseValues<ResourceGroup>(result, 'name', 'name');
});
}
@ -199,7 +203,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
if (skipToken) {
url += `&$skiptoken=${skipToken}`;
}
return this.getResource(url).then(async (result: any) => {
return this.getResource(url).then(async (result) => {
let list: Array<{ text: string; value: string }> = [];
if (startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/')) {
list = ResponseParser.parseResourceNames(result, 'microsoft.storage/storageaccounts');
@ -239,7 +243,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
this.templateSrv
);
return this.getResource(url)
.then((result: AzureMonitorMetricNamespacesResponse) => {
.then((result: AzureAPIResponse<Namespace>) => {
return ResponseParser.parseResponseValues(
result,
'properties.metricNamespaceName',
@ -273,7 +277,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
this.replaceSingleTemplateVariables(query),
this.templateSrv
);
return this.getResource(url).then((result: AzureMonitorMetricNamesResponse) => {
return this.getResource(url).then((result: AzureAPIResponse<Metric>) => {
return ResponseParser.parseResponseValues(result, 'name.localizedValue', 'name.value');
});
}
@ -366,7 +370,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
const locationMap = new Map<string, AzureMonitorLocations>();
for (const subscription of subscriptions) {
const subLocations = ResponseParser.parseLocations(
await this.getResource<AzureMonitorLocationsResponse>(
await this.getResource<AzureAPIResponse<Location>>(
`${routeNames.azureMonitor}/subscriptions/${this.templateSrv.replace(subscription)}/locations?api-version=${
this.locationsApiVersion
}`

@ -1,5 +1,7 @@
import { find, get } from 'lodash';
import { FetchResponse } from '@grafana/runtime';
import TimeGrainConverter from '../time_grain_converter';
import {
AzureMonitorLocalizedValue,
@ -7,11 +9,14 @@ import {
AzureMonitorMetricAvailabilityMetadata,
AzureMonitorMetricsMetadataResponse,
AzureMonitorOption,
AzureMonitorLocationsResponse,
AzureAPIResponse,
Location,
Subscription,
Resource,
} from '../types';
export default class ResponseParser {
static parseResponseValues(
result: any,
static parseResponseValues<T>(
result: AzureAPIResponse<T>,
textFieldName: string,
valueFieldName: string
): Array<{ text: string; value: string }> {
@ -35,7 +40,10 @@ export default class ResponseParser {
return list;
}
static parseResourceNames(result: any, metricNamespace?: string): Array<{ text: string; value: string }> {
static parseResourceNames(
result: AzureAPIResponse<Resource>,
metricNamespace?: string
): Array<{ text: string; value: string }> {
const list: Array<{ text: string; value: string }> = [];
if (!result) {
@ -110,7 +118,7 @@ export default class ResponseParser {
});
}
static parseSubscriptions(result: any): Array<{ text: string; value: string }> {
static parseSubscriptions(result: AzureAPIResponse<Subscription>): Array<{ text: string; value: string }> {
const list: Array<{ text: string; value: string }> = [];
if (!result) {
@ -131,7 +139,9 @@ export default class ResponseParser {
return list;
}
static parseSubscriptionsForSelect(result: any): Array<{ label: string; value: string }> {
static parseSubscriptionsForSelect(
result?: FetchResponse<AzureAPIResponse<Subscription>>
): Array<{ label: string; value: string }> {
const list: Array<{ label: string; value: string }> = [];
if (!result) {
@ -152,28 +162,7 @@ export default class ResponseParser {
return list;
}
static parseWorkspacesForSelect(result: any): Array<{ label: string; value: string }> {
const list: Array<{ label: string; value: string }> = [];
if (!result) {
return list;
}
const valueFieldName = 'customerId';
const textFieldName = 'name';
for (let i = 0; i < result.data.value.length; i++) {
if (!find(list, ['value', get(result.data.value[i].properties, valueFieldName)])) {
list.push({
label: get(result.data.value[i], textFieldName),
value: get(result.data.value[i].properties, valueFieldName),
});
}
}
return list;
}
static parseLocations(result: AzureMonitorLocationsResponse) {
static parseLocations(result: AzureAPIResponse<Location>) {
const locations: AzureMonitorLocations[] = [];
if (!result) {

@ -3,14 +3,13 @@ import { set, get } from 'lodash';
import { backendSrv } from 'app/core/services/backend_srv';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { Context, createContext } from '../__mocks__/datasource';
import createMockQuery from '../__mocks__/query';
import { createTemplateVariables } from '../__mocks__/utils';
import { multiVariable, singleVariable, subscriptionsVariable } from '../__mocks__/variables';
import AzureMonitorDatasource from '../datasource';
import { AzureQueryType } from '../types';
import AzureResourceGraphDatasource from './azure_resource_graph_datasource';
const templateSrv = new TemplateSrv({
getVariables: () => [subscriptionsVariable, singleVariable, multiVariable],
getVariableWithName: jest.fn(),
@ -32,15 +31,15 @@ describe('AzureResourceGraphDatasource', () => {
datasourceRequestMock.mockImplementation(jest.fn());
});
const ctx: any = {};
let ctx: Context;
beforeEach(() => {
ctx.instanceSettings = {
url: 'http://azureresourcegraphapi',
jsonData: { subscriptionId: '9935389e-9122-4ef9-95f9-1513dd24753f', cloudName: 'azuremonitor' },
};
ctx.ds = new AzureResourceGraphDatasource(ctx.instanceSettings);
ctx = createContext({
instanceSettings: {
url: 'http://azureresourcegraphapi',
jsonData: { subscriptionId: '9935389e-9122-4ef9-95f9-1513dd24753f', cloudName: 'azuremonitor' },
},
});
});
describe('When performing interpolateVariablesInQueries for azure_resource_graph', () => {
@ -51,7 +50,7 @@ describe('AzureResourceGraphDatasource', () => {
it('should return a query unchanged if no template variables are provided', () => {
const query = createMockQuery();
query.queryType = AzureQueryType.AzureResourceGraph;
const templatedQuery = ctx.ds.interpolateVariablesInQueries([query], {});
const templatedQuery = ctx.datasource.azureResourceGraphDatasource.interpolateVariablesInQueries([query], {});
expect(templatedQuery[0]).toEqual(query);
});
@ -70,7 +69,7 @@ describe('AzureResourceGraphDatasource', () => {
...query.azureResourceGraph,
...azureResourceGraph,
};
const templatedQuery = ctx.ds.interpolateVariablesInQueries([query], {});
const templatedQuery = ctx.datasource.azureResourceGraphDatasource.interpolateVariablesInQueries([query], {});
expect(templatedQuery[0]).toHaveProperty('datasource');
for (const [path, templateVariable] of templateVariables.entries()) {
expect(get(templatedQuery[0].azureResourceGraph, path)).toEqual(
@ -86,53 +85,63 @@ describe('AzureResourceGraphDatasource', () => {
});
it('should expand single value template variable', () => {
const target = {
const target = createMockQuery({
subscriptions: [],
azureResourceGraph: {
query: 'Resources | $var1',
resultFormat: '',
},
};
expect(ctx.ds.applyTemplateVariables(target)).toStrictEqual({
azureResourceGraph: { query: 'Resources | var1-foo', resultFormat: 'table' },
queryType: 'Azure Resource Graph',
subscriptions: [],
});
expect(ctx.datasource.azureResourceGraphDatasource.applyTemplateVariables(target, {})).toEqual(
expect.objectContaining({
...target,
azureResourceGraph: { query: 'Resources | var1-foo', resultFormat: 'table' },
queryType: 'Azure Resource Graph',
subscriptions: [],
})
);
});
it('should expand multi value template variable', () => {
const target = {
const target = createMockQuery({
subscriptions: [],
azureResourceGraph: {
query: 'resources | where $__contains(name, $var3)',
resultFormat: '',
},
};
expect(ctx.ds.applyTemplateVariables(target)).toStrictEqual({
azureResourceGraph: {
query: `resources | where $__contains(name, 'var3-foo','var3-baz')`,
resultFormat: 'table',
},
queryType: 'Azure Resource Graph',
subscriptions: [],
});
expect(ctx.datasource.azureResourceGraphDatasource.applyTemplateVariables(target, {})).toEqual(
expect.objectContaining({
...target,
azureResourceGraph: {
query: `resources | where $__contains(name, 'var3-foo','var3-baz')`,
resultFormat: 'table',
},
queryType: 'Azure Resource Graph',
subscriptions: [],
})
);
});
});
it('should apply subscription variable', () => {
const target = {
const target = createMockQuery({
subscriptions: ['$subs'],
azureResourceGraph: {
query: 'resources | where $__contains(name, $var3)',
resultFormat: '',
},
};
expect(ctx.ds.applyTemplateVariables(target)).toStrictEqual({
azureResourceGraph: {
query: `resources | where $__contains(name, 'var3-foo','var3-baz')`,
resultFormat: 'table',
},
queryType: 'Azure Resource Graph',
subscriptions: ['sub-foo', 'sub-baz'],
});
expect(ctx.datasource.azureResourceGraphDatasource.applyTemplateVariables(target, {})).toEqual(
expect.objectContaining({
azureResourceGraph: {
query: `resources | where $__contains(name, 'var3-foo','var3-baz')`,
resultFormat: 'table',
},
queryType: 'Azure Resource Graph',
subscriptions: ['sub-foo', 'sub-baz'],
})
);
});
describe('When performing targetContainsTemplate', () => {

@ -25,7 +25,7 @@ export default class AzureResourceGraphDatasource extends DataSourceWithBackend<
const variableNames = templateSrv.getVariables().map((v) => `$${v.name}`);
const subscriptionVar = _.find(target.subscriptions, (sub) => _.includes(variableNames, sub));
const interpolatedSubscriptions = templateSrv
.replace(subscriptionVar, scopedVars, (v: any) => v)
.replace(subscriptionVar, scopedVars, (v: string[] | string) => v)
.split(',')
.filter((v) => v.length > 0);
const subscriptions = [

@ -6,7 +6,13 @@ import { Alert, SecureSocksProxySettings } from '@grafana/ui';
import { config } from 'app/core/config';
import ResponseParser from '../azure_monitor/response_parser';
import { AzureDataSourceJsonData, AzureDataSourceSecureJsonData, AzureDataSourceSettings } from '../types';
import {
AzureAPIResponse,
AzureDataSourceJsonData,
AzureDataSourceSecureJsonData,
AzureDataSourceSettings,
Subscription,
} from '../types';
import { routeNames } from '../utils/common';
import { MonitorConfig } from './MonitorConfig';
@ -62,7 +68,7 @@ export class ConfigEditor extends PureComponent<Props, State> {
const query = `?api-version=2019-03-01`;
try {
const result = await getBackendSrv()
.fetch({
.fetch<AzureAPIResponse<Subscription>>({
url: this.baseURL + query,
method: 'GET',
})

@ -1,3 +1,4 @@
import { Uri } from 'monaco-editor';
import React, { useCallback, useEffect, useRef } from 'react';
import { CodeEditor, Monaco, MonacoEditor } from '@grafana/ui';
@ -15,7 +16,7 @@ interface MonacoPromise {
interface MonacoLanguages {
kusto: {
getKustoWorker: () => Promise<
(url: any) => Promise<{
(url: Uri) => Promise<{
setSchema: (schema: any, clusterUrl: string, name: string) => void;
}>
>;

@ -139,10 +139,6 @@ export default class Datasource extends DataSourceWithBackend<AzureMonitorQuery,
return !!subQuery && this.templateSrv.containsTemplate(subQuery);
}
async annotationQuery(options: any) {
return this.azureLogAnalyticsDatasource.annotationQuery(options);
}
/* Azure Monitor REST API methods */
getResourceGroups(subscriptionId: string) {
return this.azureMonitorDatasource.getResourceGroups(this.templateSrv.replace(subscriptionId));

@ -1,186 +0,0 @@
import { dateTime } from '@grafana/data';
import LogAnalyticsQuerystringBuilder from './querystring_builder';
describe('LogAnalyticsDatasource', () => {
let builder: LogAnalyticsQuerystringBuilder;
beforeEach(() => {
builder = new LogAnalyticsQuerystringBuilder(
'query=Tablename | where $__timeFilter()',
{
interval: '5m',
range: {
from: dateTime().subtract(24, 'hours'),
to: dateTime(),
},
rangeRaw: {
from: 'now-24h',
to: 'now',
},
},
'TimeGenerated'
);
});
describe('when $__timeFilter has no column parameter', () => {
it('should generate a time filter condition with TimeGenerated as the datetime field', () => {
const query = builder.generate().uriString;
expect(query).toContain('where%20TimeGenerated%20%3E%3D%20datetime(');
});
});
describe('when $__timeFilter has a column parameter', () => {
beforeEach(() => {
builder.rawQueryString = 'query=Tablename | where $__timeFilter(myTime)';
});
it('should generate a time filter condition with myTime as the datetime field', () => {
const query = builder.generate().uriString;
expect(query).toContain('where%20myTime%20%3E%3D%20datetime(');
});
});
describe('when $__contains and multi template variable has custom All value', () => {
beforeEach(() => {
builder.rawQueryString = 'query=Tablename | where $__contains(col, all)';
});
it('should generate a where..in clause', () => {
const query = builder.generate().rawQuery;
expect(query).toContain(`where 1 == 1`);
});
});
describe('when $__contains and multi template variable has one selected value', () => {
beforeEach(() => {
builder.rawQueryString = `query=Tablename | where $__contains(col, 'val1')`;
});
it('should generate a where..in clause', () => {
const query = builder.generate().rawQuery;
expect(query).toContain(`where col in ('val1')`);
});
});
describe('when $__contains and multi template variable has multiple selected values', () => {
beforeEach(() => {
builder.rawQueryString = `query=Tablename | where $__contains(col, 'val1','val2')`;
});
it('should generate a where..in clause', () => {
const query = builder.generate().rawQuery;
expect(query).toContain(`where col in ('val1','val2')`);
});
});
describe('when $__interval is in the query', () => {
beforeEach(() => {
builder.rawQueryString = 'query=Tablename | summarize count() by Category, bin(TimeGenerated, $__interval)';
});
it('should replace $__interval with the inbuilt interval option', () => {
const query = builder.generate().uriString;
expect(query).toContain('bin(TimeGenerated%2C%205m');
});
});
describe('when using $__timeFrom and $__timeTo is in the query and range is until now', () => {
beforeEach(() => {
builder.rawQueryString = 'query=Tablename | where myTime >= $__timeFrom() and myTime <= $__timeTo()';
});
it('should replace $__timeFrom and $__timeTo with a datetime and the now() function', () => {
const query = builder.generate().uriString;
expect(query).toContain('where%20myTime%20%3E%3D%20datetime(');
expect(query).toContain('myTime%20%3C%3D%20datetime(');
});
});
describe('when using $__timeFrom and $__timeTo is in the query and range is a specific interval', () => {
beforeEach(() => {
builder.rawQueryString = 'query=Tablename | where myTime >= $__timeFrom() and myTime <= $__timeTo()';
builder.options.range.to = dateTime().subtract(1, 'hour');
builder.options.rangeRaw.to = 'now-1h';
});
it('should replace $__timeFrom and $__timeTo with datetimes', () => {
const query = builder.generate().uriString;
expect(query).toContain('where%20myTime%20%3E%3D%20datetime(');
expect(query).toContain('myTime%20%3C%3D%20datetime(');
});
});
describe('when using $__escape and multi template variable has one selected value', () => {
beforeEach(() => {
builder.rawQueryString = `$__escapeMulti('\\grafana-vm\Network(eth0)\Total Bytes Received')`;
});
it('should replace $__escape(val) with KQL style escaped string', () => {
const query = builder.generate().uriString;
expect(query).toContain(`%40'%5Cgrafana-vmNetwork(eth0)Total%20Bytes%20Received'`);
});
});
describe('when using $__escape and multi template variable has multiple selected values', () => {
beforeEach(() => {
builder.rawQueryString = `CounterPath in ($__escapeMulti('\\grafana-vm\Network(eth0)\Total','\\grafana-vm\Network(eth0)\Total'))`;
});
it('should replace $__escape(val) with multiple KQL style escaped string', () => {
const query = builder.generate().uriString;
expect(query).toContain(
`CounterPath%20in%20(%40'%5Cgrafana-vmNetwork(eth0)Total'%2C%20%40'%5Cgrafana-vmNetwork(eth0)Total')`
);
});
});
describe('when using $__escape and multi template variable has one selected value that contains comma', () => {
beforeEach(() => {
builder.rawQueryString = `$__escapeMulti('\\grafana-vm,\Network(eth0)\Total Bytes Received')`;
});
it('should replace $__escape(val) with KQL style escaped string', () => {
const query = builder.generate().uriString;
expect(query).toContain(`%40'%5Cgrafana-vm%2CNetwork(eth0)Total%20Bytes%20Received'`);
});
});
describe(`when using $__escape and multi template variable value is not wrapped in single '`, () => {
beforeEach(() => {
builder.rawQueryString = `$__escapeMulti(\\grafana-vm,\Network(eth0)\Total Bytes Received)`;
});
it('should not replace macro', () => {
const query = builder.generate().uriString;
expect(query).toContain(`%24__escapeMulti(%5Cgrafana-vm%2CNetwork(eth0)Total%20Bytes%20Received)`);
});
});
describe('when there is no raw range', () => {
it('should still generate a time filter condition', () => {
builder = new LogAnalyticsQuerystringBuilder(
'query=Tablename | where $__timeFilter()',
{
interval: '5m',
range: {
from: dateTime().subtract(24, 'hours'),
to: dateTime(),
},
},
'TimeGenerated'
);
const query = builder.generate().uriString;
expect(query).toContain('where%20TimeGenerated%20%20%3E%3D%20datetime(');
});
});
});

@ -1,85 +0,0 @@
import { dateTime } from '@grafana/data';
export default class LogAnalyticsQuerystringBuilder {
constructor(public rawQueryString: string, public options: any, public defaultTimeField: any) {}
generate() {
let queryString = this.rawQueryString;
const macroRegexp = /\$__([_a-zA-Z0-9]+)\(([^()]*)\)/gi;
queryString = queryString.replace(macroRegexp, (match, p1, p2) => {
if (p1 === 'contains') {
return this.getMultiContains(p2);
}
return match;
});
queryString = queryString.replace(/\$__escapeMulti\(('[^]*')\)/gi, (match, p1) => this.escape(p1));
if (this.options) {
queryString = queryString.replace(macroRegexp, (match, p1, p2) => {
if (p1 === 'timeFilter') {
return this.getTimeFilter(p2, this.options);
}
if (p1 === 'timeFrom') {
return this.getFrom(this.options);
}
if (p1 === 'timeTo') {
return this.getUntil(this.options);
}
return match;
});
queryString = queryString.replace(/\$__interval/gi, this.options.interval);
}
const rawQuery = queryString;
queryString = encodeURIComponent(queryString);
const uriString = `query=${queryString}`;
return { uriString, rawQuery };
}
getFrom(options: any) {
const from = options.range.from;
return `datetime(${dateTime(from).startOf('minute').toISOString()})`;
}
getUntil(options: any) {
if (options.rangeRaw?.to === 'now') {
const now = Date.now();
return `datetime(${dateTime(now).startOf('minute').toISOString()})`;
} else {
const until = options.range.to;
return `datetime(${dateTime(until).startOf('minute').toISOString()})`;
}
}
getTimeFilter(timeFieldArg: any, options: any) {
const timeField = timeFieldArg || this.defaultTimeField;
if (options.rangeRaw?.to === 'now') {
return `${timeField} >= ${this.getFrom(options)}`;
} else {
return `${timeField} >= ${this.getFrom(options)} and ${timeField} <= ${this.getUntil(options)}`;
}
}
getMultiContains(inputs: string) {
const firstCommaIndex = inputs.indexOf(',');
const field = inputs.substring(0, firstCommaIndex);
const templateVar = inputs.substring(inputs.indexOf(',') + 1);
if (templateVar && templateVar.toLowerCase().trim() === 'all') {
return '1 == 1';
}
return `${field.trim()} in (${templateVar.trim()})`;
}
escape(inputs: string) {
return inputs
.substring(1, inputs.length - 1)
.split(`','`)
.map((v) => `@'${v}'`)
.join(', ');
}
}

@ -3,7 +3,7 @@ import { includes, filter } from 'lodash';
import { rangeUtil } from '@grafana/data';
export default class TimeGrainConverter {
static createISO8601Duration(timeGrain: string | number, timeGrainUnit: any) {
static createISO8601Duration(timeGrain: string | number, timeGrainUnit: string) {
const timeIntervals = ['hour', 'minute', 'h', 'm'];
if (includes(timeIntervals, timeGrainUnit)) {
return `PT${timeGrain}${timeGrainUnit[0].toUpperCase()}`;
@ -33,7 +33,7 @@ export default class TimeGrainConverter {
return TimeGrainConverter.createISO8601Duration(timeGrain, unit);
}
static findClosestTimeGrain(interval: any, allowedTimeGrains: string[]) {
static findClosestTimeGrain(interval: string, allowedTimeGrains: string[]) {
const timeGrains = filter(allowedTimeGrains, (o) => o !== 'auto');
let closest = timeGrains[0];

@ -4,7 +4,6 @@ import {
DataSourceSettings,
PanelData,
SelectableValue,
TableData,
} from '@grafana/data';
import Datasource from '../datasource';
@ -101,23 +100,6 @@ export interface AzureMonitorMetricMetadataItem {
metricAvailabilities?: AzureMonitorMetricAvailabilityMetadata[];
}
export interface AzureMonitorMetricNamespacesResponse {
value: AzureMonitorMetricNamespaceItem[];
}
export interface AzureMonitorMetricNamespaceItem {
name: string;
properties: { metricNamespacename: string };
}
export interface AzureMonitorMetricNamesResponse {
value: AzureMonitorMetricNameItem[];
}
export interface AzureMonitorMetricNameItem {
name: { value: string; localizedValue: string };
}
export interface AzureMonitorMetricAvailabilityMetadata {
timeGrain: string;
retention: string;
@ -128,30 +110,11 @@ export interface AzureMonitorLocalizedValue {
localizedValue: string;
}
export interface AzureMonitorResourceGroupsResponse {
data: {
value: Array<{ name: string }>;
};
status: number;
statusText: string;
}
export interface AzureLogsVariable {
text: string;
value: string;
}
export interface AzureLogsTableData extends TableData {
columns: AzureLogsTableColumn[];
rows: any[];
type: string;
}
export interface AzureLogsTableColumn {
text: string;
type: string;
}
export interface AzureMonitorOption<T = string> {
label: string;
value: T;
@ -293,11 +256,17 @@ export interface ProviderResourceType {
capabilities: string;
}
export interface AzureMonitorLocationsResponse {
value: Location[];
export interface AzureAPIResponse<T> {
value: T[];
count?: {
type: string;
value: number;
};
status?: number;
statusText?: string;
}
interface Location {
export interface Location {
id: string;
name: string;
displayName: string;
@ -319,3 +288,100 @@ interface LocationPairedRegion {
name: string;
id: string;
}
export interface Subscription {
id: string;
authorizationSource: string;
subscriptionId: string;
tenantId: string;
displayName: string;
state: string;
subscriptionPolicies: {
locationPlacementId: string;
quotaId: string;
spendingLimit: string;
};
}
export interface Workspace {
properties: {
customerId: string;
provisioningState: string;
sku: {
name: string;
};
retentionInDays: number;
publicNetworkAccessForQuery: string;
publicNetworkAccessForIngestion: string;
};
id: string;
name: string;
type: string;
location: string;
tags: Record<string, string>;
}
export interface Resource {
changedTime: string;
createdTime: string;
extendedLocation: { name: string; type: string };
id: string;
identity: { principalId: string; tenantId: string; type: string; userAssignedIdentities: string[] };
kind: string;
location: string;
managedBy: string;
name: string;
plan: { name: string; product: string; promotionCode: string; publisher: string; version: string };
properties: Record<string, string>;
provisioningState: string;
sku: { capacity: number; family: string; model: string; name: string; size: string; tier: string };
tags: Record<string, string>;
type: string;
}
export interface ResourceGroup {
id: string;
location: string;
managedBy: string;
name: string;
properties: { provisioningState: string };
tags: object;
type: string;
}
export interface Namespace {
classification: {
Custom: string;
Platform: string;
Qos: string;
};
id: string;
name: string;
properties: { metricNamespaceName: string };
type: string;
}
export interface Metric {
displayDescription: string;
errorCode: string;
errorMessage: string;
id: string;
name: AzureMonitorLocalizedValue;
timeseries: Array<{ data: MetricValue[]; metadatavalues: MetricMetadataValue[] }>;
type: string;
unit: string;
}
interface MetricValue {
average: number;
count: number;
maximum: number;
minimum: number;
timeStamp: string;
total: number;
}
interface MetricMetadataValue {
name: AzureMonitorLocalizedValue;
value: string;
}

@ -1,9 +1,8 @@
import { map } from 'lodash';
import { rangeUtil, SelectableValue } from '@grafana/data';
import { SelectableValue } from '@grafana/data';
import { VariableWithMultiSupport } from 'app/features/variables/types';
import TimegrainConverter from '../time_grain_converter';
import { AzureMonitorOption, VariableOptionGroup } from '../types';
export const hasOption = (options: AzureMonitorOption[], value: string): boolean =>
@ -37,16 +36,6 @@ export const addValueToOptions = (
return options;
};
export function convertTimeGrainsToMs<T extends { value: string }>(timeGrains: T[]) {
const allowedTimeGrainsMs: number[] = [];
timeGrains.forEach((tg: any) => {
if (tg.value !== 'auto') {
allowedTimeGrainsMs.push(rangeUtil.intervalToMs(TimegrainConverter.createKbnUnitFromISO8601Duration(tg.value)));
}
});
return allowedTimeGrainsMs;
}
// Route definitions shared with the backend.
// Check: /pkg/tsdb/azuremonitor/azuremonitor-resource-handler.go <registerRoutes>
export const routeNames = {
@ -56,7 +45,10 @@ export const routeNames = {
resourceGraph: 'resourcegraph',
};
export function interpolateVariable(value: any, variable: VariableWithMultiSupport) {
export function interpolateVariable(
value: string | number | Array<string | number>,
variable: VariableWithMultiSupport
) {
if (typeof value === 'string') {
// When enabling multiple responses, quote the value to mimic the array result below
// even if only one response is selected. This does not apply if only the "include all"

Loading…
Cancel
Save