Azure Monitor: Add support for Metric Names variables (#52322)

* Azure Monitor: Add support for Metric Names variables

* Azure Monitor: Add support for Workspaces variables (#52323)
This commit is contained in:
Andres Martinez Gotor 2022-07-20 11:04:01 +02:00 committed by GitHub
parent d6d49d8ba3
commit 2538fca53a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 230 additions and 63 deletions

View File

@ -321,5 +321,25 @@ describe('AzureMonitorUrlBuilder', () => {
);
});
});
describe('when metric definition does not contain a metric namespace', () => {
it('should build the getMetricNames url in the longer format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
'',
'2017-05-01-preview',
{
subscription: 'sub1',
resourceGroup: 'rg',
metricDefinition: 'microsoft.compute/virtualmachines',
resourceName: 'rn1',
},
templateSrv
);
expect(url).toBe(
'/subscriptions/sub1/resourceGroups/rg/providers/microsoft.compute/virtualmachines/rn1/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview'
);
});
});
});
});

View File

@ -82,9 +82,10 @@ export default class UrlBuilder {
);
}
return (
`${baseUrl}${resourceUri}/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}` +
`&metricnamespace=${encodeURIComponent(metricNamespace)}`
);
let url = `${baseUrl}${resourceUri}/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}`;
if (metricNamespace) {
url += `&metricnamespace=${encodeURIComponent(metricNamespace)}`;
}
return url;
}
}

View File

