AzureMonitor: Improve Log Analytics query efficiency (#74675)

* Promisify loading schema

- Move schema loading to LogsQueryEditor
- Improve typing
- Switch callbacks to promises

* Update types

* Refactor backend for new props

- Rename intersectTime
- Support setting timeColumn
- Add additional properties to logs request body

* Update applyTemplateVariables

* Update set functions

* Add new TimeManagement component

* Update LogsQueryEditor

* Hardcode timestamp column for traces queries

* Ensure timeColumn is always set for log queries

* Update tests

* Update frontend tests

* Readd type to make migration easier

* Add migration

* Add fake schema

* Use predefined type

* Update checks and defaults

* Add tests

* README updates

* README update

* Type update

* Lint

* More linting and type fixing

* Error silently

* More linting and typing

* Update betterer

* Update test

* Simplify default column setting

* Fix default column setting

* Add tracking

* Review

- Fix typo on comment
- Destructure and remove type assertion
- Break out await into two variables
- Remove lets and rename variable for clarity
This commit is contained in:
Andreas Christou 2023-09-18 18:38:39 +01:00 committed by GitHub
parent 025979df75
commit b779ce5687
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 693 additions and 167 deletions

View File

@ -3163,9 +3163,8 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/QueryField.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"]
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
],
"public/app/plugins/datasource/azuremonitor/components/MonitorConfig.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]

View File

@ -147,10 +147,10 @@ The Azure documentation includes resources to help you learn KQL:
- [SQL to Kusto cheat sheet](https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/sqlcheatsheet)
> **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.
> Specifying `Dashboard` will only make use of the Grafana time-range.
> If there are no time-ranges specified within the query, the default Log Analytics time-range will apply.
> 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.
> If the `Intersection` option was previously chosen it will be migrated by default to `Dashboard`.
This example query returns a virtual machine's CPU performance, averaged over 5ms time grains:

View File

@ -165,7 +165,11 @@ export const defaultAzureMetricQuery: Partial<AzureMetricQuery> = {
*/
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
* 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?: boolean;
/**
* @deprecated Use dashboardTime instead
*/
intersectTime?: boolean;
/**
@ -185,7 +189,11 @@ export interface AzureLogsQuery {
*/
resultFormat?: ResultFormat;
/**
* Workspace ID. This was removed in Grafana 8, but remains for backwards compat
* 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;
/**
* Workspace ID. This was removed in Grafana 8, but remains for backwards compat.
*/
workspace?: string;
}

View File

@ -118,7 +118,10 @@ 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
// 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"`
// @deprecated Use dashboardTime instead
IntersectTime *bool `json:"intersectTime,omitempty"`
// KQL query to be executed.
@ -131,7 +134,10 @@ type AzureLogsQuery struct {
Resources []string `json:"resources,omitempty"`
ResultFormat *ResultFormat `json:"resultFormat,omitempty"`
// Workspace ID. This was removed in Grafana 8, but remains for backwards compat
// 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 `json:"timeColumn,omitempty"`
// Workspace ID. This was removed in Grafana 8, but remains for backwards compat.
Workspace *string `json:"workspace,omitempty"`
}

View File

