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:
co-authored by
Jocelyn
jcolladokuri
parent
0b2fab9967
commit
3eea71cc6b
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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{},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user