Azure: Basic Logs support (#88025)

* Azure monitor: Basic Logs frontend (#85905)

* adds datasource level config for enabling basic logs

* add basiclogsquery type to query json

* add toggle between basic and analytics

* adds basic logs toggle from UI, blocks time picker to only dashboard if basic logs is selected

* add check to remove UI if alerting

* tests for logsmanagement component

* tests for logs query editor

* tests for time mangement control

* remove unused imports

* clears query whenever toggle changes from basic <-> analytics

* add test to account for clearning query

* Update public/app/plugins/datasource/azuremonitor/components/ConfigEditor/BasicLogsToggle.tsx

wording

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>

* Update public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx

spelling

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>

* Update public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx

spelling

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>

* update dependency list

* clear basic logs if resources change

* fix tests

---------

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>

* Azure Monitor: Basic Logs modal acknowledgement (#86244)

* adds datasource level config for enabling basic logs

* add basiclogsquery type to query json

* add toggle between basic and analytics

* adds basic logs toggle from UI, blocks time picker to only dashboard if basic logs is selected

* add check to remove UI if alerting

* tests for logsmanagement component

* tests for logs query editor

* tests for time mangement control

* remove unused imports

* add confirm modal

* clears query whenever toggle changes from basic <-> analytics

* add test to account for clearning query

* adds modal acknowledgement for basic logs query

* tests for handling modal logic

* basic logs ack type

* Update public/app/plugins/datasource/azuremonitor/components/ConfigEditor/BasicLogsToggle.tsx

wording

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>

* Update public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx

spelling

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>

* Update public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx

spelling

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>

* update dependency list

* clear basic logs if resources change

* remove modal from config page

* remove basic logs query ack type

* add modal acknowledgement to toggle between basic and analytics

* clear query if resources change

* fix tests

* fix tests

* Update public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsManagement.tsx

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>

* fix tests

---------

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>

* Azure Monitor: Basic Logs Backend (#87653)

* fix logic for showingBasicLogsToggle

* move to utils function and add basiclogsquery in apply template variable

* add backend safeguards for basiclogsqueries

* adds support for calling search or query apis based on whether it is basic logs or not

* add tests for utils

* initial test for basic logs query in the backend

* tests for basic logs

* remve comment

* simplify checks for basic logs

* adds fromAlert prop for azure monitor backend services

* adds fromAlert check fo basic logs

* fix working and empty tags

* add telemetry for basic logs

* remove import from grafana core package

* change fromAlert true in tests

* change the way tests catch errors

* Update pkg/tsdb/azuremonitor/loganalytics/utils.go

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>

* Update pkg/tsdb/azuremonitor/loganalytics/utils.go

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>

* Update pkg/tsdb/azuremonitor/loganalytics/utils.go

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>

* Update pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>

* restructure code to only run basic logs checks if basiclogsflag is true

* data retention warning

* tests for calculate time range

* Simplify determining if request is from alerting

* Fix lint and bool check

* Fix tests

* clarify data retention

---------

Co-authored-by: Jocelyn <jcolladokuri@microsoft.com>
Co-authored-by: jcolladokuri <jocelyncollado52@gmail.com>

* Azure Monitor: Basic Logs data volume notification (#88009)

* frontend changes for data ingested warning

* initial logic for getResource

* payload processing

* create basicLogs usage function

* add utils for converting time and getting the data volume query for basic logs

* frontend updates for showing the data ingested for the given query

* frontend tests

* add check for when no dataIngested is returned

* remove backend.logger prints

* comment on what function does

* fix merge

* make resource URI regex case insensitive

* add support for workspace variables in basic logs flow

* add undefined check

* structure and add tests for variable support

* Update pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>

* add tracing for basic logs usage request

* clean up data volume query struct

* use async/await instead of callback

* fix parameters for getApiURL

* restrict time on usage query to 8 days max

* add time to dependency array to refetch basic logs usage

* move time check implementation to backend

* fix utils tests

---------

Co-authored-by: Jocelyn <jcolladokuri@microsoft.com>
Co-authored-by: jcolladokuri <jocelyncollado52@gmail.com>

---------

Co-authored-by: jcolladokuri <jcolladokuri@microsoft.com>
Co-authored-by: jcolladokuri <jocelyncollado52@gmail.com>
This commit is contained in:
Andreas Christou
2024-05-28 18:06:27 +01:00
committed by GitHub
co-authored by Jocelyn jcolladokuri
parent 0b2fab9967
commit 3eea71cc6b
35 changed files with 1188 additions and 55 deletions
+4 -2
View File
@@ -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
+1 -1
View File
@@ -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 {
@@ -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"`
@@ -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),
@@ -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)
}
})
@@ -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"}
+1 -1
View File
@@ -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 {
@@ -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"`
}
+47 -3
View File
@@ -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)
}
@@ -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)
@@ -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{},
}