@ -47,7 +47,8 @@ type AzureLogAnalyticsQuery struct {
Resources []string
QueryType string
AppInsightsQuery bool
IntersectTime bool
DashboardTime bool
TimeColumn string
}
func (e *AzureLogAnalyticsDatasource) ResourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) (http.ResponseWriter, error) {
@ -107,7 +108,8 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, queries
traceExploreQuery := ""
traceParentExploreQuery := ""
traceLogsExploreQuery := ""
intersectTime := false
dashboardTime := false
timeColumn := ""
if query.QueryType == string(dataquery.AzureQueryTypeAzureLogAnalytics) {
queryJSONModel := types.LogJSONQuery{}
err := json.Unmarshal(query.JSON, &queryJSONModel)
@ -143,8 +145,16 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, queries
queryString = *azureLogAnalyticsTarget.Query
}
if azureLogAnalyticsTarget.IntersectTime != nil {
intersectTime = *azureLogAnalyticsTarget.IntersectTime
if azureLogAnalyticsTarget.DashboardTime != nil {
dashboardTime = *azureLogAnalyticsTarget.DashboardTime
if dashboardTime {
if azureLogAnalyticsTarget.TimeColumn != nil {
timeColumn = *azureLogAnalyticsTarget.TimeColumn
} else {
// Final fallback to TimeGenerated if no column is provided
timeColumn = "TimeGenerated"
}
}
}
}
@ -218,7 +228,8 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, queries
return nil, fmt.Errorf("failed to create traces logs explore query: %s", err)
}
intersectTime = true
dashboardTime = true
timeColumn = "timestamp"
}
apiURL := getApiURL(resourceOrWorkspace, appInsightsQuery)
@ -241,7 +252,8 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, queries
TraceParentExploreQuery: traceParentExploreQuery,
TraceLogsExploreQuery: traceLogsExploreQuery,
AppInsightsQuery: appInsightsQuery,
IntersectTime: intersectTime,
DashboardTime: dashboardTime,
TimeColumn: timeColumn,
})
}
@ -432,11 +444,14 @@ func (e *AzureLogAnalyticsDatasource) createRequest(ctx context.Context, queryUR
"query": query.Query,
}
if query.IntersectTime {
if query.DashboardTime {
from := query.TimeRange.From.Format(time.RFC3339)
to := query.TimeRange.To.Format(time.RFC3339)
timespan := fmt.Sprintf("%s/%s", from, to)
body["timespan"] = timespan
body["query_datetimescope_from"] = from
body["query_datetimescope_to"] = to
body["query_datetimescope_column"] = query.TimeColumn
}
if len(query.Resources) > 1 && query.QueryType == string(dataquery.AzureQueryTypeAzureLogAnalytics) && !query.AppInsightsQuery {

View File

@ -107,7 +107,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) {
"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",
"intersectTime": false
"dashboardTime": false
}
}`, types.TimeSeries)),
RefID: "A",
@ -126,7 +126,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) {
"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",
"intersectTime": false
"dashboardTime": 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",
@ -134,7 +134,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) {
TimeRange: timeRange,
QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics),
AppInsightsQuery: false,
IntersectTime: false,
DashboardTime: false,
},
},
Err: require.NoError,
@ -172,7 +172,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) {
Resources: []string{},
QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics),
AppInsightsQuery: false,
IntersectTime: false,
DashboardTime: false,
},
},
Err: require.NoError,
@ -210,7 +210,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) {
Resources: []string{},
QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics),
AppInsightsQuery: false,
IntersectTime: false,
DashboardTime: false,
},
},
Err: require.NoError,
@ -225,7 +225,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) {
"resource": "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace",
"query": "Perf",
"resultFormat": "%s",
"intersectTime": false
"dashboardTime": false
}
}`, types.TimeSeries)),
RefID: "A",
@ -243,14 +243,14 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) {
"resource": "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace",
"query": "Perf",
"resultFormat": "%s",
"intersectTime": false
"dashboardTime": 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,
DashboardTime: false,
},
},
Err: require.NoError,
@ -265,7 +265,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) {
"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",
"intersectTime": false
"dashboardTime": false
}
}`, types.TimeSeries)),
RefID: "A",
@ -284,7 +284,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) {
"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",
"intersectTime": false
"dashboardTime": false
}
}`, types.TimeSeries)),
Query: "Perf",
@ -292,7 +292,52 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) {
TimeRange: timeRange,
QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics),
AppInsightsQuery: false,
IntersectTime: false,
DashboardTime: false,
},
},
Err: require.NoError,
},
{
name: "Query that uses dashboard time",
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/AppInsightsTestDataWorkspace"],
"query": "Perf",
"resultFormat": "%s",
"dashboardTime": true,
"timeColumn": "TimeGenerated"
}
}`, types.TimeSeries)),
RefID: "A",
TimeRange: timeRange,
QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics),
},
},
azureLogAnalyticsQueries: []*AzureLogAnalyticsQuery{
{
RefID: "A",
ResultFormat: types.TimeSeries,
URL: "v1/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace/query",
JSON: []byte(fmt.Sprintf(`{
"queryType": "Azure Log Analytics",
"azureLogAnalytics": {
"resources": ["/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace"],
"query": "Perf",
"resultFormat": "%s",
"dashboardTime": true,
"timeColumn": "TimeGenerated"
}
}`, types.TimeSeries)),
Query: "Perf",
Resources: []string{"/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace"},
TimeRange: timeRange,
QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics),
AppInsightsQuery: false,
DashboardTime: true,
TimeColumn: "TimeGenerated",
},
},
Err: require.NoError,
@ -373,7 +418,8 @@ 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,
DashboardTime: true,
TimeColumn: "timestamp",
},
},
Err: require.NoError,
@ -451,7 +497,8 @@ 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,
DashboardTime: true,
TimeColumn: "timestamp",
},
},
Err: require.NoError,
@ -526,7 +573,8 @@ 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,
DashboardTime: true,
TimeColumn: "timestamp",
},
},
Err: require.NoError,
@ -604,7 +652,8 @@ 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,
DashboardTime: true,
TimeColumn: "timestamp",
},
},
Err: require.NoError,
@ -687,7 +736,8 @@ 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,
DashboardTime: true,
TimeColumn: "timestamp",
},
},
Err: require.NoError,
@ -770,7 +820,8 @@ 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,
DashboardTime: true,
TimeColumn: "timestamp",
},
},
Err: require.NoError,
@ -853,7 +904,8 @@ 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,
DashboardTime: true,
TimeColumn: "timestamp",
},
},
Err: require.NoError,
@ -928,7 +980,8 @@ 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,
DashboardTime: true,
TimeColumn: "timestamp",
},
},
Err: require.NoError,
@ -1006,7 +1059,8 @@ 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,
DashboardTime: true,
TimeColumn: "timestamp",
},
},
Err: require.NoError,
@ -1052,7 +1106,8 @@ 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,
DashboardTime: true,
TimeColumn: "timestamp",
},
},
Err: require.NoError,
@ -1134,7 +1189,8 @@ 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,
DashboardTime: true,
TimeColumn: "timestamp",
},
},
Err: require.NoError,
@ -1213,7 +1269,8 @@ 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,
DashboardTime: true,
TimeColumn: "timestamp",
},
},
Err: require.NoError,
@ -1295,7 +1352,8 @@ 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,
DashboardTime: true,
TimeColumn: "timestamp",
},
},
Err: require.NoError,
@ -1384,7 +1442,8 @@ 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,
DashboardTime: true,
TimeColumn: "timestamp",
},
},
Err: require.NoError,
@ -1411,7 +1470,7 @@ func TestLogAnalyticsCreateRequest(t *testing.T) {
req, err := ds.createRequest(ctx, url, &AzureLogAnalyticsQuery{
Resources: []string{"r"},
Query: "Perf",
IntersectTime: false,
DashboardTime: false,
AppInsightsQuery: false,
})
require.NoError(t, err)
@ -1430,30 +1489,6 @@ func TestLogAnalyticsCreateRequest(t *testing.T) {
}
})
t.Run("creates a request with timespan", func(t *testing.T) {
ds := AzureLogAnalyticsDatasource{}
req, err := ds.createRequest(ctx, url, &AzureLogAnalyticsQuery{
Resources: []string{"r"},
Query: "Perf",
IntersectTime: true,
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","timespan":"0001-01-01T00:00:00Z/0001-01-01T00:00:00Z"}`
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 multiple resources", func(t *testing.T) {
ds := AzureLogAnalyticsDatasource{}
req, err := ds.createRequest(ctx, url, &AzureLogAnalyticsQuery{
@ -1461,7 +1496,7 @@ func TestLogAnalyticsCreateRequest(t *testing.T) {
Query: "Perf",
QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics),
AppInsightsQuery: false,
IntersectTime: false,
DashboardTime: false,
})
require.NoError(t, err)
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"]}`
@ -1472,7 +1507,7 @@ func TestLogAnalyticsCreateRequest(t *testing.T) {
}
})
t.Run("creates a request with timerange from query", func(t *testing.T) {
t.Run("creates a request with timerange from dashboard", func(t *testing.T) {
ds := AzureLogAnalyticsDatasource{}
from := time.Now()
to := from.Add(3 * time.Hour)
@ -1485,10 +1520,11 @@ func TestLogAnalyticsCreateRequest(t *testing.T) {
To: to,
},
AppInsightsQuery: false,
IntersectTime: true,
DashboardTime: true,
TimeColumn: "TimeGenerated",
})
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))
expectedBody := fmt.Sprintf(`{"query":"Perf","query_datetimescope_column":"TimeGenerated","query_datetimescope_from":"%s","query_datetimescope_to":"%s","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), from.Format(time.RFC3339), to.Format(time.RFC3339))
body, err := io.ReadAll(req.Body)
require.NoError(t, err)
if !cmp.Equal(string(body), expectedBody) {
@ -1508,10 +1544,11 @@ func TestLogAnalyticsCreateRequest(t *testing.T) {
To: to,
},
AppInsightsQuery: true,
IntersectTime: true,
DashboardTime: true,
TimeColumn: "timestamp",
})
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))
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":"","query_datetimescope_column":"timestamp","query_datetimescope_from":"%s","query_datetimescope_to":"%s","timespan":"%s/%s"}`, from.Format(time.RFC3339), to.Format(time.RFC3339), from.Format(time.RFC3339), to.Format(time.RFC3339))
body, err := io.ReadAll(req.Body)
require.NoError(t, err)
if !cmp.Equal(string(body), expectedBody) {

View File

@ -18,7 +18,8 @@ export default function createMockQuery(overrides?: Partial<AzureMonitorQuery>):
resultFormat: ResultFormat.Table,
workspace: 'e3fe4fde-ad5e-4d60-9974-e2f3562ffdf2',
resources: ['test-resource'],
intersectTime: false,
dashboardTime: false,
timeColumn: 'TimeGenerated',
...overrides?.azureLogAnalytics,
},

