AzureMonitor: Prometheus exemplars support (#87742)

* Update types

* Mark datasource as supporting traces

* Add logic to transform exemplar query to traces query

* Render appropriate editor

* Run trace query for exemplars

* Refactor out common functions

- Add function to retrieve first/default subscription

* Add route for trace exemplars

* Update logic to appropriately query exemplars

* Update traces query builder

* Update instance test

* Remove unneeded import

* Set traces pseudo data source

* Replace deprecated function calls

* Add helper for setting default traces query

* Don't show resource field for exemplars query

* When resetting operation ID for exemplars set query to default

- Update tests

* Update query header to appropriately set the service value

* Fix response frame creation and update tests

* Correctly select resource

* Convert subscriptionsApiVersion to const

* Add feature toggle
This commit is contained in:
Andreas Christou
2024-06-06 17:53:17 +01:00
committed by GitHub
parent 5f33943397
commit c9778c3332
32 changed files with 715 additions and 97 deletions
@@ -188,6 +188,7 @@ Experimental features might be changed or removed without prior notice.
| `notificationBanner` | Enables the notification banner UI and API |
| `dashboardRestore` | Enables deleted dashboard restore feature |
| `alertingCentralAlertHistory` | Enables the new central alert history. |
| `azureMonitorPrometheusExemplars` | Allows configuration of Azure Monitor as a data source that can provide Prometheus exemplars |
## Development feature toggles
@@ -191,4 +191,5 @@ export interface FeatureToggles {
preserveDashboardStateWhenNavigating?: boolean;
alertingCentralAlertHistory?: boolean;
pluginProxyPreserveTrailingSlash?: boolean;
azureMonitorPrometheusExemplars?: boolean;
}
@@ -3,7 +3,7 @@ import React, { useState } from 'react';
import { DataSourceInstanceSettings } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { DataSourcePicker } from '@grafana/runtime';
import { config, DataSourcePicker } from '@grafana/runtime';
import { Button, InlineField, Input, Switch, useTheme2 } from '@grafana/ui';
import { ExemplarTraceIdDestination } from '../types';
@@ -56,6 +56,11 @@ export function ExemplarSetting({ value, onChange, onDelete, disabled }: Props)
interactive={true}
>
<DataSourcePicker
filter={
config.featureToggles.azureMonitorPrometheusExemplars
? undefined
: (ds) => ds.type !== 'grafana-azure-monitor-datasource'
}
tracing={true}
current={value.datasourceUid}
noDefault={true}
@@ -35,9 +35,9 @@ export interface AzureMonitorQuery extends common.DataQuery {
grafanaTemplateVariableFn?: GrafanaTemplateVariableQuery;
namespace?: string;
/**
* Azure Monitor query type.
* queryType: #AzureQueryType
* Used only for exemplar queries from Prometheus
*/
query?: string;
region?: string;
resource?: string;
/**
@@ -73,6 +73,7 @@ export enum AzureQueryType {
ResourceGroupsQuery = 'Azure Resource Groups',
ResourceNamesQuery = 'Azure Resource Names',
SubscriptionsQuery = 'Azure Subscriptions',
TraceExemplar = 'traceql',
WorkspacesQuery = 'Azure Workspaces',
}
+6
View File
@@ -1291,6 +1291,12 @@ var (
Owner: grafanaPluginsPlatformSquad,
Expression: "false", // disabled by default
},
{
Name: "azureMonitorPrometheusExemplars",
Description: "Allows configuration of Azure Monitor as a data source that can provide Prometheus exemplars",
Stage: FeatureStageExperimental,
Owner: grafanaPartnerPluginsSquad,
},
}
)
+1
View File
@@ -172,3 +172,4 @@ alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,fal
preserveDashboardStateWhenNavigating,experimental,@grafana/dashboards-squad,false,false,false
alertingCentralAlertHistory,experimental,@grafana/alerting-squad,false,false,true
pluginProxyPreserveTrailingSlash,GA,@grafana/plugins-platform-backend,false,false,false
azureMonitorPrometheusExemplars,experimental,@grafana/partner-datasources,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
172 preserveDashboardStateWhenNavigating experimental @grafana/dashboards-squad false false false
173 alertingCentralAlertHistory experimental @grafana/alerting-squad false false true
174 pluginProxyPreserveTrailingSlash GA @grafana/plugins-platform-backend false false false
175 azureMonitorPrometheusExemplars experimental @grafana/partner-datasources false false false
+4
View File
@@ -698,4 +698,8 @@ const (
// FlagPluginProxyPreserveTrailingSlash
// Preserve plugin proxy trailing slash.
FlagPluginProxyPreserveTrailingSlash = "pluginProxyPreserveTrailingSlash"
// FlagAzureMonitorPrometheusExemplars
// Allows configuration of Azure Monitor as a data source that can provide Prometheus exemplars
FlagAzureMonitorPrometheusExemplars = "azureMonitorPrometheusExemplars"
)
+12
View File
@@ -2238,6 +2238,18 @@
"codeowner": "@grafana/grafana-app-platform-squad",
"frontend": true
}
},
{
"metadata": {
"name": "azureMonitorPrometheusExemplars",
"resourceVersion": "1717667267324",
"creationTimestamp": "2024-06-06T09:47:47Z"
},
"spec": {
"description": "Allows configuration of Azure Monitor as a data source that can provide Prometheus exemplars",
"stage": "experimental",
"codeowner": "@grafana/partner-datasources"
}
}
]
}
+4 -27
View File
@@ -24,6 +24,7 @@ import (
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/metrics"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/resourcegraph"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/types"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/utils"
)
func ProvideService(httpClientProvider *httpclient.Provider) *Service {
@@ -36,6 +37,7 @@ func ProvideService(httpClientProvider *httpclient.Provider) *Service {
azureLogAnalytics: &loganalytics.AzureLogAnalyticsDatasource{Proxy: proxy, Logger: logger},
azureResourceGraph: &resourcegraph.AzureResourceGraphDatasource{Proxy: proxy, Logger: logger},
azureTraces: &loganalytics.AzureLogAnalyticsDatasource{Proxy: proxy, Logger: logger},
traceExemplar: &loganalytics.AzureLogAnalyticsDatasource{Proxy: proxy, Logger: logger},
}
im := datasource.NewInstanceManager(NewInstanceSettings(httpClientProvider, executors, logger))
@@ -195,8 +197,7 @@ func (s *Service) getDSInfo(ctx context.Context, pluginCtx backend.PluginContext
}
func queryMetricHealth(ctx context.Context, dsInfo types.DatasourceInfo) (*http.Response, error) {
subscriptionsApiVersion := "2020-01-01"
url := fmt.Sprintf("%v/subscriptions?api-version=%v", dsInfo.Routes["Azure Monitor"].URL, subscriptionsApiVersion)
url := fmt.Sprintf("%v/subscriptions?api-version=%v", dsInfo.Routes["Azure Monitor"].URL, utils.SubscriptionsApiVersion)
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
@@ -301,7 +302,7 @@ func metricCheckHealth(ctx context.Context, dsInfo types.DatasourceInfo, logger
}
return fmt.Sprintf("Error connecting to Azure Monitor endpoint: %s", string(body)), defaultSubscription, backend.HealthStatusError
}
subscriptions, err := parseSubscriptions(metricsRes, logger)
subscriptions, err := utils.ParseSubscriptions(metricsRes, logger)
if err != nil {
return err.Error(), defaultSubscription, backend.HealthStatusError
}
@@ -365,30 +366,6 @@ func graphLogHealthCheck(ctx context.Context, dsInfo types.DatasourceInfo, defau
return "Successfully connected to Azure Resource Graph endpoint.", backend.HealthStatusOk
}
func parseSubscriptions(res *http.Response, logger log.Logger) ([]string, error) {
var target struct {
Value []struct {
SubscriptionId string `json:"subscriptionId"`
}
}
err := json.NewDecoder(res.Body).Decode(&target)
if err != nil {
return nil, err
}
defer func() {
if err := res.Body.Close(); err != nil {
logger.Warn("Failed to close response body", "err", err)
}
}()
result := make([]string, len(target.Value))
for i, v := range target.Value {
result[i] = v.SubscriptionId
}
return result, nil
}
func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
ctx = azusercontext.WithUserFromHealthCheckReq(ctx, req)
dsInfo, err := s.getDSInfo(ctx, req.PluginContext)
@@ -44,6 +44,11 @@ var testRoutes = map[string]types.AzRoute{
Scopes: []string{"https://api.loganalytics.io/.default"},
Headers: map[string]string{"x-ms-app": "Grafana", "Cache-Control": "public, max-age=60"},
},
traceExemplar: {
URL: "https://api.loganalytics.io",
Scopes: []string{"https://api.loganalytics.io/.default"},
Headers: map[string]string{"x-ms-app": "Grafana", "Cache-Control": "public, max-age=60"},
},
azurePortal: {
URL: "https://portal.azure.com",
},
@@ -33,6 +33,7 @@ const (
AzureQueryTypeAzureTraces AzureQueryType = "Azure Traces"
AzureQueryTypeAzureWorkspaces AzureQueryType = "Azure Workspaces"
AzureQueryTypeGrafanaTemplateVariableFunction AzureQueryType = "Grafana Template Variable Function"
AzureQueryTypeTraceql AzureQueryType = "traceql"
)
// Defines values for GrafanaTemplateVariableQueryType.
@@ -241,6 +242,9 @@ type AzureMonitorQuery struct {
Hide *bool `json:"hide,omitempty"`
Namespace *string `json:"namespace,omitempty"`
// Used only for exemplar queries from Prometheus
Query *string `json:"query,omitempty"`
// Specify the query flavor
// TODO make this required and give it a default
QueryType *string `json:"queryType,omitempty"`
@@ -248,10 +252,7 @@ type AzureMonitorQuery struct {
// A unique identifier for the query within the list of targets.
// In server side expressions, the refId is used as a variable name to identify results.
// By default, the UI will assign A->Z; however setting meaningful names may be useful.
RefId *string `json:"refId,omitempty"`
// Azure Monitor query type.
// queryType: #AzureQueryType
RefId *string `json:"refId,omitempty"`
Region *string `json:"region,omitempty"`
Resource *string `json:"resource,omitempty"`
@@ -240,8 +240,15 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, queries
azureLogAnalyticsQueries = append(azureLogAnalyticsQueries, azureLogAnalyticsQuery)
}
if query.QueryType == string(dataquery.AzureQueryTypeAzureTraces) {
azureAppInsightsQuery, err := buildAppInsightsQuery(ctx, query, dsInfo, appInsightsRegExp)
if query.QueryType == string(dataquery.AzureQueryTypeAzureTraces) || query.QueryType == string(dataquery.AzureQueryTypeTraceql) {
if query.QueryType == string(dataquery.AzureQueryTypeTraceql) {
cfg := backend.GrafanaConfigFromContext(ctx)
hasPromExemplarsToggle := cfg.FeatureToggles().IsEnabled("azureMonitorPrometheusExemplars")
if !hasPromExemplarsToggle {
return nil, fmt.Errorf("query type unsupported as azureMonitorPrometheusExemplars feature toggle is not enabled")
}
}
azureAppInsightsQuery, err := buildAppInsightsQuery(ctx, query, dsInfo, appInsightsRegExp, e.Logger)
if err != nil {
return nil, fmt.Errorf("failed to build azure application insights query: %w", err)
}
@@ -321,7 +328,7 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *A
return nil, err
}
if query.QueryType == dataquery.AzureQueryTypeAzureTraces && query.ResultFormat == dataquery.ResultFormatTrace {
if (query.QueryType == dataquery.AzureQueryTypeAzureTraces || query.QueryType == dataquery.AzureQueryTypeTraceql) && query.ResultFormat == dataquery.ResultFormatTrace {
frame.Meta.PreferredVisualization = data.VisTypeTrace
}
@@ -687,3 +687,82 @@ func Test_executeQueryErrorWithDifferentLogAnalyticsCreds(t *testing.T) {
t.Error("expecting the error to inform of bad credentials")
}
}
func Test_exemplarsFeatureToggle(t *testing.T) {
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
correlationRes := AzureCorrelationAPIResponse{
ID: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1",
Name: "guid-1",
Type: "microsoft.insights/transactions",
Properties: AzureCorrelationAPIResponseProperties{
Resources: []string{
"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1",
},
NextLink: nil,
},
}
err := json.NewEncoder(w).Encode(correlationRes)
if err != nil {
t.Errorf("failed to encode correlation API response")
}
}))
provider := httpclient.NewProvider(httpclient.ProviderOptions{Timeout: &httpclient.DefaultTimeoutOptions})
client, err := provider.New()
if err != nil {
t.Errorf("failed to create fake client")
}
ds := AzureLogAnalyticsDatasource{}
dsInfo := types.DatasourceInfo{
Services: map[string]types.DatasourceService{
"Azure Log Analytics": {URL: "http://ds"},
"Azure Monitor": {URL: svr.URL, HTTPClient: client},
},
Settings: types.AzureMonitorSettings{
SubscriptionId: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
},
}
t.Run("does not error if feature toggle enabled", func(t *testing.T) {
ctx := context.Background()
ctx = backend.WithGrafanaConfig(ctx, backend.NewGrafanaCfg(map[string]string{"GF_INSTANCE_FEATURE_TOGGLES_ENABLE": "azureMonitorPrometheusExemplars"}))
query := backend.DataQuery{
JSON: []byte(`{
"queryType": "traceql",
"azureTraces": {
"operationId": "traceid"
},
"query": "traceid"
}`),
RefID: "A",
QueryType: string(dataquery.AzureQueryTypeTraceql),
}
_, err := ds.buildQueries(ctx, []backend.DataQuery{query}, dsInfo, false)
require.NoError(t, err)
})
t.Run("errors if feature toggle disabled", func(t *testing.T) {
ctx := context.Background()
ctx = backend.WithGrafanaConfig(ctx, backend.NewGrafanaCfg(map[string]string{"GF_INSTANCE_FEATURE_TOGGLES_ENABLE": ""}))
query := backend.DataQuery{
JSON: []byte(`{
"queryType": "traceql",
"azureTraces": {
"operationId": "traceid"
},
"query": "traceid"
}`),
RefID: "A",
QueryType: string(dataquery.AzureQueryTypeTraceql),
}
_, err := ds.buildQueries(ctx, []backend.DataQuery{query}, dsInfo, false)
require.Error(t, err, "query type unsupported as azureMonitorPrometheusExemplars feature toggle is not enabled")
})
}
@@ -78,7 +78,7 @@ func converterFrameForTable(t *types.AzureResponseTable, queryType dataquery.Azu
if !ok {
return nil, fmt.Errorf("unsupported analytics column type %v", col.Type)
}
if queryType == dataquery.AzureQueryTypeAzureTraces && resultFormat == dataquery.ResultFormatTrace && (col.Name == "serviceTags" || col.Name == "tags") {
if (queryType == dataquery.AzureQueryTypeAzureTraces || queryType == dataquery.AzureQueryTypeTraceql) && resultFormat == dataquery.ResultFormatTrace && (col.Name == "serviceTags" || col.Name == "tags") {
converter = tagsConverter
}
converters = append(converters, converter)
@@ -69,38 +69,50 @@ func TestTraceTableToFrame(t *testing.T) {
testFile string
expectedFrame func() *data.Frame
resultFormat dataquery.ResultFormat
queryType dataquery.AzureQueryType
}{
{
name: "multi trace",
testFile: "traces/1-traces-multiple-table.json",
resultFormat: dataquery.ResultFormatTable,
queryType: dataquery.AzureQueryTypeAzureTraces,
},
{
name: "multi trace as trace format",
testFile: "traces/1-traces-multiple-table.json",
resultFormat: dataquery.ResultFormatTrace,
queryType: dataquery.AzureQueryTypeAzureTraces,
},
{
name: "single trace",
testFile: "traces/2-traces-single-table.json",
resultFormat: dataquery.ResultFormatTable,
queryType: dataquery.AzureQueryTypeAzureTraces,
},
{
name: "single trace as trace format",
testFile: "traces/2-traces-single-table.json",
resultFormat: dataquery.ResultFormatTrace,
queryType: dataquery.AzureQueryTypeAzureTraces,
},
{
name: "single trace with empty serviceTags and tags",
testFile: "traces/3-traces-empty-dynamics.json",
resultFormat: dataquery.ResultFormatTrace,
queryType: dataquery.AzureQueryTypeAzureTraces,
},
{
name: "single trace as trace format from exemplars query",
testFile: "traces/2-traces-single-table.json",
resultFormat: dataquery.ResultFormatTrace,
queryType: dataquery.AzureQueryTypeTraceql,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
res := loadTestFileWithNumber(t, tt.testFile)
frame, err := ResponseTableToFrame(&res.Tables[0], "A", "query", dataquery.AzureQueryTypeAzureTraces, tt.resultFormat)
frame, err := ResponseTableToFrame(&res.Tables[0], "A", "query", tt.queryType, tt.resultFormat)
appendErrorNotice(frame, res.Error)
require.NoError(t, err)
+17 -2
View File
@@ -9,9 +9,11 @@ import (
"strings"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/kinds/dataquery"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/macros"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/types"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/utils"
"k8s.io/utils/strings/slices"
)
@@ -182,7 +184,7 @@ func buildTraceQueries(query backend.DataQuery, dsInfo types.DatasourceInfo, tra
return queryString, &traceQueries, nil
}
func buildAppInsightsQuery(ctx context.Context, query backend.DataQuery, dsInfo types.DatasourceInfo, appInsightsRegExp *regexp.Regexp) (*AzureLogAnalyticsQuery, error) {
func buildAppInsightsQuery(ctx context.Context, query backend.DataQuery, dsInfo types.DatasourceInfo, appInsightsRegExp *regexp.Regexp, logger log.Logger) (*AzureLogAnalyticsQuery, error) {
dashboardTime := true
timeColumn := ""
queryJSONModel := types.TracesJSONQuery{}
@@ -196,7 +198,15 @@ func buildAppInsightsQuery(ctx context.Context, query backend.DataQuery, dsInfo
resultFormat := ParseResultFormat(azureTracesTarget.ResultFormat, dataquery.AzureQueryTypeAzureTraces)
resources := azureTracesTarget.Resources
resourceOrWorkspace := azureTracesTarget.Resources[0]
if query.QueryType == string(dataquery.AzureQueryTypeTraceql) {
subscription, err := utils.GetFirstSubscriptionOrDefault(ctx, dsInfo, logger)
if err != nil {
return nil, fmt.Errorf("failed to retrieve subscription for trace exemplars query: %w", err)
}
resources = []string{fmt.Sprintf("/subscriptions/%s", subscription)}
}
resourceOrWorkspace := resources[0]
appInsightsQuery := appInsightsRegExp.Match([]byte(resourceOrWorkspace))
resourcesMap := make(map[string]bool, 0)
if len(resources) > 1 {
@@ -222,6 +232,11 @@ func buildAppInsightsQuery(ctx context.Context, query backend.DataQuery, dsInfo
}
sort.Strings(queryResources)
if query.QueryType == string(dataquery.AzureQueryTypeTraceql) {
resources = queryResources
resourceOrWorkspace = resources[0]
}
queryString, traceQueries, err := buildTraceQueries(query, dsInfo, queryJSONModel.AzureTraces, operationId, resultFormat, queryResources)
if err != nil {
return nil, err
@@ -1150,7 +1150,7 @@ func TestBuildAppInsightsQuery(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
query, err := buildAppInsightsQuery(ctx, tt.queryModel, dsInfo, appInsightsRegExp)
query, err := buildAppInsightsQuery(ctx, tt.queryModel, dsInfo, appInsightsRegExp, backend.Logger)
tt.Err(t, err)
if diff := cmp.Diff(&tt.azureLogAnalyticsQuery, query); diff != "" {
t.Errorf("Result mismatch (-want +got): \n%s", diff)
+2
View File
@@ -19,6 +19,7 @@ const (
azureResourceGraph = "Azure Resource Graph"
azureTraces = "Azure Traces"
azurePortal = "Azure Portal"
traceExemplar = "traceql"
)
func getAzureMonitorRoutes(settings *azsettings.AzureSettings, credentials azcredentials.AzureCredentials, jsonData json.RawMessage) (map[string]types.AzRoute, error) {
@@ -82,6 +83,7 @@ func getAzureMonitorRoutes(settings *azsettings.AzureSettings, credentials azcre
azureLogAnalytics: logAnalyticsRoute,
azureResourceGraph: resourceManagerRoute,
azureTraces: logAnalyticsRoute,
traceExemplar: logAnalyticsRoute,
azurePortal: portalRoute,
}
@@ -0,0 +1,321 @@
// 🌟 This was machine generated. Do not edit. 🌟
//
// Frame[0] {
// "typeVersion": [
// 0,
// 0
// ],
// "custom": {
// "azureColumnTypes": [
// "string",
// "string",
// "string",
// "real",
// "string",
// "string",
// "datetime",
// "dynamic",
// "dynamic",
// "string",
// "string"
// ]
// }
// }
// Name:
// Dimensions: 11 Fields by 1 Rows
// +----------------------------------+--------------------------------------+------------------------------------+------------------+-------------------+-------------------------------------+-----------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------+-----------------+
// | Name: traceID | Name: spanID | Name: parentSpanID | Name: duration | Name: serviceName | Name: operationName | Name: startTime | Name: serviceTags | Name: tags | Name: itemId | Name: itemType |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []*string | Type: []*string | Type: []*string | Type: []*float64 | Type: []*string | Type: []*string | Type: []*time.Time | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []*string | Type: []*string |
// +----------------------------------+--------------------------------------+------------------------------------+------------------+-------------------+-------------------------------------+-----------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------+-----------------+
// | cfae497bfd7a44169f35643940820938 | b52403c5-5b27-43a8-9bc6-5938667a4470 | |cfae497bfd7a44169f35643940820938. | 0 | | GET /github/grafana/grafana/commits | 2023-04-17 14:58:10.176 +0000 UTC | [{"value":"5000","key":"limit"},{"value":"4351","key":"remaining"},{"value":"1681746512","key":"reset"},{"value":"github-test-data","key":"service"},{"value":"2023-04-17T14:58:10.0000000Z","key":"timestamp"},{"value":"649","key":"used"}] | [{"value":"4ad5a808-11f7-49d5-9713-f6ede83141e4","key":"appId"},{"value":"test-app","key":"appName"},{"value":"1.0.0","key":"application_Version"},{"value":"Dublin","key":"client_City"},{"value":"Ireland","key":"client_CountryOrRegion"},{"value":"0.0.0.0","key":"client_IP"},{"value":"Linux 5.4.0-1036-azure","key":"client_OS"},{"value":"Dublin","key":"client_StateOrProvince"},{"value":"PC","key":"client_Type"},{"value":"test-vm","key":"cloud_RoleInstance"},{"value":"Web","key":"cloud_RoleName"},{"value":{"limit":"5000","remaining":"4351","reset":"1681746512","service":"github-test-data","timestamp":"2023-04-17T14:58:10.0000000Z","used":"649"},"key":"customDimensions"},{"value":0,"key":"duration"},{"value":"195b4fe4-7b01-4814-abca-ffceb1f62c8f","key":"iKey"},{"value":1,"key":"itemCount"},{"value":"65863e6b-dd30-11ed-a808-002248268105","key":"itemId"},{"value":"trace","key":"itemType"},{"value":"github commits rate limiting info","key":"message"},{"value":"cfae497bfd7a44169f35643940820938","key":"operation_Id"},{"value":"GET /github/grafana/grafana/commits","key":"operation_Name"},{"value":"|cfae497bfd7a44169f35643940820938.","key":"operation_ParentId"},{"value":"node:1.8.9","key":"sdkVersion"},{"value":1,"key":"severityLevel"},{"value":"2023-04-17T14:58:10.1760000Z","key":"timestamp"}] | 65863e6b-dd30-11ed-a808-002248268105 | trace |
// +----------------------------------+--------------------------------------+------------------------------------+------------------+-------------------+-------------------------------------+-----------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------+-----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
{
"status": 200,
"frames": [
{
"schema": {
"meta": {
"typeVersion": [
0,
0
],
"custom": {
"azureColumnTypes": [
"string",
"string",
"string",
"real",
"string",
"string",
"datetime",
"dynamic",
"dynamic",
"string",
"string"
]
}
},
"fields": [
{
"name": "traceID",
"type": "string",
"typeInfo": {
"frame": "string",
"nullable": true
}
},
{
"name": "spanID",
"type": "string",
"typeInfo": {
"frame": "string",
"nullable": true
}
},
{
"name": "parentSpanID",
"type": "string",
"typeInfo": {
"frame": "string",
"nullable": true
}
},
{
"name": "duration",
"type": "number",
"typeInfo": {
"frame": "float64",
"nullable": true
}
},
{
"name": "serviceName",
"type": "string",
"typeInfo": {
"frame": "string",
"nullable": true
}
},
{
"name": "operationName",
"type": "string",
"typeInfo": {
"frame": "string",
"nullable": true
}
},
{
"name": "startTime",
"type": "time",
"typeInfo": {
"frame": "time.Time",
"nullable": true
}
},
{
"name": "serviceTags",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
}
},
{
"name": "tags",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
}
},
{
"name": "itemId",
"type": "string",
"typeInfo": {
"frame": "string",
"nullable": true
}
},
{
"name": "itemType",
"type": "string",
"typeInfo": {
"frame": "string",
"nullable": true
}
}
]
},
"data": {
"values": [
[
"cfae497bfd7a44169f35643940820938"
],
[
"b52403c5-5b27-43a8-9bc6-5938667a4470"
],
[
"|cfae497bfd7a44169f35643940820938."
],
[
0
],
[
""
],
[
"GET /github/grafana/grafana/commits"
],
[
1681743490176
],
[
[
{
"value": "5000",
"key": "limit"
},
{
"value": "4351",
"key": "remaining"
},
{
"value": "1681746512",
"key": "reset"
},
{
"value": "github-test-data",
"key": "service"
},
{
"value": "2023-04-17T14:58:10.0000000Z",
"key": "timestamp"
},
{
"value": "649",
"key": "used"
}
]
],
[
[
{
"value": "4ad5a808-11f7-49d5-9713-f6ede83141e4",
"key": "appId"
},
{
"value": "test-app",
"key": "appName"
},
{
"value": "1.0.0",
"key": "application_Version"
},
{
"value": "Dublin",
"key": "client_City"
},
{
"value": "Ireland",
"key": "client_CountryOrRegion"
},
{
"value": "0.0.0.0",
"key": "client_IP"
},
{
"value": "Linux 5.4.0-1036-azure",
"key": "client_OS"
},
{
"value": "Dublin",
"key": "client_StateOrProvince"
},
{
"value": "PC",
"key": "client_Type"
},
{
"value": "test-vm",
"key": "cloud_RoleInstance"
},
{
"value": "Web",
"key": "cloud_RoleName"
},
{
"value": {
"limit": "5000",
"remaining": "4351",
"reset": "1681746512",
"service": "github-test-data",
"timestamp": "2023-04-17T14:58:10.0000000Z",
"used": "649"
},
"key": "customDimensions"
},
{
"value": 0,
"key": "duration"
},
{
"value": "195b4fe4-7b01-4814-abca-ffceb1f62c8f",
"key": "iKey"
},
{
"value": 1,
"key": "itemCount"
},
{
"value": "65863e6b-dd30-11ed-a808-002248268105",
"key": "itemId"
},
{
"value": "trace",
"key": "itemType"
},
{
"value": "github commits rate limiting info",
"key": "message"
},
{
"value": "cfae497bfd7a44169f35643940820938",
"key": "operation_Id"
},
{
"value": "GET /github/grafana/grafana/commits",
"key": "operation_Name"
},
{
"value": "|cfae497bfd7a44169f35643940820938.",
"key": "operation_ParentId"
},
{
"value": "node:1.8.9",
"key": "sdkVersion"
},
{
"value": 1,
"key": "severityLevel"
},
{
"value": "2023-04-17T14:58:10.1760000Z",
"key": "timestamp"
}
]
],
[
"65863e6b-dd30-11ed-a808-002248268105"
],
[
"trace"
]
]
}
}
]
}
+70
View File
@@ -0,0 +1,70 @@
package utils
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/types"
)
const SubscriptionsApiVersion = "2020-01-01"
func GetFirstSubscriptionOrDefault(ctx context.Context, dsInfo types.DatasourceInfo, logger log.Logger) (string, error) {
if dsInfo.Settings.SubscriptionId != "" {
return dsInfo.Settings.SubscriptionId, nil
}
url := fmt.Sprintf("%v/subscriptions?api-version=%v", dsInfo.Routes["Azure Monitor"].URL, SubscriptionsApiVersion)
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return "", err
}
res, err := dsInfo.Services["Azure Monitor"].HTTPClient.Do(request)
if err != nil {
return "", fmt.Errorf("failed to retrieve subscriptions: %v", err)
}
defer func() {
if err := res.Body.Close(); err != nil {
logger.Warn("Failed to close response body", "err", err)
}
}()
subscriptions, err := ParseSubscriptions(res, logger)
if err != nil {
return "", fmt.Errorf("failed to parse subscriptions: %v", err)
}
if len(subscriptions) == 0 {
return "", fmt.Errorf("no subscriptions found: %v", err)
}
return subscriptions[0], nil
}
func ParseSubscriptions(res *http.Response, logger log.Logger) ([]string, error) {
var target struct {
Value []struct {
SubscriptionId string `json:"subscriptionId"`
}
}
err := json.NewDecoder(res.Body).Decode(&target)
if err != nil {
return nil, err
}
defer func() {
if err := res.Body.Close(); err != nil {
logger.Warn("Failed to close response body", "err", err)
}
}()
result := make([]string, len(target.Value))
for i, v := range target.Value {
result[i] = v.SubscriptionId
}
return result, nil
}
@@ -75,7 +75,7 @@ describe('LogsQueryEditor', () => {
await userEvent.click(await screen.findByRole('button', { name: 'Apply' }));
expect(onChange).toBeCalledWith(
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
azureLogAnalytics: expect.objectContaining({
resources: [
@@ -189,7 +189,7 @@ describe('LogsQueryEditor', () => {
const applyButton = screen.getByRole('button', { name: 'Apply' });
await userEvent.click(applyButton);
expect(onChange).toBeCalledWith(
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
azureLogAnalytics: expect.objectContaining({
resources: ['/subscriptions/def-123'],
@@ -218,7 +218,7 @@ describe('LogsQueryEditor', () => {
const dashboardTimeOption = await screen.findByLabelText('Dashboard');
await userEvent.click(dashboardTimeOption);
expect(onChange).toBeCalledWith(
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
azureLogAnalytics: expect.objectContaining({
dashboardTime: true,
@@ -188,6 +188,7 @@ const EditorForQueryType = ({
);
case AzureQueryType.AzureTraces:
case AzureQueryType.TraceExemplar:
return (
<TracesQueryEditor
subscriptionId={subscriptionId}
@@ -37,7 +37,7 @@ export const QueryHeader = ({ query, onQueryChange }: QueryTypeFieldProps) => {
<EditorHeader>
<InlineSelect
label="Service"
value={query.queryType}
value={query.queryType === AzureQueryType.TraceExemplar ? AzureQueryType.AzureTraces : query.queryType}
placeholder="Service..."
allowCustomValue
options={queryTypes}
@@ -2,17 +2,29 @@ import deepEqual from 'fast-deep-equal';
import { defaults } from 'lodash';
import { useEffect, useMemo } from 'react';
import { AzureMonitorQuery, AzureQueryType } from '../../types';
import { AzureMonitorQuery, AzureQueryType, ResultFormat } from '../../types';
import migrateQuery from '../../utils/migrateQuery';
const DEFAULT_QUERY = {
queryType: AzureQueryType.AzureMonitor,
};
const transformExemplarQuery = (query: AzureMonitorQuery) => {
if (query.queryType === AzureQueryType.TraceExemplar && query.query !== '' && !query.azureTraces) {
query.azureTraces = {
operationId: query.query,
resultFormat: ResultFormat.Trace,
};
}
return query;
};
const prepareQuery = (query: AzureMonitorQuery) => {
// Note: _.defaults does not apply default values deeply.
const withDefaults = defaults({}, query, DEFAULT_QUERY);
const migratedQuery = migrateQuery(withDefaults);
const transformedQuery = transformExemplarQuery(withDefaults);
const migratedQuery = migrateQuery(transformedQuery);
// If we didn't make any changes to the object, then return the original object to keep the
// identity the same, and not trigger any other useEffects or anything.
@@ -154,7 +154,7 @@ describe('AzureMonitor ResourcePicker', () => {
expect(applyButton).toBeEnabled();
await userEvent.click(applyButton);
expect(onApply).toBeCalledTimes(1);
expect(onApply).toBeCalledWith(['/subscriptions/def-123']);
expect(onApply).toHaveBeenCalledWith(['/subscriptions/def-123']);
});
it('should call onApply removing an element', async () => {
@@ -169,7 +169,7 @@ describe('AzureMonitor ResourcePicker', () => {
const applyButton = screen.getByRole('button', { name: 'Apply' });
await userEvent.click(applyButton);
expect(onApply).toBeCalledTimes(1);
expect(onApply).toBeCalledWith([]);
expect(onApply).toHaveBeenCalledWith([]);
});
it('should call onApply removing an element ignoring the case', async () => {
@@ -186,7 +186,7 @@ describe('AzureMonitor ResourcePicker', () => {
const applyButton = screen.getByRole('button', { name: 'Apply' });
await userEvent.click(applyButton);
expect(onApply).toBeCalledTimes(1);
expect(onApply).toBeCalledWith([]);
expect(onApply).toHaveBeenCalledWith([]);
});
it('should call onApply with a new resource when a user clicks on the checkbox in the row', async () => {
@@ -207,7 +207,7 @@ describe('AzureMonitor ResourcePicker', () => {
await userEvent.click(applyButton);
expect(onApply).toBeCalledTimes(1);
expect(onApply).toBeCalledWith([
expect(onApply).toHaveBeenCalledWith([
{
metricNamespace: 'Microsoft.Compute/virtualMachines',
region: 'northeurope',
@@ -247,7 +247,7 @@ describe('AzureMonitor ResourcePicker', () => {
const applyButton = screen.getByRole('button', { name: 'Apply' });
await userEvent.click(applyButton);
expect(onApply).toBeCalledTimes(1);
expect(onApply).toBeCalledWith([]);
expect(onApply).toHaveBeenCalledWith([]);
});
it('renders a search field which show search results when there are results', async () => {
@@ -4,6 +4,7 @@ import React from 'react';
import createMockDatasource from '../../__mocks__/datasource';
import createMockQuery from '../../__mocks__/query';
import { AzureQueryType } from '../../dataquery.gen';
import { createMockResourcePickerData } from '../MetricsQueryEditor/MetricsQueryEditor.test';
import TracesQueryEditor from './TracesQueryEditor';
@@ -68,7 +69,7 @@ describe('TracesQueryEditor', () => {
await userEvent.click(await screen.findByRole('button', { name: 'Apply' }));
expect(onChange).toBeCalledWith(
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
azureTraces: expect.objectContaining({
resources: [
@@ -176,7 +177,7 @@ describe('TracesQueryEditor', () => {
const applyButton = screen.getByRole('button', { name: 'Apply' });
await userEvent.click(applyButton);
expect(onChange).toBeCalledWith(
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
azureTraces: expect.objectContaining({
resources: ['/subscriptions/def-123'],
@@ -184,4 +185,58 @@ describe('TracesQueryEditor', () => {
})
);
});
it('should not display the resource selector for exemplar type queries', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() });
const query = createMockQuery();
delete query?.subscription;
delete query?.azureTraces?.resources;
query.queryType = AzureQueryType.TraceExemplar;
query.azureTraces = { operationId: 'test-operation-id' };
const onChange = jest.fn();
render(
<TracesQueryEditor
query={query}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={onChange}
setError={() => {}}
/>
);
expect(await screen.queryByRole('button', { name: 'Select a resource' })).not.toBeInTheDocument();
expect(await screen.getByDisplayValue('test-operation-id')).toBeInTheDocument();
});
it('should not display the resource selector for exemplar type queries', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() });
const query = createMockQuery();
delete query?.subscription;
delete query?.azureTraces?.resources;
query.queryType = AzureQueryType.TraceExemplar;
query.azureTraces = { operationId: 'test-operation-id' };
const onChange = jest.fn();
render(
<TracesQueryEditor
query={query}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={onChange}
setError={() => {}}
/>
);
const operationIDInput = await screen.getByDisplayValue('test-operation-id');
await userEvent.clear(operationIDInput);
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
azureTraces: undefined,
queryType: AzureQueryType.AzureTraces,
query: undefined,
})
);
});
});
@@ -7,7 +7,7 @@ import { Input } from '@grafana/ui';
import Datasource from '../../datasource';
import { selectors } from '../../e2e/selectors';
import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery, ResultFormat } from '../../types';
import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery, AzureQueryType, ResultFormat } from '../../types';
import AdvancedResourcePicker from '../LogsQueryEditor/AdvancedResourcePicker';
import ResourceField from '../ResourceField';
import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../ResourcePicker/types';
@@ -17,7 +17,7 @@ import FormatAsField from '../shared/FormatAsField';
import Filters from './Filters';
import TraceTypeField from './TraceTypeField';
import { setFormatAs, setQueryOperationId } from './setQueryValue';
import { setDefaultTracesQuery, setFormatAs, setQueryOperationId } from './setQueryValue';
interface TracesQueryEditorProps {
query: AzureMonitorQuery;
@@ -63,11 +63,18 @@ const TracesQueryEditor = ({
}
}, [setOperationId, previousOperationId, query, operationId]);
const handleChange = useCallback((ev: React.FormEvent) => {
if (ev.target instanceof HTMLInputElement) {
setOperationId(ev.target.value);
}
}, []);
const handleChange = useCallback(
(ev: React.FormEvent) => {
if (ev.target instanceof HTMLInputElement) {
setOperationId(ev.target.value);
if (query.queryType === AzureQueryType.TraceExemplar && ev.target.value === '') {
// If this is an exemplars query and the operation ID is cleared we reset this to a default traces query
onChange(setDefaultTracesQuery(query));
}
}
},
[onChange, query]
);
const handleBlur = useCallback(
(ev: React.FormEvent) => {
@@ -80,35 +87,37 @@ const TracesQueryEditor = ({
return (
<span data-testid={selectors.components.queryEditor.tracesQueryEditor.container.input}>
<EditorRows>
<EditorRow>
<EditorFieldGroup>
<ResourceField
query={query}
datasource={datasource}
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
selectableEntryTypes={[
ResourceRowType.Subscription,
ResourceRowType.ResourceGroup,
ResourceRowType.Resource,
ResourceRowType.Variable,
]}
resources={query.azureTraces?.resources ?? []}
queryType="traces"
disableRow={disableRow}
renderAdvanced={(resources, onChange) => (
// It's required to cast resources because the resource picker
// specifies the type to string | AzureMonitorResource.
// eslint-disable-next-line
<AdvancedResourcePicker resources={resources as string[]} onChange={onChange} />
)}
selectionNotice={() => 'You may only choose items of the same resource type.'}
range={range}
/>
</EditorFieldGroup>
</EditorRow>
{query.queryType !== AzureQueryType.TraceExemplar && (
<EditorRow>
<EditorFieldGroup>
<ResourceField
query={query}
datasource={datasource}
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
selectableEntryTypes={[
ResourceRowType.Subscription,
ResourceRowType.ResourceGroup,
ResourceRowType.Resource,
ResourceRowType.Variable,
]}
resources={query.azureTraces?.resources ?? []}
queryType="traces"
disableRow={disableRow}
renderAdvanced={(resources, onChange) => (
// It's required to cast resources because the resource picker
// specifies the type to string | AzureMonitorResource.
// eslint-disable-next-line
<AdvancedResourcePicker resources={resources as string[]} onChange={onChange} />
)}
selectionNotice={() => 'You may only choose items of the same resource type.'}
range={range}
/>
</EditorFieldGroup>
</EditorRow>
)}
<EditorRow>
<EditorFieldGroup>
<TraceTypeField
@@ -1,4 +1,14 @@
import { AzureMonitorQuery, AzureTracesFilter, ResultFormat } from '../../types';
import { AzureMonitorQuery, AzureQueryType, AzureTracesFilter, ResultFormat } from '../../types';
// Used when switching from a traces exemplar query to a standard Azure Traces query
export function setDefaultTracesQuery(query: AzureMonitorQuery): AzureMonitorQuery {
return {
...query,
query: undefined,
queryType: AzureQueryType.AzureTraces,
azureTraces: undefined,
};
}
export function setQueryOperationId(query: AzureMonitorQuery, operationId?: string): AzureMonitorQuery {
return {
@@ -50,10 +50,13 @@ composableKinds: DataQuery: {
region?: string
// Azure Monitor query type.
// queryType: #AzureQueryType
// Used only for exemplar queries from Prometheus
query?: string
} @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" @cuetsy(kind="enum", memberNames="AzureMonitor|LogAnalytics|AzureResourceGraph|AzureTraces|SubscriptionsQuery|ResourceGroupsQuery|NamespacesQuery|ResourceNamesQuery|MetricNamesQuery|WorkspacesQuery|LocationsQuery|GrafanaTemplateVariableFn")
#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")
#AzureMetricQuery: {
// Array of resource URIs to be queried.
@@ -33,9 +33,9 @@ export interface AzureMonitorQuery extends common.DataQuery {
grafanaTemplateVariableFn?: GrafanaTemplateVariableQuery;
namespace?: string;
/**
* Azure Monitor query type.
* queryType: #AzureQueryType
* Used only for exemplar queries from Prometheus
*/
query?: string;
region?: string;
resource?: string;
/**
@@ -71,6 +71,7 @@ export enum AzureQueryType {
ResourceGroupsQuery = 'Azure Resource Groups',
ResourceNamesQuery = 'Azure Resource Names',
SubscriptionsQuery = 'Azure Subscriptions',
TraceExemplar = 'traceql',
WorkspacesQuery = 'Azure Workspaces',
}
@@ -55,6 +55,7 @@ export default class Datasource extends DataSourceWithBackend<AzureMonitorQuery,
[AzureQueryType.AzureMonitor]: this.azureMonitorDatasource,
[AzureQueryType.LogAnalytics]: this.azureLogAnalyticsDatasource,
[AzureQueryType.AzureResourceGraph]: this.azureResourceGraphDatasource,
[AzureQueryType.AzureTraces]: this.azureLogAnalyticsDatasource,
};
this.variables = new VariableSupport(this);
@@ -107,7 +108,11 @@ export default class Datasource extends DataSourceWithBackend<AzureMonitorQuery,
}
const observables: Array<Observable<DataQueryResponse>> = Array.from(byType.entries()).map(([queryType, req]) => {
const mappedQueryType = queryType === AzureQueryType.AzureTraces ? AzureQueryType.LogAnalytics : queryType;
let mappedQueryType = queryType;
if (queryType === AzureQueryType.AzureTraces || queryType === AzureQueryType.TraceExemplar) {
mappedQueryType = AzureQueryType.LogAnalytics;
}
const ds = this.pseudoDatasource[mappedQueryType];
if (!ds) {
throw new Error('Data source not created for query type ' + queryType);
@@ -250,6 +255,7 @@ function hasQueryForType(query: AzureMonitorQuery): boolean {
return !!query.azureResourceGraph;
case AzureQueryType.AzureTraces:
case AzureQueryType.TraceExemplar:
return !!query.azureTraces;
case AzureQueryType.GrafanaTemplateVariableFn:
@@ -110,5 +110,6 @@
"annotations": true,
"alerting": true,
"backend": true,
"logs": true
"logs": true,
"tracing": true
}