@ -33,6 +33,7 @@ const defaultProps = {
getSubscriptions: jest.fn().mockResolvedValue([{ text: 'Primary Subscription', value: 'sub' }]),
getResourceGroups: jest.fn().mockResolvedValue([{ text: 'rg', value: 'rg' }]),
getMetricNamespaces: jest.fn().mockResolvedValue([{ text: 'foo/bar', value: 'foo/bar' }]),
getResourceNames: jest.fn().mockResolvedValue([{ text: 'foobar', value: 'foobar' }]),
getVariablesRaw: jest.fn().mockReturnValue([
{ label: 'query0', name: 'sub0' },
{ label: 'query1', name: 'rg', query: { queryType: AzureQueryType.ResourceGroupsQuery } },
@ -143,8 +144,24 @@ describe('VariableEditor:', () => {
});
describe('predefined queries:', () => {
it('should show the new query types if feature gate is enabled', async () => {
const selectAndRerender = async (
label: string,
text: string,
onChange: jest.Mock,
rerender: (ui: React.ReactElement) => void
) => {
openMenu(screen.getByLabelText(label));
screen.getByText(text).click();
// Simulate onChange behavior
const newQuery = onChange.mock.calls.at(-1)[0];
rerender(<VariableEditor {...defaultProps} query={newQuery} onChange={onChange} />);
await waitFor(() => expect(screen.getByText(text)).toBeInTheDocument());
};
beforeEach(() => {
grafanaRuntime.config.featureToggles.azTemplateVars = true;
});
it('should show the new query types if feature gate is enabled', async () => {
render(<VariableEditor {...defaultProps} />);
openMenu(screen.getByLabelText('select query type'));
await waitFor(() => expect(screen.getByText('Subscriptions')).toBeInTheDocument());
@ -158,36 +175,21 @@ describe('VariableEditor:', () => {
});
it('should run the query if requesting subscriptions', async () => {
grafanaRuntime.config.featureToggles.azTemplateVars = true;
const onChange = jest.fn();
const { rerender } = render(<VariableEditor {...defaultProps} onChange={onChange} />);
openMenu(screen.getByLabelText('select query type'));
screen.getByText('Subscriptions').click();
// Simulate onChange behavior
const newQuery = onChange.mock.calls.at(-1)[0];
rerender(<VariableEditor {...defaultProps} query={newQuery} onChange={onChange} />);
await waitFor(() => expect(screen.getByText('Subscriptions')).toBeInTheDocument());
await selectAndRerender('select query type', 'Subscriptions', onChange, rerender);
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({ queryType: AzureQueryType.SubscriptionsQuery, refId: 'A' })
);
});
it('should run the query if requesting resource groups', async () => {
grafanaRuntime.config.featureToggles.azTemplateVars = true;
const onChange = jest.fn();
const { rerender } = render(<VariableEditor {...defaultProps} onChange={onChange} />);
// wait for initial load
await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument());
// Select RGs variable
openMenu(screen.getByLabelText('select query type'));
screen.getByText('Resource Groups').click();
// Simulate onChange behavior
const newQuery = onChange.mock.calls.at(-1)[0];
rerender(<VariableEditor {...defaultProps} query={newQuery} onChange={onChange} />);
await waitFor(() => expect(screen.getByText('Select subscription')).toBeInTheDocument());
// Select a subscription
openMenu(screen.getByLabelText('select subscription'));
screen.getByText('Primary Subscription').click();
await selectAndRerender('select query type', 'Resource Groups', onChange, rerender);
await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender);
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
queryType: AzureQueryType.ResourceGroupsQuery,
@ -199,17 +201,10 @@ describe('VariableEditor:', () => {
it('should show template variables as options ', async () => {
const onChange = jest.fn();
grafanaRuntime.config.featureToggles.azTemplateVars = true;
const { rerender } = render(<VariableEditor {...defaultProps} onChange={onChange} />);
// wait for initial load
await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument());
// Select RGs variable
openMenu(screen.getByLabelText('select query type'));
screen.getByText('Resource Groups').click();
// Simulate onChange behavior
const newQuery = onChange.mock.calls.at(-1)[0];
rerender(<VariableEditor {...defaultProps} query={newQuery} onChange={onChange} />);
await waitFor(() => expect(screen.getByText('Select subscription')).toBeInTheDocument());
await selectAndRerender('select query type', 'Resource Groups', onChange, rerender);
// Select a subscription
openMenu(screen.getByLabelText('select subscription'));
await waitFor(() => expect(screen.getByText('Primary Subscription')).toBeInTheDocument());
@ -223,21 +218,12 @@ describe('VariableEditor:', () => {
});
it('should run the query if requesting namespaces', async () => {
grafanaRuntime.config.featureToggles.azTemplateVars = true;
const onChange = jest.fn();
const { rerender } = render(<VariableEditor {...defaultProps} onChange={onChange} />);
// wait for initial load
await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument());
// Select RGs variable
openMenu(screen.getByLabelText('select query type'));
screen.getByText('Namespaces').click();
// Simulate onChange behavior
const newQuery = onChange.mock.calls.at(-1)[0];
rerender(<VariableEditor {...defaultProps} query={newQuery} onChange={onChange} />);
await waitFor(() => expect(screen.getByText('Select subscription')).toBeInTheDocument());
// Select a subscription
openMenu(screen.getByLabelText('select subscription'));
screen.getByText('Primary Subscription').click();
await selectAndRerender('select query type', 'Namespaces', onChange, rerender);
await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender);
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
queryType: AzureQueryType.NamespacesQuery,
@ -248,21 +234,12 @@ describe('VariableEditor:', () => {
});
it('should run the query if requesting resource names', async () => {
grafanaRuntime.config.featureToggles.azTemplateVars = true;
const onChange = jest.fn();
const { rerender } = render(<VariableEditor {...defaultProps} onChange={onChange} />);
// wait for initial load
await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument());
// Select RGs variable
openMenu(screen.getByLabelText('select query type'));
screen.getByText('Resource Names').click();
// Simulate onChange behavior
const newQuery = onChange.mock.calls.at(-1)[0];
rerender(<VariableEditor {...defaultProps} query={newQuery} onChange={onChange} />);
await waitFor(() => expect(screen.getByText('Select subscription')).toBeInTheDocument());
// Select a subscription
openMenu(screen.getByLabelText('select subscription'));
screen.getByText('Primary Subscription').click();
await selectAndRerender('select query type', 'Resource Names', onChange, rerender);
await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender);
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
queryType: AzureQueryType.ResourceNamesQuery,
@ -271,5 +248,43 @@ describe('VariableEditor:', () => {
})
);
});
it('should run the query if requesting metric names', async () => {
const onChange = jest.fn();
const { rerender } = render(<VariableEditor {...defaultProps} onChange={onChange} />);
// wait for initial load
await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument());
await selectAndRerender('select query type', 'Metric Names', onChange, rerender);
await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender);
await selectAndRerender('select resource group', 'rg', onChange, rerender);
await selectAndRerender('select namespace', 'foo/bar', onChange, rerender);
await selectAndRerender('select resource', 'foobar', onChange, rerender);
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
queryType: AzureQueryType.MetricNamesQuery,
subscription: 'sub',
resourceGroup: 'rg',
namespace: 'foo/bar',
resource: 'foobar',
refId: 'A',
})
);
});
it('should run the query if requesting workspaces', async () => {
const onChange = jest.fn();
const { rerender } = render(<VariableEditor {...defaultProps} onChange={onChange} />);
// wait for initial load
await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument());
await selectAndRerender('select query type', 'Workspaces', onChange, rerender);
await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender);
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
queryType: AzureQueryType.WorkspacesQuery,
subscription: 'sub',
refId: 'A',
})
);
});
});
});

View File