View File

@ -1,3 +1,5 @@
import { AzureLogAnalyticsMetadataTable, EngineSchema } from '../../types';
export default class FakeSchemaData {
static getLogAnalyticsFakeSchema() {
return {
@ -317,4 +319,68 @@ export default class FakeSchemaData {
],
};
}
static getLogAnalyticsFakeEngineSchema(tableOverride?: AzureLogAnalyticsMetadataTable[]): EngineSchema {
const database = {
name: 'test',
tables: tableOverride ?? [
{
id: 't/Alert',
name: 'Alert',
timespanColumn: 'TimeGenerated',
columns: [
{ name: 'TimeGenerated', type: 'datetime' },
{ name: 'AlertSeverity', type: 'string' },
{ name: 'SourceDisplayName', type: 'string' },
{ name: 'AlertName', type: 'string' },
{ name: 'AlertDescription', type: 'string' },
{ name: 'SourceSystem', type: 'string' },
{ name: 'QueryExecutionStartTime', type: 'datetime' },
{ name: 'QueryExecutionEndTime', type: 'datetime' },
{ name: 'Query', type: 'string' },
{ name: 'RemediationJobId', type: 'string' },
{ name: 'RemediationRunbookName', type: 'string' },
{ name: 'AlertRuleId', type: 'string' },
{ name: 'AlertRuleInstanceId', type: 'string' },
{ name: 'ThresholdOperator', type: 'string' },
{ name: 'ThresholdValue', type: 'int' },
{ name: 'LinkToSearchResults', type: 'string' },
{ name: 'ServiceDeskConnectionName', type: 'string' },
{ name: 'ServiceDeskId', type: 'string' },
{ name: 'ServiceDeskWorkItemLink', type: 'string' },
{ name: 'ServiceDeskWorkItemType', type: 'string' },
{ name: 'ResourceId', type: 'string' },
{ name: 'ResourceType', type: 'string' },
{ name: 'ResourceValue', type: 'string' },
{ name: 'RootObjectName', type: 'string' },
{ name: 'ObjectDisplayName', type: 'string' },
{ name: 'Computer', type: 'string' },
{ name: 'AlertPriority', type: 'string' },
{ name: 'SourceFullName', type: 'string' },
{ name: 'AlertId', type: 'string' },
{ name: 'RepeatCount', type: 'int' },
{ name: 'AlertState', type: 'string' },
{ name: 'ResolvedBy', type: 'string' },
{ name: 'LastModifiedBy', type: 'string' },
{ name: 'TimeRaised', type: 'datetime' },
{ name: 'TimeResolved', type: 'datetime' },
{ name: 'TimeLastModified', type: 'datetime' },
],
related: { solutions: [] },
},
],
functions: [],
majorVersion: 0,
minorVersion: 0,
};
return {
clusterType: 'Engine',
cluster: {
connectionString: 'test',
databases: [database],
},
database: database,
globalScalarParameters: [],
};
}
}

