diff --git a/.betterer.results b/.betterer.results index e7f6277ab73..5181ddd6032 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5489,10 +5489,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "4"], [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"], - [0, 0, 0, "Unexpected any. Specify a different type.", "9"] + [0, 0, 0, "Unexpected any. Specify a different type.", "6"] ], "public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go index 6c6b74695ca..6d56483b23c 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go @@ -4,10 +4,12 @@ import ( "bytes" "compress/gzip" "context" + "encoding/base64" "encoding/json" "fmt" "io" "net/http" + "net/url" "path" "regexp" "time" @@ -16,7 +18,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" "go.opentelemetry.io/otel/attribute" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/tsdb/azuremonitor/macros" @@ -197,17 +198,16 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, logger l return dataResponse } - model, err := simplejson.NewJson(query.JSON) + azurePortalBaseUrl, err := GetAzurePortalUrl(dsInfo.Cloud) if err != nil { - return dataResponseErrorWithExecuted(err) + dataResponse.Error = err + return dataResponse } - err = setAdditionalFrameMeta(frame, - query.Query, - model.Get("azureLogAnalytics").Get("resource").MustString()) + queryUrl, err := getQueryUrl(query.Query, query.Resources, azurePortalBaseUrl) if err != nil { - frame.AppendNotices(data.Notice{Severity: data.NoticeSeverityWarning, Text: "could not add custom metadata: " + err.Error()}) - logger.Warn("failed to add custom metadata to azure log analytics response", err) + dataResponse.Error = err + return dataResponse } if query.ResultFormat == types.TimeSeries { @@ -222,6 +222,8 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, logger l } } + AddConfigLinks(*frame, queryUrl) + dataResponse.Frames = data.Frames{frame} return dataResponse } @@ -265,6 +267,43 @@ func (e *AzureLogAnalyticsDatasource) createRequest(ctx context.Context, logger return req, nil } +type AzureLogAnalyticsURLResources struct { + Resources []AzureLogAnalyticsURLResource `json:"resources"` +} + +type AzureLogAnalyticsURLResource struct { + ResourceID string `json:"resourceId"` +} + +func getQueryUrl(query string, resources []string, azurePortalUrl string) (string, error) { + encodedQuery, err := encodeQuery(query) + if err != nil { + return "", fmt.Errorf("failed to encode the query: %s", err) + } + + portalUrl := azurePortalUrl + if err != nil { + return "", fmt.Errorf("failed to parse base portal URL: %s", err) + } + + portalUrl += "/#blade/Microsoft_OperationsManagementSuite_Workspace/AnalyticsBlade/initiator/AnalyticsShareLinkToQuery/isQueryEditorVisible/true/scope/" + resourcesJson := AzureLogAnalyticsURLResources{ + Resources: make([]AzureLogAnalyticsURLResource, 0), + } + for _, resource := range resources { + resourcesJson.Resources = append(resourcesJson.Resources, AzureLogAnalyticsURLResource{ + ResourceID: resource, + }) + } + resourcesMarshalled, err := json.Marshal(resourcesJson) + if err != nil { + return "", fmt.Errorf("failed to marshal log analytics resources: %s", err) + } + portalUrl += url.QueryEscape(string(resourcesMarshalled)) + portalUrl += "/query/" + url.PathEscape(encodedQuery) + "/isQueryBase64Compressed/true/timespanInIsoFormat/P1D" + return portalUrl, nil +} + // Error definition has been inferred from real data and other model definitions like // https://github.com/Azure/azure-sdk-for-go/blob/3640559afddbad452d265b54fb1c20b30be0b062/services/preview/virtualmachineimagebuilder/mgmt/2019-05-01-preview/virtualmachineimagebuilder/models.go type AzureLogAnalyticsAPIError struct { @@ -333,41 +372,20 @@ func (e *AzureLogAnalyticsDatasource) unmarshalResponse(logger log.Logger, res * // LogAnalyticsMeta is a type for the a Frame's Meta's Custom property. type LogAnalyticsMeta struct { - ColumnTypes []string `json:"azureColumnTypes"` - EncodedQuery []byte `json:"encodedQuery"` // EncodedQuery is used for deep links. - Resource string `json:"resource"` -} - -func setAdditionalFrameMeta(frame *data.Frame, query, resource string) error { - if frame.Meta == nil || frame.Meta.Custom == nil { - // empty response - return nil - } - frame.Meta.ExecutedQueryString = query - la, ok := frame.Meta.Custom.(*LogAnalyticsMeta) - if !ok { - return fmt.Errorf("unexpected type found for frame's custom metadata") - } - la.Resource = resource - encodedQuery, err := encodeQuery(query) - if err == nil { - la.EncodedQuery = encodedQuery - return nil - } - return fmt.Errorf("failed to encode the query into the encodedQuery property") + ColumnTypes []string `json:"azureColumnTypes"` } // encodeQuery encodes the query in gzip so the frontend can build links. -func encodeQuery(rawQuery string) ([]byte, error) { +func encodeQuery(rawQuery string) (string, error) { var b bytes.Buffer gz := gzip.NewWriter(&b) if _, err := gz.Write([]byte(rawQuery)); err != nil { - return nil, err + return "", err } if err := gz.Close(); err != nil { - return nil, err + return "", err } - return b.Bytes(), nil + return base64.StdEncoding.EncodeToString(b.Bytes()), nil } 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 07a598a8125..19a42663b54 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go @@ -11,7 +11,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" @@ -308,11 +307,3 @@ func Test_executeQueryErrorWithDifferentLogAnalyticsCreds(t *testing.T) { t.Error("expecting the error to inform of bad credentials") } } - -func Test_setAdditionalFrameMeta(t *testing.T) { - t.Run("it should not error with an empty response", func(t *testing.T) { - frame := data.NewFrame("test") - err := setAdditionalFrameMeta(frame, "", "") - require.NoError(t, err) - }) -} diff --git a/pkg/tsdb/azuremonitor/loganalytics/utils.go b/pkg/tsdb/azuremonitor/loganalytics/utils.go new file mode 100644 index 00000000000..c96b7697a80 --- /dev/null +++ b/pkg/tsdb/azuremonitor/loganalytics/utils.go @@ -0,0 +1,36 @@ +package loganalytics + +import ( + "fmt" + + "github.com/grafana/grafana-azure-sdk-go/azsettings" + "github.com/grafana/grafana-plugin-sdk-go/data" +) + +func AddConfigLinks(frame data.Frame, dl string) data.Frame { + for i := range frame.Fields { + if frame.Fields[i].Config == nil { + frame.Fields[i].Config = &data.FieldConfig{} + } + deepLink := data.DataLink{ + Title: "View in Azure Portal", + TargetBlank: true, + URL: dl, + } + frame.Fields[i].Config.Links = append(frame.Fields[i].Config.Links, deepLink) + } + return frame +} + +func GetAzurePortalUrl(azureCloud string) (string, error) { + switch azureCloud { + case azsettings.AzurePublic: + return "https://portal.azure.com", nil + case azsettings.AzureChina: + return "https://portal.azure.cn", nil + case azsettings.AzureUSGovernment: + return "https://portal.azure.us", nil + default: + return "", fmt.Errorf("the cloud is not supported") + } +} diff --git a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go index b22b0597918..9a121988629 100644 --- a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go +++ b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go @@ -20,7 +20,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/tsdb/azuremonitor/resourcegraph" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/loganalytics" azTime "github.com/grafana/grafana/pkg/tsdb/azuremonitor/time" "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" ) @@ -246,7 +246,7 @@ func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, logger log.Lo return dataResponse } - azurePortalUrl, err := resourcegraph.GetAzurePortalUrl(dsInfo.Cloud) + azurePortalUrl, err := loganalytics.GetAzurePortalUrl(dsInfo.Cloud) if err != nil { dataResponse.Error = err return dataResponse @@ -379,7 +379,7 @@ func (e *AzureMonitorDatasource) parseResponse(amr types.AzureMonitorResponse, q return nil, err } - frameWithLink := resourcegraph.AddConfigLinks(*frame, queryUrl) + frameWithLink := loganalytics.AddConfigLinks(*frame, queryUrl) frames = append(frames, &frameWithLink) } diff --git a/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go b/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go index d58503f2679..4f3d9d50445 100644 --- a/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go +++ b/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go @@ -11,7 +11,6 @@ import ( "path" "time" - "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" "go.opentelemetry.io/otel/attribute" @@ -197,13 +196,13 @@ func (e *AzureResourceGraphDatasource) executeQuery(ctx context.Context, logger return dataResponse } - azurePortalUrl, err := GetAzurePortalUrl(dsInfo.Cloud) + azurePortalUrl, err := loganalytics.GetAzurePortalUrl(dsInfo.Cloud) if err != nil { return dataResponseErrorWithExecuted(err) } url := azurePortalUrl + "/#blade/HubsExtension/ArgQueryBlade/query/" + url.PathEscape(query.InterpolatedQuery) - frameWithLink := AddConfigLinks(*frame, url) + frameWithLink := loganalytics.AddConfigLinks(*frame, url) if frameWithLink.Meta == nil { frameWithLink.Meta = &data.FrameMeta{} } @@ -213,21 +212,6 @@ func (e *AzureResourceGraphDatasource) executeQuery(ctx context.Context, logger return dataResponse } -func AddConfigLinks(frame data.Frame, dl string) data.Frame { - for i := range frame.Fields { - if frame.Fields[i].Config == nil { - frame.Fields[i].Config = &data.FieldConfig{} - } - deepLink := data.DataLink{ - Title: "View in Azure Portal", - TargetBlank: true, - URL: dl, - } - frame.Fields[i].Config.Links = append(frame.Fields[i].Config.Links, deepLink) - } - return frame -} - func (e *AzureResourceGraphDatasource) createRequest(ctx context.Context, logger log.Logger, reqBody []byte, url string) (*http.Request, error) { req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(reqBody)) if err != nil { @@ -268,16 +252,3 @@ func (e *AzureResourceGraphDatasource) unmarshalResponse(logger log.Logger, res return data, nil } - -func GetAzurePortalUrl(azureCloud string) (string, error) { - switch azureCloud { - case azsettings.AzurePublic: - return "https://portal.azure.com", nil - case azsettings.AzureChina: - return "https://portal.azure.cn", nil - case azsettings.AzureUSGovernment: - return "https://portal.azure.us", nil - default: - return "", fmt.Errorf("the cloud is not supported") - } -} diff --git a/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource_test.go b/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource_test.go index 1ae4a462ac2..915bb7fe56f 100644 --- a/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource_test.go +++ b/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource_test.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/loganalytics" "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" ) @@ -125,7 +126,7 @@ func TestAddConfigData(t *testing.T) { frame := data.Frame{ Fields: []*data.Field{&field}, } - frameWithLink := AddConfigLinks(frame, "http://ds") + frameWithLink := loganalytics.AddConfigLinks(frame, "http://ds") expectedFrameWithLink := data.Frame{ Fields: []*data.Field{ { @@ -149,7 +150,7 @@ func TestGetAzurePortalUrl(t *testing.T) { } for _, cloud := range clouds { - azurePortalUrl, err := GetAzurePortalUrl(cloud) + azurePortalUrl, err := loganalytics.GetAzurePortalUrl(cloud) if err != nil { t.Errorf("The cloud not supported") } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts index 42984e611b8..b98f18e0ddf 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts @@ -1,14 +1,6 @@ import { map } from 'lodash'; -import { from, Observable } from 'rxjs'; -import { mergeMap } from 'rxjs/operators'; -import { - DataQueryRequest, - DataQueryResponse, - DataSourceInstanceSettings, - DataSourceRef, - ScopedVars, -} from '@grafana/data'; +import { DataSourceInstanceSettings, DataSourceRef, ScopedVars } from '@grafana/data'; import { DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime'; import { isGUIDish } from '../components/ResourcePicker/utils'; @@ -43,11 +35,9 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< azureMonitorPath: string; firstWorkspace?: string; - cache: Map; constructor(private instanceSettings: DataSourceInstanceSettings) { super(instanceSettings); - this.cache = new Map(); this.resourcePath = `${routeNames.logAnalytics}`; this.azureMonitorPath = `${routeNames.azureMonitor}/subscriptions`; @@ -148,78 +138,6 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< }; } - /** - * Augment the results with links back to the azure console - */ - query(request: DataQueryRequest): Observable { - return super.query(request).pipe( - mergeMap((res: DataQueryResponse) => { - return from(this.processResponse(res)); - }) - ); - } - - async processResponse(res: DataQueryResponse): Promise { - if (res.data) { - for (const df of res.data) { - const encodedQuery = df.meta?.custom?.encodedQuery; - if (encodedQuery && encodedQuery.length > 0) { - const url = await this.buildDeepLink(df.meta.custom); - if (url?.length) { - for (const field of df.fields) { - field.config.links = [ - { - url: url, - title: 'View in Azure Portal', - targetBlank: true, - }, - ]; - } - } - } - } - } - return res; - } - - private async buildDeepLink(customMeta: Record) { - const base64Enc = encodeURIComponent(customMeta.encodedQuery); - const resource = encodeURIComponent(customMeta.resource); - - const url = - `${this.azurePortalUrl}/#blade/Microsoft_OperationsManagementSuite_Workspace/` + - `AnalyticsBlade/initiator/AnalyticsShareLinkToQuery/isQueryEditorVisible/true/scope/` + - `%7B%22resources%22%3A%5B%7B%22resourceId%22%3A%22${resource}` + - `%22%7D%5D%7D/query/${base64Enc}/isQueryBase64Compressed/true/timespanInIsoFormat/P1D`; - return url; - } - - async getWorkspaceDetails(workspaceId: string) { - if (!this.defaultSubscriptionId) { - return {}; - } - const response = await this.getWorkspaceList(this.defaultSubscriptionId); - - const details = response.value.find((o: any) => { - return o.properties.customerId === workspaceId; - }); - - if (!details) { - return {}; - } - - const regex = /.*resourcegroups\/(.*)\/providers.*/; - const results = regex.exec(details.id); - if (!results || results.length < 2) { - return {}; - } - - return { - workspace: details.name, - resourceGroup: results[1], - }; - } - /* In 7.5.x it used to be possible to set a default workspace id in the config on the auth page. This has been deprecated, however is still used by a few legacy template queries.