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:
@@ -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}
|
||||
|
||||
+3
-2
@@ -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',
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
+321
@@ -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"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+3
-3
@@ -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}
|
||||
|
||||
+14
-2
@@ -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.
|
||||
|
||||
+5
-5
@@ -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 () => {
|
||||
|
||||
+57
-2
@@ -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,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+45
-36
@@ -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
|
||||
|
||||
+11
-1
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user