diff --git a/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts index d6d068b7e1e..1caf676f79a 100644 --- a/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts @@ -163,6 +163,10 @@ export const defaultAzureMetricQuery: Partial = { * Azure Monitor Logs sub-query properties */ export interface AzureLogsQuery { + /** + * If set to true the query will be run as a basic logs query + */ + basicLogsQuery?: boolean; /** * If set to true the dashboard time range will be used as a filter for the query. Otherwise the query time ranges will be used. Defaults to false. */ diff --git a/pkg/tsdb/azuremonitor/azuremonitor.go b/pkg/tsdb/azuremonitor/azuremonitor.go index 085f05426d2..3b860832924 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor.go +++ b/pkg/tsdb/azuremonitor/azuremonitor.go @@ -137,7 +137,7 @@ func NewInstanceSettings(clientProvider *httpclient.Provider, executors map[stri } type azDatasourceExecutor interface { - ExecuteTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo types.DatasourceInfo, client *http.Client, url string) (*backend.QueryDataResponse, error) + ExecuteTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo types.DatasourceInfo, client *http.Client, url string, fromAlert bool) (*backend.QueryDataResponse, error) ResourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) (http.ResponseWriter, error) } @@ -172,7 +172,9 @@ func (s *Service) newQueryMux() *datasource.QueryTypeMux { if !ok { return nil, fmt.Errorf("missing service for %s", dst) } - return executor.ExecuteTimeSeriesQuery(ctx, req.Queries, dsInfo, service.HTTPClient, service.URL) + // FromAlert header is defined in pkg/services/ngalert/models/constants.go + fromAlert := req.Headers["FromAlert"] == "true" + return executor.ExecuteTimeSeriesQuery(ctx, req.Queries, dsInfo, service.HTTPClient, service.URL, fromAlert) }) } return mux diff --git a/pkg/tsdb/azuremonitor/azuremonitor_test.go b/pkg/tsdb/azuremonitor/azuremonitor_test.go index 28dff1e4683..e8dcf304f4e 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor_test.go +++ b/pkg/tsdb/azuremonitor/azuremonitor_test.go @@ -149,7 +149,7 @@ func (f *fakeExecutor) ResourceRequest(rw http.ResponseWriter, req *http.Request return nil, nil } -func (f *fakeExecutor) ExecuteTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo types.DatasourceInfo, client *http.Client, url string) (*backend.QueryDataResponse, error) { +func (f *fakeExecutor) ExecuteTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo types.DatasourceInfo, client *http.Client, url string, fromAlert bool) (*backend.QueryDataResponse, error) { if client == nil { f.t.Errorf("The HTTP client for %s is missing", f.queryType) } else { diff --git a/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go index 0741de9a085..551083d96e7 100644 --- a/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go @@ -117,6 +117,9 @@ type AppInsightsMetricNameQueryKind string // Azure Monitor Logs sub-query properties type AzureLogsQuery struct { + // If set to true the query will be run as a basic logs query + BasicLogsQuery *bool `json:"basicLogsQuery,omitempty"` + // If set to true the dashboard time range will be used as a filter for the query. Otherwise the query time ranges will be used. Defaults to false. DashboardTime *bool `json:"dashboardTime,omitempty"` diff --git a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go index 09ee29e1c1d..d3c79793772 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go @@ -27,16 +27,116 @@ import ( ) func (e *AzureLogAnalyticsDatasource) ResourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) (http.ResponseWriter, error) { + if req.URL.Path == "/usage/basiclogs" { + newUrl := &url.URL{ + Scheme: req.URL.Scheme, + Host: req.URL.Host, + Path: "/v1/query", + } + return e.GetBasicLogsUsage(req.Context(), newUrl.String(), cli, rw, req.Body) + } return e.Proxy.Do(rw, req, cli) } +// builds and executes a new query request that will get the data ingeted for the given table in the basic logs query +func (e *AzureLogAnalyticsDatasource) GetBasicLogsUsage(ctx context.Context, url string, client *http.Client, rw http.ResponseWriter, reqBody io.ReadCloser) (http.ResponseWriter, error) { + // read the full body + originalPayload, readErr := io.ReadAll(reqBody) + if readErr != nil { + return rw, fmt.Errorf("failed to read request body %w", readErr) + } + var payload BasicLogsUsagePayload + jsonErr := json.Unmarshal(originalPayload, &payload) + if jsonErr != nil { + return rw, fmt.Errorf("error decoding basic logs table usage payload: %w", jsonErr) + } + table := payload.Table + + from, fromErr := ConvertTime(payload.From) + if fromErr != nil { + return rw, fmt.Errorf("failed to convert from time: %w", fromErr) + } + + to, toErr := ConvertTime(payload.To) + if toErr != nil { + return rw, fmt.Errorf("failed to convert to time: %w", toErr) + } + + // basic logs queries only show data for last 8 days or less + // data volume query should also only calculate volume for last 8 days if time range exceeds that. + diff := to.Sub(from).Hours() + if diff > float64(MaxHoursBasicLogs) { + from = to.Add(-time.Duration(MaxHoursBasicLogs) * time.Hour) + } + + dataVolumeQueryRaw := GetDataVolumeRawQuery(table) + dataVolumeQuery := &AzureLogAnalyticsQuery{ + Query: dataVolumeQueryRaw, + DashboardTime: true, // necessary to ensure TimeRange property is used since query will not have an in-query time filter + TimeRange: backend.TimeRange{ + From: from, + To: to, + }, + TimeColumn: "TimeGenerated", + Resources: []string{payload.Resource}, + QueryType: dataquery.AzureQueryTypeAzureLogAnalytics, + URL: getApiURL(payload.Resource, false, false), + } + + req, err := e.createRequest(ctx, url, dataVolumeQuery) + if err != nil { + return rw, err + } + + _, span := tracing.DefaultTracer().Start(ctx, "azure basic logs usage query", trace.WithAttributes( + attribute.String("target", dataVolumeQuery.Query), + attribute.String("table", table), + attribute.Int64("from", dataVolumeQuery.TimeRange.From.UnixNano()/int64(time.Millisecond)), + attribute.Int64("until", dataVolumeQuery.TimeRange.To.UnixNano()/int64(time.Millisecond)), + )) + defer span.End() + + resp, err := client.Do(req) + if err != nil { + return rw, err + } + + defer func() { + if err := resp.Body.Close(); err != nil { + e.Logger.Warn("Failed to close response body for data volume request", "err", err) + } + }() + + logResponse, err := e.unmarshalResponse(resp) + if err != nil { + return rw, err + } + + t, err := logResponse.GetPrimaryResultTable() + if err != nil { + return rw, err + } + + num := t.Rows[0][0].(json.Number) + value, err := num.Float64() + if err != nil { + return rw, err + } + _, err = rw.Write([]byte(fmt.Sprintf("%f", value))) + if err != nil { + return rw, err + } + + return rw, err +} + // executeTimeSeriesQuery does the following: // 1. build the AzureMonitor url and querystring for each query // 2. executes each query by calling the Azure Monitor API // 3. parses the responses for each query into data frames -func (e *AzureLogAnalyticsDatasource) ExecuteTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo types.DatasourceInfo, client *http.Client, url string) (*backend.QueryDataResponse, error) { +func (e *AzureLogAnalyticsDatasource) ExecuteTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo types.DatasourceInfo, client *http.Client, url string, fromAlert bool) (*backend.QueryDataResponse, error) { result := backend.NewQueryDataResponse() - queries, err := e.buildQueries(ctx, originalQueries, dsInfo) + queries, err := e.buildQueries(ctx, originalQueries, dsInfo, fromAlert) if err != nil { return nil, err } @@ -53,23 +153,38 @@ func (e *AzureLogAnalyticsDatasource) ExecuteTimeSeriesQuery(ctx context.Context return result, nil } -func buildLogAnalyticsQuery(query backend.DataQuery, dsInfo types.DatasourceInfo, appInsightsRegExp *regexp.Regexp) (*AzureLogAnalyticsQuery, error) { +func buildLogAnalyticsQuery(query backend.DataQuery, dsInfo types.DatasourceInfo, appInsightsRegExp *regexp.Regexp, fromAlert bool) (*AzureLogAnalyticsQuery, error) { queryJSONModel := types.LogJSONQuery{} err := json.Unmarshal(query.JSON, &queryJSONModel) if err != nil { return nil, fmt.Errorf("failed to decode the Azure Log Analytics query object from JSON: %w", err) } + var queryString string appInsightsQuery := false dashboardTime := false timeColumn := "" azureLogAnalyticsTarget := queryJSONModel.AzureLogAnalytics + basicLogsQuery := false resultFormat := ParseResultFormat(azureLogAnalyticsTarget.ResultFormat, dataquery.AzureQueryTypeAzureLogAnalytics) + basicLogsQueryFlag := false + if azureLogAnalyticsTarget.BasicLogsQuery != nil { + basicLogsQueryFlag = *azureLogAnalyticsTarget.BasicLogsQuery + } + resources, resourceOrWorkspace := retrieveResources(azureLogAnalyticsTarget) appInsightsQuery = appInsightsRegExp.Match([]byte(resourceOrWorkspace)) + if basicLogsQueryFlag { + if meetsBasicLogsCriteria, meetsBasicLogsCriteriaErr := meetsBasicLogsCriteria(resources, fromAlert); meetsBasicLogsCriteriaErr != nil { + return nil, meetsBasicLogsCriteriaErr + } else { + basicLogsQuery = meetsBasicLogsCriteria + } + } + if azureLogAnalyticsTarget.Query != nil { queryString = *azureLogAnalyticsTarget.Query } @@ -86,7 +201,7 @@ func buildLogAnalyticsQuery(query backend.DataQuery, dsInfo types.DatasourceInfo } } - apiURL := getApiURL(resourceOrWorkspace, appInsightsQuery) + apiURL := getApiURL(resourceOrWorkspace, appInsightsQuery, basicLogsQuery) rawQuery, err := macros.KqlInterpolate(query, dsInfo, queryString, "TimeGenerated") if err != nil { @@ -105,10 +220,11 @@ func buildLogAnalyticsQuery(query backend.DataQuery, dsInfo types.DatasourceInfo AppInsightsQuery: appInsightsQuery, DashboardTime: dashboardTime, TimeColumn: timeColumn, + BasicLogs: basicLogsQuery, }, nil } -func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, queries []backend.DataQuery, dsInfo types.DatasourceInfo) ([]*AzureLogAnalyticsQuery, error) { +func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, queries []backend.DataQuery, dsInfo types.DatasourceInfo, fromAlert bool) ([]*AzureLogAnalyticsQuery, error) { azureLogAnalyticsQueries := []*AzureLogAnalyticsQuery{} appInsightsRegExp, err := regexp.Compile("providers/Microsoft.Insights/components") if err != nil { @@ -117,7 +233,7 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, queries for _, query := range queries { if query.QueryType == string(dataquery.AzureQueryTypeAzureLogAnalytics) { - azureLogAnalyticsQuery, err := buildLogAnalyticsQuery(query, dsInfo, appInsightsRegExp) + azureLogAnalyticsQuery, err := buildLogAnalyticsQuery(query, dsInfo, appInsightsRegExp, fromAlert) if err != nil { return nil, fmt.Errorf("failed to build azure log analytics query: %w", err) } @@ -161,6 +277,7 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *A _, span := tracing.DefaultTracer().Start(ctx, "azure log analytics query", trace.WithAttributes( attribute.String("target", query.Query), + attribute.Bool("basic_logs", query.BasicLogs), attribute.Int64("from", query.TimeRange.From.UnixNano()/int64(time.Millisecond)), attribute.Int64("until", query.TimeRange.To.UnixNano()/int64(time.Millisecond)), attribute.Int64("datasource_id", dsInfo.DatasourceID), 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 adaa1e840d9..5e00c197670 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go @@ -21,6 +21,10 @@ import ( "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" ) +func makeQueryPointer(q AzureLogAnalyticsQuery) *AzureLogAnalyticsQuery { + return &q +} + func TestBuildLogAnalyticsQuery(t *testing.T) { fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local) timeRange := backend.TimeRange{From: fromStart, To: fromStart.Add(34 * time.Minute)} @@ -95,12 +99,14 @@ func TestBuildLogAnalyticsQuery(t *testing.T) { tests := []struct { name string + fromAlert bool queryModel backend.DataQuery - azureLogAnalyticsQuery AzureLogAnalyticsQuery + azureLogAnalyticsQuery *AzureLogAnalyticsQuery Err require.ErrorAssertionFunc }{ { - name: "Query with macros should be interpolated", + name: "Query with macros should be interpolated", + fromAlert: false, queryModel: backend.DataQuery{ JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Log Analytics", @@ -115,7 +121,7 @@ func TestBuildLogAnalyticsQuery(t *testing.T) { TimeRange: timeRange, QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), }, - azureLogAnalyticsQuery: AzureLogAnalyticsQuery{ + azureLogAnalyticsQuery: makeQueryPointer(AzureLogAnalyticsQuery{ RefID: "A", ResultFormat: dataquery.ResultFormatTimeSeries, URL: "v1/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace/query", @@ -134,11 +140,12 @@ func TestBuildLogAnalyticsQuery(t *testing.T) { QueryType: dataquery.AzureQueryTypeAzureLogAnalytics, AppInsightsQuery: false, DashboardTime: false, - }, + }), Err: require.NoError, }, { - name: "Legacy queries with a workspace GUID should use workspace-centric url", + name: "Legacy queries with a workspace GUID should use workspace-centric url", + fromAlert: false, queryModel: backend.DataQuery{ JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Log Analytics", @@ -151,7 +158,7 @@ func TestBuildLogAnalyticsQuery(t *testing.T) { RefID: "A", QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), }, - azureLogAnalyticsQuery: AzureLogAnalyticsQuery{ + azureLogAnalyticsQuery: makeQueryPointer(AzureLogAnalyticsQuery{ RefID: "A", ResultFormat: dataquery.ResultFormatTimeSeries, URL: "v1/workspaces/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/query", @@ -168,11 +175,12 @@ func TestBuildLogAnalyticsQuery(t *testing.T) { QueryType: dataquery.AzureQueryTypeAzureLogAnalytics, AppInsightsQuery: false, DashboardTime: false, - }, + }), Err: require.NoError, }, { - name: "Legacy workspace queries with a resource URI (from a template variable) should use resource-centric url", + name: "Legacy workspace queries with a resource URI (from a template variable) should use resource-centric url", + fromAlert: false, queryModel: backend.DataQuery{ JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Log Analytics", @@ -185,7 +193,7 @@ func TestBuildLogAnalyticsQuery(t *testing.T) { RefID: "A", QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), }, - azureLogAnalyticsQuery: AzureLogAnalyticsQuery{ + azureLogAnalyticsQuery: makeQueryPointer(AzureLogAnalyticsQuery{ RefID: "A", ResultFormat: dataquery.ResultFormatTimeSeries, URL: "v1/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace/query", @@ -202,11 +210,12 @@ func TestBuildLogAnalyticsQuery(t *testing.T) { QueryType: dataquery.AzureQueryTypeAzureLogAnalytics, AppInsightsQuery: false, DashboardTime: false, - }, + }), Err: require.NoError, }, { - name: "Queries with multiple resources", + name: "Queries with multiple resources", + fromAlert: false, queryModel: backend.DataQuery{ JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Log Analytics", @@ -220,7 +229,7 @@ func TestBuildLogAnalyticsQuery(t *testing.T) { RefID: "A", QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), }, - azureLogAnalyticsQuery: AzureLogAnalyticsQuery{ + azureLogAnalyticsQuery: makeQueryPointer(AzureLogAnalyticsQuery{ RefID: "A", ResultFormat: dataquery.ResultFormatTimeSeries, URL: "v1/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace/query", @@ -238,11 +247,12 @@ func TestBuildLogAnalyticsQuery(t *testing.T) { QueryType: dataquery.AzureQueryTypeAzureLogAnalytics, AppInsightsQuery: false, DashboardTime: false, - }, + }), Err: require.NoError, }, { - name: "Query with multiple resources", + name: "Query with multiple resources", + fromAlert: false, queryModel: backend.DataQuery{ JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Log Analytics", @@ -257,7 +267,7 @@ func TestBuildLogAnalyticsQuery(t *testing.T) { TimeRange: timeRange, QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), }, - azureLogAnalyticsQuery: AzureLogAnalyticsQuery{ + azureLogAnalyticsQuery: makeQueryPointer(AzureLogAnalyticsQuery{ RefID: "A", ResultFormat: dataquery.ResultFormatTimeSeries, URL: "v1/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace/query", @@ -276,11 +286,12 @@ func TestBuildLogAnalyticsQuery(t *testing.T) { QueryType: dataquery.AzureQueryTypeAzureLogAnalytics, AppInsightsQuery: false, DashboardTime: false, - }, + }), Err: require.NoError, }, { - name: "Query that uses dashboard time", + name: "Query that uses dashboard time", + fromAlert: false, queryModel: backend.DataQuery{ JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Log Analytics", @@ -296,7 +307,7 @@ func TestBuildLogAnalyticsQuery(t *testing.T) { TimeRange: timeRange, QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), }, - azureLogAnalyticsQuery: AzureLogAnalyticsQuery{ + azureLogAnalyticsQuery: makeQueryPointer(AzureLogAnalyticsQuery{ RefID: "A", ResultFormat: dataquery.ResultFormatTimeSeries, URL: "v1/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace/query", @@ -317,16 +328,127 @@ func TestBuildLogAnalyticsQuery(t *testing.T) { AppInsightsQuery: false, DashboardTime: true, TimeColumn: "TimeGenerated", - }, + }), Err: require.NoError, }, + { + name: "Basic Logs query", + fromAlert: false, + queryModel: backend.DataQuery{ + JSON: []byte(fmt.Sprintf(`{ + "queryType": "Azure Log Analytics", + "azureLogAnalytics": { + "resources": ["/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/TestDataWorkspace"], + "query": "Perf", + "resultFormat": "%s", + "dashboardTime": true, + "timeColumn": "TimeGenerated", + "basicLogsQuery": true + } + }`, dataquery.ResultFormatTimeSeries)), + RefID: "A", + TimeRange: timeRange, + QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), + }, + azureLogAnalyticsQuery: makeQueryPointer(AzureLogAnalyticsQuery{ + RefID: "A", + ResultFormat: dataquery.ResultFormatTimeSeries, + URL: "v1/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/TestDataWorkspace/search", + JSON: []byte(fmt.Sprintf(`{ + "queryType": "Azure Log Analytics", + "azureLogAnalytics": { + "resources": ["/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/TestDataWorkspace"], + "query": "Perf", + "resultFormat": "%s", + "dashboardTime": true, + "timeColumn": "TimeGenerated", + "basicLogsQuery": true + } + }`, dataquery.ResultFormatTimeSeries)), + Query: "Perf", + Resources: []string{"/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/TestDataWorkspace"}, + TimeRange: timeRange, + QueryType: dataquery.AzureQueryTypeAzureLogAnalytics, + AppInsightsQuery: false, + DashboardTime: true, + BasicLogs: true, + TimeColumn: "TimeGenerated", + }), + Err: require.NoError, + }, + { + name: "Basic Logs query with multiple resources", + fromAlert: false, + queryModel: backend.DataQuery{ + JSON: []byte(fmt.Sprintf(`{ + "queryType": "Azure Log Analytics", + "azureLogAnalytics": { + "resources": ["/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/TestDataWorkspace1", "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/TestDataWorkspace2"], + "query": "Perf", + "resultFormat": "%s", + "dashboardTime": true, + "timeColumn": "TimeGenerated", + "basicLogsQuery": true + } + }`, dataquery.ResultFormatTimeSeries)), + RefID: "A", + TimeRange: timeRange, + QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), + }, + azureLogAnalyticsQuery: nil, + Err: require.Error, + }, + { + name: "Basic Logs query with non LA workspace resources", + fromAlert: false, + queryModel: backend.DataQuery{ + JSON: []byte(fmt.Sprintf(`{ + "queryType": "Azure Log Analytics", + "azureLogAnalytics": { + "resources": ["/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"], + "query": "Perf", + "resultFormat": "%s", + "dashboardTime": true, + "timeColumn": "TimeGenerated", + "basicLogsQuery": true + } + }`, dataquery.ResultFormatTimeSeries)), + RefID: "A", + TimeRange: timeRange, + QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), + }, + azureLogAnalyticsQuery: nil, + Err: require.Error, + }, + { + name: "Basic Logs query from alerts", + fromAlert: true, + queryModel: backend.DataQuery{ + JSON: []byte(fmt.Sprintf(`{ + "queryType": "Azure Log Analytics", + "azureLogAnalytics": { + "resources": ["/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"], + "query": "Perf", + "resultFormat": "%s", + "dashboardTime": true, + "timeColumn": "TimeGenerated", + "basicLogsQuery": true + } + }`, dataquery.ResultFormatTimeSeries)), + RefID: "A", + TimeRange: timeRange, + QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), + }, + azureLogAnalyticsQuery: nil, + Err: require.Error, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - query, err := buildLogAnalyticsQuery(tt.queryModel, dsInfo, appInsightsRegExp) + query, err := buildLogAnalyticsQuery(tt.queryModel, dsInfo, appInsightsRegExp, tt.fromAlert) tt.Err(t, err) - if diff := cmp.Diff(&tt.azureLogAnalyticsQuery, query); diff != "" { + if diff := cmp.Diff(tt.azureLogAnalyticsQuery, query); diff != "" { t.Errorf("Result mismatch (-want +got): \n%s", diff) } }) diff --git a/pkg/tsdb/azuremonitor/loganalytics/consts.go b/pkg/tsdb/azuremonitor/loganalytics/consts.go index 08f4270c5ca..9a199ef0d32 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/consts.go +++ b/pkg/tsdb/azuremonitor/loganalytics/consts.go @@ -2,6 +2,8 @@ package loganalytics var Tables = []string{"availabilityResults", "dependencies", "customEvents", "exceptions", "pageViews", "requests", "traces"} +var MaxHoursBasicLogs = 192 // 8 days in hours + // AttributesOmit - Properties to omit when generating the attributes bag var AttributesOmit = map[string]string{"operationId": "operationId", "duration": "duration", "id": "id", "name": "name", "problemId": "problemId", "operation_ParentId": "operation_ParentId", "timestamp": "timestamp", "customDimensions": "customDimensions", "operation_Name": "operation_Name"} diff --git a/pkg/tsdb/azuremonitor/loganalytics/traces.go b/pkg/tsdb/azuremonitor/loganalytics/traces.go index bdd0075388d..3a3004a2a93 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/traces.go +++ b/pkg/tsdb/azuremonitor/loganalytics/traces.go @@ -227,7 +227,7 @@ func buildAppInsightsQuery(ctx context.Context, query backend.DataQuery, dsInfo return nil, err } - apiURL := getApiURL(resourceOrWorkspace, appInsightsQuery) + apiURL := getApiURL(resourceOrWorkspace, appInsightsQuery, false) rawQuery, err := macros.KqlInterpolate(query, dsInfo, queryString, "TimeGenerated") if err != nil { diff --git a/pkg/tsdb/azuremonitor/loganalytics/types.go b/pkg/tsdb/azuremonitor/loganalytics/types.go index d75ad879092..388456a8e81 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/types.go +++ b/pkg/tsdb/azuremonitor/loganalytics/types.go @@ -32,6 +32,7 @@ type AzureLogAnalyticsQuery struct { AppInsightsQuery bool DashboardTime bool TimeColumn string + BasicLogs bool } // Error definition has been inferred from real data and other model definitions like @@ -73,3 +74,12 @@ type AzureCorrelationAPIResponseProperties struct { Resources []string `json:"resources"` NextLink *string `json:"nextLink,omitempty"` } + +// BasicLogsUsagePayload is the payload that the frontend resourcerequest will send to the backend to calculate the basic logs query usage +type BasicLogsUsagePayload struct { + Table string `json:"table"` + Resource string `json:"resource"` + QueryType string `json:"queryType"` + From string `json:"from"` + To string `json:"to"` +} diff --git a/pkg/tsdb/azuremonitor/loganalytics/utils.go b/pkg/tsdb/azuremonitor/loganalytics/utils.go index b5810316108..5f1a91f2433 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/utils.go +++ b/pkg/tsdb/azuremonitor/loganalytics/utils.go @@ -3,7 +3,9 @@ package loganalytics import ( "fmt" "regexp" + "strconv" "strings" + "time" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/tsdb/azuremonitor/kinds/dataquery" @@ -37,6 +39,25 @@ func AddConfigLinks(frame data.Frame, dl string, title *string) data.Frame { return frame } +// Check whether a query should be handled as basic logs query +// 2. resource selected is a workspace +// 3. query is not an alerts query +// 4. number of selected resources is exactly one +func meetsBasicLogsCriteria(resources []string, fromAlert bool) (bool, error) { + if fromAlert { + return false, fmt.Errorf("basic Logs queries cannot be used for alerts") + } + if len(resources) != 1 { + return false, fmt.Errorf("basic logs queries cannot be run against multiple resources") + } + + if !strings.Contains(strings.ToLower(resources[0]), "microsoft.operationalinsights/workspaces") { + return false, fmt.Errorf("basic Logs queries may only be run against Log Analytics workspaces") + } + + return true, nil +} + func ParseResultFormat(queryResultFormat *dataquery.ResultFormat, queryType dataquery.AzureQueryType) dataquery.ResultFormat { var resultFormat dataquery.ResultFormat if queryResultFormat != nil { @@ -55,17 +76,22 @@ func ParseResultFormat(queryResultFormat *dataquery.ResultFormat, queryType data return resultFormat } -func getApiURL(resourceOrWorkspace string, isAppInsightsQuery bool) string { +func getApiURL(resourceOrWorkspace string, isAppInsightsQuery bool, basicLogsQuery bool) string { matchesResourceURI, _ := regexp.MatchString("^/subscriptions/", resourceOrWorkspace) + queryOrSearch := "query" + if basicLogsQuery { + queryOrSearch = "search" + } + if matchesResourceURI { if isAppInsightsQuery { componentName := resourceOrWorkspace[strings.LastIndex(resourceOrWorkspace, "/")+1:] return fmt.Sprintf("v1/apps/%s/query", componentName) } - return fmt.Sprintf("v1%s/query", resourceOrWorkspace) + return fmt.Sprintf("v1%s/%s", resourceOrWorkspace, queryOrSearch) } else { - return fmt.Sprintf("v1/workspaces/%s/query", resourceOrWorkspace) + return fmt.Sprintf("v1/workspaces/%s/%s", resourceOrWorkspace, queryOrSearch) } } @@ -88,3 +114,21 @@ func retrieveResources(query dataquery.AzureLogsQuery) ([]string, string) { return resources, resourceOrWorkspace } + +func ConvertTime(timeStamp string) (time.Time, error) { + // Convert the timestamp string to an int64 + timestampInt, err := strconv.ParseInt(timeStamp, 10, 64) + if err != nil { + // Handle error + return time.Time{}, err + } + + // Convert the Unix timestamp (in milliseconds) to a time.Time + convTimeStamp := time.Unix(0, timestampInt*int64(time.Millisecond)) + + return convTimeStamp, nil +} + +func GetDataVolumeRawQuery(table string) string { + return fmt.Sprintf("Usage \n| where DataType == \"%s\"\n| where IsBillable == true\n| summarize BillableDataGB = round(sum(Quantity) / 1000, 3)", table) +} diff --git a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go index ad55bbb4a7a..14203a85310 100644 --- a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go +++ b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go @@ -48,7 +48,7 @@ func (e *AzureMonitorDatasource) ResourceRequest(rw http.ResponseWriter, req *ht // 1. build the AzureMonitor url and querystring for each query // 2. executes each query by calling the Azure Monitor API // 3. parses the responses for each query into data frames -func (e *AzureMonitorDatasource) ExecuteTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo types.DatasourceInfo, client *http.Client, url string) (*backend.QueryDataResponse, error) { +func (e *AzureMonitorDatasource) ExecuteTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo types.DatasourceInfo, client *http.Client, url string, fromAlert bool) (*backend.QueryDataResponse, error) { result := backend.NewQueryDataResponse() queries, err := e.buildQueries(originalQueries, dsInfo) diff --git a/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go b/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go index b9616ae8cc1..ce3373bdbb4 100644 --- a/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go +++ b/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go @@ -58,7 +58,7 @@ func (e *AzureResourceGraphDatasource) ResourceRequest(rw http.ResponseWriter, r // 1. builds the AzureMonitor url and querystring for each query // 2. executes each query by calling the Azure Monitor API // 3. parses the responses for each query into data frames -func (e *AzureResourceGraphDatasource) ExecuteTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo types.DatasourceInfo, client *http.Client, url string) (*backend.QueryDataResponse, error) { +func (e *AzureResourceGraphDatasource) ExecuteTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo types.DatasourceInfo, client *http.Client, url string, fromAlert bool) (*backend.QueryDataResponse, error) { result := &backend.QueryDataResponse{ Responses: map[string]backend.DataResponse{}, } diff --git a/public/app/plugins/datasource/azuremonitor/__mocks__/datasource.ts b/public/app/plugins/datasource/azuremonitor/__mocks__/datasource.ts index a51004bf812..311bd737f8b 100644 --- a/public/app/plugins/datasource/azuremonitor/__mocks__/datasource.ts +++ b/public/app/plugins/datasource/azuremonitor/__mocks__/datasource.ts @@ -66,6 +66,7 @@ export default function createMockDatasource(overrides?: DeepPartial azureLogAnalyticsDatasource: { getKustoSchema: () => Promise.resolve(), getDeprecatedDefaultWorkSpace: () => 'defaultWorkspaceId', + getBasicLogsQueryUsage: jest.fn(), }, resourcePickerData: { getSubscriptions: () => jest.fn().mockResolvedValue([]), diff --git a/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.ts b/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.ts index bf035aed1d8..ca5a8d83b2c 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.ts @@ -130,6 +130,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< // Workspace was removed in Grafana 8, but remains for backwards compat workspace, dashboardTime: item.dashboardTime, + basicLogsQuery: item.basicLogsQuery, timeColumn: this.templateSrv.replace(item.timeColumn, scopedVars), }, }; @@ -252,4 +253,17 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< async getAzureLogAnalyticsCheatsheetQueries() { return await this.getResource(`${this.resourcePath}/v1/metadata`); } + + async getBasicLogsQueryUsage(query: AzureMonitorQuery, table: string): Promise { + const templateSrv = getTemplateSrv(); + + const data = { + table: table, + resource: templateSrv.replace(query.azureLogAnalytics?.resources?.[0]), + queryType: query.queryType, + from: templateSrv.replace('$__from'), + to: templateSrv.replace('$__to'), + }; + return await this.postResource(`${this.resourcePath}/usage/basiclogs`, data); + } } diff --git a/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts b/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts index 68e93304011..fee1af9dfa1 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts @@ -44,6 +44,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend { + it('should render component', () => { + render(); + + expect(screen.getByText('Enable Basic Logs')).toBeInTheDocument(); + }); +}); diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/BasicLogsToggle.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/BasicLogsToggle.tsx new file mode 100644 index 00000000000..7cf47c13a80 --- /dev/null +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/BasicLogsToggle.tsx @@ -0,0 +1,52 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { Field, Switch, useTheme2 } from '@grafana/ui'; + +import { AzureDataSourceJsonData } from '../../types'; + +export interface Props { + options: AzureDataSourceJsonData; + onBasicLogsEnabledChange: (basicLogsEnabled: boolean) => void; +} + +export const BasicLogsToggle = (props: Props) => { + const { options, onBasicLogsEnabledChange } = props; + + const theme = useTheme2(); + const styles = { + text: css({ + ...theme.typography.body, + color: theme.colors.text.secondary, + fontSize: '11px', + a: css({ + color: theme.colors.text.link, + textDecoration: 'underline', + '&:hover': { + textDecoration: 'none', + }, + }), + }), + }; + const onChange = (e: React.ChangeEvent) => onBasicLogsEnabledChange(e.target.checked); + const description = ( +

+ Enabling this feature incurs Azure Monitor per-query costs on dashboard panels that query tables configured for{' '} + + Basic Logs + + . +

+ ); + return ( + +
+ +
+
+ ); +}; diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.tsx index 8a2dd691284..03f6e4779db 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.tsx @@ -8,6 +8,7 @@ import { getCredentials, updateCredentials } from '../../credentials'; import { AzureDataSourceSettings, AzureCredentials } from '../../types'; import { AzureCredentialsForm } from './AzureCredentialsForm'; +import { BasicLogsToggle } from './BasicLogsToggle'; import { DefaultSubscription } from './DefaultSubscription'; const legacyAzureClouds: SelectableValue[] = [ @@ -49,6 +50,9 @@ export const MonitorConfig = (props: Props) => { const onSubscriptionChange = (subscriptionId?: string) => updateOptions((options) => ({ ...options, jsonData: { ...options.jsonData, subscriptionId } })); + const onBasicLogsEnabledChange = (enableBasicLogs: boolean) => + updateOptions((options) => ({ ...options, jsonData: { ...options.jsonData, basicLogsEnabled: enableBasicLogs } })); + // The auth type needs to be set on the first load of the data source useEffectOnce(() => { if (!options.jsonData.authType) { @@ -68,15 +72,18 @@ export const MonitorConfig = (props: Props) => { onCredentialsChange={onCredentialsChange} disabled={props.options.readOnly} > - + <> + + + ); diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsManagement.test.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsManagement.test.tsx new file mode 100644 index 00000000000..a9a12762410 --- /dev/null +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsManagement.test.tsx @@ -0,0 +1,142 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import createMockDatasource from '../../__mocks__/datasource'; +import createMockQuery from '../../__mocks__/query'; + +import { LogsManagement } from './LogsManagement'; + +const variableOptionGroup = { + label: 'Template variables', + options: [], +}; + +describe('LogsQueryEditor.LogsManagement', () => { + it('should set Basic Logs to true if Basic is clicked and acknowledged', async () => { + const mockDatasource = createMockDatasource(); + const query = createMockQuery({ azureLogAnalytics: { basicLogsQuery: undefined } }); + const onChange = jest.fn(); + + render( + {}} + /> + ); + + const logsManagementOption = await screen.findByLabelText('Basic'); + await userEvent.click(logsManagementOption); + + // ensures that modal shows + expect(await screen.findByText('Basic Logs Queries')).toBeInTheDocument(); + const acknowledgedAction = await screen.findByText('Confirm'); + await userEvent.click(acknowledgedAction); + + expect(onChange).toBeCalledWith( + expect.objectContaining({ + azureLogAnalytics: expect.objectContaining({ + basicLogsQuery: true, + query: '', + dashboardTime: true, + }), + }) + ); + }); + + it('should set Basic Logs to false if Analytics is clicked', async () => { + const mockDatasource = createMockDatasource(); + const query = createMockQuery({ azureLogAnalytics: { basicLogsQuery: true } }); + const onChange = jest.fn(); + + render( + {}} + /> + ); + + const logsManagementOption = await screen.findByLabelText('Analytics'); + await userEvent.click(logsManagementOption); + + expect(onChange).toBeCalledWith( + expect.objectContaining({ + azureLogAnalytics: expect.objectContaining({ + basicLogsQuery: false, + query: '', + }), + }) + ); + }); + + it('should set Basic Logs to true if Basic is clicked and clear query', async () => { + const mockDatasource = createMockDatasource(); + const query = createMockQuery({ azureLogAnalytics: { basicLogsQuery: undefined, query: 'table | my test query' } }); + const onChange = jest.fn(); + + render( + {}} + /> + ); + + const logsManagementOption = await screen.findByLabelText('Basic'); + await userEvent.click(logsManagementOption); + const acknowledgedAction = await screen.findByText('Confirm'); + await userEvent.click(acknowledgedAction); + + expect(onChange).toBeCalledWith( + expect.objectContaining({ + azureLogAnalytics: expect.objectContaining({ + basicLogsQuery: true, + query: '', + dashboardTime: true, + }), + }) + ); + }); + + it('should handle modal acknowledgements - cancel', async () => { + const mockDatasource = createMockDatasource(); + const query = createMockQuery({ azureLogAnalytics: { basicLogsQuery: undefined } }); + const onChange = jest.fn(); + + render( + {}} + /> + ); + + const logsManagementOption = await screen.findByLabelText('Basic'); + await userEvent.click(logsManagementOption); + + // ensures that modal shows + expect(await screen.findByText('Basic Logs Queries')).toBeInTheDocument(); + + const cancelAcknowledgement = await screen.findByText('Cancel'); + await userEvent.click(cancelAcknowledgement); + + //ensures that if cancel is clicked, Logs is set back to analytics + expect(onChange).toBeCalledWith( + expect.objectContaining({ + azureLogAnalytics: expect.objectContaining({ + basicLogsQuery: false, + }), + }) + ); + }); +}); diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsManagement.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsManagement.tsx new file mode 100644 index 00000000000..fb4de553c42 --- /dev/null +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsManagement.tsx @@ -0,0 +1,51 @@ +import React, { useState } from 'react'; + +import { ConfirmModal, InlineField, RadioButtonGroup } from '@grafana/ui'; + +import { AzureQueryEditorFieldProps } from '../../types'; + +import { setBasicLogsQuery, setDashboardTime, setKustoQuery } from './setQueryValue'; + +export function LogsManagement({ query, onQueryChange: onChange }: AzureQueryEditorFieldProps) { + const [basicLogsAckOpen, setBasicLogsAckOpen] = useState(false); + return ( + <> + { + setBasicLogsAckOpen(false); + let updatedBasicLogsQuery = setBasicLogsQuery(query, true); + // if basic logs selected, set dashboard time + updatedBasicLogsQuery = setDashboardTime(updatedBasicLogsQuery, 'dashboard'); + onChange(setKustoQuery(updatedBasicLogsQuery, '')); + }} + onDismiss={() => { + setBasicLogsAckOpen(false); + onChange(setBasicLogsQuery(query, false)); + }} + confirmButtonVariant="primary" + /> + Specifies whether to run a Basic or Analytics Logs query.}> + { + setBasicLogsAckOpen(val); + if (!val) { + const updatedBasicLogsQuery = setBasicLogsQuery(query, val); + onChange(setKustoQuery(updatedBasicLogsQuery, '')); + } + }} + /> + + + ); +} diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.test.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.test.tsx index d77ff945ea4..7c5b61cd025 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import { act, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; @@ -14,6 +14,9 @@ jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getTemplateSrv: () => ({ replace: (val: string) => { + if (val === '$ws') { + return '/subscriptions/def-456/resourceGroups/dev-3/providers/microsoft.operationalinsights/workspaces/la-workspace'; + } return val; }, }), @@ -40,6 +43,7 @@ describe('LogsQueryEditor', () => { delete query?.subscription; delete query?.azureLogAnalytics?.resources; const onChange = jest.fn(); + const basicLogsEnabled = false; render( { variableOptionGroup={variableOptionGroup} onChange={onChange} setError={() => {}} + basicLogsEnabled={basicLogsEnabled} /> ); @@ -87,6 +92,7 @@ describe('LogsQueryEditor', () => { const query = createMockQuery(); delete query?.subscription; delete query?.azureLogAnalytics?.resources; + const basicLogsEnabled = false; const onChange = jest.fn(); render( @@ -96,6 +102,7 @@ describe('LogsQueryEditor', () => { variableOptionGroup={variableOptionGroup} onChange={onChange} setError={() => {}} + basicLogsEnabled={basicLogsEnabled} /> ); @@ -120,6 +127,7 @@ describe('LogsQueryEditor', () => { const query = createMockQuery(); delete query?.subscription; delete query?.azureLogAnalytics?.resources; + const basicLogsEnabled = false; const onChange = jest.fn(); render( @@ -129,6 +137,7 @@ describe('LogsQueryEditor', () => { variableOptionGroup={variableOptionGroup} onChange={onChange} setError={() => {}} + basicLogsEnabled={basicLogsEnabled} /> ); @@ -153,6 +162,7 @@ describe('LogsQueryEditor', () => { const query = createMockQuery(); delete query?.subscription; delete query?.azureLogAnalytics?.resources; + const basicLogsEnabled = false; const onChange = jest.fn(); render( @@ -162,6 +172,7 @@ describe('LogsQueryEditor', () => { variableOptionGroup={variableOptionGroup} onChange={onChange} setError={() => {}} + basicLogsEnabled={basicLogsEnabled} /> ); @@ -190,6 +201,7 @@ describe('LogsQueryEditor', () => { it('should update the dashboardTime prop', async () => { const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); const query = createMockQuery(); + const basicLogsEnabled = false; const onChange = jest.fn(); render( @@ -199,6 +211,7 @@ describe('LogsQueryEditor', () => { variableOptionGroup={variableOptionGroup} onChange={onChange} setError={() => {}} + basicLogsEnabled={basicLogsEnabled} /> ); @@ -218,6 +231,7 @@ describe('LogsQueryEditor', () => { it('should show the link button', async () => { const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); const query = createMockQuery(); + const basicLogsEnabled = false; const onChange = jest.fn(); const date = dateTime(new Date()); @@ -228,6 +242,7 @@ describe('LogsQueryEditor', () => { variableOptionGroup={variableOptionGroup} onChange={onChange} setError={() => {}} + basicLogsEnabled={basicLogsEnabled} data={{ state: LoadingState.Done, timeRange: { @@ -246,4 +261,242 @@ describe('LogsQueryEditor', () => { expect(await screen.findByText('View query in Azure Portal')).toBeInTheDocument(); }); }); + + describe('basic logs toggle', () => { + it('should show basic logs toggle', async () => { + const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); + const query = createMockQuery({ + azureLogAnalytics: { + resources: [ + '/subscriptions/def-456/resourceGroups/dev-3/providers/microsoft.operationalinsights/workspaces/la-workspace', + ], + }, + }); + const basicLogsEnabled = true; + const onChange = jest.fn(); + + await act(async () => { + render( + {}} + basicLogsEnabled={basicLogsEnabled} + /> + ); + }); + + expect(await screen.findByLabelText('Basic')).toBeInTheDocument(); + }); + + it('should show basic logs toggle for workspace variables', async () => { + const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); + const query = createMockQuery({ + azureLogAnalytics: { + resources: ['$ws'], + }, + }); + const basicLogsEnabled = true; + const onChange = jest.fn(); + + await act(async () => { + render( + {}} + basicLogsEnabled={basicLogsEnabled} + /> + ); + }); + + expect(await screen.findByLabelText('Basic')).toBeInTheDocument(); + }); + + it('should not show basic logs toggle - basic logs not enabled', async () => { + const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); + const query = createMockQuery({ + azureLogAnalytics: { + resources: [ + '/subscriptions/def-456/resourceGroups/dev-3/providers/microsoft.operationalinsights/workspaces/la-workspace', + ], + }, + }); + const basicLogsEnabled = false; + const onChange = jest.fn(); + + await act(async () => { + render( + {}} + basicLogsEnabled={basicLogsEnabled} + /> + ); + }); + + expect(await screen.queryByLabelText('Basic')).not.toBeInTheDocument(); + }); + + it('should not show basic logs toggle for non workspace variables', async () => { + const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); + const query = createMockQuery({ + azureLogAnalytics: { + resources: ['$non_ws_var'], + }, + }); + const basicLogsEnabled = true; + const onChange = jest.fn(); + + await act(async () => { + render( + {}} + basicLogsEnabled={basicLogsEnabled} + /> + ); + }); + + expect(await screen.queryByLabelText('Basic')).not.toBeInTheDocument(); + }); + + it('should not show basic logs toggle - selected resource is not LA workspace', async () => { + const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); + const query = createMockQuery({ + azureLogAnalytics: { + resources: [ + '/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/web-server', + ], + }, + }); + const basicLogsEnabled = true; + const onChange = jest.fn(); + + await act(async () => { + render( + {}} + basicLogsEnabled={basicLogsEnabled} + /> + ); + }); + + expect(await screen.queryByLabelText('Basic')).not.toBeInTheDocument(); + }); + }); + + describe('data ingestion warning', () => { + it('should show generic data ingested warning when running basic logs queries', async () => { + const mockDatasource = createMockDatasource(); + const onChange = jest.fn(); + const query = createMockQuery({ + azureLogAnalytics: { + resources: [ + '/subscriptions/def-456/resourceGroups/dev-3/providers/microsoft.operationalinsights/workspaces/la-workspace', + ], + basicLogsQuery: true, + }, + }); + + mockDatasource.azureLogAnalyticsDatasource.getBasicLogsQueryUsage.mockResolvedValue(0); + await act(async () => { + render( + {}} + basicLogsEnabled={true} + /> + ); + }); + + await act(async () => { + await waitFor(() => + expect( + screen.findByText(/This is a Basic Logs query and incurs cost per GiB scanned./) + ).resolves.toBeInTheDocument() + ); + }); + }); + + it('should show data ingested warning when running basic logs queries', async () => { + const mockDatasource = createMockDatasource(); + const onChange = jest.fn(); + const query = createMockQuery({ + azureLogAnalytics: { + resources: [ + '/subscriptions/def-456/resourceGroups/dev-3/providers/microsoft.operationalinsights/workspaces/la-workspace', + ], + basicLogsQuery: true, + }, + }); + + mockDatasource.azureLogAnalyticsDatasource.getBasicLogsQueryUsage.mockResolvedValue(0.45); + await act(async () => { + render( + {}} + basicLogsEnabled={true} + /> + ); + }); + + await act(async () => { + await waitFor(() => + expect(screen.findByText(/This query is processing 0.45 GiB when run./)).resolves.toBeInTheDocument() + ); + }); + }); + + it('should not show data ingested warning when running basic logs queries', async () => { + const mockDatasource = createMockDatasource(); + const onChange = jest.fn(); + const query = createMockQuery({ + azureLogAnalytics: { + resources: [ + '/subscriptions/def-456/resourceGroups/dev-3/providers/microsoft.operationalinsights/workspaces/la-workspace', + ], + basicLogsQuery: true, + query: '', + }, + }); + + mockDatasource.azureLogAnalyticsDatasource.getBasicLogsQueryUsage.mockResolvedValue(0.5); + await act(async () => { + render( + {}} + basicLogsEnabled={true} + /> + ); + }); + + expect(await screen.queryByLabelText(/This query is processing 0.50 GiB when run./)).not.toBeInTheDocument(); + }); + }); }); diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx index 96a321c9e63..77da2ba7793 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx @@ -2,7 +2,8 @@ import React, { useEffect, useState } from 'react'; import { PanelData, TimeRange } from '@grafana/data'; import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/experimental'; -import { Alert, LinkButton } from '@grafana/ui'; +import { getTemplateSrv } from '@grafana/runtime'; +import { Alert, LinkButton, Text, TextLink } from '@grafana/ui'; import Datasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; @@ -13,14 +14,19 @@ import { parseResourceDetails } from '../ResourcePicker/utils'; import FormatAsField from '../shared/FormatAsField'; import AdvancedResourcePicker from './AdvancedResourcePicker'; +import { LogsManagement } from './LogsManagement'; import QueryField from './QueryField'; import { TimeManagement } from './TimeManagement'; -import { setFormatAs } from './setQueryValue'; +import { setBasicLogsQuery, setFormatAs, setKustoQuery } from './setQueryValue'; import useMigrations from './useMigrations'; +import { calculateTimeRange, shouldShowBasicLogsToggle } from './utils'; + +const MAX_DATA_RETENTION_DAYS = 8; // limit is only for basic logs interface LogsQueryEditorProps { query: AzureMonitorQuery; datasource: Datasource; + basicLogsEnabled: boolean; subscriptionId?: string; onChange: (newQuery: AzureMonitorQuery) => void; variableOptionGroup: { label: string; options: AzureMonitorOption[] }; @@ -33,6 +39,7 @@ interface LogsQueryEditorProps { const LogsQueryEditor = ({ query, datasource, + basicLogsEnabled, subscriptionId, variableOptionGroup, onChange, @@ -42,6 +49,15 @@ const LogsQueryEditor = ({ data, }: LogsQueryEditorProps) => { const migrationError = useMigrations(datasource, query, onChange); + const [showBasicLogsToggle, setShowBasicLogsToggle] = useState( + shouldShowBasicLogsToggle(query.azureLogAnalytics?.resources || [], basicLogsEnabled) + ); + const [showDataRetentionWarning, setShowDataRetentionWarning] = useState(false); + const [dataIngestedWarning, setDataIngestedWarning] = useState(null); + const templateSrv = getTemplateSrv(); + const from = templateSrv?.replace('$__from'); + const to = templateSrv?.replace('$__to'); + const disableRow = (row: ResourceRow, selectedRows: ResourceRowGroup) => { if (selectedRows.length === 0) { // Only if there is some resource(s) selected we should disable rows @@ -65,6 +81,66 @@ const LogsQueryEditor = ({ } }, [query.azureLogAnalytics?.resources, datasource.azureLogAnalyticsDatasource]); + useEffect(() => { + if (shouldShowBasicLogsToggle(query.azureLogAnalytics?.resources || [], basicLogsEnabled)) { + setShowBasicLogsToggle(true); + } else { + setShowBasicLogsToggle(false); + } + }, [basicLogsEnabled, query.azureLogAnalytics?.resources, templateSrv]); + + useEffect(() => { + if ((!basicLogsEnabled || !showBasicLogsToggle) && query.azureLogAnalytics?.basicLogsQuery) { + const updatedBasicLogsQuery = setBasicLogsQuery(query, false); + onChange(setKustoQuery(updatedBasicLogsQuery, '')); + } + }, [basicLogsEnabled, onChange, query, showBasicLogsToggle]); + + useEffect(() => { + const timeRange = calculateTimeRange(parseInt(from, 10), parseInt(to, 10)); + // Basic logs data retention is fixed at 8 days + // need to add this check to make user aware of this limitation in case they have selected a longer time range + if (showBasicLogsToggle && query.azureLogAnalytics?.basicLogsQuery && timeRange > MAX_DATA_RETENTION_DAYS) { + setShowDataRetentionWarning(true); + } else { + setShowDataRetentionWarning(false); + } + }, [query.azureLogAnalytics?.basicLogsQuery, showBasicLogsToggle, from, to]); + + useEffect(() => { + const getBasicLogsUsage = async (query: AzureMonitorQuery) => { + try { + if (showBasicLogsToggle && query.azureLogAnalytics?.basicLogsQuery && !!query.azureLogAnalytics.query) { + const querySplit = query.azureLogAnalytics.query.split('|'); + // Basic Logs queries are required to start the query with a table + const table = querySplit[0].trim(); + const dataIngested = await datasource.azureLogAnalyticsDatasource.getBasicLogsQueryUsage(query, table); + const textToShow = !!dataIngested + ? `This query is processing ${dataIngested} GiB when run. ` + : 'This is a Basic Logs query and incurs cost per GiB scanned. '; + setDataIngestedWarning( + <> + + {textToShow}{' '} + + Learn More + + + + ); + } else { + setDataIngestedWarning(null); + } + } catch (err) { + console.error(err); + } + }; + + getBasicLogsUsage(query).catch((err) => console.error(err)); + }, [datasource.azureLogAnalyticsDatasource, query, showBasicLogsToggle, from, to]); let portalLinkButton = null; if (data?.series) { @@ -116,6 +192,15 @@ const LogsQueryEditor = ({ )} selectionNotice={() => 'You may only choose items of the same resource type.'} /> + {showBasicLogsToggle && ( + + )} + {dataIngestedWarning} {!hideFormatAs && ( @@ -161,6 +247,13 @@ const LogsQueryEditor = ({ + {showDataRetentionWarning && ( + + + Data retention for Basic Logs is fixed at eight days. You will only see data within this timeframe. + + + )} ); }; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/TimeManagement.test.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/TimeManagement.test.tsx index c86eec3a841..a8c4b7dc6fe 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/TimeManagement.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/TimeManagement.test.tsx @@ -169,4 +169,24 @@ describe('LogsQueryEditor.TimeManagement', () => { expect(onChange).not.toBeCalled(); expect(screen.getByText('Alert > TestTimeColumn')).toBeInTheDocument(); }); + + it('should set time to dashboard and query disabled if basic logs is selected', async () => { + const mockDatasource = createMockDatasource(); + const query = createMockQuery({ azureLogAnalytics: { basicLogsQuery: true, dashboardTime: true } }); + const onChange = jest.fn(); + + render( + {}} + schema={FakeSchemaData.getLogAnalyticsFakeEngineSchema()} + /> + ); + + expect(screen.getByLabelText('Query')).toBeDisabled(); + expect(screen.getByLabelText('Dashboard')).toBeChecked(); + }); }); diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/TimeManagement.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/TimeManagement.tsx index a97bea313bd..cf910c14f0c 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/TimeManagement.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/TimeManagement.tsx @@ -10,6 +10,7 @@ import { setDashboardTime, setTimeColumn } from './setQueryValue'; export function TimeManagement({ query, onQueryChange: onChange, schema }: AzureQueryEditorFieldProps) { const [defaultTimeColumns, setDefaultTimeColumns] = useState(); const [timeColumns, setTimeColumns] = useState(); + const [disabledTimePicker, setDisabledTimePicker] = useState(false); const setDefaultColumn = useCallback((column: string) => onChange(setTimeColumn(query, column)), [query, onChange]); @@ -76,6 +77,14 @@ export function TimeManagement({ query, onQueryChange: onChange, schema }: Azure }, [onChange, query] ); + + useEffect(() => { + if (query.azureLogAnalytics?.basicLogsQuery) { + setDisabledTimePicker(true); + } else { + setDisabledTimePicker(false); + } + }, [query.azureLogAnalytics?.basicLogsQuery]); return ( <> onChange(setDashboardTime(query, val))} + disabled={disabledTimePicker} + disabledOptions={disabledTimePicker ? ['query'] : []} /> {query.azureLogAnalytics?.dashboardTime && ( diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/setQueryValue.ts b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/setQueryValue.ts index 88fdfbd5b29..c4938971d0b 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/setQueryValue.ts +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/setQueryValue.ts @@ -20,12 +20,12 @@ export function setFormatAs(query: AzureMonitorQuery, formatAs: ResultFormat): A }; } -export function setDashboardTime(query: AzureMonitorQuery, dashboardTime: boolean): AzureMonitorQuery { +export function setDashboardTime(query: AzureMonitorQuery, dashboardTime: string): AzureMonitorQuery { return { ...query, azureLogAnalytics: { ...query.azureLogAnalytics, - dashboardTime, + dashboardTime: dashboardTime === 'dashboard' ? true : false, }, }; } @@ -39,3 +39,13 @@ export function setTimeColumn(query: AzureMonitorQuery, timeColumn: string): Azu }, }; } + +export function setBasicLogsQuery(query: AzureMonitorQuery, basicLogsQuery: boolean): AzureMonitorQuery { + return { + ...query, + azureLogAnalytics: { + ...query.azureLogAnalytics, + basicLogsQuery, + }, + }; +} diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/utils.test.ts b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/utils.test.ts new file mode 100644 index 00000000000..9e4977448fa --- /dev/null +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/utils.test.ts @@ -0,0 +1,101 @@ +import { calculateTimeRange, shouldShowBasicLogsToggle } from './utils'; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getTemplateSrv: () => ({ + replace: (val: string) => { + if (val === '$ws') { + return '/subscriptions/def-456/resourceGroups/dev-3/providers/microsoft.operationalinsights/workspaces/la-workspace'; + } + return val; + }, + }), +})); + +describe('LogsQueryEditor utils', () => { + describe('shouldShowBasicLogsToggle', () => { + it('should return false if basic logs are not enabled', () => { + expect( + shouldShowBasicLogsToggle( + [ + '/subscriptions/def-456/resourceGroups/dev-3/providers/microsoft.operationalinsights/workspaces/la-workspace', + ], + false + ) + ).toBe(false); + }); + + it('should return false if selected resource is not an LA workspace', () => { + expect( + shouldShowBasicLogsToggle( + [ + '/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.Storage/storageAccounts/csb100320016c43d2d0/fileServices/default', + ], + true + ) + ).toBe(false); + }); + + it('should return false if no resources are selected', () => { + expect(shouldShowBasicLogsToggle([], true)).toBe(false); + }); + + it('should return false if more than one resource is selected', () => { + expect( + shouldShowBasicLogsToggle( + [ + '/subscriptions/def-456/resourceGroups/dev-3/providers/microsoft.operationalinsights/workspaces/la-workspace', + '/subscriptions/def-456/resourceGroups/dev-3/providers/microsoft.OperationalInsights/workspaces/la-workspace2', + ], + true + ) + ).toBe(false); + }); + + it('should return true if basic logs are enabled and selected single resource is an LA workspace', () => { + expect( + shouldShowBasicLogsToggle( + [ + '/subscriptions/def-456/resourceGroups/dev-3/providers/microsoft.operationalinsights/workspaces/la-workspace', + ], + true + ) + ).toBe(true); + }); + + it('should return true if basic logs are enabled and selected single resource is an LA workspace variable', () => { + expect(shouldShowBasicLogsToggle(['$ws'], true)).toBe(true); + }); + }); + + describe('calculateTimeRange', () => { + it('should correctly calculate the time range in days', () => { + const from = Date.now() - 1000 * 60 * 60 * 24 * 3; // 3 days ago + const to = Date.now(); + + const result = calculateTimeRange(from, to); + + // The result should be approximately 3 + expect(result).toBeCloseTo(3, 0); + }); + + it('should return 0 when from and to are the same', () => { + const from = Date.now(); + const to = from; + + const result = calculateTimeRange(from, to); + + expect(result).toBe(0); + }); + + it('should return a negative number when from is later than to', () => { + const from = Date.now(); + const to = from - 1000 * 60 * 60 * 24; // 1 day ago + + const result = calculateTimeRange(from, to); + + // The result should be approximately -1 + expect(result).toBeCloseTo(-1, 0); + }); + }); +}); diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/utils.ts b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/utils.ts new file mode 100644 index 00000000000..79b00006e6f --- /dev/null +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/utils.ts @@ -0,0 +1,22 @@ +import { getTemplateSrv } from '@grafana/runtime'; + +import { parseResourceURI } from '../ResourcePicker/utils'; + +export function shouldShowBasicLogsToggle(resources: string[], basicLogsEnabled: boolean) { + const selectedResource = getTemplateSrv()?.replace(resources[0]); + return ( + basicLogsEnabled && + resources.length === 1 && + parseResourceURI(selectedResource).metricNamespace?.toLowerCase() === 'microsoft.operationalinsights/workspaces' + ); +} + +export function calculateTimeRange(from: number, to: number): number { + const second = 1000; + const minute = second * 60; + const hour = minute * 60; + const day = hour * 24; + const timeRange = (to - from) / day; + + return timeRange; +} diff --git a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.test.tsx b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.test.tsx index fe200de0678..744aec9063c 100644 --- a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.test.tsx @@ -25,6 +25,11 @@ jest.mock('@grafana/ui', () => ({ jest.mock('@grafana/runtime', () => ({ ___esModule: true, ...jest.requireActual('@grafana/runtime'), + getTemplateSrv: () => ({ + replace: (val: string) => { + return val; + }, + }), })); describe('Azure Monitor QueryEditor', () => { diff --git a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx index 96239098982..c4b5e5fad97 100644 --- a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx @@ -55,13 +55,16 @@ const QueryEditor = ({ const query = usePreparedQuery(baseQuery, onQueryChange); const subscriptionId = query.subscription || datasource.azureMonitorDatasource.defaultSubscriptionId; + const basicLogsEnabled = + datasource.azureMonitorDatasource.basicLogsEnabled && + app !== CoreApp.UnifiedAlerting && + app !== CoreApp.CloudAlerting; const variableOptionGroup = { label: 'Template Variables', options: datasource.getVariables().map((v) => ({ label: v, value: v })), }; const isAzureAuthenticated = config.bootData.user.authenticatedBy === 'oauth_azuread'; - if (datasource.currentUserAuth) { if ( app === CoreApp.UnifiedAlerting && @@ -105,6 +108,7 @@ const QueryEditor = ({ { subscriptionId?: string; + basicLogsEnabled: boolean; variableOptionGroup: { label: string; options: AzureMonitorOption[] }; setError: (source: string, error: AzureMonitorErrorish | undefined) => void; } @@ -134,6 +139,7 @@ interface EditorForQueryTypeProps extends Omit[^/]+)(?:\/resourceGroups\/(?[^/]+)(?:\/providers\/(?.+))?)?/; + /\/subscriptions\/(?[^/]+)(?:\/resourceGroups\/(?[^/]+)(?:\/providers\/(?.+))?)?/i; type RegexGroups = Record; diff --git a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx index de3b4cfd9db..9798ab24bcc 100644 --- a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx @@ -18,6 +18,15 @@ jest.mock('@grafana/ui', () => ({ }, })); +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getTemplateSrv: () => ({ + replace: (val: string) => { + return val; + }, + }), +})); + const defaultProps = { query: { refId: 'A', diff --git a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx index 94a2e1b281a..6bb9c033d23 100644 --- a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx @@ -246,6 +246,7 @@ const VariableEditor = (props: Props) => { variableOptionGroup={variableOptionGroup} setError={setError} hideFormatAs={true} + basicLogsEnabled={datasource.azureMonitorDatasource.basicLogsEnabled ?? false} /> {errorMessage && ( <> diff --git a/public/app/plugins/datasource/azuremonitor/dataquery.cue b/public/app/plugins/datasource/azuremonitor/dataquery.cue index ffefdaab339..15f8c090e49 100644 --- a/public/app/plugins/datasource/azuremonitor/dataquery.cue +++ b/public/app/plugins/datasource/azuremonitor/dataquery.cue @@ -115,6 +115,8 @@ composableKinds: DataQuery: { dashboardTime?: bool // If dashboardTime is set to true this value dictates which column the time filter will be applied to. Defaults to the first tables timeSpan column, the first datetime column found, or TimeGenerated timeColumn?: string + // If set to true the query will be run as a basic logs query + basicLogsQuery?: bool // Workspace ID. This was removed in Grafana 8, but remains for backwards compat. workspace?: string diff --git a/public/app/plugins/datasource/azuremonitor/dataquery.gen.ts b/public/app/plugins/datasource/azuremonitor/dataquery.gen.ts index 8616ca3ac59..6c21df2a6cb 100644 --- a/public/app/plugins/datasource/azuremonitor/dataquery.gen.ts +++ b/public/app/plugins/datasource/azuremonitor/dataquery.gen.ts @@ -161,6 +161,10 @@ export const defaultAzureMetricQuery: Partial = { * Azure Monitor Logs sub-query properties */ export interface AzureLogsQuery { + /** + * If set to true the query will be run as a basic logs query + */ + basicLogsQuery?: boolean; /** * If set to true the dashboard time range will be used as a filter for the query. Otherwise the query time ranges will be used. Defaults to false. */ diff --git a/public/app/plugins/datasource/azuremonitor/types/types.ts b/public/app/plugins/datasource/azuremonitor/types/types.ts index dde36a69ee6..3a30331b108 100644 --- a/public/app/plugins/datasource/azuremonitor/types/types.ts +++ b/public/app/plugins/datasource/azuremonitor/types/types.ts @@ -81,6 +81,7 @@ export interface AzureDataSourceJsonData extends DataSourceJsonData { subscriptionId?: string; oauthPassThru?: boolean; azureCredentials?: AzureCredentials; + basicLogsEnabled?: boolean; // logs /** @deprecated Azure Logs credentials */