mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Azure: Add support for custom namespace and custom metrics variable queries (#99279)
* Add custom metric namespace and metric name queries * Fix outdated type * Support specifying custom - Add custom support to getMetricNamespaces - Ensure the customNamespace is specified in getMetricNames calls * Update data source tests * Support custom namespace/metrics variable queries - Add tests * Add fields to variable editor - Update tests - Update docs - Update selectors * Remove unneeded Promise.resolve * Add comment * Don't mutate expected path * Lint * Update docs/sources/datasources/azure-monitor/template-variables/index.md Co-authored-by: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com> * Update docs/sources/datasources/azure-monitor/template-variables/index.md Co-authored-by: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com> * Update docs * Update conditionals * Lint --------- Co-authored-by: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com>
This commit is contained in:
parent
c6ba0910b4
commit
e01d8ad5b5
@ -48,19 +48,24 @@ For an introduction to templating and template variables, refer to the [Templati
|
||||
|
||||
You can specify these Azure Monitor data source queries in the Variable edit view's **Query Type** field.
|
||||
|
||||
| Name | Description |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| **Subscriptions** | Returns subscriptions. |
|
||||
| **Resource Groups** | Returns resource groups for a specified. Supports multi-value. subscription. |
|
||||
| **Namespaces** | Returns metric namespaces for the specified subscription and resource group. |
|
||||
| **Regions** | Returns regions for the specified subscription |
|
||||
| **Resource Names** | Returns a list of resource names for a specified subscription, resource group and namespace. Supports multi-value. |
|
||||
| **Metric Names** | Returns a list of metric names for a resource. |
|
||||
| **Workspaces** | Returns a list of workspaces for the specified subscription. |
|
||||
| **Logs** | Use a KQL query to return values. |
|
||||
| **Resource Graph** | Use an ARG query to return values. |
|
||||
| Name | Description |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| **Subscriptions** | Returns subscriptions. |
|
||||
| **Resource Groups** | Returns resource groups for a specified. Supports multi-value. subscription. |
|
||||
| **Namespaces** | Returns metric namespaces for the specified subscription and resource group. |
|
||||
| **Regions** | Returns regions for the specified subscription |
|
||||
| **Resource Names** | Returns a list of resource names for a specified subscription, resource group and namespace. Supports multi-value. |
|
||||
| **Metric Names** | Returns a list of metric names for a resource. |
|
||||
| **Workspaces** | Returns a list of workspaces for the specified subscription. |
|
||||
| **Logs** | Use a KQL query to return values. |
|
||||
| **Custom Namespaces** | Returns metric namespaces for the specified resource. |
|
||||
| **Custom Metric Names** | Returns a list of custom metric names for the specified resource. |
|
||||
|
||||
Any Log Analytics Kusto Query Language (KQL) query that returns a single list of values can also be used in the Query field.
|
||||
{{< admonition type="note" >}}
|
||||
Custom metrics cannot be emitted against a subscription or resource group. Select resources only when you need to retrieve custom metric namespaces or custom metric names associated with a specific resource.
|
||||
{{< /admonition >}}
|
||||
|
||||
You can use any Log Analytics Kusto Query Language (KQL) query that returns a single list of values in the `Query` field.
|
||||
For example:
|
||||
|
||||
| Query | List of values returned |
|
||||
|
@ -29,23 +29,37 @@ export interface AzureMonitorQuery extends common.DataQuery {
|
||||
* Application Insights Traces sub-query properties.
|
||||
*/
|
||||
azureTraces?: AzureTracesQuery;
|
||||
/**
|
||||
* Custom namespace used in template variable queries
|
||||
*/
|
||||
customNamespace?: string;
|
||||
/**
|
||||
* @deprecated Legacy template variable support.
|
||||
*/
|
||||
grafanaTemplateVariableFn?: GrafanaTemplateVariableQuery;
|
||||
/**
|
||||
* Namespace used in template variable queries
|
||||
*/
|
||||
namespace?: string;
|
||||
/**
|
||||
* Used only for exemplar queries from Prometheus
|
||||
*/
|
||||
query?: string;
|
||||
/**
|
||||
* Region used in template variable queries
|
||||
*/
|
||||
region?: string;
|
||||
/**
|
||||
* Resource used in template variable queries
|
||||
*/
|
||||
resource?: string;
|
||||
/**
|
||||
* Template variables params. These exist for backwards compatiblity with legacy template variables.
|
||||
* Resource group used in template variable queries
|
||||
*/
|
||||
resourceGroup?: string;
|
||||
/**
|
||||
* Azure subscription containing the resource(s) to be queried.
|
||||
* Also used for template variable queries
|
||||
*/
|
||||
subscription?: string;
|
||||
/**
|
||||
@ -65,6 +79,8 @@ export enum AzureQueryType {
|
||||
AzureMonitor = 'Azure Monitor',
|
||||
AzureResourceGraph = 'Azure Resource Graph',
|
||||
AzureTraces = 'Azure Traces',
|
||||
CustomMetricNamesQuery = 'Azure Custom Metric Names',
|
||||
CustomNamespacesQuery = 'Azure Custom Namespaces',
|
||||
GrafanaTemplateVariableFn = 'Grafana Template Variable Function',
|
||||
LocationsQuery = 'Azure Regions',
|
||||
LogAnalytics = 'Azure Log Analytics',
|
||||
|
@ -28,6 +28,7 @@ type AzureMonitorQuery struct {
|
||||
// TODO make this required and give it a default
|
||||
QueryType *string `json:"queryType,omitempty"`
|
||||
// Azure subscription containing the resource(s) to be queried.
|
||||
// Also used for template variable queries
|
||||
Subscription *string `json:"subscription,omitempty"`
|
||||
// Subscriptions to be queried via Azure Resource Graph.
|
||||
Subscriptions []string `json:"subscriptions,omitempty"`
|
||||
@ -41,11 +42,16 @@ type AzureMonitorQuery struct {
|
||||
AzureTraces *AzureTracesQuery `json:"azureTraces,omitempty"`
|
||||
// @deprecated Legacy template variable support.
|
||||
GrafanaTemplateVariableFn *GrafanaTemplateVariableQuery `json:"grafanaTemplateVariableFn,omitempty"`
|
||||
// Template variables params. These exist for backwards compatiblity with legacy template variables.
|
||||
// Resource group used in template variable queries
|
||||
ResourceGroup *string `json:"resourceGroup,omitempty"`
|
||||
Namespace *string `json:"namespace,omitempty"`
|
||||
Resource *string `json:"resource,omitempty"`
|
||||
Region *string `json:"region,omitempty"`
|
||||
// Namespace used in template variable queries
|
||||
Namespace *string `json:"namespace,omitempty"`
|
||||
// Resource used in template variable queries
|
||||
Resource *string `json:"resource,omitempty"`
|
||||
// Region used in template variable queries
|
||||
Region *string `json:"region,omitempty"`
|
||||
// Custom namespace used in template variable queries
|
||||
CustomNamespace *string `json:"customNamespace,omitempty"`
|
||||
// For mixed data sources the selected datasource is on the query level.
|
||||
// For non mixed scenarios this is undefined.
|
||||
// TODO find a better way to do this ^ that's friendly to schema
|
||||
@ -77,6 +83,8 @@ const (
|
||||
AzureQueryTypeLocationsQuery AzureQueryType = "Azure Regions"
|
||||
AzureQueryTypeGrafanaTemplateVariableFn AzureQueryType = "Grafana Template Variable Function"
|
||||
AzureQueryTypeTraceExemplar AzureQueryType = "traceql"
|
||||
AzureQueryTypeCustomNamespacesQuery AzureQueryType = "Azure Custom Namespaces"
|
||||
AzureQueryTypeCustomMetricNamesQuery AzureQueryType = "Azure Custom Metric Names"
|
||||
)
|
||||
|
||||
type AzureMetricQuery struct {
|
||||
|
@ -6,7 +6,10 @@ import { multiVariable } from '../__mocks__/variables';
|
||||
import AzureMonitorDatasource from '../datasource';
|
||||
import { AzureAPIResponse, AzureMonitorDataSourceInstanceSettings, Location } from '../types';
|
||||
|
||||
let replace = () => '';
|
||||
// We want replace to just return the value as is in general/
|
||||
// We declare this as a function so that we can overwrite it in each test
|
||||
// without affecting the rest of the @grafana/runtime module.
|
||||
let replace = (val: string) => val;
|
||||
|
||||
jest.mock('@grafana/runtime', () => {
|
||||
return {
|
||||
@ -251,8 +254,8 @@ describe('AzureMonitorDatasource', () => {
|
||||
const basePath = 'azuremonitor/subscriptions/mock-subscription-id/resourceGroups/nodeapp';
|
||||
const expected =
|
||||
basePath +
|
||||
'/providers/microsoft.insights/components/resource1' +
|
||||
'/providers/microsoft.insights/metricNamespaces?api-version=2017-12-01-preview®ion=global';
|
||||
'/providers/microsoft.insights/components/resource1/providers/microsoft.insights/metricNamespaces?api-version=2017-12-01-preview' +
|
||||
(path.includes('®ion=global') ? '®ion=global' : '');
|
||||
expect(path).toBe(expected);
|
||||
return Promise.resolve(response);
|
||||
});
|
||||
@ -295,6 +298,24 @@ describe('AzureMonitorDatasource', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('when custom is specified will only return custom namespaces', () => {
|
||||
return ctx.ds.azureMonitorDatasource
|
||||
.getMetricNamespaces(
|
||||
{
|
||||
resourceUri:
|
||||
'/subscriptions/mock-subscription-id/resourceGroups/nodeapp/providers/microsoft.insights/components/resource1',
|
||||
},
|
||||
false,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
.then((results: Array<{ text: string; value: string }>) => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0].text).toEqual('Azure.ApplicationInsights');
|
||||
expect(results[0].value).toEqual('Azure.ApplicationInsights');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing getMetricNames', () => {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { Namespace } from 'i18next';
|
||||
import { find, startsWith } from 'lodash';
|
||||
|
||||
import { AzureCredentials } from '@grafana/azure-sdk';
|
||||
@ -26,6 +25,7 @@ import {
|
||||
Location,
|
||||
ResourceGroup,
|
||||
Metric,
|
||||
MetricNamespace,
|
||||
} from '../types';
|
||||
import { routeNames } from '../utils/common';
|
||||
import migrateQuery from '../utils/migrateQuery';
|
||||
@ -238,7 +238,8 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<
|
||||
return (await Promise.all(promises)).flat();
|
||||
}
|
||||
|
||||
getMetricNamespaces(query: GetMetricNamespacesQuery, globalRegion: boolean, region?: string) {
|
||||
// Note globalRegion should be false when querying custom metric namespaces
|
||||
getMetricNamespaces(query: GetMetricNamespacesQuery, globalRegion: boolean, region?: string, custom?: boolean) {
|
||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
|
||||
this.resourcePath,
|
||||
this.apiPreviewVersion,
|
||||
@ -249,7 +250,10 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<
|
||||
region
|
||||
);
|
||||
return this.getResource(url)
|
||||
.then((result: AzureAPIResponse<Namespace>) => {
|
||||
.then((result: AzureAPIResponse<MetricNamespace>) => {
|
||||
if (custom) {
|
||||
result.value = result.value.filter((namespace) => namespace.classification === 'Custom');
|
||||
}
|
||||
return ResponseParser.parseResponseValues(
|
||||
result,
|
||||
'properties.metricNamespaceName',
|
||||
|
@ -40,7 +40,16 @@ const defaultProps = {
|
||||
datasource: createMockDatasource({
|
||||
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' }]),
|
||||
getMetricNamespaces: jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
async (_subscriptionId: string, _resourceGroup?: string, _resourceUri?: string, custom?: boolean) => {
|
||||
if (custom !== true) {
|
||||
return [{ text: 'foo/bar', value: 'foo/bar' }];
|
||||
}
|
||||
return [{ text: 'foo/custom', value: 'foo/custom' }];
|
||||
}
|
||||
),
|
||||
getResourceNames: jest.fn().mockResolvedValue([{ text: 'foobar', value: 'foobar' }]),
|
||||
getVariablesRaw: jest.fn().mockReturnValue([
|
||||
{ label: 'query0', name: 'sub0' },
|
||||
@ -350,5 +359,51 @@ describe('VariableEditor:', () => {
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should run the query if requesting custom metric namespaces', 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', 'Custom Namespaces', 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.CustomNamespacesQuery,
|
||||
subscription: 'sub',
|
||||
resourceGroup: 'rg',
|
||||
namespace: 'foo/bar',
|
||||
resource: 'foobar',
|
||||
refId: 'A',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should run the query if requesting custom metrics for a resource', 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', 'Custom 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);
|
||||
await selectAndRerender('select custom namespace', 'foo/custom', onChange, rerender);
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
queryType: AzureQueryType.CustomMetricNamesQuery,
|
||||
subscription: 'sub',
|
||||
resourceGroup: 'rg',
|
||||
namespace: 'foo/bar',
|
||||
resource: 'foobar',
|
||||
refId: 'A',
|
||||
customNamespace: 'foo/custom',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -3,8 +3,10 @@ import { useEffect, useState } from 'react';
|
||||
import { useEffectOnce } from 'react-use';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { Alert, Field, Select, Space } from '@grafana/ui';
|
||||
|
||||
import UrlBuilder from '../../azure_monitor/url_builder';
|
||||
import DataSource from '../../datasource';
|
||||
import { selectors } from '../../e2e/selectors';
|
||||
import { migrateQuery } from '../../grafanaTemplateVariableFns';
|
||||
@ -35,6 +37,8 @@ const VariableEditor = (props: Props) => {
|
||||
{ label: 'Workspaces', value: AzureQueryType.WorkspacesQuery },
|
||||
{ label: 'Resource Graph', value: AzureQueryType.AzureResourceGraph },
|
||||
{ label: 'Logs', value: AzureQueryType.LogAnalytics },
|
||||
{ label: 'Custom Namespaces', value: AzureQueryType.CustomNamespacesQuery },
|
||||
{ label: 'Custom Metric Names', value: AzureQueryType.CustomMetricNamesQuery },
|
||||
];
|
||||
if (typeof props.query === 'object' && props.query.queryType === AzureQueryType.GrafanaTemplateVariableFn) {
|
||||
// Add the option for the GrafanaTemplateVariableFn only if it's already in use
|
||||
@ -53,10 +57,12 @@ const VariableEditor = (props: Props) => {
|
||||
const [hasRegion, setHasRegion] = useState(false);
|
||||
const [requireResourceGroup, setRequireResourceGroup] = useState(false);
|
||||
const [requireNamespace, setRequireNamespace] = useState(false);
|
||||
const [requireCustomNamespace, setRequireCustomNamespace] = useState(false);
|
||||
const [requireResource, setRequireResource] = useState(false);
|
||||
const [subscriptions, setSubscriptions] = useState<SelectableValue[]>([]);
|
||||
const [resourceGroups, setResourceGroups] = useState<SelectableValue[]>([]);
|
||||
const [namespaces, setNamespaces] = useState<SelectableValue[]>([]);
|
||||
const [customNamespaces, setCustomNamespaces] = useState<SelectableValue[]>([]);
|
||||
const [resources, setResources] = useState<SelectableValue[]>([]);
|
||||
const [regions, setRegions] = useState<SelectableValue[]>([]);
|
||||
const [errorMessage, setError] = useLastError();
|
||||
@ -77,6 +83,7 @@ const VariableEditor = (props: Props) => {
|
||||
setRequireResourceGroup(false);
|
||||
setRequireNamespace(false);
|
||||
setRequireResource(false);
|
||||
setRequireCustomNamespace(false);
|
||||
switch (queryType) {
|
||||
case AzureQueryType.ResourceGroupsQuery:
|
||||
case AzureQueryType.WorkspacesQuery:
|
||||
@ -101,6 +108,19 @@ const VariableEditor = (props: Props) => {
|
||||
case AzureQueryType.LocationsQuery:
|
||||
setRequireSubscription(true);
|
||||
break;
|
||||
case AzureQueryType.CustomNamespacesQuery:
|
||||
setRequireSubscription(true);
|
||||
setRequireResourceGroup(true);
|
||||
setRequireNamespace(true);
|
||||
setRequireResource(true);
|
||||
break;
|
||||
case AzureQueryType.CustomMetricNamesQuery:
|
||||
setRequireSubscription(true);
|
||||
setRequireResourceGroup(true);
|
||||
setRequireResource(true);
|
||||
setRequireNamespace(true);
|
||||
setRequireCustomNamespace(true);
|
||||
break;
|
||||
}
|
||||
}, [queryType]);
|
||||
|
||||
@ -117,6 +137,7 @@ const VariableEditor = (props: Props) => {
|
||||
});
|
||||
}, [datasource, queryType]);
|
||||
|
||||
// Always retrieve subscriptions first as they're used in most template variable queries
|
||||
useEffectOnce(() => {
|
||||
datasource.getSubscriptions().then((subs) => {
|
||||
setSubscriptions(subs.map((s) => ({ label: s.text, value: s.value })));
|
||||
@ -124,6 +145,7 @@ const VariableEditor = (props: Props) => {
|
||||
});
|
||||
|
||||
const subscription = typeof query === 'object' && query.subscription;
|
||||
// When subscription is set, retrieve resource groups
|
||||
useEffect(() => {
|
||||
if (subscription) {
|
||||
datasource.getResourceGroups(subscription).then((rgs) => {
|
||||
@ -133,14 +155,16 @@ const VariableEditor = (props: Props) => {
|
||||
}, [datasource, subscription]);
|
||||
|
||||
const resourceGroup = (typeof query === 'object' && query.resourceGroup) || '';
|
||||
// When resource group is set, retrieve metric namespaces (aka resource types for a custom metric and custom metric namespace query)
|
||||
useEffect(() => {
|
||||
if (subscription) {
|
||||
if (subscription && resourceGroup) {
|
||||
datasource.getMetricNamespaces(subscription, resourceGroup).then((rgs) => {
|
||||
setNamespaces(rgs.map((s) => ({ label: s.text, value: s.value })));
|
||||
});
|
||||
}
|
||||
}, [datasource, subscription, resourceGroup]);
|
||||
|
||||
// When subscription is set also retrieve locations
|
||||
useEffect(() => {
|
||||
if (subscription) {
|
||||
datasource.azureMonitorDatasource.getLocations([subscription]).then((rgs) => {
|
||||
@ -152,14 +176,31 @@ const VariableEditor = (props: Props) => {
|
||||
}, [datasource, subscription, resourceGroup]);
|
||||
|
||||
const namespace = (typeof query === 'object' && query.namespace) || '';
|
||||
// When subscription, resource group, and namespace are all set, retrieve resource names
|
||||
useEffect(() => {
|
||||
if (subscription) {
|
||||
if (subscription && resourceGroup && namespace) {
|
||||
datasource.getResourceNames(subscription, resourceGroup, namespace).then((rgs) => {
|
||||
setResources(rgs.map((s) => ({ label: s.text, value: s.value })));
|
||||
});
|
||||
}
|
||||
}, [datasource, subscription, resourceGroup, namespace]);
|
||||
|
||||
const resource = (typeof query === 'object' && query.resource) || '';
|
||||
// When subscription, resource group, namespace, and resource name are all set, retrieve custom metric namespaces
|
||||
useEffect(() => {
|
||||
if (subscription && resourceGroup && namespace && resource) {
|
||||
const resourceUri = UrlBuilder.buildResourceUri(getTemplateSrv(), {
|
||||
subscription,
|
||||
resourceGroup,
|
||||
metricNamespace: namespace,
|
||||
resourceName: resource,
|
||||
});
|
||||
datasource.getMetricNamespaces(subscription, resourceGroup, resourceUri, true).then((rgs) => {
|
||||
setCustomNamespaces(rgs.map((s) => ({ label: s.text, value: s.value })));
|
||||
});
|
||||
}
|
||||
}, [datasource, subscription, resourceGroup, namespace, resource]);
|
||||
|
||||
if (typeof query === 'string') {
|
||||
// still migrating the query
|
||||
return null;
|
||||
@ -225,6 +266,13 @@ const VariableEditor = (props: Props) => {
|
||||
onChange(queryChange);
|
||||
};
|
||||
|
||||
const onChangeCustomNamespace = (selectableValue: SelectableValue) => {
|
||||
onChange({
|
||||
...query,
|
||||
customNamespace: selectableValue.value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Field label="Query Type" data-testid={selectors.components.variableEditor.queryType.input}>
|
||||
@ -289,7 +337,14 @@ const VariableEditor = (props: Props) => {
|
||||
</Field>
|
||||
)}
|
||||
{(requireNamespace || hasNamespace) && (
|
||||
<Field label="Namespace" data-testid={selectors.components.variableEditor.namespace.input}>
|
||||
<Field
|
||||
label={
|
||||
queryType === AzureQueryType.CustomNamespacesQuery || queryType === AzureQueryType.CustomMetricNamesQuery
|
||||
? 'Resource Type'
|
||||
: 'Namespace'
|
||||
}
|
||||
data-testid={selectors.components.variableEditor.namespace.input}
|
||||
>
|
||||
<Select
|
||||
aria-label="select namespace"
|
||||
onChange={onChangeNamespace}
|
||||
@ -327,6 +382,22 @@ const VariableEditor = (props: Props) => {
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
{requireCustomNamespace && (
|
||||
<Field label={'Custom Namespace'} data-testid={selectors.components.variableEditor.customNamespace.input}>
|
||||
<Select
|
||||
aria-label="select custom namespace"
|
||||
onChange={onChangeCustomNamespace}
|
||||
options={
|
||||
requireCustomNamespace
|
||||
? customNamespaces.concat(variableOptionGroup)
|
||||
: customNamespaces.concat(variableOptionGroup, removeOption)
|
||||
}
|
||||
width={25}
|
||||
value={query.customNamespace || null}
|
||||
placeholder={requireCustomNamespace ? undefined : 'Optional'}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
{query.queryType === AzureQueryType.AzureResourceGraph && (
|
||||
<>
|
||||
<ArgQueryEditor
|
||||
|
@ -27,6 +27,7 @@ composableKinds: DataQuery: {
|
||||
schema: {
|
||||
#AzureMonitorQuery: common.DataQuery & {
|
||||
// Azure subscription containing the resource(s) to be queried.
|
||||
// Also used for template variable queries
|
||||
subscription?: string
|
||||
|
||||
// Subscriptions to be queried via Azure Resource Graph.
|
||||
@ -43,11 +44,16 @@ composableKinds: DataQuery: {
|
||||
// @deprecated Legacy template variable support.
|
||||
grafanaTemplateVariableFn?: #GrafanaTemplateVariableQuery
|
||||
|
||||
// Template variables params. These exist for backwards compatiblity with legacy template variables.
|
||||
// Resource group used in template variable queries
|
||||
resourceGroup?: string
|
||||
// Namespace used in template variable queries
|
||||
namespace?: string
|
||||
// Resource used in template variable queries
|
||||
resource?: string
|
||||
// Region used in template variable queries
|
||||
region?: string
|
||||
// Custom namespace used in template variable queries
|
||||
customNamespace?: string
|
||||
// Azure Monitor query type.
|
||||
// queryType: #AzureQueryType
|
||||
|
||||
@ -56,7 +62,7 @@ composableKinds: DataQuery: {
|
||||
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
|
||||
|
||||
// Defines the supported queryTypes. GrafanaTemplateVariableFn is deprecated
|
||||
#AzureQueryType: "Azure Monitor" | "Azure Log Analytics" | "Azure Resource Graph" | "Azure Traces" | "Azure Subscriptions" | "Azure Resource Groups" | "Azure Namespaces" | "Azure Resource Names" | "Azure Metric Names" | "Azure Workspaces" | "Azure Regions" | "Grafana Template Variable Function" | "traceql" @cuetsy(kind="enum", memberNames="AzureMonitor|LogAnalytics|AzureResourceGraph|AzureTraces|SubscriptionsQuery|ResourceGroupsQuery|NamespacesQuery|ResourceNamesQuery|MetricNamesQuery|WorkspacesQuery|LocationsQuery|GrafanaTemplateVariableFn|TraceExemplar")
|
||||
#AzureQueryType: "Azure Monitor" | "Azure Log Analytics" | "Azure Resource Graph" | "Azure Traces" | "Azure Subscriptions" | "Azure Resource Groups" | "Azure Namespaces" | "Azure Resource Names" | "Azure Metric Names" | "Azure Workspaces" | "Azure Regions" | "Grafana Template Variable Function" | "traceql" | "Azure Custom Namespaces" | "Azure Custom Metric Names" @cuetsy(kind="enum", memberNames="AzureMonitor|LogAnalytics|AzureResourceGraph|AzureTraces|SubscriptionsQuery|ResourceGroupsQuery|NamespacesQuery|ResourceNamesQuery|MetricNamesQuery|WorkspacesQuery|LocationsQuery|GrafanaTemplateVariableFn|TraceExemplar|CustomNamespacesQuery|CustomMetricNamesQuery")
|
||||
|
||||
#AzureMetricQuery: {
|
||||
// Array of resource URIs to be queried.
|
||||
|
@ -27,23 +27,37 @@ export interface AzureMonitorQuery extends common.DataQuery {
|
||||
* Application Insights Traces sub-query properties.
|
||||
*/
|
||||
azureTraces?: AzureTracesQuery;
|
||||
/**
|
||||
* Custom namespace used in template variable queries
|
||||
*/
|
||||
customNamespace?: string;
|
||||
/**
|
||||
* @deprecated Legacy template variable support.
|
||||
*/
|
||||
grafanaTemplateVariableFn?: GrafanaTemplateVariableQuery;
|
||||
/**
|
||||
* Namespace used in template variable queries
|
||||
*/
|
||||
namespace?: string;
|
||||
/**
|
||||
* Used only for exemplar queries from Prometheus
|
||||
*/
|
||||
query?: string;
|
||||
/**
|
||||
* Region used in template variable queries
|
||||
*/
|
||||
region?: string;
|
||||
/**
|
||||
* Resource used in template variable queries
|
||||
*/
|
||||
resource?: string;
|
||||
/**
|
||||
* Template variables params. These exist for backwards compatiblity with legacy template variables.
|
||||
* Resource group used in template variable queries
|
||||
*/
|
||||
resourceGroup?: string;
|
||||
/**
|
||||
* Azure subscription containing the resource(s) to be queried.
|
||||
* Also used for template variable queries
|
||||
*/
|
||||
subscription?: string;
|
||||
/**
|
||||
@ -63,6 +77,8 @@ export enum AzureQueryType {
|
||||
AzureMonitor = 'Azure Monitor',
|
||||
AzureResourceGraph = 'Azure Resource Graph',
|
||||
AzureTraces = 'Azure Traces',
|
||||
CustomMetricNamesQuery = 'Azure Custom Metric Names',
|
||||
CustomNamespacesQuery = 'Azure Custom Namespaces',
|
||||
GrafanaTemplateVariableFn = 'Grafana Template Variable Function',
|
||||
LocationsQuery = 'Azure Regions',
|
||||
LogAnalytics = 'Azure Log Analytics',
|
||||
|
@ -168,24 +168,41 @@ export default class Datasource extends DataSourceWithBackend<AzureMonitorQuery,
|
||||
return this.azureMonitorDatasource.getResourceGroups(this.templateSrv.replace(subscriptionId));
|
||||
}
|
||||
|
||||
getMetricNamespaces(subscriptionId: string, resourceGroup?: string) {
|
||||
getMetricNamespaces(subscriptionId: string, resourceGroup?: string, resourceUri?: string, custom?: boolean) {
|
||||
let url = `/subscriptions/${subscriptionId}`;
|
||||
if (resourceGroup) {
|
||||
url += `/resourceGroups/${resourceGroup}`;
|
||||
}
|
||||
return this.azureMonitorDatasource.getMetricNamespaces({ resourceUri: url }, true);
|
||||
if (resourceUri) {
|
||||
url = resourceUri;
|
||||
}
|
||||
return this.azureMonitorDatasource.getMetricNamespaces(
|
||||
{ resourceUri: url },
|
||||
// If custom namespaces are being queried we do not issue the query against the global region
|
||||
// as resources have a specific region
|
||||
custom ? false : true,
|
||||
undefined,
|
||||
custom
|
||||
);
|
||||
}
|
||||
|
||||
getResourceNames(subscriptionId: string, resourceGroup?: string, metricNamespace?: string, region?: string) {
|
||||
return this.azureMonitorDatasource.getResourceNames({ subscriptionId, resourceGroup, metricNamespace, region });
|
||||
}
|
||||
|
||||
getMetricNames(subscriptionId: string, resourceGroup: string, metricNamespace: string, resourceName: string) {
|
||||
getMetricNames(
|
||||
subscriptionId: string,
|
||||
resourceGroup: string,
|
||||
metricNamespace: string,
|
||||
resourceName: string,
|
||||
customNamespace?: string
|
||||
) {
|
||||
return this.azureMonitorDatasource.getMetricNames({
|
||||
subscription: subscriptionId,
|
||||
resourceGroup,
|
||||
metricNamespace,
|
||||
resourceName,
|
||||
customNamespace,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -113,6 +113,9 @@ export const components = {
|
||||
region: {
|
||||
input: 'data-testid region',
|
||||
},
|
||||
customNamespace: {
|
||||
input: 'data-testid custom-namespace',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -343,12 +343,8 @@ export interface ResourceGroup {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface Namespace {
|
||||
classification: {
|
||||
Custom: string;
|
||||
Platform: string;
|
||||
Qos: string;
|
||||
};
|
||||
export interface MetricNamespace {
|
||||
classification: 'Custom' | 'Platform' | 'Qos';
|
||||
id: string;
|
||||
name: string;
|
||||
properties: { metricNamespaceName: string };
|
||||
|
@ -543,4 +543,88 @@ describe('VariableSupport', () => {
|
||||
expect(result.data[0].fields[0].values).toEqual(expectedResults);
|
||||
});
|
||||
});
|
||||
|
||||
it('can fetch custom namespaces', async () => {
|
||||
const expectedResults = ['test-custom/namespace'];
|
||||
const variableSupport = new VariableSupport(
|
||||
createMockDatasource({
|
||||
getMetricNamespaces: jest.fn().mockResolvedValueOnce(expectedResults),
|
||||
})
|
||||
);
|
||||
const mockRequest = {
|
||||
targets: [
|
||||
{
|
||||
refId: 'A',
|
||||
queryType: AzureQueryType.CustomNamespacesQuery,
|
||||
subscription: 'sub',
|
||||
resourceGroup: 'rg',
|
||||
namespace: 'ns',
|
||||
resource: 'rn',
|
||||
} as AzureMonitorQuery,
|
||||
],
|
||||
} as DataQueryRequest<AzureMonitorQuery>;
|
||||
const result = await lastValueFrom(variableSupport.query(mockRequest));
|
||||
expect(result.data[0].fields[0].values).toEqual(expectedResults);
|
||||
});
|
||||
|
||||
it('returns no data if calling custom namespaces but the subscription is a template variable with no value', async () => {
|
||||
const variableSupport = new VariableSupport(createMockDatasource());
|
||||
const mockRequest = {
|
||||
targets: [
|
||||
{
|
||||
refId: 'A',
|
||||
queryType: AzureQueryType.CustomNamespacesQuery,
|
||||
subscription: '$sub',
|
||||
resourceGroup: 'rg',
|
||||
namespace: 'ns',
|
||||
resource: 'rn',
|
||||
} as AzureMonitorQuery,
|
||||
],
|
||||
} as DataQueryRequest<AzureMonitorQuery>;
|
||||
const result = await lastValueFrom(variableSupport.query(mockRequest));
|
||||
expect(result.data).toEqual([]);
|
||||
});
|
||||
|
||||
it('can fetch custom metric names', async () => {
|
||||
const expectedResults = ['test-custom-metric'];
|
||||
const variableSupport = new VariableSupport(
|
||||
createMockDatasource({
|
||||
getMetricNames: jest.fn().mockResolvedValueOnce(expectedResults),
|
||||
})
|
||||
);
|
||||
const mockRequest = {
|
||||
targets: [
|
||||
{
|
||||
refId: 'A',
|
||||
queryType: AzureQueryType.CustomMetricNamesQuery,
|
||||
subscription: 'sub',
|
||||
resourceGroup: 'rg',
|
||||
namespace: 'ns',
|
||||
resource: 'rn',
|
||||
customNamespace: 'test-custom/namespace',
|
||||
} as AzureMonitorQuery,
|
||||
],
|
||||
} as DataQueryRequest<AzureMonitorQuery>;
|
||||
const result = await lastValueFrom(variableSupport.query(mockRequest));
|
||||
expect(result.data[0].fields[0].values).toEqual(expectedResults);
|
||||
});
|
||||
|
||||
it('returns no data if calling custom metric names but the subscription is a template variable with no value', async () => {
|
||||
const variableSupport = new VariableSupport(createMockDatasource());
|
||||
const mockRequest = {
|
||||
targets: [
|
||||
{
|
||||
refId: 'A',
|
||||
queryType: AzureQueryType.CustomMetricNamesQuery,
|
||||
subscription: '$sub',
|
||||
resourceGroup: 'rg',
|
||||
namespace: 'ns',
|
||||
resource: 'rn',
|
||||
customNamespace: 'test-custom/namespace',
|
||||
} as AzureMonitorQuery,
|
||||
],
|
||||
} as DataQueryRequest<AzureMonitorQuery>;
|
||||
const result = await lastValueFrom(variableSupport.query(mockRequest));
|
||||
expect(result.data).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
|
||||
|
||||
import UrlBuilder from './azure_monitor/url_builder';
|
||||
import VariableEditor from './components/VariableEditor/VariableEditor';
|
||||
import DataSource from './datasource';
|
||||
import { migrateQuery } from './grafanaTemplateVariableFns';
|
||||
@ -119,6 +120,58 @@ export class VariableSupport extends CustomVariableSupport<DataSource, AzureMoni
|
||||
data: res?.length ? [toDataFrame(res)] : [],
|
||||
};
|
||||
}
|
||||
case AzureQueryType.CustomNamespacesQuery:
|
||||
if (
|
||||
queryObj.subscription &&
|
||||
queryObj.resourceGroup &&
|
||||
queryObj.namespace &&
|
||||
queryObj.resource &&
|
||||
this.hasValue(queryObj.subscription, queryObj.resourceGroup, queryObj.namespace, queryObj.resource)
|
||||
) {
|
||||
const resourceUri = UrlBuilder.buildResourceUri(this.templateSrv, {
|
||||
subscription: queryObj.subscription,
|
||||
resourceGroup: queryObj.resourceGroup,
|
||||
metricNamespace: queryObj.namespace,
|
||||
resourceName: queryObj.resource,
|
||||
});
|
||||
const res = await this.datasource.getMetricNamespaces(
|
||||
queryObj.subscription,
|
||||
queryObj.resourceGroup,
|
||||
resourceUri,
|
||||
true
|
||||
);
|
||||
return {
|
||||
data: res?.length ? [toDataFrame(res)] : [],
|
||||
};
|
||||
}
|
||||
return { data: [] };
|
||||
case AzureQueryType.CustomMetricNamesQuery:
|
||||
if (
|
||||
queryObj.subscription &&
|
||||
queryObj.resourceGroup &&
|
||||
queryObj.namespace &&
|
||||
queryObj.resource &&
|
||||
queryObj.customNamespace &&
|
||||
this.hasValue(
|
||||
queryObj.subscription,
|
||||
queryObj.resourceGroup,
|
||||
queryObj.namespace,
|
||||
queryObj.resource,
|
||||
queryObj.customNamespace
|
||||
)
|
||||
) {
|
||||
const rgs = await this.datasource.getMetricNames(
|
||||
queryObj.subscription,
|
||||
queryObj.resourceGroup,
|
||||
queryObj.namespace,
|
||||
queryObj.resource,
|
||||
queryObj.customNamespace
|
||||
);
|
||||
return {
|
||||
data: rgs?.length ? [toDataFrame(rgs)] : [],
|
||||
};
|
||||
}
|
||||
return { data: [] };
|
||||
default:
|
||||
request.targets[0] = queryObj;
|
||||
const queryResp = await lastValueFrom(this.datasource.query(request));
|
||||
|
Loading…
Reference in New Issue
Block a user