From 480ccf6e8f75f0508351bed06fe6c50d2d43129a Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Mon, 17 Jul 2023 12:02:16 +0100 Subject: [PATCH] AzureMonitor: Add switch to control time-range for Logs queries (#71278) * Update types * Update migration - Default intersectTime property to false * Update frontend components - Add intersectTime field - Update tests - Update mocks - Add onChange function * Update backend - Appropriately set intersectTime for logs queries - intersectTime is always true for Traces queries - Update tests * Update docs * Fix test and lint --- .../azure-monitor/query-editor/index.md | 9 +-- .../x/AzureMonitorDataQuery_types.gen.ts | 4 ++ .../kinds/dataquery/types_dataquery_gen.go | 3 + .../azure-log-analytics-datasource.go | 22 ++++-- .../azure-log-analytics-datasource_test.go | 67 +++++++++++++++++-- .../azuremonitor/__mocks__/query.ts | 1 + .../azure_log_analytics_datasource.ts | 1 + .../LogsQueryEditor/LogsQueryEditor.test.tsx | 27 ++++++++ .../LogsQueryEditor/LogsQueryEditor.tsx | 20 +++++- .../LogsQueryEditor/setQueryValue.ts | 10 +++ .../datasource/azuremonitor/dataquery.cue | 2 + .../datasource/azuremonitor/dataquery.gen.ts | 4 ++ .../azuremonitor/utils/migrateQuery.test.ts | 33 +++++++++ .../azuremonitor/utils/migrateQuery.ts | 10 +++ 14 files changed, 195 insertions(+), 18 deletions(-) diff --git a/docs/sources/datasources/azure-monitor/query-editor/index.md b/docs/sources/datasources/azure-monitor/query-editor/index.md index 1a24403a8eb..4c0b1d0e3ee 100644 --- a/docs/sources/datasources/azure-monitor/query-editor/index.md +++ b/docs/sources/datasources/azure-monitor/query-editor/index.md @@ -141,10 +141,11 @@ The Azure documentation includes resources to help you learn KQL: - [Tutorial: Use Kusto queries in Azure Monitor](https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/tutorial?pivots=azuremonitor) - [SQL to Kusto cheat sheet](https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/sqlcheatsheet) -> **Implicit dashboard time range usage:** As of Grafana v9.4.12 and v10.0, all Azure Monitor Logs queries -> will use the specified dashboard or Explore time range by default. -> Any query making use of a time range explicitly specified in the query body will have their query -> executed against the intersection of the two time ranges. For more details on this change, refer to the [Azure Monitor Logs API documentation](https://learn.microsoft.com/en-us/rest/api/loganalytics/dataaccess/query/get?tabs=HTTP#uri-parameters). +> **Time-range:** The time-range that will be used for the query can be modified via the time-range switch. Selecting `Query` will only make use of time-ranges specified within the query. +> Specifying `Intersection` will make use of the intersection between the time-ranges within the query and the Grafana time-range. +> If there are no time-ranges specified within the query, the Grafana time-range will be used. +> For more details on this change, refer to the [Azure Monitor Logs API documentation](https://learn.microsoft.com/en-us/rest/api/loganalytics/dataaccess/query/get?tabs=HTTP#uri-parameters). +> Note: v9.4.12, v10.0, and v10.0.1 do not have this switch and will implicitly use the intersection of the Grafana and query time-ranges. This example query returns a virtual machine's CPU performance, averaged over 5ms time grains: 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 96239df72f0..68a300d4b50 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 @@ -164,6 +164,10 @@ export const defaultAzureMetricQuery: Partial = { * Azure Monitor Logs sub-query properties */ export interface AzureLogsQuery { + /** + * If set to true the intersection of time ranges specified in the query and Grafana will be used. Otherwise the query time ranges will be used. Defaults to false + */ + intersectTime?: boolean; /** * KQL query to be executed. */ diff --git a/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go index 6e868f3f9f5..4e3f9667865 100644 --- a/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go @@ -118,6 +118,9 @@ type AppInsightsMetricNameQueryKind string // Azure Monitor Logs sub-query properties type AzureLogsQuery struct { + // If set to true the intersection of time ranges specified in the query and Grafana will be used. Otherwise the query time ranges will be used. Defaults to false + IntersectTime *bool `json:"intersectTime,omitempty"` + // KQL query to be executed. Query *string `json:"query,omitempty"` diff --git a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go index e553334a9bc..60b9c7a2cd7 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go @@ -48,6 +48,7 @@ type AzureLogAnalyticsQuery struct { Resources []string QueryType string AppInsightsQuery bool + IntersectTime bool } func (e *AzureLogAnalyticsDatasource) ResourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) { @@ -103,6 +104,7 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, logger l traceExploreQuery := "" traceParentExploreQuery := "" traceLogsExploreQuery := "" + intersectTime := false if query.QueryType == string(dataquery.AzureQueryTypeAzureLogAnalytics) { queryJSONModel := types.LogJSONQuery{} err := json.Unmarshal(query.JSON, &queryJSONModel) @@ -138,6 +140,10 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, logger l if azureLogAnalyticsTarget.Query != nil { queryString = *azureLogAnalyticsTarget.Query } + + if azureLogAnalyticsTarget.IntersectTime != nil { + intersectTime = *azureLogAnalyticsTarget.IntersectTime + } } if query.QueryType == string(dataquery.AzureQueryTypeAzureTraces) { @@ -210,6 +216,8 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, logger l if err != nil { return nil, fmt.Errorf("failed to create traces logs explore query: %s", err) } + + intersectTime = true } apiURL := getApiURL(resourceOrWorkspace, appInsightsQuery) @@ -232,6 +240,7 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, logger l TraceParentExploreQuery: traceParentExploreQuery, TraceLogsExploreQuery: traceLogsExploreQuery, AppInsightsQuery: appInsightsQuery, + IntersectTime: intersectTime, }) } @@ -442,12 +451,15 @@ func appendErrorNotice(frame *data.Frame, err *AzureLogAnalyticsAPIError) *data. } func (e *AzureLogAnalyticsDatasource) createRequest(ctx context.Context, logger log.Logger, queryURL string, query *AzureLogAnalyticsQuery) (*http.Request, error) { - from := query.TimeRange.From.Format(time.RFC3339) - to := query.TimeRange.To.Format(time.RFC3339) - timespan := fmt.Sprintf("%s/%s", from, to) body := map[string]interface{}{ - "query": query.Query, - "timespan": timespan, + "query": query.Query, + } + + if query.IntersectTime { + from := query.TimeRange.From.Format(time.RFC3339) + to := query.TimeRange.To.Format(time.RFC3339) + timespan := fmt.Sprintf("%s/%s", from, to) + body["timespan"] = timespan } if len(query.Resources) > 1 && query.QueryType == string(dataquery.AzureQueryTypeAzureLogAnalytics) && !query.AppInsightsQuery { 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 54222576c1d..91bdec91702 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go @@ -109,7 +109,8 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "azureLogAnalytics": { "resource": "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace", "query": "Perf | where $__timeFilter() | where $__contains(Computer, 'comp1','comp2') | summarize avg(CounterValue) by bin(TimeGenerated, $__interval), Computer", - "resultFormat": "%s" + "resultFormat": "%s", + "intersectTime": false } }`, types.TimeSeries)), RefID: "A", @@ -127,7 +128,8 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "azureLogAnalytics": { "resource": "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace", "query": "Perf | where $__timeFilter() | where $__contains(Computer, 'comp1','comp2') | summarize avg(CounterValue) by bin(TimeGenerated, $__interval), Computer", - "resultFormat": "%s" + "resultFormat": "%s", + "intersectTime": false } }`, types.TimeSeries)), Query: "Perf | where ['TimeGenerated'] >= datetime('2018-03-15T13:00:00Z') and ['TimeGenerated'] <= datetime('2018-03-15T13:34:00Z') | where ['Computer'] in ('comp1','comp2') | summarize avg(CounterValue) by bin(TimeGenerated, 34000ms), Computer", @@ -135,6 +137,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TimeRange: timeRange, QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), AppInsightsQuery: false, + IntersectTime: false, }, }, Err: require.NoError, @@ -172,6 +175,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { Resources: []string{}, QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), AppInsightsQuery: false, + IntersectTime: false, }, }, Err: require.NoError, @@ -209,6 +213,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { Resources: []string{}, QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), AppInsightsQuery: false, + IntersectTime: false, }, }, Err: require.NoError, @@ -222,7 +227,8 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "azureLogAnalytics": { "resource": "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace", "query": "Perf", - "resultFormat": "%s" + "resultFormat": "%s", + "intersectTime": false } }`, types.TimeSeries)), RefID: "A", @@ -239,13 +245,15 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "azureLogAnalytics": { "resource": "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace", "query": "Perf", - "resultFormat": "%s" + "resultFormat": "%s", + "intersectTime": false } }`, types.TimeSeries)), Query: "Perf", Resources: []string{"/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace"}, QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), AppInsightsQuery: false, + IntersectTime: false, }, }, Err: require.NoError, @@ -259,7 +267,8 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "azureLogAnalytics": { "resources": ["/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace", "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace2"], "query": "Perf", - "resultFormat": "%s" + "resultFormat": "%s", + "intersectTime": false } }`, types.TimeSeries)), RefID: "A", @@ -277,7 +286,8 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "azureLogAnalytics": { "resources": ["/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace", "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace2"], "query": "Perf", - "resultFormat": "%s" + "resultFormat": "%s", + "intersectTime": false } }`, types.TimeSeries)), Query: "Perf", @@ -285,6 +295,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TimeRange: timeRange, QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), AppInsightsQuery: false, + IntersectTime: false, }, }, Err: require.NoError, @@ -365,6 +376,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", AppInsightsQuery: true, + IntersectTime: true, }, }, Err: require.NoError, @@ -442,6 +454,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", AppInsightsQuery: true, + IntersectTime: true, }, }, Err: require.NoError, @@ -516,6 +529,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"${__data.fields.traceID}\"", AppInsightsQuery: true, + IntersectTime: true, }, }, Err: require.NoError, @@ -593,6 +607,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", AppInsightsQuery: true, + IntersectTime: true, }, }, Err: require.NoError, @@ -675,6 +690,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", AppInsightsQuery: true, + IntersectTime: true, }, }, Err: require.NoError, @@ -757,6 +773,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", AppInsightsQuery: true, + IntersectTime: true, }, }, Err: require.NoError, @@ -839,6 +856,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", AppInsightsQuery: true, + IntersectTime: true, }, }, Err: require.NoError, @@ -913,6 +931,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"${__data.fields.traceID}\"", AppInsightsQuery: true, + IntersectTime: true, }, }, Err: require.NoError, @@ -990,6 +1009,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", AppInsightsQuery: true, + IntersectTime: true, }, }, Err: require.NoError, @@ -1035,6 +1055,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", AppInsightsQuery: true, + IntersectTime: true, }, }, Err: require.NoError, @@ -1116,6 +1137,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').traces\n" + "| where operation_Id == \"op-id-multi\"", AppInsightsQuery: true, + IntersectTime: true, }, }, Err: require.NoError, @@ -1194,6 +1216,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').traces\n" + "| where operation_Id == \"${__data.fields.traceID}\"", AppInsightsQuery: true, + IntersectTime: true, }, }, Err: require.NoError, @@ -1275,6 +1298,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').traces\n" + "| where operation_Id == \"op-id-multi\"", AppInsightsQuery: true, + IntersectTime: true, }, }, Err: require.NoError, @@ -1363,6 +1387,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r3').traces\n" + "| where operation_Id == \"op-id-non-overlapping\"", AppInsightsQuery: true, + IntersectTime: true, }, }, Err: require.NoError, @@ -1389,6 +1414,31 @@ func TestLogAnalyticsCreateRequest(t *testing.T) { req, err := ds.createRequest(ctx, logger, url, &AzureLogAnalyticsQuery{ Resources: []string{"r"}, Query: "Perf", + IntersectTime: false, + AppInsightsQuery: false, + }) + require.NoError(t, err) + if req.URL.String() != url { + t.Errorf("Expecting %s, got %s", url, req.URL.String()) + } + expectedHeaders := http.Header{"Content-Type": []string{"application/json"}} + if !cmp.Equal(req.Header, expectedHeaders) { + t.Errorf("Unexpected HTTP headers: %v", cmp.Diff(req.Header, expectedHeaders)) + } + expectedBody := `{"query":"Perf"}` + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + if !cmp.Equal(string(body), expectedBody) { + t.Errorf("Unexpected Body: %v", cmp.Diff(string(body), expectedBody)) + } + }) + + t.Run("creates a request with timespan", func(t *testing.T) { + ds := AzureLogAnalyticsDatasource{} + req, err := ds.createRequest(ctx, logger, url, &AzureLogAnalyticsQuery{ + Resources: []string{"r"}, + Query: "Perf", + IntersectTime: true, AppInsightsQuery: false, }) require.NoError(t, err) @@ -1414,9 +1464,10 @@ func TestLogAnalyticsCreateRequest(t *testing.T) { Query: "Perf", QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), AppInsightsQuery: false, + IntersectTime: false, }) require.NoError(t, err) - expectedBody := `{"query":"Perf","timespan":"0001-01-01T00:00:00Z/0001-01-01T00:00:00Z","workspaces":["/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.OperationalInsights/workspaces/r1","/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.OperationalInsights/workspaces/r2"]}` + expectedBody := `{"query":"Perf","workspaces":["/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.OperationalInsights/workspaces/r1","/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.OperationalInsights/workspaces/r2"]}` body, err := io.ReadAll(req.Body) require.NoError(t, err) if !cmp.Equal(string(body), expectedBody) { @@ -1437,6 +1488,7 @@ func TestLogAnalyticsCreateRequest(t *testing.T) { To: to, }, AppInsightsQuery: false, + IntersectTime: true, }) require.NoError(t, err) expectedBody := fmt.Sprintf(`{"query":"Perf","timespan":"%s/%s","workspaces":["/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.OperationalInsights/workspaces/r1","/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.OperationalInsights/workspaces/r2"]}`, from.Format(time.RFC3339), to.Format(time.RFC3339)) @@ -1459,6 +1511,7 @@ func TestLogAnalyticsCreateRequest(t *testing.T) { To: to, }, AppInsightsQuery: true, + IntersectTime: true, }) require.NoError(t, err) expectedBody := fmt.Sprintf(`{"applications":["/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1","/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r2"],"query":"","timespan":"%s/%s"}`, from.Format(time.RFC3339), to.Format(time.RFC3339)) diff --git a/public/app/plugins/datasource/azuremonitor/__mocks__/query.ts b/public/app/plugins/datasource/azuremonitor/__mocks__/query.ts index b5f19425316..7b194c48179 100644 --- a/public/app/plugins/datasource/azuremonitor/__mocks__/query.ts +++ b/public/app/plugins/datasource/azuremonitor/__mocks__/query.ts @@ -18,6 +18,7 @@ export default function createMockQuery(overrides?: Partial): resultFormat: ResultFormat.Table, workspace: 'e3fe4fde-ad5e-4d60-9974-e2f3562ffdf2', resources: ['test-resource'], + intersectTime: false, ...overrides?.azureLogAnalytics, }, 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 23ff4921b92..ed6092bcbe6 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 @@ -131,6 +131,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< resources, // Workspace was removed in Grafana 8, but remains for backwards compat workspace, + intersectTime: target.azureLogAnalytics.intersectTime, }, }; } 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 0dc882bf9fa..cb8d96001cf 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.test.tsx @@ -184,4 +184,31 @@ describe('LogsQueryEditor', () => { }) ); }); + + it('should update the intersectTime prop', async () => { + const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); + const query = createMockQuery(); + const onChange = jest.fn(); + + render( + {}} + /> + ); + + const intersectionOption = await screen.findByLabelText('Intersection'); + await userEvent.click(intersectionOption); + + expect(onChange).toBeCalledWith( + expect.objectContaining({ + azureLogAnalytics: expect.objectContaining({ + intersectTime: true, + }), + }) + ); + }); }); diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx index f967a0d8daa..601ce14da8e 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/experimental'; -import { Alert } from '@grafana/ui'; +import { Alert, InlineField, RadioButtonGroup } from '@grafana/ui'; import Datasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; @@ -13,7 +13,7 @@ import { parseResourceDetails } from '../ResourcePicker/utils'; import AdvancedResourcePicker from './AdvancedResourcePicker'; import QueryField from './QueryField'; -import { setFormatAs } from './setQueryValue'; +import { setFormatAs, setIntersectTime } from './setQueryValue'; import useMigrations from './useMigrations'; interface LogsQueryEditorProps { @@ -81,6 +81,22 @@ const LogsQueryEditor = ({ )} selectionNotice={() => 'You may only choose items of the same resource type.'} /> + + onChange(setIntersectTime(query, val))} + /> + = { * Azure Monitor Logs sub-query properties */ export interface AzureLogsQuery { + /** + * If set to true the intersection of time ranges specified in the query and Grafana will be used. Otherwise the query time ranges will be used. Defaults to false + */ + intersectTime?: boolean; /** * KQL query to be executed. */ diff --git a/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.test.ts b/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.test.ts index ed420e939c4..6b4bbe72f63 100644 --- a/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.test.ts +++ b/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.test.ts @@ -12,6 +12,11 @@ const azureMonitorQueryV8 = { resourceName: 'AppInsightsTestData', timeGrain: 'auto', }, + azureLogAnalytics: { + query: + '//change this example to create your own time series query\n //the table to query (e.g. Usage, Heartbeat, Perf)\n| where $__timeFilter(TimeGenerated) //this is a macro used to show the full chart’s time range, choose the datetime column here\n| summarize count() by , bin(TimeGenerated, $__interval) //change “group by column” to a column in your table, such as “Computer”. The $__interval macro is used to auto-select the time grain. Can also use 1h, 5m etc.\n| order by TimeGenerated asc', + resultFormat: ResultFormat.TimeSeries, + }, datasource: { type: 'grafana-azure-monitor-datasource', uid: 'sD-ZuB87k', @@ -33,6 +38,11 @@ const azureMonitorQueryV9_0 = { '/subscriptions/44693801-6ee6-49de-9b2d-9106972f9572/resourceGroups/cloud-datasources/providers/microsoft.insights/components/AppInsightsTestData', timeGrain: 'auto', }, + azureLogAnalytics: { + query: + '//change this example to create your own time series query\n
//the table to query (e.g. Usage, Heartbeat, Perf)\n| where $__timeFilter(TimeGenerated) //this is a macro used to show the full chart’s time range, choose the datetime column here\n| summarize count() by , bin(TimeGenerated, $__interval) //change “group by column” to a column in your table, such as “Computer”. The $__interval macro is used to auto-select the time grain. Can also use 1h, 5m etc.\n| order by TimeGenerated asc', + resultFormat: ResultFormat.TimeSeries, + }, datasource: { type: 'grafana-azure-monitor-datasource', uid: 'sD-ZuB87k', @@ -47,6 +57,7 @@ const modernMetricsQuery: AzureMonitorQuery = { '//change this example to create your own time series query\n
//the table to query (e.g. Usage, Heartbeat, Perf)\n| where $__timeFilter(TimeGenerated) //this is a macro used to show the full chart’s time range, choose the datetime column here\n| summarize count() by , bin(TimeGenerated, $__interval) //change “group by column” to a column in your table, such as “Computer”. The $__interval macro is used to auto-select the time grain. Can also use 1h, 5m etc.\n| order by TimeGenerated asc', resultFormat: ResultFormat.TimeSeries, workspace: 'mock-workspace-id', + intersectTime: false, }, azureMonitor: { aggregation: 'Average', @@ -190,6 +201,17 @@ describe('AzureMonitor: migrateQuery', () => { }) ); }); + + it('correctly adds the intersectTime property', () => { + const result = migrateQuery({ ...azureMonitorQueryV8 }); + expect(result).toMatchObject( + expect.objectContaining({ + azureLogAnalytics: expect.objectContaining({ + intersectTime: false, + }), + }) + ); + }); }); describe('migrating from a v9.0 query to the latest query version', () => { @@ -219,6 +241,17 @@ describe('AzureMonitor: migrateQuery', () => { expect(result.azureMonitor).not.toHaveProperty('resourceGroup'); expect(result.azureMonitor).not.toHaveProperty('resourceName'); }); + + it('correctly adds the intersectTime property', () => { + const result = migrateQuery({ ...azureMonitorQueryV9_0 }); + expect(result).toMatchObject( + expect.objectContaining({ + azureLogAnalytics: expect.objectContaining({ + intersectTime: false, + }), + }) + ); + }); }); it('should migrate a single resource for Logs', () => { diff --git a/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.ts b/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.ts index 536660a342d..be4cb996cd3 100644 --- a/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.ts +++ b/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.ts @@ -44,6 +44,16 @@ export default function migrateQuery(query: AzureMonitorQuery): AzureMonitorQuer delete workingQuery.azureLogAnalytics?.resource; } + if (workingQuery.azureLogAnalytics && workingQuery.azureLogAnalytics.intersectTime === undefined) { + workingQuery = { + ...workingQuery, + azureLogAnalytics: { + ...workingQuery.azureLogAnalytics, + intersectTime: false, + }, + }; + } + return workingQuery; }