diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index c01043e20ba..0e5cc17b47c 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -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 diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index a2a1d22c3e2..82d0ae0dd73 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -191,4 +191,5 @@ export interface FeatureToggles { preserveDashboardStateWhenNavigating?: boolean; alertingCentralAlertHistory?: boolean; pluginProxyPreserveTrailingSlash?: boolean; + azureMonitorPrometheusExemplars?: boolean; } diff --git a/packages/grafana-prometheus/src/configuration/ExemplarSetting.tsx b/packages/grafana-prometheus/src/configuration/ExemplarSetting.tsx index acb9c35cbe6..cc2f94b1251 100644 --- a/packages/grafana-prometheus/src/configuration/ExemplarSetting.tsx +++ b/packages/grafana-prometheus/src/configuration/ExemplarSetting.tsx @@ -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} > ds.type !== 'grafana-azure-monitor-datasource' + } tracing={true} current={value.datasourceUid} noDefault={true} diff --git a/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts index 1caf676f79a..11d29c84a8c 100644 --- a/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts @@ -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', } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index fd2b9500784..55802717cb5 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -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, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 555e99ec0d7..b757ce9971c 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -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 diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index b54853a9b42..98a0b580ad0 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -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" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index e0a882ca222..cc4c48e445d 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -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" + } } ] } \ No newline at end of file diff --git a/pkg/tsdb/azuremonitor/azuremonitor.go b/pkg/tsdb/azuremonitor/azuremonitor.go index 3b860832924..b5bf3dcd5a0 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor.go +++ b/pkg/tsdb/azuremonitor/azuremonitor.go @@ -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) diff --git a/pkg/tsdb/azuremonitor/azuremonitor_test.go b/pkg/tsdb/azuremonitor/azuremonitor_test.go index e8dcf304f4e..6cde8cb6792 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor_test.go +++ b/pkg/tsdb/azuremonitor/azuremonitor_test.go @@ -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", }, diff --git a/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go index 551083d96e7..5da56de1e77 100644 --- a/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go @@ -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"` diff --git a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go index ead41f16ed7..86c9f4cf3b0 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go @@ -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 } diff --git a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go index 29fb137f608..a02ea2046eb 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go @@ -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") + }) +} diff --git a/pkg/tsdb/azuremonitor/loganalytics/azure-response-table-frame.go b/pkg/tsdb/azuremonitor/loganalytics/azure-response-table-frame.go index 9b8ca167aba..3b72999914e 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-response-table-frame.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-response-table-frame.go @@ -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) diff --git a/pkg/tsdb/azuremonitor/loganalytics/azure-response-table-frame_test.go b/pkg/tsdb/azuremonitor/loganalytics/azure-response-table-frame_test.go index 6ba687f0642..139ba7987d9 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-response-table-frame_test.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-response-table-frame_test.go @@ -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) diff --git a/pkg/tsdb/azuremonitor/loganalytics/traces.go b/pkg/tsdb/azuremonitor/loganalytics/traces.go index 3a3004a2a93..fbc8e961799 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/traces.go +++ b/pkg/tsdb/azuremonitor/loganalytics/traces.go @@ -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 diff --git a/pkg/tsdb/azuremonitor/loganalytics/traces_test.go b/pkg/tsdb/azuremonitor/loganalytics/traces_test.go index 16725132ef7..da069c41db4 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/traces_test.go +++ b/pkg/tsdb/azuremonitor/loganalytics/traces_test.go @@ -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) diff --git a/pkg/tsdb/azuremonitor/routes.go b/pkg/tsdb/azuremonitor/routes.go index 1624debb03d..c6772834de8 100644 --- a/pkg/tsdb/azuremonitor/routes.go +++ b/pkg/tsdb/azuremonitor/routes.go @@ -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, } diff --git a/pkg/tsdb/azuremonitor/testdata/traces/2-traces-single-table.json.single-trace-as-trace-format-from-exemplars-query.golden.jsonc b/pkg/tsdb/azuremonitor/testdata/traces/2-traces-single-table.json.single-trace-as-trace-format-from-exemplars-query.golden.jsonc new file mode 100644 index 00000000000..86a6297d6ef --- /dev/null +++ b/pkg/tsdb/azuremonitor/testdata/traces/2-traces-single-table.json.single-trace-as-trace-format-from-exemplars-query.golden.jsonc @@ -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" + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/tsdb/azuremonitor/utils/utils.go b/pkg/tsdb/azuremonitor/utils/utils.go new file mode 100644 index 00000000000..d6149c8e21b --- /dev/null +++ b/pkg/tsdb/azuremonitor/utils/utils.go @@ -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 +} diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.test.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.test.tsx index 7c5b61cd025..3c55dfcba66 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.test.tsx @@ -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, diff --git a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx index c4b5e5fad97..6dfed0a2146 100644 --- a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx @@ -188,6 +188,7 @@ const EditorForQueryType = ({ ); case AzureQueryType.AzureTraces: + case AzureQueryType.TraceExemplar: return ( { { + 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. diff --git a/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/ResourcePicker.test.tsx b/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/ResourcePicker.test.tsx index 3e1b20c4d0f..63559579ef8 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/ResourcePicker.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/ResourcePicker.test.tsx @@ -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 () => { diff --git a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.test.tsx b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.test.tsx index 2f74c93a1a0..90df5bc9b41 100644 --- a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.test.tsx @@ -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( + {}} + /> + ); + + 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( + {}} + /> + ); + + const operationIDInput = await screen.getByDisplayValue('test-operation-id'); + await userEvent.clear(operationIDInput); + + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + azureTraces: undefined, + queryType: AzureQueryType.AzureTraces, + query: undefined, + }) + ); + }); }); diff --git a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx index d4303efe819..c6573cfc22b 100644 --- a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx @@ -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 ( - - - ( - // It's required to cast resources because the resource picker - // specifies the type to string | AzureMonitorResource. - // eslint-disable-next-line - - )} - selectionNotice={() => 'You may only choose items of the same resource type.'} - range={range} - /> - - + {query.queryType !== AzureQueryType.TraceExemplar && ( + + + ( + // It's required to cast resources because the resource picker + // specifies the type to string | AzureMonitorResource. + // eslint-disable-next-line + + )} + selectionNotice={() => 'You may only choose items of the same resource type.'} + range={range} + /> + + + )} > = 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: diff --git a/public/app/plugins/datasource/azuremonitor/plugin.json b/public/app/plugins/datasource/azuremonitor/plugin.json index 9e1d8c49be3..240259a9141 100644 --- a/public/app/plugins/datasource/azuremonitor/plugin.json +++ b/public/app/plugins/datasource/azuremonitor/plugin.json @@ -110,5 +110,6 @@ "annotations": true, "alerting": true, "backend": true, - "logs": true + "logs": true, + "tracing": true }