Azure Monitor: Azure routes from Grafana Azure SDK (#82043)

Prometheus: Azure routes from Grafana Azure SDK
pull/82145/head
Sergey Kostrukov 1 year ago committed by GitHub
parent 7814c817b9
commit 9bfb7e1f0b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      pkg/tsdb/azuremonitor/azuremonitor-resource-handler_test.go
  2. 34
      pkg/tsdb/azuremonitor/azuremonitor.go
  3. 83
      pkg/tsdb/azuremonitor/azuremonitor_test.go
  4. 9
      pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go
  5. 16
      pkg/tsdb/azuremonitor/loganalytics/utils.go
  6. 7
      pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go
  7. 7
      pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go
  8. 18
      pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource_test.go
  9. 140
      pkg/tsdb/azuremonitor/routes.go
  10. 1
      pkg/tsdb/azuremonitor/types/types.go

@ -6,7 +6,6 @@ import (
"net/http/httptest"
"testing"
"github.com/grafana/grafana-azure-sdk-go/azsettings"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/metrics"
@ -105,7 +104,7 @@ func Test_handleResourceReq(t *testing.T) {
im: &fakeInstance{
services: map[string]types.DatasourceService{
azureMonitor: {
URL: routes[azsettings.AzurePublic][azureMonitor].URL,
URL: "https://management.azure.com",
HTTPClient: &http.Client{},
Logger: log.DefaultLogger,
},

@ -10,7 +10,6 @@ import (
"net/http"
"strconv"
"github.com/grafana/grafana-azure-sdk-go/azcredentials"
"github.com/grafana/grafana-azure-sdk-go/azsettings"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
@ -109,18 +108,12 @@ func NewInstanceSettings(clientProvider *httpclient.Provider, executors map[stri
credentials = azmoncredentials.GetDefaultCredentials(azureSettings)
}
cloud, err := azcredentials.GetAzureCloud(azureSettings, credentials)
if err != nil {
return nil, fmt.Errorf("error getting credentials: %w", err)
}
routesForModel, err := getAzureRoutes(cloud, settings.JSONData)
routesForModel, err := getAzureMonitorRoutes(azureSettings, credentials, settings.JSONData)
if err != nil {
return nil, err
}
model := types.DatasourceInfo{
Cloud: cloud,
Credentials: credentials,
Settings: azMonitorSettings,
JSONData: jsonData,
@ -142,31 +135,6 @@ func NewInstanceSettings(clientProvider *httpclient.Provider, executors map[stri
}
}
func getCustomizedCloudSettings(cloud string, jsonData json.RawMessage) (types.AzureMonitorCustomizedCloudSettings, error) {
customizedCloudSettings := types.AzureMonitorCustomizedCloudSettings{}
err := json.Unmarshal(jsonData, &customizedCloudSettings)
if err != nil {
return types.AzureMonitorCustomizedCloudSettings{}, fmt.Errorf("error getting customized cloud settings: %w", err)
}
return customizedCloudSettings, nil
}
func getAzureRoutes(cloud string, jsonData json.RawMessage) (map[string]types.AzRoute, error) {
if cloud == azsettings.AzureCustomized {
customizedCloudSettings, err := getCustomizedCloudSettings(cloud, jsonData)
if err != nil {
return nil, err
}
if customizedCloudSettings.CustomizedRoutes == nil {
return nil, fmt.Errorf("unable to instantiate routes, customizedRoutes must be set")
}
azureRoutes := customizedCloudSettings.CustomizedRoutes
return azureRoutes, nil
} else {
return routes[cloud], nil
}
}
type azDatasourceExecutor interface {
ExecuteTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo types.DatasourceInfo, client *http.Client, url string) (*backend.QueryDataResponse, error)
ResourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) (http.ResponseWriter, error)

@ -12,7 +12,6 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/grafana/grafana-azure-sdk-go/azcredentials"
"github.com/grafana/grafana-azure-sdk-go/azsettings"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
@ -24,6 +23,32 @@ import (
"github.com/stretchr/testify/require"
)
var testRoutes = map[string]types.AzRoute{
azureMonitor: {
URL: "https://management.azure.com",
Scopes: []string{"https://management.azure.com/.default"},
Headers: map[string]string{"x-ms-app": "Grafana"},
},
azureLogAnalytics: {
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"},
},
azureResourceGraph: {
URL: "https://management.azure.com",
Scopes: []string{"https://management.azure.com/.default"},
Headers: map[string]string{"x-ms-app": "Grafana"},
},
azureTraces: {
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",
},
}
func TestNewInstanceSettings(t *testing.T) {
tests := []struct {
name string
@ -39,10 +64,9 @@ func TestNewInstanceSettings(t *testing.T) {
ID: 40,
},
expectedModel: types.DatasourceInfo{
Cloud: azsettings.AzurePublic,
Credentials: &azcredentials.AzureManagedIdentityCredentials{},
Settings: types.AzureMonitorSettings{},
Routes: routes[azsettings.AzurePublic],
Routes: testRoutes,
JSONData: map[string]any{"azureAuthType": "msi"},
DatasourceID: 40,
DecryptedSecureJSONData: map[string]string{"key": "value"},
@ -58,7 +82,6 @@ func TestNewInstanceSettings(t *testing.T) {
ID: 50,
},
expectedModel: types.DatasourceInfo{
Cloud: "AzureCustomizedCloud",
Credentials: &azcredentials.AzureClientSecretCredentials{
AzureCloud: "AzureCustomizedCloud",
ClientSecret: "secret",
@ -99,7 +122,6 @@ func TestNewInstanceSettings(t *testing.T) {
}
type fakeInstance struct {
cloud string
routes map[string]types.AzRoute
services map[string]types.DatasourceService
settings types.AzureMonitorSettings
@ -107,7 +129,6 @@ type fakeInstance struct {
func (f *fakeInstance) Get(_ context.Context, _ backend.PluginContext) (instancemgmt.Instance, error) {
return types.DatasourceInfo{
Cloud: f.cloud,
Routes: f.routes,
Services: f.services,
Settings: f.settings,
@ -149,19 +170,19 @@ func Test_newMux(t *testing.T) {
{
name: "creates an Azure Monitor executor",
queryType: azureMonitor,
expectedURL: routes[azsettings.AzurePublic][azureMonitor].URL,
expectedURL: testRoutes[azureMonitor].URL,
Err: require.NoError,
},
{
name: "creates an Azure Log Analytics executor",
queryType: azureLogAnalytics,
expectedURL: routes[azsettings.AzurePublic][azureLogAnalytics].URL,
expectedURL: testRoutes[azureLogAnalytics].URL,
Err: require.NoError,
},
{
name: "creates an Azure Traces executor",
queryType: azureTraces,
expectedURL: routes[azsettings.AzurePublic][azureLogAnalytics].URL,
expectedURL: testRoutes[azureLogAnalytics].URL,
Err: require.NoError,
},
}
@ -170,10 +191,10 @@ func Test_newMux(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
s := &Service{
im: &fakeInstance{
routes: routes[azsettings.AzurePublic],
routes: testRoutes,
services: map[string]types.DatasourceService{
tt.queryType: {
URL: routes[azsettings.AzurePublic][tt.queryType].URL,
URL: testRoutes[tt.queryType].URL,
HTTPClient: &http.Client{},
},
},
@ -302,7 +323,6 @@ func TestCheckHealth(t *testing.T) {
})
}
cloud := "AzureCloud"
tests := []struct {
name string
errorExpected bool
@ -318,15 +338,15 @@ func TestCheckHealth(t *testing.T) {
},
customServices: map[string]types.DatasourceService{
azureMonitor: {
URL: routes[cloud]["Azure Monitor"].URL,
URL: testRoutes["Azure Monitor"].URL,
HTTPClient: azureMonitorClient(false, false),
},
azureLogAnalytics: {
URL: routes[cloud]["Azure Log Analytics"].URL,
URL: testRoutes["Azure Log Analytics"].URL,
HTTPClient: okClient,
},
azureResourceGraph: {
URL: routes[cloud]["Azure Resource Graph"].URL,
URL: testRoutes["Azure Resource Graph"].URL,
HTTPClient: okClient,
}},
},
@ -341,15 +361,15 @@ func TestCheckHealth(t *testing.T) {
},
customServices: map[string]types.DatasourceService{
azureMonitor: {
URL: routes[cloud]["Azure Monitor"].URL,
URL: testRoutes["Azure Monitor"].URL,
HTTPClient: azureMonitorClient(false, true),
},
azureLogAnalytics: {
URL: routes[cloud]["Azure Log Analytics"].URL,
URL: testRoutes["Azure Log Analytics"].URL,
HTTPClient: okClient,
},
azureResourceGraph: {
URL: routes[cloud]["Azure Resource Graph"].URL,
URL: testRoutes["Azure Resource Graph"].URL,
HTTPClient: okClient,
}},
},
@ -364,15 +384,15 @@ func TestCheckHealth(t *testing.T) {
},
customServices: map[string]types.DatasourceService{
azureMonitor: {
URL: routes[cloud]["Azure Monitor"].URL,
URL: testRoutes["Azure Monitor"].URL,
HTTPClient: azureMonitorClient(false, false),
},
azureLogAnalytics: {
URL: routes[cloud]["Azure Log Analytics"].URL,
URL: testRoutes["Azure Log Analytics"].URL,
HTTPClient: failClient(false),
},
azureResourceGraph: {
URL: routes[cloud]["Azure Resource Graph"].URL,
URL: testRoutes["Azure Resource Graph"].URL,
HTTPClient: okClient,
}},
},
@ -387,15 +407,15 @@ func TestCheckHealth(t *testing.T) {
},
customServices: map[string]types.DatasourceService{
azureMonitor: {
URL: routes[cloud]["Azure Monitor"].URL,
URL: testRoutes["Azure Monitor"].URL,
HTTPClient: azureMonitorClient(false, false),
},
azureLogAnalytics: {
URL: routes[cloud]["Azure Log Analytics"].URL,
URL: testRoutes["Azure Log Analytics"].URL,
HTTPClient: okClient,
},
azureResourceGraph: {
URL: routes[cloud]["Azure Resource Graph"].URL,
URL: testRoutes["Azure Resource Graph"].URL,
HTTPClient: failClient(false),
}},
},
@ -410,15 +430,15 @@ func TestCheckHealth(t *testing.T) {
},
customServices: map[string]types.DatasourceService{
azureMonitor: {
URL: routes[cloud]["Azure Monitor"].URL,
URL: testRoutes["Azure Monitor"].URL,
HTTPClient: azureMonitorClient(true, false),
},
azureLogAnalytics: {
URL: routes[cloud]["Azure Log Analytics"].URL,
URL: testRoutes["Azure Log Analytics"].URL,
HTTPClient: okClient,
},
azureResourceGraph: {
URL: routes[cloud]["Azure Resource Graph"].URL,
URL: testRoutes["Azure Resource Graph"].URL,
HTTPClient: okClient,
}},
},
@ -433,23 +453,22 @@ func TestCheckHealth(t *testing.T) {
},
customServices: map[string]types.DatasourceService{
azureMonitor: {
URL: routes[cloud]["Azure Monitor"].URL,
URL: testRoutes["Azure Monitor"].URL,
HTTPClient: failClient(true),
},
azureLogAnalytics: {
URL: routes[cloud]["Azure Log Analytics"].URL,
URL: testRoutes["Azure Log Analytics"].URL,
HTTPClient: failClient(true),
},
azureResourceGraph: {
URL: routes[cloud]["Azure Resource Graph"].URL,
URL: testRoutes["Azure Resource Graph"].URL,
HTTPClient: failClient(true),
}},
},
}
instance := &fakeInstance{
cloud: cloud,
routes: routes[cloud],
routes: testRoutes,
services: map[string]types.DatasourceService{},
settings: types.AzureMonitorSettings{
LogAnalyticsDefaultWorkspace: "workspace-id",

@ -326,12 +326,7 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *A
return &dataResponse, nil
}
azurePortalBaseUrl, err := GetAzurePortalUrl(dsInfo.Cloud)
if err != nil {
return nil, err
}
queryUrl, err := getQueryUrl(query.Query, query.Resources, azurePortalBaseUrl, query.TimeRange)
queryUrl, err := getQueryUrl(query.Query, query.Resources, dsInfo.Routes["Azure Portal"].URL, query.TimeRange)
if err != nil {
return nil, err
}
@ -365,7 +360,7 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *A
}
// Use the parent span query for the parent span data link
err = addDataLinksToFields(query, azurePortalBaseUrl, frame, dsInfo, queryUrl)
err = addDataLinksToFields(query, dsInfo.Routes["Azure Portal"].URL, frame, dsInfo, queryUrl)
if err != nil {
return nil, err
}

@ -1,9 +1,6 @@
package loganalytics
import (
"fmt"
"github.com/grafana/grafana-azure-sdk-go/azsettings"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
@ -34,16 +31,3 @@ func AddConfigLinks(frame data.Frame, dl string, title *string) data.Frame {
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")
}
}

@ -339,17 +339,12 @@ func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, query *types.
return nil, err
}
azurePortalUrl, err := loganalytics.GetAzurePortalUrl(dsInfo.Cloud)
if err != nil {
return nil, err
}
subscription, err := e.retrieveSubscriptionDetails(cli, ctx, query.Subscription, dsInfo.Routes["Azure Monitor"].URL, dsInfo.DatasourceID, dsInfo.OrgID)
if err != nil {
return nil, err
}
frames, err := e.parseResponse(data, query, azurePortalUrl, subscription)
frames, err := e.parseResponse(data, query, dsInfo.Routes["Azure Portal"].URL, subscription)
if err != nil {
return nil, err
}

@ -190,12 +190,7 @@ func (e *AzureResourceGraphDatasource) executeQuery(ctx context.Context, query *
return &dataResponse, nil
}
azurePortalUrl, err := loganalytics.GetAzurePortalUrl(dsInfo.Cloud)
if err != nil {
return nil, err
}
url := azurePortalUrl + "/#blade/HubsExtension/ArgQueryBlade/query/" + url.PathEscape(query.InterpolatedQuery)
url := dsInfo.Routes["Azure Portal"].URL + "/#blade/HubsExtension/ArgQueryBlade/query/" + url.PathEscape(query.InterpolatedQuery)
frameWithLink := loganalytics.AddConfigLinks(*frame, url, nil)
if frameWithLink.Meta == nil {
frameWithLink.Meta = &data.FrameMeta{}

@ -10,7 +10,6 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/grafana/grafana-azure-sdk-go/azsettings"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
@ -136,23 +135,6 @@ func TestAddConfigData(t *testing.T) {
}
}
func TestGetAzurePortalUrl(t *testing.T) {
clouds := []string{azsettings.AzurePublic, azsettings.AzureChina, azsettings.AzureUSGovernment}
expectedAzurePortalUrl := map[string]any{
azsettings.AzurePublic: "https://portal.azure.com",
azsettings.AzureChina: "https://portal.azure.cn",
azsettings.AzureUSGovernment: "https://portal.azure.us",
}
for _, cloud := range clouds {
azurePortalUrl, err := loganalytics.GetAzurePortalUrl(cloud)
if err != nil {
t.Errorf("The cloud not supported")
}
assert.Equal(t, expectedAzurePortalUrl[cloud], azurePortalUrl)
}
}
func TestUnmarshalResponse400(t *testing.T) {
datasource := &AzureResourceGraphDatasource{}
res, err := datasource.unmarshalResponse(&http.Response{

@ -1,6 +1,12 @@
package azuremonitor
import (
"encoding/json"
"fmt"
"net/url"
"path"
"github.com/grafana/grafana-azure-sdk-go/azcredentials"
"github.com/grafana/grafana-azure-sdk-go/azsettings"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/types"
@ -12,63 +18,99 @@ const (
azureLogAnalytics = "Azure Log Analytics"
azureResourceGraph = "Azure Resource Graph"
azureTraces = "Azure Traces"
azurePortal = "Azure Portal"
)
var azManagement = types.AzRoute{
URL: "https://management.azure.com",
Scopes: []string{"https://management.azure.com/.default"},
Headers: map[string]string{"x-ms-app": "Grafana"},
}
func getAzureMonitorRoutes(settings *azsettings.AzureSettings, credentials azcredentials.AzureCredentials, jsonData json.RawMessage) (map[string]types.AzRoute, error) {
azureCloud, err := azcredentials.GetAzureCloud(settings, credentials)
if err != nil {
return nil, err
}
var azUSGovManagement = types.AzRoute{
URL: "https://management.usgovcloudapi.net",
Scopes: []string{"https://management.usgovcloudapi.net/.default"},
Headers: map[string]string{"x-ms-app": "Grafana"},
}
if azureCloud == azsettings.AzureCustomized {
routes, err := getCustomizedCloudRoutes(jsonData)
if err != nil {
return nil, err
}
return routes, nil
}
var azChinaManagement = types.AzRoute{
URL: "https://management.chinacloudapi.cn",
Scopes: []string{"https://management.chinacloudapi.cn/.default"},
Headers: map[string]string{"x-ms-app": "Grafana"},
}
cloudSettings, err := settings.GetCloud(azureCloud)
if err != nil {
return nil, err
}
var azLogAnalytics = types.AzRoute{
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"},
}
resourceManagerUrl, ok := cloudSettings.Properties["resourceManager"]
if !ok {
err := fmt.Errorf("the Azure cloud '%s' doesn't have configuration for Azure Resource Manager", azureCloud)
return nil, err
}
resourceManagerScopes, err := audienceToScopes(resourceManagerUrl)
if err != nil {
return nil, err
}
resourceManagerRoute := types.AzRoute{
URL: resourceManagerUrl,
Scopes: resourceManagerScopes,
Headers: map[string]string{"x-ms-app": "Grafana"},
}
logAnalyticsUrl, ok := cloudSettings.Properties["logAnalytics"]
if !ok {
err := fmt.Errorf("the Azure cloud '%s' doesn't have configuration for Azure Log Analytics", azureCloud)
return nil, err
}
logAnalyticsScopes, err := audienceToScopes(logAnalyticsUrl)
if err != nil {
return nil, err
}
logAnalyticsRoute := types.AzRoute{
URL: logAnalyticsUrl,
Scopes: logAnalyticsScopes,
Headers: map[string]string{"x-ms-app": "Grafana", "Cache-Control": "public, max-age=60"},
}
portalUrl, ok := cloudSettings.Properties["portal"]
if !ok {
err := fmt.Errorf("the Azure cloud '%s' doesn't have configuration for Azure Portal", azureCloud)
return nil, err
}
portalRoute := types.AzRoute{
URL: portalUrl,
}
var azChinaLogAnalytics = types.AzRoute{
URL: "https://api.loganalytics.azure.cn",
Scopes: []string{"https://api.loganalytics.azure.cn/.default"},
Headers: map[string]string{"x-ms-app": "Grafana", "Cache-Control": "public, max-age=60"},
routes := map[string]types.AzRoute{
azureMonitor: resourceManagerRoute,
azureLogAnalytics: logAnalyticsRoute,
azureResourceGraph: resourceManagerRoute,
azureTraces: logAnalyticsRoute,
azurePortal: portalRoute,
}
return routes, nil
}
var azUSGovLogAnalytics = types.AzRoute{
URL: "https://api.loganalytics.us",
Scopes: []string{"https://api.loganalytics.us/.default"},
Headers: map[string]string{"x-ms-app": "Grafana", "Cache-Control": "public, max-age=60"},
func getCustomizedCloudRoutes(jsonData json.RawMessage) (map[string]types.AzRoute, error) {
customizedCloudSettings := types.AzureMonitorCustomizedCloudSettings{}
err := json.Unmarshal(jsonData, &customizedCloudSettings)
if err != nil {
return nil, fmt.Errorf("error getting customized cloud settings: %w", err)
}
if customizedCloudSettings.CustomizedRoutes == nil {
return nil, fmt.Errorf("unable to instantiate routes, customizedRoutes must be set")
}
azureRoutes := customizedCloudSettings.CustomizedRoutes
return azureRoutes, nil
}
var (
// The different Azure routes are identified by its cloud (e.g. public or gov)
// and the service to query (e.g. Azure Monitor or Azure Log Analytics)
routes = map[string]map[string]types.AzRoute{
azsettings.AzurePublic: {
azureMonitor: azManagement,
azureLogAnalytics: azLogAnalytics,
azureResourceGraph: azManagement,
azureTraces: azLogAnalytics,
},
azsettings.AzureUSGovernment: {
azureMonitor: azUSGovManagement,
azureLogAnalytics: azUSGovLogAnalytics,
azureResourceGraph: azUSGovManagement,
},
azsettings.AzureChina: {
azureMonitor: azChinaManagement,
azureLogAnalytics: azChinaLogAnalytics,
azureResourceGraph: azChinaManagement,
},
func audienceToScopes(audience string) ([]string, error) {
resourceId, err := url.Parse(audience)
if err != nil || resourceId.Scheme == "" || resourceId.Host == "" {
err = fmt.Errorf("endpoint resource ID (audience) '%s' invalid", audience)
return nil, err
}
)
resourceId.Path = path.Join(resourceId.Path, ".default")
scopes := []string{resourceId.String()}
return scopes, nil
}

@ -49,7 +49,6 @@ type DatasourceService struct {
}
type DatasourceInfo struct {
Cloud string
Credentials azcredentials.AzureCredentials
Settings AzureMonitorSettings
Routes map[string]AzRoute

Loading…
Cancel
Save