View File

@ -40,15 +40,15 @@ describe('AzureLogAnalyticsDatasource', () => {
});
it('should return a schema to use with monaco-kusto', async () => {
const result = await ctx.datasource.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace');
const { database } = await ctx.datasource.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace');
expect(result.database.tables).toHaveLength(2);
expect(result.database.tables[0].name).toBe('Alert');
expect(result.database.tables[0].timespanColumn).toBe('TimeGenerated');
expect(result.database.tables[1].name).toBe('AzureActivity');
expect(result.database.tables[0].columns).toHaveLength(69);
expect(database?.tables).toHaveLength(2);
expect(database?.tables[0].name).toBe('Alert');
expect(database?.tables[0].timespanColumn).toBe('TimeGenerated');
expect(database?.tables[1].name).toBe('AzureActivity');
expect(database?.tables[0].columns).toHaveLength(69);
expect(result.database.functions[1].inputParameters).toEqual([
expect(database?.functions[1].inputParameters).toEqual([
{
name: 'RangeStart',
type: 'datetime',
@ -77,7 +77,7 @@ describe('AzureLogAnalyticsDatasource', () => {
it('should include macros as suggested functions', async () => {
const result = await ctx.datasource.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace');
expect(result.database.functions.map((f: { name: string }) => f.name)).toEqual([
expect(result.database?.functions.map((f: { name: string }) => f.name)).toEqual([
'Func1',
'_AzureBackup_GetVaults',
'$__timeFilter',
@ -90,7 +90,7 @@ describe('AzureLogAnalyticsDatasource', () => {
it('should include template variables as global parameters', async () => {
const result = await ctx.datasource.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace');
expect(result.globalParameters.map((f: { name: string }) => f.name)).toEqual([`$${singleVariable.name}`]);
expect(result.globalScalarParameters?.map((f: { name: string }) => f.name)).toEqual([`$${singleVariable.name}`]);
});
});

View File

@ -131,7 +131,8 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
resources,
// Workspace was removed in Grafana 8, but remains for backwards compat
workspace,
intersectTime: target.azureLogAnalytics.intersectTime,
dashboardTime: item.dashboardTime,
timeColumn: templateSrv.replace(item.timeColumn, scopedVars),
},
};
}

View File

@ -1,5 +1,6 @@
import { VariableModel } from '@grafana/data';
import { EngineSchema } from '../types';
import { AzureLogAnalyticsMetadata } from '../types/logAnalyticsMetadata';
// matches (name):(type) = (defaultValue)
@ -48,7 +49,7 @@ export function transformMetadataToKustoSchema(
sourceSchema: AzureLogAnalyticsMetadata,
nameOrIdOrSomething: string,
templateVariables: VariableModel[]
) {
): EngineSchema {
const database = {
name: nameOrIdOrSomething,
tables: sourceSchema.tables,
@ -114,7 +115,7 @@ export function transformMetadataToKustoSchema(
);
// Adding macros as global parameters
const globalParameters = templateVariables.map((v) => {
const globalScalarParameters = templateVariables.map((v) => {
return {
name: `$${v.name}`,
type: 'dynamic',
@ -128,6 +129,6 @@ export function transformMetadataToKustoSchema(
databases: [database],
},
database: database,
globalParameters,
globalScalarParameters,
};
}

View File

@ -185,7 +185,7 @@ describe('LogsQueryEditor', () => {
);
});
it('should update the intersectTime prop', async () => {
it('should update the dashboardTime prop', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() });
const query = createMockQuery();
const onChange = jest.fn();
@ -200,13 +200,13 @@ describe('LogsQueryEditor', () => {
/>
);
const intersectionOption = await screen.findByLabelText('Intersection');
await userEvent.click(intersectionOption);
const dashboardTimeOption = await screen.findByLabelText('Dashboard');
await userEvent.click(dashboardTimeOption);
expect(onChange).toBeCalledWith(
expect.objectContaining({
azureLogAnalytics: expect.objectContaining({
intersectTime: true,
dashboardTime: true,
}),
})
);

View File

@ -1,11 +1,11 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/experimental';
import { Alert, InlineField, RadioButtonGroup } from '@grafana/ui';
import { Alert } from '@grafana/ui';
import Datasource from '../../datasource';
import { selectors } from '../../e2e/selectors';
import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery, ResultFormat } from '../../types';
import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery, ResultFormat, EngineSchema } from '../../types';
import FormatAsField from '../FormatAsField';
import ResourceField from '../ResourceField';
import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../ResourcePicker/types';
@ -13,7 +13,8 @@ import { parseResourceDetails } from '../ResourcePicker/utils';
import AdvancedResourcePicker from './AdvancedResourcePicker';
import QueryField from './QueryField';
import { setFormatAs, setIntersectTime } from './setQueryValue';
import { TimeManagement } from './TimeManagement';
import { setFormatAs } from './setQueryValue';
import useMigrations from './useMigrations';
interface LogsQueryEditorProps {
@ -49,6 +50,15 @@ const LogsQueryEditor = ({
// Only resources with the same metricNamespace can be selected
return rowResourceNS !== selectedRowSampleNs;
};
const [schema, setSchema] = useState<EngineSchema | undefined>();
useEffect(() => {
if (query.azureLogAnalytics?.resources && query.azureLogAnalytics.resources.length) {
datasource.azureLogAnalyticsDatasource.getKustoSchema(query.azureLogAnalytics.resources[0]).then((schema) => {
setSchema(schema);
});
}
}, [query.azureLogAnalytics?.resources, datasource.azureLogAnalyticsDatasource]);
return (
<span data-testid={selectors.components.queryEditor.logsQueryEditor.container.input}>
@ -81,22 +91,14 @@ const LogsQueryEditor = ({
)}
selectionNotice={() => 'You may only choose items of the same resource type.'}
/>
<InlineField
label="Time-range"
tooltip={
'Specifies the time-range used to query. The query option will only use time-ranges specified in the query. Intersection will combine query time-ranges with the Grafana time-range.'
}
>
<RadioButtonGroup
options={[
{ label: 'Query', value: false },
{ label: 'Intersection', value: true },
]}
value={query.azureLogAnalytics?.intersectTime ?? false}
size={'md'}
onChange={(val) => onChange(setIntersectTime(query, val))}
<TimeManagement
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
schema={schema}
/>
</InlineField>
</EditorFieldGroup>
</EditorRow>
<QueryField
@ -106,6 +108,7 @@ const LogsQueryEditor = ({
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
schema={schema}
/>
<EditorRow>
<EditorFieldGroup>

View File

@ -1,14 +1,14 @@
import { EngineSchema, Schema } from '@kusto/monaco-kusto';
import { Uri } from 'monaco-editor';
import React, { useCallback, useEffect, useRef } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { CodeEditor, Monaco, MonacoEditor } from '@grafana/ui';
import { Deferred } from 'app/core/utils/deferred';
import { AzureQueryEditorFieldProps } from '../../types';
import { setKustoQuery } from './setQueryValue';
interface MonacoPromise {
interface MonacoEditorValues {
editor: MonacoEditor;
monaco: Monaco;
}
@ -17,50 +17,39 @@ interface MonacoLanguages {
kusto: {
getKustoWorker: () => Promise<
(url: Uri) => Promise<{
setSchema: (schema: any, clusterUrl: string, name: string) => void;
setSchema: (schema: Schema) => void;
}>
>;
};
}
const QueryField = ({ query, datasource, onQueryChange }: AzureQueryEditorFieldProps) => {
const monacoPromiseRef = useRef<Deferred<MonacoPromise>>();
function getPromise() {
if (!monacoPromiseRef.current) {
monacoPromiseRef.current = new Deferred<MonacoPromise>();
}
return monacoPromiseRef.current.promise;
}
const QueryField = ({ query, onQueryChange, schema }: AzureQueryEditorFieldProps) => {
const [monaco, setMonaco] = useState<MonacoEditorValues | undefined>();
useEffect(() => {
if (!query.azureLogAnalytics?.resources || !query.azureLogAnalytics.resources.length) {
if (!schema || !monaco) {
return;
}
const promises = [
datasource.azureLogAnalyticsDatasource.getKustoSchema(query.azureLogAnalytics.resources[0]),
getPromise(),
] as const;
// the kusto schema call might fail, but it's okay for that to happen silently
Promise.all(promises).then(([schema, { monaco, editor }]) => {
const setupEditor = async ({ monaco, editor }: MonacoEditorValues, schema: EngineSchema) => {
try {
const languages = monaco.languages as unknown as MonacoLanguages;
languages.kusto
.getKustoWorker()
.then((kusto) => {
const model = editor.getModel();
return model && kusto(model.uri);
})
.then((worker) => {
worker?.setSchema(schema, 'https://help.kusto.windows.net', 'Samples');
});
});
}, [datasource.azureLogAnalyticsDatasource, query.azureLogAnalytics?.resources]);
if (model) {
const kustoWorker = await languages.kusto.getKustoWorker();
const kustoMode = await kustoWorker(model?.uri);
await kustoMode.setSchema(schema);
}
} catch (err) {
console.error(err);
}
};
setupEditor(monaco, schema).catch((err) => console.error(err));
}, [schema, monaco]);
const handleEditorMount = useCallback((editor: MonacoEditor, monaco: Monaco) => {
monacoPromiseRef.current?.resolve?.({ editor, monaco });
setMonaco({ monaco, editor });
}, []);
const onChange = useCallback(

View File

@ -0,0 +1,172 @@
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 FakeSchemaData from '../../azure_log_analytics/__mocks__/schema';
import { TimeManagement } from './TimeManagement';
const variableOptionGroup = {
label: 'Template variables',
options: [],
};
describe('LogsQueryEditor.TimeManagement', () => {
it('should render the column picker if Dashboard is chosen', async () => {
const mockDatasource = createMockDatasource();
const query = createMockQuery({ azureLogAnalytics: { timeColumn: undefined } });
const onChange = jest.fn();
const { rerender } = render(
<TimeManagement
query={query}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={() => {}}
schema={FakeSchemaData.getLogAnalyticsFakeEngineSchema()}
/>
);
const dashboardTimeOption = await screen.findByLabelText('Dashboard');
await userEvent.click(dashboardTimeOption);
expect(onChange).toBeCalledWith(
expect.objectContaining({
azureLogAnalytics: expect.objectContaining({
dashboardTime: true,
}),
})
);
rerender(
<TimeManagement
query={{ ...query, azureLogAnalytics: { ...query.azureLogAnalytics, dashboardTime: true } }}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={() => {}}
schema={FakeSchemaData.getLogAnalyticsFakeEngineSchema()}
/>
);
expect(onChange).toBeCalledWith(
expect.objectContaining({
azureLogAnalytics: expect.objectContaining({
timeColumn: 'TimeGenerated',
}),
})
);
});
it('should render the default value if no time columns exist', async () => {
const mockDatasource = createMockDatasource();
const query = createMockQuery();
const onChange = jest.fn();
render(
<TimeManagement
query={{
...query,
azureLogAnalytics: { ...query.azureLogAnalytics, dashboardTime: true, timeColumn: undefined },
}}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={() => {}}
schema={FakeSchemaData.getLogAnalyticsFakeEngineSchema([
{
id: 't/Alert',
name: 'Alert',
timespanColumn: 'TimeGenerated',
columns: [],
related: {
solutions: [],
},
},
])}
/>
);
expect(onChange).toBeCalledWith(
expect.objectContaining({
azureLogAnalytics: expect.objectContaining({
timeColumn: 'TimeGenerated',
}),
})
);
});
it('should render the first time column if no default exists', async () => {
const mockDatasource = createMockDatasource();
const query = createMockQuery();
const onChange = jest.fn();
render(
<TimeManagement
query={{
...query,
azureLogAnalytics: { ...query.azureLogAnalytics, dashboardTime: true, timeColumn: undefined },
}}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={() => {}}
schema={FakeSchemaData.getLogAnalyticsFakeEngineSchema([
{
id: 't/Alert',
name: 'Alert',
timespanColumn: '',
columns: [{ name: 'Timespan', type: 'datetime' }],
related: {
solutions: [],
},
},
])}
/>
);
expect(onChange).toBeCalledWith(
expect.objectContaining({
azureLogAnalytics: expect.objectContaining({
timeColumn: 'Timespan',
}),
})
);
});
it('should render the query time column if it exists', async () => {
const mockDatasource = createMockDatasource();
const query = createMockQuery();
const onChange = jest.fn();
render(
<TimeManagement
query={{
...query,
azureLogAnalytics: { ...query.azureLogAnalytics, dashboardTime: true, timeColumn: 'TestTimeColumn' },
}}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={() => {}}
schema={FakeSchemaData.getLogAnalyticsFakeEngineSchema([
{
id: 't/Alert',
name: 'Alert',
timespanColumn: '',
columns: [{ name: 'TestTimeColumn', type: 'datetime' }],
related: {
solutions: [],
},
},
])}
/>
);
expect(onChange).not.toBeCalled();
expect(screen.getByText('Alert > TestTimeColumn')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,137 @@
import React, { useCallback, useEffect, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { InlineField, RadioButtonGroup, Select } from '@grafana/ui';
import { AzureQueryEditorFieldProps } from '../../types';
import { setDashboardTime, setTimeColumn } from './setQueryValue';
export function TimeManagement({ query, onQueryChange: onChange, schema }: AzureQueryEditorFieldProps) {
const [defaultTimeColumns, setDefaultTimeColumns] = useState<SelectableValue[] | undefined>();
const [timeColumns, setTimeColumns] = useState<SelectableValue[] | undefined>();
const setDefaultColumn = useCallback((column: string) => onChange(setTimeColumn(query, column)), [query, onChange]);
useEffect(() => {
if (schema && query.azureLogAnalytics?.dashboardTime) {
const timeColumnOptions: SelectableValue[] = [];
const timeColumnsSet: Set<string> = new Set();
const defaultColumnsMap: Map<string, SelectableValue> = new Map();
const db = schema.database;
if (db) {
for (const table of db.tables) {
const cols = table.columns.reduce<SelectableValue[]>((prev, curr, i) => {
if (curr.type === 'datetime') {
if (!table.timespanColumn || table.timespanColumn !== curr.name) {
prev.push({ value: curr.name, label: `${table.name} > ${curr.name}` });
timeColumnsSet.add(curr.name);
}
}
return prev;
}, []);
timeColumnOptions.push(...cols);
if (table.timespanColumn && !defaultColumnsMap.has(table.timespanColumn)) {
defaultColumnsMap.set(table.timespanColumn, {
value: table.timespanColumn,
label: table.timespanColumn,
});
}
}
}
setTimeColumns(timeColumnOptions);
const defaultColumns = Array.from(defaultColumnsMap.values());
setDefaultTimeColumns(defaultColumns);
// Set default value
if (
!query.azureLogAnalytics.timeColumn ||
(query.azureLogAnalytics.timeColumn &&
!timeColumnsSet.has(query.azureLogAnalytics.timeColumn) &&
!defaultColumnsMap.has(query.azureLogAnalytics.timeColumn))
) {
if (defaultColumns && defaultColumns.length) {
setDefaultColumn(defaultColumns[0].value);
setDefaultColumn(defaultColumns[0].value);
return;
} else if (timeColumnOptions && timeColumnOptions.length) {
setDefaultColumn(timeColumnOptions[0].value);
return;
} else {
setDefaultColumn('TimeGenerated');
return;
}
}
}
}, [schema, query.azureLogAnalytics?.dashboardTime, query.azureLogAnalytics?.timeColumn, setDefaultColumn]);
const handleTimeColumnChange = useCallback(
(change: SelectableValue<string>) => {
if (!change.value) {
return;
}
const newQuery = setTimeColumn(query, change.value);
onChange(newQuery);
},
[onChange, query]
);
return (
<>
<InlineField
label="Time-range"
tooltip={
<span>
Specifies the time-range used to query. The <code>Query</code> option will only use time-ranges specified in
the query. <code>Dashboard</code> will only use the Grafana time-range.
</span>
}
>
<RadioButtonGroup
options={[
{ label: 'Query', value: false },
{ label: 'Dashboard', value: true },
]}
value={query.azureLogAnalytics?.dashboardTime ?? false}
size={'md'}
onChange={(val) => onChange(setDashboardTime(query, val))}
/>
</InlineField>
{query.azureLogAnalytics?.dashboardTime && (
<InlineField
label="Time Column"
tooltip={
<span>
Specifies the time column used for filtering. Defaults to the first tables <code>timeSpan</code> column,
the first <code>datetime</code> column found or <code>TimeGenerated</code>.
</span>
}
>
<Select
options={[
{
label: 'Default time columns',
options: defaultTimeColumns ?? [{ value: 'TimeGenerated', label: 'TimeGenerated' }],
},
{
label: 'Other time columns',
options: timeColumns ?? [],
},
]}
onChange={handleTimeColumnChange}
value={
query.azureLogAnalytics?.timeColumn
? query.azureLogAnalytics?.timeColumn
: defaultTimeColumns
? defaultTimeColumns[0]
: timeColumns
? timeColumns[0]
: { value: 'TimeGenerated', label: 'TimeGenerated' }
}
allowCustomValue
/>
</InlineField>
)}
</>
);
}

View File

@ -20,12 +20,22 @@ export function setFormatAs(query: AzureMonitorQuery, formatAs: ResultFormat): A
};
}
export function setIntersectTime(query: AzureMonitorQuery, intersectTime: boolean): AzureMonitorQuery {
export function setDashboardTime(query: AzureMonitorQuery, dashboardTime: boolean): AzureMonitorQuery {
return {
...query,
azureLogAnalytics: {
...query.azureLogAnalytics,
intersectTime,
dashboardTime,
},
};
}
export function setTimeColumn(query: AzureMonitorQuery, timeColumn: string): AzureMonitorQuery {
return {
...query,
azureLogAnalytics: {
...query.azureLogAnalytics,
timeColumn,
},
};
}

View File

@ -111,13 +111,17 @@ composableKinds: DataQuery: {
resultFormat?: #ResultFormat
// Array of resource URIs to be queried.
resources?: [...string]
// 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
// Workspace ID. This was removed in Grafana 8, but remains for backwards compat
// 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
// 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
// Workspace ID. This was removed in Grafana 8, but remains for backwards compat.
workspace?: string
// @deprecated Use resources instead
resource?: string
// @deprecated Use dashboardTime instead
intersectTime?: bool
} @cuetsy(kind="interface")
// Application Insights Traces sub-query properties

View File

@ -162,7 +162,11 @@ export const defaultAzureMetricQuery: Partial<AzureMetricQuery> = {
*/
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
* 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?: boolean;
/**
* @deprecated Use dashboardTime instead
*/
intersectTime?: boolean;
/**
@ -182,7 +186,11 @@ export interface AzureLogsQuery {
*/
resultFormat?: ResultFormat;
/**
* Workspace ID. This was removed in Grafana 8, but remains for backwards compat
* 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;
/**
* Workspace ID. This was removed in Grafana 8, but remains for backwards compat.
*/
workspace?: string;
}

View File

@ -57,6 +57,14 @@ jest.mock('@grafana/runtime', () => {
{ queryType: 'Azure Regions' },
{ queryType: 'Grafana Template Variable Function' },
{ queryType: 'unknown' },
{
queryType: 'Azure Log Analytics',
azureLogAnalytics: { dashboardTime: true },
},
{
queryType: 'Azure Log Analytics',
azureLogAnalytics: { dashboardTime: false },
},
] as AzureMonitorQuery[],
},
})
@ -78,10 +86,12 @@ describe('queriesOnInitDashboard', () => {
azure_monitor_multiple_resource: 1,
azure_monitor_query: 2,
azure_log_analytics_queries: 1,
azure_log_analytics_queries: 3,
azure_log_analytics_queries_hidden: 1,
azure_log_analytics_queries_grafana_time: 1,
azure_log_analytics_queries_query_time: 3,
azure_log_multiple_resource: 1,
azure_log_query: 2,
azure_log_query: 4,
azure_resource_graph_queries: 1,
azure_resource_graph_queries_hidden: 1,

View File

@ -37,6 +37,8 @@ getAppEvents().subscribe<DashboardLoadedEvent<AzureMonitorQuery>>(
},
[AzureQueryType.LogAnalytics]: {
...common,
grafanaTime: 0,
queryTime: 0,
},
[AzureQueryType.AzureResourceGraph]: {
...common,
@ -69,6 +71,7 @@ getAppEvents().subscribe<DashboardLoadedEvent<AzureMonitorQuery>>(
}
if (query.queryType === AzureQueryType.LogAnalytics) {
stats[AzureQueryType.LogAnalytics][query.hide ? 'hidden' : 'visible']++;
stats[AzureQueryType.LogAnalytics][query.azureLogAnalytics?.dashboardTime ? 'grafanaTime' : 'queryTime']++;
if (query.azureLogAnalytics?.resources && query.azureLogAnalytics.resources.length > 1) {
stats[AzureQueryType.LogAnalytics].multiResource++;
}
@ -137,6 +140,8 @@ getAppEvents().subscribe<DashboardLoadedEvent<AzureMonitorQuery>>(
azure_log_analytics_queries: stats[AzureQueryType.LogAnalytics].visible,
azure_log_analytics_queries_hidden: stats[AzureQueryType.LogAnalytics].hidden,
azure_log_multiple_resource: stats[AzureQueryType.LogAnalytics].multiResource,
azure_log_analytics_queries_grafana_time: stats[AzureQueryType.LogAnalytics].grafanaTime,
azure_log_analytics_queries_query_time: stats[AzureQueryType.LogAnalytics].queryTime,
azure_log_query: stats[AzureQueryType.LogAnalytics].count,
// ARG queries stats

View File

@ -38,6 +38,10 @@ export type AzureMonitorDashboardLoadedProps = {
azure_log_analytics_queries_hidden: number;
/** number of Azure Log Analytics queries using multiple resources */
azure_log_multiple_resource: number;
/** number of Azure Log Analytics queries using time-range defined explicitly in query */
azure_log_analytics_queries_query_time: number;
/** number of Azure Log Analytics queries using Grafana time-range */
azure_log_analytics_queries_grafana_time: number;
/** number of Azure Log Analytics queries */
azure_log_query: number;

View File

@ -1,3 +1,5 @@
import { ScalarParameter, TabularParameter, Function } from '@kusto/monaco-kusto';
import {
DataSourceInstanceSettings,
DataSourceJsonData,
@ -8,6 +10,7 @@ import {
import Datasource from '../datasource';
import { AzureLogAnalyticsMetadataTable } from './logAnalyticsMetadata';
import { AzureMonitorQuery, ResultFormat } from './query';
export type AzureDataSourceSettings = DataSourceSettings<AzureDataSourceJsonData, AzureDataSourceSecureJsonData>;
@ -131,11 +134,32 @@ export interface AzureQueryEditorFieldProps {
datasource: Datasource;
subscriptionId?: string;
variableOptionGroup: VariableOptionGroup;
schema?: EngineSchema;
onQueryChange: (newQuery: AzureMonitorQuery) => void;
setError: (source: string, error: AzureMonitorErrorish | undefined) => void;
}
// To avoid a type issue we redeclare the EngineSchema type from @kusto/monaco-kusto
export interface EngineSchema {
clusterType: 'Engine';
cluster: {
connectionString: string;
databases: Database[];
};
database: Database | undefined;
globalScalarParameters?: ScalarParameter[];
globalTabularParameters?: TabularParameter[];
}
export interface Database {
name: string;
tables: AzureLogAnalyticsMetadataTable[];
functions: Function[];
majorVersion: number;
minorVersion: number;
}
export interface FormatAsFieldProps extends AzureQueryEditorFieldProps {
inputId: string;
options: Array<SelectableValue<ResultFormat>>;

View File

@ -57,7 +57,7 @@ const modernMetricsQuery: AzureMonitorQuery = {
'//change this example to create your own time series query\n<table name> //the table to query (e.g. Usage, Heartbeat, Perf)\n| where $__timeFilter(TimeGenerated) //this is a macro used to show the full charts time range, choose the datetime column here\n| summarize count() by <group by column>, 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,
dashboardTime: false,
},
azureMonitor: {
aggregation: 'Average',
@ -202,12 +202,12 @@ describe('AzureMonitor: migrateQuery', () => {
);
});
it('correctly adds the intersectTime property', () => {
it('correctly adds the dashboardTime property', () => {
const result = migrateQuery({ ...azureMonitorQueryV8 });
expect(result).toMatchObject(
expect.objectContaining({
azureLogAnalytics: expect.objectContaining({
intersectTime: false,
dashboardTime: false,
}),
})
);
@ -242,12 +242,12 @@ describe('AzureMonitor: migrateQuery', () => {
expect(result.azureMonitor).not.toHaveProperty('resourceName');
});
it('correctly adds the intersectTime property', () => {
it('correctly adds the dashboardTime property', () => {
const result = migrateQuery({ ...azureMonitorQueryV9_0 });
expect(result).toMatchObject(
expect.objectContaining({
azureLogAnalytics: expect.objectContaining({
intersectTime: false,
dashboardTime: false,
}),
})
);
@ -265,4 +265,20 @@ describe('AzureMonitor: migrateQuery', () => {
const result = migrateQuery(q);
expect(result.azureLogAnalytics?.resources).toEqual(['foo']);
});
it('correctly migrates intersectTime to dashboardTime', () => {
const query = modernMetricsQuery;
delete query.azureLogAnalytics?.dashboardTime;
const result = migrateQuery({
...query,
azureLogAnalytics: { ...query.azureLogAnalytics, intersectTime: true },
});
expect(result).toMatchObject(
expect.objectContaining({
azureLogAnalytics: expect.objectContaining({
dashboardTime: true,
}),
})
);
});
});

View File

@ -44,14 +44,24 @@ export default function migrateQuery(query: AzureMonitorQuery): AzureMonitorQuer
delete workingQuery.azureLogAnalytics?.resource;
}
if (workingQuery.azureLogAnalytics && workingQuery.azureLogAnalytics.intersectTime === undefined) {
if (workingQuery.azureLogAnalytics && workingQuery.azureLogAnalytics.dashboardTime === undefined) {
if (workingQuery.azureLogAnalytics.intersectTime) {
workingQuery = {
...workingQuery,
azureLogAnalytics: {
...workingQuery.azureLogAnalytics,
intersectTime: false,
dashboardTime: true,
},
};
} else {
workingQuery = {
...workingQuery,
azureLogAnalytics: {
...workingQuery.azureLogAnalytics,
dashboardTime: false,
},
};
}
}
return workingQuery;