@ -34,6 +34,8 @@ const VariableEditor = (props: Props) => {
AZURE_QUERY_VARIABLE_TYPE_OPTIONS.push({ label: 'Resource Groups', value: AzureQueryType.ResourceGroupsQuery });
AZURE_QUERY_VARIABLE_TYPE_OPTIONS.push({ label: 'Namespaces', value: AzureQueryType.NamespacesQuery });
AZURE_QUERY_VARIABLE_TYPE_OPTIONS.push({ label: 'Resource Names', value: AzureQueryType.ResourceNamesQuery });
AZURE_QUERY_VARIABLE_TYPE_OPTIONS.push({ label: 'Metric Names', value: AzureQueryType.MetricNamesQuery });
AZURE_QUERY_VARIABLE_TYPE_OPTIONS.push({ label: 'Workspaces', value: AzureQueryType.WorkspacesQuery });
}
const [variableOptionGroup, setVariableOptionGroup] = useState<{ label: string; options: AzureMonitorOption[] }>({
label: 'Template Variables',
@ -42,9 +44,13 @@ const VariableEditor = (props: Props) => {
const [requireSubscription, setRequireSubscription] = useState(false);
const [hasResourceGroup, setHasResourceGroup] = useState(false);
const [hasNamespace, setHasNamespace] = useState(false);
const [requireResourceGroup, setRequireResourceGroup] = useState(false);
const [requireNamespace, setRequireNamespace] = useState(false);
const [requireResource, setRequireResource] = useState(false);
const [subscriptions, setSubscriptions] = useState<SelectableValue[]>([]);
const [resourceGroups, setResourceGroups] = useState<SelectableValue[]>([]);
const [namespaces, setNamespaces] = useState<SelectableValue[]>([]);
const [resources, setResources] = useState<SelectableValue[]>([]);
const [errorMessage, setError] = useLastError();
const queryType = typeof query === 'string' ? '' : query.queryType;
@ -58,8 +64,12 @@ const VariableEditor = (props: Props) => {
setRequireSubscription(false);
setHasResourceGroup(false);
setHasNamespace(false);
setRequireResourceGroup(false);
setRequireNamespace(false);
setRequireResource(false);
switch (queryType) {
case AzureQueryType.ResourceGroupsQuery:
case AzureQueryType.WorkspacesQuery:
setRequireSubscription(true);
break;
case AzureQueryType.NamespacesQuery:
@ -71,6 +81,12 @@ const VariableEditor = (props: Props) => {
setHasResourceGroup(true);
setHasNamespace(true);
break;
case AzureQueryType.MetricNamesQuery:
setRequireSubscription(true);
setRequireResourceGroup(true);
setRequireNamespace(true);
setRequireResource(true);
break;
}
}, [queryType]);
@ -111,6 +127,15 @@ const VariableEditor = (props: Props) => {
}
}, [datasource, subscription, resourceGroup]);
const namespace = (typeof query === 'object' && query.namespace) || '';
useEffect(() => {
if (subscription) {
datasource.getResourceNames(subscription, resourceGroup, namespace).then((rgs) => {
setResources(rgs.map((s) => ({ label: s.text, value: s.value })));
});
}
}, [datasource, subscription, resourceGroup, namespace]);
if (typeof query === 'string') {
// still migrating the query
return null;
@ -148,6 +173,13 @@ const VariableEditor = (props: Props) => {
});
};
const onChangeResource = (selectableValue: SelectableValue) => {
onChange({
...query,
resource: selectableValue.value,
});
};
const onLogsQueryChange = (queryChange: AzureMonitorQuery) => {
onChange(queryChange);
};
@ -198,27 +230,46 @@ const VariableEditor = (props: Props) => {
/>
</InlineField>
)}
{hasResourceGroup && (
<InlineField label="Select Resource Group" labelWidth={20}>
{(requireResourceGroup || hasResourceGroup) && (
<InlineField label="Select resource group" labelWidth={20}>
<Select
aria-label="select resource group"
onChange={onChangeResourceGroup}
options={resourceGroups.concat(variableOptionGroup, removeOption)}
options={
requireResourceGroup
? resourceGroups.concat(variableOptionGroup)
: resourceGroups.concat(variableOptionGroup, removeOption)
}
width={25}
value={query.resourceGroup}
placeholder="Optional"
placeholder={requireResourceGroup ? '' : 'Optional'}
/>
</InlineField>
)}
{hasNamespace && (
<InlineField label="Select Namespace" labelWidth={20}>
{(requireNamespace || hasNamespace) && (
<InlineField label="Select namespace" labelWidth={20}>
<Select
aria-label="select namespace"
onChange={onChangeNamespace}
options={namespaces.concat(variableOptionGroup, removeOption)}
options={
requireNamespace
? namespaces.concat(variableOptionGroup)
: namespaces.concat(variableOptionGroup, removeOption)
}
width={25}
value={query.namespace}
placeholder="Optional"
placeholder={requireNamespace ? '' : 'Optional'}
/>
</InlineField>
)}
{requireResource && (
<InlineField label="Select resource" labelWidth={20}>
<Select
aria-label="select resource"
onChange={onChangeResource}
options={resources.concat(variableOptionGroup)}
width={25}
value={query.resource}
/>
</InlineField>
)}

View File

@ -170,6 +170,15 @@ export default class Datasource extends DataSourceWithBackend<AzureMonitorQuery,
);
}
getMetricNames(subscriptionId: string, resourceGroup: string, metricDefinition: string, resourceName: string) {
return this.azureMonitorDatasource.getMetricNames({
subscription: subscriptionId,
resourceGroup,
metricDefinition,
resourceName,
});
}
/*Azure Log Analytics */
getAzureLogAnalyticsWorkspaces(subscriptionId: string) {
return this.azureLogAnalyticsDatasource.getWorkspaces(subscriptionId);

View File

@ -10,6 +10,8 @@ export enum AzureQueryType {
ResourceGroupsQuery = 'Azure Resource Groups',
NamespacesQuery = 'Azure Namespaces',
ResourceNamesQuery = 'Azure Resource Names',
MetricNamesQuery = 'Azure Metric Names',
WorkspacesQuery = 'Azure Workspaces',
/** Deprecated */
GrafanaTemplateVariableFn = 'Grafana Template Variable Function',
}
@ -34,6 +36,7 @@ export interface AzureMonitorQuery extends DataQuery {
/** Template variables params */
resourceGroup?: string;
namespace?: string;
resource?: string;
}
/**

View File

@ -239,7 +239,7 @@ export interface LegacyAzureGetMetricNamespacesQuery {
export interface AzureGetMetricNamesQuery {
resourceUri: string;
metricNamespace: string;
metricNamespace?: string;
}
export interface LegacyAzureGetMetricNamesQuery {
@ -247,7 +247,7 @@ export interface LegacyAzureGetMetricNamesQuery {
resourceGroup: string;
metricDefinition: string;
resourceName: string;
metricNamespace: string;
metricNamespace?: string;
}
export interface AzureGetMetricMetadataQuery {

View File

@ -605,5 +605,54 @@ describe('VariableSupport', () => {
done();
});
});
it('can fetch metric names', (done) => {
const expectedResults = ['test'];
const variableSupport = new VariableSupport(
createMockDatasource({
getMetricNames: jest.fn().mockResolvedValueOnce(expectedResults),
})
);
const mockRequest = {
targets: [
{
refId: 'A',
queryType: AzureQueryType.MetricNamesQuery,
subscription: 'sub',
resourceGroup: 'rg',
namespace: 'ns',
resource: 'rn',
} as AzureMonitorQuery,
],
} as DataQueryRequest<AzureMonitorQuery>;
const observables = variableSupport.query(mockRequest);
observables.subscribe((result: DataQueryResponseData) => {
expect(result.data[0].source).toEqual(expectedResults);
done();
});
});
it('can fetch workspaces', (done) => {
const expectedResults = ['test'];
const variableSupport = new VariableSupport(
createMockDatasource({
getAzureLogAnalyticsWorkspaces: jest.fn().mockResolvedValueOnce(expectedResults),
})
);
const mockRequest = {
targets: [
{
refId: 'A',
queryType: AzureQueryType.WorkspacesQuery,
subscription: 'sub',
} as AzureMonitorQuery,
],
} as DataQueryRequest<AzureMonitorQuery>;
const observables = variableSupport.query(mockRequest);
observables.subscribe((result: DataQueryResponseData) => {
expect(result.data[0].source).toEqual(expectedResults);
done();
});
});
});
});

View File

@ -61,6 +61,25 @@ export class VariableSupport extends CustomVariableSupport<DataSource, AzureMoni
data: rgs?.length ? [toDataFrame(rgs)] : [],
};
}
case AzureQueryType.MetricNamesQuery:
if (queryObj.subscription && queryObj.resourceGroup && queryObj.namespace && queryObj.resource) {
const rgs = await this.datasource.getMetricNames(
queryObj.subscription,
queryObj.resourceGroup,
queryObj.namespace,
queryObj.resource
);
return {
data: rgs?.length ? [toDataFrame(rgs)] : [],
};
}
case AzureQueryType.WorkspacesQuery:
if (queryObj.subscription) {
const rgs = await this.datasource.getAzureLogAnalyticsWorkspaces(queryObj.subscription);
return {
data: rgs?.length ? [toDataFrame(rgs)] : [],
};
}
case AzureQueryType.GrafanaTemplateVariableFn:
if (queryObj.grafanaTemplateVariableFn) {
const templateVariablesResults = await this.callGrafanaTemplateVariableFn(