diff --git a/docs/sources/explore/logs-integration.md b/docs/sources/explore/logs-integration.md index 2e192f0ca88..e1cde45c8a4 100644 --- a/docs/sources/explore/logs-integration.md +++ b/docs/sources/explore/logs-integration.md @@ -20,6 +20,7 @@ Explore is a powerful tool for logging and log analysis. It allows you to invest - [Elasticsearch]({{< relref "../datasources/elasticsearch" >}}) - [Cloudwatch]({{< relref "../datasources/aws-cloudwatch" >}}) - [InfluxDB]({{< relref "../datasources/influxdb" >}}) +- [Azure Monitor]({{< relref "../datasources/azure-monitor" >}}) With Explore, you can efficiently monitor, troubleshoot, and respond to incidents by analyzing your logs and identifying the root causes. It also helps you to correlate logs with other telemetry signals such as metrics, traces or profiles, by viewing them side-by-side. diff --git a/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts index 86c383bf696..712b35a32f8 100644 --- a/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts @@ -258,6 +258,7 @@ export const defaultAzureTracesFilter: Partial = { }; export enum ResultFormat { + Logs = 'logs', Table = 'table', TimeSeries = 'time_series', Trace = 'trace', diff --git a/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go index 36d3d3767e5..95a779e019b 100644 --- a/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go @@ -75,6 +75,7 @@ const ( // Defines values for ResultFormat. const ( + ResultFormatLogs ResultFormat = "logs" ResultFormatTable ResultFormat = "table" ResultFormatTimeSeries ResultFormat = "time_series" ResultFormatTrace ResultFormat = "trace" diff --git a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go index 42007a7720f..edb636b3baf 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go @@ -37,7 +37,7 @@ type AzureLogAnalyticsDatasource struct { // from the UI type AzureLogAnalyticsQuery struct { RefID string - ResultFormat string + ResultFormat dataquery.ResultFormat URL string TraceExploreQuery string TraceParentExploreQuery string @@ -46,7 +46,7 @@ type AzureLogAnalyticsQuery struct { TimeRange backend.TimeRange Query string Resources []string - QueryType string + QueryType dataquery.AzureQueryType AppInsightsQuery bool DashboardTime bool TimeColumn string @@ -242,13 +242,13 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, queries azureLogAnalyticsQueries = append(azureLogAnalyticsQueries, &AzureLogAnalyticsQuery{ RefID: query.RefID, - ResultFormat: string(resultFormat), + ResultFormat: resultFormat, URL: apiURL, JSON: query.JSON, TimeRange: query.TimeRange, Query: rawQuery, Resources: resources, - QueryType: query.QueryType, + QueryType: dataquery.AzureQueryType(query.QueryType), TraceExploreQuery: traceExploreQuery, TraceParentExploreQuery: traceParentExploreQuery, TraceLogsExploreQuery: traceLogsExploreQuery, @@ -273,8 +273,8 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *A return nil, err } - if query.QueryType == string(dataquery.AzureQueryTypeAzureTraces) { - if dataquery.ResultFormat(query.ResultFormat) == (dataquery.ResultFormatTrace) && query.Query == "" { + if query.QueryType == dataquery.AzureQueryTypeAzureTraces { + if query.ResultFormat == dataquery.ResultFormatTrace && query.Query == "" { return nil, fmt.Errorf("cannot visualise trace events using the trace visualiser") } } @@ -314,7 +314,7 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *A return nil, err } - frame, err := ResponseTableToFrame(t, query.RefID, query.Query, dataquery.AzureQueryType(query.QueryType), dataquery.ResultFormat(query.ResultFormat)) + frame, err := ResponseTableToFrame(t, query.RefID, query.Query, query.QueryType, query.ResultFormat) if err != nil { return nil, err } @@ -329,12 +329,25 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *A return nil, err } - if query.QueryType == string(dataquery.AzureQueryTypeAzureTraces) && query.ResultFormat == string(dataquery.ResultFormatTrace) { - frame.Meta.PreferredVisualization = "trace" + queryUrl, err := getQueryUrl(query.Query, query.Resources, azurePortalBaseUrl, query.TimeRange) + if err != nil { + return nil, err } - if query.ResultFormat == string(dataquery.ResultFormatTable) { - frame.Meta.PreferredVisualization = "table" + if query.QueryType == dataquery.AzureQueryTypeAzureTraces && query.ResultFormat == dataquery.ResultFormatTrace { + frame.Meta.PreferredVisualization = data.VisTypeTrace + } + + if query.ResultFormat == dataquery.ResultFormatTable { + frame.Meta.PreferredVisualization = data.VisTypeTable + } + + if query.ResultFormat == dataquery.ResultFormatLogs { + frame.Meta.PreferredVisualization = data.VisTypeLogs + frame.Meta.Custom = &LogAnalyticsMeta{ + ColumnTypes: frame.Meta.Custom.(*LogAnalyticsMeta).ColumnTypes, + AzurePortalLink: queryUrl, + } } if query.ResultFormat == types.TimeSeries { @@ -349,82 +362,103 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *A } } - queryUrl, err := getQueryUrl(query.Query, query.Resources, azurePortalBaseUrl, query.TimeRange) + // Use the parent span query for the parent span data link + err = addDataLinksToFields(query, azurePortalBaseUrl, frame, dsInfo, queryUrl) if err != nil { return nil, err } - if query.QueryType == string(dataquery.AzureQueryTypeAzureTraces) { - tracesUrl, err := getTracesQueryUrl(query.Resources, azurePortalBaseUrl) + dataResponse := backend.DataResponse{Frames: data.Frames{frame}} + return &dataResponse, nil +} + +func addDataLinksToFields(query *AzureLogAnalyticsQuery, azurePortalBaseUrl string, frame *data.Frame, dsInfo types.DatasourceInfo, queryUrl string) error { + if query.QueryType == dataquery.AzureQueryTypeAzureTraces { + err := addTraceDataLinksToFields(query, azurePortalBaseUrl, frame, dsInfo) if err != nil { - return nil, err + return err } - queryJSONModel := dataquery.AzureMonitorQuery{} - err = json.Unmarshal(query.JSON, &queryJSONModel) - if err != nil { - return nil, err - } - traceIdVariable := "${__data.fields.traceID}" - resultFormat := dataquery.ResultFormatTrace - queryJSONModel.AzureTraces.ResultFormat = &resultFormat - queryJSONModel.AzureTraces.Query = &query.TraceExploreQuery - if queryJSONModel.AzureTraces.OperationId == nil || *queryJSONModel.AzureTraces.OperationId == "" { - queryJSONModel.AzureTraces.OperationId = &traceIdVariable - } + return nil + } - logsQueryType := string(dataquery.AzureQueryTypeAzureLogAnalytics) - logsJSONModel := dataquery.AzureMonitorQuery{ - DataQuery: dataquery.DataQuery{ - QueryType: &logsQueryType, - }, - AzureLogAnalytics: &dataquery.AzureLogsQuery{ - Query: &query.TraceLogsExploreQuery, - Resources: []string{queryJSONModel.AzureTraces.Resources[0]}, - }, - } + if query.ResultFormat == dataquery.ResultFormatLogs { + return nil + } - if query.ResultFormat == string(dataquery.ResultFormatTable) { - AddCustomDataLink(*frame, data.DataLink{ - Title: "Explore Trace: ${__data.fields.traceID}", - URL: "", - Internal: &data.InternalDataLink{ - DatasourceUID: dsInfo.DatasourceUID, - DatasourceName: dsInfo.DatasourceName, - Query: queryJSONModel, - }, - }) + AddConfigLinks(*frame, queryUrl, nil) - // Use the parent span query for the parent span data link - queryJSONModel.AzureTraces.Query = &query.TraceParentExploreQuery - AddCustomDataLink(*frame, data.DataLink{ - Title: "Explore Parent Span: ${__data.fields.parentSpanID}", - URL: "", - Internal: &data.InternalDataLink{ - DatasourceUID: dsInfo.DatasourceUID, - DatasourceName: dsInfo.DatasourceName, - Query: queryJSONModel, - }, - }) + return nil +} - linkTitle := "Explore Trace in Azure Portal" - AddConfigLinks(*frame, tracesUrl, &linkTitle) - } +func addTraceDataLinksToFields(query *AzureLogAnalyticsQuery, azurePortalBaseUrl string, frame *data.Frame, dsInfo types.DatasourceInfo) error { + tracesUrl, err := getTracesQueryUrl(query.Resources, azurePortalBaseUrl) + if err != nil { + return err + } + queryJSONModel := dataquery.AzureMonitorQuery{} + err = json.Unmarshal(query.JSON, &queryJSONModel) + if err != nil { + return err + } + + traceIdVariable := "${__data.fields.traceID}" + resultFormat := dataquery.ResultFormatTrace + queryJSONModel.AzureTraces.ResultFormat = &resultFormat + queryJSONModel.AzureTraces.Query = &query.TraceExploreQuery + if queryJSONModel.AzureTraces.OperationId == nil || *queryJSONModel.AzureTraces.OperationId == "" { + queryJSONModel.AzureTraces.OperationId = &traceIdVariable + } + + logsQueryType := string(dataquery.AzureQueryTypeAzureLogAnalytics) + logsJSONModel := dataquery.AzureMonitorQuery{ + DataQuery: dataquery.DataQuery{ + QueryType: &logsQueryType, + }, + AzureLogAnalytics: &dataquery.AzureLogsQuery{ + Query: &query.TraceLogsExploreQuery, + Resources: []string{queryJSONModel.AzureTraces.Resources[0]}, + }, + } + + if query.ResultFormat == dataquery.ResultFormatTable { AddCustomDataLink(*frame, data.DataLink{ - Title: "Explore Trace Logs", + Title: "Explore Trace: ${__data.fields.traceID}", URL: "", Internal: &data.InternalDataLink{ DatasourceUID: dsInfo.DatasourceUID, DatasourceName: dsInfo.DatasourceName, - Query: logsJSONModel, + Query: queryJSONModel, }, }) - } else { - AddConfigLinks(*frame, queryUrl, nil) + + queryJSONModel.AzureTraces.Query = &query.TraceParentExploreQuery + AddCustomDataLink(*frame, data.DataLink{ + Title: "Explore Parent Span: ${__data.fields.parentSpanID}", + URL: "", + Internal: &data.InternalDataLink{ + DatasourceUID: dsInfo.DatasourceUID, + DatasourceName: dsInfo.DatasourceName, + Query: queryJSONModel, + }, + }) + + linkTitle := "Explore Trace in Azure Portal" + AddConfigLinks(*frame, tracesUrl, &linkTitle) } - dataResponse := backend.DataResponse{Frames: data.Frames{frame}} - return &dataResponse, nil + + AddCustomDataLink(*frame, data.DataLink{ + Title: "Explore Trace Logs", + URL: "", + Internal: &data.InternalDataLink{ + DatasourceUID: dsInfo.DatasourceUID, + DatasourceName: dsInfo.DatasourceName, + Query: logsJSONModel, + }, + }) + + return nil } func appendErrorNotice(frame *data.Frame, err *AzureLogAnalyticsAPIError) *data.Frame { @@ -453,7 +487,7 @@ func (e *AzureLogAnalyticsDatasource) createRequest(ctx context.Context, queryUR body["query_datetimescope_column"] = query.TimeColumn } - if len(query.Resources) > 1 && query.QueryType == string(dataquery.AzureQueryTypeAzureLogAnalytics) && !query.AppInsightsQuery { + if len(query.Resources) > 1 && query.QueryType == dataquery.AzureQueryTypeAzureLogAnalytics && !query.AppInsightsQuery { body["workspaces"] = query.Resources } if query.AppInsightsQuery { @@ -701,7 +735,8 @@ func (e *AzureLogAnalyticsDatasource) unmarshalResponse(res *http.Response) (Azu // LogAnalyticsMeta is a type for the a Frame's Meta's Custom property. type LogAnalyticsMeta struct { - ColumnTypes []string `json:"azureColumnTypes"` + ColumnTypes []string `json:"azureColumnTypes"` + AzurePortalLink string `json:"azurePortalLink,omitempty"` } // encodeQuery encodes the query in gzip so the frontend can build links. diff --git a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go index 4790664322d..69a7edbf4bc 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go @@ -130,7 +130,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { 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", Resources: []string{"/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace"}, TimeRange: timeRange, - QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), + QueryType: dataquery.AzureQueryTypeAzureLogAnalytics, AppInsightsQuery: false, DashboardTime: false, }, @@ -168,7 +168,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { }`, types.TimeSeries)), Query: "Perf", Resources: []string{}, - QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), + QueryType: dataquery.AzureQueryTypeAzureLogAnalytics, AppInsightsQuery: false, DashboardTime: false, }, @@ -206,7 +206,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { }`, types.TimeSeries)), Query: "Perf", Resources: []string{}, - QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), + QueryType: dataquery.AzureQueryTypeAzureLogAnalytics, AppInsightsQuery: false, DashboardTime: false, }, @@ -246,7 +246,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { }`, types.TimeSeries)), Query: "Perf", Resources: []string{"/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace"}, - QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), + QueryType: dataquery.AzureQueryTypeAzureLogAnalytics, AppInsightsQuery: false, DashboardTime: false, }, @@ -288,7 +288,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { Query: "Perf", Resources: []string{"/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"}, TimeRange: timeRange, - QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), + QueryType: dataquery.AzureQueryTypeAzureLogAnalytics, AppInsightsQuery: false, DashboardTime: false, }, @@ -332,7 +332,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { Query: "Perf", Resources: []string{"/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace"}, TimeRange: timeRange, - QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), + QueryType: dataquery.AzureQueryTypeAzureLogAnalytics, AppInsightsQuery: false, DashboardTime: true, TimeColumn: "TimeGenerated", @@ -362,7 +362,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { azureLogAnalyticsQueries: []*AzureLogAnalyticsQuery{ { RefID: "A", - ResultFormat: string(dataquery.ResultFormatTable), + ResultFormat: dataquery.ResultFormatTable, URL: "v1/apps/r1/query", JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Traces", @@ -387,7 +387,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"}, TimeRange: timeRange, - QueryType: string(dataquery.AzureQueryTypeAzureTraces), + QueryType: dataquery.AzureQueryTypeAzureTraces, TraceExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true trace` + `| where (operation_Id != '' and operation_Id == 'test-op-id') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == 'test-op-id')` + `| extend duration = iff(isnull(column_ifexists("duration", real(null))), toreal(0), column_ifexists("duration", real(null)))` + @@ -442,7 +442,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { azureLogAnalyticsQueries: []*AzureLogAnalyticsQuery{ { RefID: "A", - ResultFormat: string(dataquery.ResultFormatTable), + ResultFormat: dataquery.ResultFormatTable, URL: "v1/apps/r1/query", JSON: []byte(`{ "queryType": "Azure Traces", @@ -466,7 +466,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"}, TimeRange: timeRange, - QueryType: string(dataquery.AzureQueryTypeAzureTraces), + QueryType: dataquery.AzureQueryTypeAzureTraces, TraceExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true trace` + `| where (operation_Id != '' and operation_Id == 'test-op-id') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == 'test-op-id')` + `| extend duration = iff(isnull(column_ifexists("duration", real(null))), toreal(0), column_ifexists("duration", real(null)))` + @@ -520,7 +520,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { azureLogAnalyticsQueries: []*AzureLogAnalyticsQuery{ { RefID: "A", - ResultFormat: string(dataquery.ResultFormatTable), + ResultFormat: dataquery.ResultFormatTable, URL: "v1/apps/r1/query", JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Traces", @@ -542,7 +542,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"}, TimeRange: timeRange, - QueryType: string(dataquery.AzureQueryTypeAzureTraces), + QueryType: dataquery.AzureQueryTypeAzureTraces, TraceExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true availabilityResults,customEvents,dependencies,exceptions,pageViews,requests,traces` + `| where (operation_Id != '' and operation_Id == '${__data.fields.traceID}') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == '${__data.fields.traceID}')` + `| extend duration = iff(isnull(column_ifexists("duration", real(null))), toreal(0), column_ifexists("duration", real(null)))` + @@ -597,7 +597,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { azureLogAnalyticsQueries: []*AzureLogAnalyticsQuery{ { RefID: "A", - ResultFormat: string(dataquery.ResultFormatTable), + ResultFormat: dataquery.ResultFormatTable, URL: "v1/apps/r1/query", JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Traces", @@ -621,7 +621,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"}, TimeRange: timeRange, - QueryType: string(dataquery.AzureQueryTypeAzureTraces), + QueryType: dataquery.AzureQueryTypeAzureTraces, TraceExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true availabilityResults,customEvents,dependencies,exceptions,pageViews,requests,traces` + `| where (operation_Id != '' and operation_Id == 'test-op-id') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == 'test-op-id')` + `| extend duration = iff(isnull(column_ifexists("duration", real(null))), toreal(0), column_ifexists("duration", real(null)))` + @@ -677,7 +677,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { azureLogAnalyticsQueries: []*AzureLogAnalyticsQuery{ { RefID: "A", - ResultFormat: string(dataquery.ResultFormatTable), + ResultFormat: dataquery.ResultFormatTable, URL: "v1/apps/r1/query", JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Traces", @@ -703,7 +703,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"}, TimeRange: timeRange, - QueryType: string(dataquery.AzureQueryTypeAzureTraces), + QueryType: dataquery.AzureQueryTypeAzureTraces, TraceExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true availabilityResults,customEvents,dependencies,exceptions,pageViews,requests,traces` + `| where (operation_Id != '' and operation_Id == 'test-op-id') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == 'test-op-id')` + `| extend duration = iff(isnull(column_ifexists("duration", real(null))), toreal(0), column_ifexists("duration", real(null)))` + @@ -761,7 +761,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { azureLogAnalyticsQueries: []*AzureLogAnalyticsQuery{ { RefID: "A", - ResultFormat: string(dataquery.ResultFormatTable), + ResultFormat: dataquery.ResultFormatTable, URL: "v1/apps/r1/query", JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Traces", @@ -787,7 +787,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"}, TimeRange: timeRange, - QueryType: string(dataquery.AzureQueryTypeAzureTraces), + QueryType: dataquery.AzureQueryTypeAzureTraces, TraceExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true availabilityResults,customEvents,dependencies,exceptions,pageViews,requests,traces` + `| where (operation_Id != '' and operation_Id == 'test-op-id') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == 'test-op-id')` + `| extend duration = iff(isnull(column_ifexists("duration", real(null))), toreal(0), column_ifexists("duration", real(null)))` + @@ -845,7 +845,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { azureLogAnalyticsQueries: []*AzureLogAnalyticsQuery{ { RefID: "A", - ResultFormat: string(dataquery.ResultFormatTable), + ResultFormat: dataquery.ResultFormatTable, URL: "v1/apps/r1/query", JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Traces", @@ -871,7 +871,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"}, TimeRange: timeRange, - QueryType: string(dataquery.AzureQueryTypeAzureTraces), + QueryType: dataquery.AzureQueryTypeAzureTraces, TraceExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true availabilityResults,customEvents,dependencies,exceptions,pageViews,requests,traces` + `| where (operation_Id != '' and operation_Id == 'test-op-id') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == 'test-op-id')` + `| extend duration = iff(isnull(column_ifexists("duration", real(null))), toreal(0), column_ifexists("duration", real(null)))` + @@ -927,7 +927,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { azureLogAnalyticsQueries: []*AzureLogAnalyticsQuery{ { RefID: "A", - ResultFormat: string(dataquery.ResultFormatTrace), + ResultFormat: dataquery.ResultFormatTrace, URL: "v1/apps/r1/query", JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Traces", @@ -949,7 +949,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"}, TimeRange: timeRange, - QueryType: string(dataquery.AzureQueryTypeAzureTraces), + QueryType: dataquery.AzureQueryTypeAzureTraces, TraceExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true availabilityResults,customEvents,dependencies,exceptions,pageViews,requests` + `| where (operation_Id != '' and operation_Id == '${__data.fields.traceID}') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == '${__data.fields.traceID}')` + `| extend duration = iff(isnull(column_ifexists("duration", real(null))), toreal(0), column_ifexists("duration", real(null)))` + @@ -1004,7 +1004,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { azureLogAnalyticsQueries: []*AzureLogAnalyticsQuery{ { RefID: "A", - ResultFormat: string(dataquery.ResultFormatTrace), + ResultFormat: dataquery.ResultFormatTrace, URL: "v1/apps/r1/query", JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Traces", @@ -1028,7 +1028,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"}, TimeRange: timeRange, - QueryType: string(dataquery.AzureQueryTypeAzureTraces), + QueryType: dataquery.AzureQueryTypeAzureTraces, TraceExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true availabilityResults,customEvents,dependencies,exceptions,pageViews,requests` + `| where (operation_Id != '' and operation_Id == 'test-op-id') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == 'test-op-id')` + `| extend duration = iff(isnull(column_ifexists("duration", real(null))), toreal(0), column_ifexists("duration", real(null)))` + @@ -1084,7 +1084,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { azureLogAnalyticsQueries: []*AzureLogAnalyticsQuery{ { RefID: "A", - ResultFormat: string(dataquery.ResultFormatTrace), + ResultFormat: dataquery.ResultFormatTrace, URL: "v1/apps/r1/query", JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Traces", @@ -1098,7 +1098,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { Query: "", Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"}, TimeRange: timeRange, - QueryType: string(dataquery.AzureQueryTypeAzureTraces), + QueryType: dataquery.AzureQueryTypeAzureTraces, TraceExploreQuery: "", TraceParentExploreQuery: "", TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + @@ -1130,7 +1130,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { azureLogAnalyticsQueries: []*AzureLogAnalyticsQuery{ { RefID: "A", - ResultFormat: string(dataquery.ResultFormatTrace), + ResultFormat: dataquery.ResultFormatTrace, URL: "v1/apps/r1/query", JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Traces", @@ -1153,7 +1153,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"}, TimeRange: timeRange, - QueryType: string(dataquery.AzureQueryTypeAzureTraces), + QueryType: dataquery.AzureQueryTypeAzureTraces, TraceExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true availabilityResults,customEvents,dependencies,exceptions,pageViews,requests,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').availabilityResults,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').customEvents,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').dependencies,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').exceptions,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').pageViews,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').requests` + `| where (operation_Id != '' and operation_Id == 'op-id-multi') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == 'op-id-multi')` + `| extend duration = iff(isnull(column_ifexists("duration", real(null))), toreal(0), column_ifexists("duration", real(null)))` + @@ -1212,7 +1212,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { azureLogAnalyticsQueries: []*AzureLogAnalyticsQuery{ { RefID: "A", - ResultFormat: string(dataquery.ResultFormatTrace), + ResultFormat: dataquery.ResultFormatTrace, URL: "v1/apps/r1/query", JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Traces", @@ -1233,7 +1233,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1", "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r2"}, TimeRange: timeRange, - QueryType: string(dataquery.AzureQueryTypeAzureTraces), + QueryType: dataquery.AzureQueryTypeAzureTraces, TraceExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true availabilityResults,customEvents,dependencies,exceptions,pageViews,requests,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').availabilityResults,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').customEvents,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').dependencies,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').exceptions,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').pageViews,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').requests` + `| where (operation_Id != '' and operation_Id == '${__data.fields.traceID}') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == '${__data.fields.traceID}')` + `| extend duration = iff(isnull(column_ifexists("duration", real(null))), toreal(0), column_ifexists("duration", real(null)))` + @@ -1293,7 +1293,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { azureLogAnalyticsQueries: []*AzureLogAnalyticsQuery{ { RefID: "A", - ResultFormat: string(dataquery.ResultFormatTrace), + ResultFormat: dataquery.ResultFormatTrace, URL: "v1/apps/r1/query", JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Traces", @@ -1316,7 +1316,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1", "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r2"}, TimeRange: timeRange, - QueryType: string(dataquery.AzureQueryTypeAzureTraces), + QueryType: dataquery.AzureQueryTypeAzureTraces, TraceExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true availabilityResults,customEvents,dependencies,exceptions,pageViews,requests,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').availabilityResults,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').customEvents,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').dependencies,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').exceptions,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').pageViews,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').requests` + `| where (operation_Id != '' and operation_Id == 'op-id-multi') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == 'op-id-multi')` + `| extend duration = iff(isnull(column_ifexists("duration", real(null))), toreal(0), column_ifexists("duration", real(null)))` + @@ -1376,7 +1376,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { azureLogAnalyticsQueries: []*AzureLogAnalyticsQuery{ { RefID: "A", - ResultFormat: string(dataquery.ResultFormatTrace), + ResultFormat: dataquery.ResultFormatTrace, URL: "v1/apps/r1/query", JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Traces", @@ -1399,7 +1399,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1", "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r2"}, TimeRange: timeRange, - QueryType: string(dataquery.AzureQueryTypeAzureTraces), + QueryType: dataquery.AzureQueryTypeAzureTraces, TraceExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true availabilityResults,customEvents,dependencies,exceptions,pageViews,requests,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').availabilityResults,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').customEvents,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').dependencies,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').exceptions,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').pageViews,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').requests,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r3').availabilityResults,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r3').customEvents,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r3').dependencies,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r3').exceptions,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r3').pageViews,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r3').requests` + `| where (operation_Id != '' and operation_Id == 'op-id-non-overlapping') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == 'op-id-non-overlapping')` + `| extend duration = iff(isnull(column_ifexists("duration", real(null))), toreal(0), column_ifexists("duration", real(null)))` + @@ -1492,7 +1492,7 @@ func TestLogAnalyticsCreateRequest(t *testing.T) { req, err := ds.createRequest(ctx, url, &AzureLogAnalyticsQuery{ Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.OperationalInsights/workspaces/r1", "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.OperationalInsights/workspaces/r2"}, Query: "Perf", - QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), + QueryType: dataquery.AzureQueryTypeAzureLogAnalytics, AppInsightsQuery: false, DashboardTime: false, }) @@ -1512,7 +1512,7 @@ func TestLogAnalyticsCreateRequest(t *testing.T) { req, err := ds.createRequest(ctx, url, &AzureLogAnalyticsQuery{ Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.OperationalInsights/workspaces/r1", "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.OperationalInsights/workspaces/r2"}, Query: "Perf", - QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), + QueryType: dataquery.AzureQueryTypeAzureLogAnalytics, TimeRange: backend.TimeRange{ From: from, To: to, @@ -1536,7 +1536,7 @@ func TestLogAnalyticsCreateRequest(t *testing.T) { to := from.Add(3 * time.Hour) req, err := ds.createRequest(ctx, url, &AzureLogAnalyticsQuery{ Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1", "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r2"}, - QueryType: string(dataquery.AzureQueryTypeAzureTraces), + QueryType: dataquery.AzureQueryTypeAzureTraces, TimeRange: backend.TimeRange{ From: from, To: to, diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.test.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.test.tsx index 1f0568572af..d77ff945ea4 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.test.tsx @@ -2,6 +2,8 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; +import { dateTime, LoadingState } from '@grafana/data'; + import createMockDatasource from '../../__mocks__/datasource'; import createMockQuery from '../../__mocks__/query'; import { createMockResourcePickerData } from '../MetricsQueryEditor/MetricsQueryEditor.test'; @@ -211,4 +213,37 @@ describe('LogsQueryEditor', () => { }) ); }); + + describe('azure portal link', () => { + it('should show the link button', async () => { + const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); + const query = createMockQuery(); + const onChange = jest.fn(); + + const date = dateTime(new Date()); + render( + {}} + data={{ + state: LoadingState.Done, + timeRange: { + from: date, + to: date, + raw: { + from: date, + to: date, + }, + }, + series: [{ refId: query.refId, length: 0, meta: { custom: { azurePortalLink: 'test' } }, fields: [] }], + }} + /> + ); + + expect(await screen.findByText('View query in Azure Portal')).toBeInTheDocument(); + }); + }); }); diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx index 45de3fa3f72..5bea85d8ea4 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx @@ -1,7 +1,8 @@ import React, { useEffect, useState } from 'react'; +import { PanelData, TimeRange } from '@grafana/data'; import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/experimental'; -import { Alert } from '@grafana/ui'; +import { Alert, LinkButton } from '@grafana/ui'; import Datasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; @@ -25,6 +26,8 @@ interface LogsQueryEditorProps { variableOptionGroup: { label: string; options: AzureMonitorOption[] }; setError: (source: string, error: AzureMonitorErrorish | undefined) => void; hideFormatAs?: boolean; + timeRange?: TimeRange; + data?: PanelData; } const LogsQueryEditor = ({ @@ -35,6 +38,8 @@ const LogsQueryEditor = ({ onChange, setError, hideFormatAs, + timeRange, + data, }: LogsQueryEditorProps) => { const migrationError = useMigrations(datasource, query, onChange); const disableRow = (row: ResourceRow, selectedRows: ResourceRowGroup) => { @@ -60,6 +65,26 @@ const LogsQueryEditor = ({ } }, [query.azureLogAnalytics?.resources, datasource.azureLogAnalyticsDatasource]); + let portalLinkButton = null; + + if (data?.series) { + const querySeries = data.series.find((result) => result.refId === query.refId); + if (querySeries && querySeries.meta?.custom?.azurePortalLink) { + portalLinkButton = ( + <> + + View query in Azure Portal + + + ); + } + } + return ( @@ -122,15 +147,16 @@ const LogsQueryEditor = ({ setError={setError} inputId={'azure-monitor-logs'} options={[ + { label: 'Log', value: ResultFormat.Logs }, { label: 'Time series', value: ResultFormat.TimeSeries }, { label: 'Table', value: ResultFormat.Table }, ]} - defaultValue={ResultFormat.Table} + defaultValue={ResultFormat.Logs} setFormatAs={setFormatAs} resultFormat={query.azureLogAnalytics?.resultFormat} /> )} - + {portalLinkButton} {migrationError && {migrationError.message}} diff --git a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx index 789c36000b9..3e9b85d3fd4 100644 --- a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx @@ -142,12 +142,14 @@ const EditorForQueryType = ({ case AzureQueryType.LogAnalytics: return ( ); diff --git a/public/app/plugins/datasource/azuremonitor/dataquery.cue b/public/app/plugins/datasource/azuremonitor/dataquery.cue index ad7b9b89605..ffefdaab339 100644 --- a/public/app/plugins/datasource/azuremonitor/dataquery.cue +++ b/public/app/plugins/datasource/azuremonitor/dataquery.cue @@ -149,7 +149,7 @@ composableKinds: DataQuery: { filters: [...string] } @cuetsy(kind="interface") - #ResultFormat: "table" | "time_series" | "trace" @cuetsy(kind="enum", memberNames="Table|TimeSeries|Trace") + #ResultFormat: "table" | "time_series" | "trace" | "logs" @cuetsy(kind="enum", memberNames="Table|TimeSeries|Trace|Logs") #AzureResourceGraphQuery: { // Azure Resource Graph KQL query to be executed. diff --git a/public/app/plugins/datasource/azuremonitor/dataquery.gen.ts b/public/app/plugins/datasource/azuremonitor/dataquery.gen.ts index 83e29aeb557..b9f234a9a3c 100644 --- a/public/app/plugins/datasource/azuremonitor/dataquery.gen.ts +++ b/public/app/plugins/datasource/azuremonitor/dataquery.gen.ts @@ -255,6 +255,7 @@ export const defaultAzureTracesFilter: Partial = { }; export enum ResultFormat { + Logs = 'logs', Table = 'table', TimeSeries = 'time_series', Trace = 'trace', diff --git a/public/app/plugins/datasource/azuremonitor/datasource.ts b/public/app/plugins/datasource/azuremonitor/datasource.ts index f29bf2e973b..964b958ded9 100644 --- a/public/app/plugins/datasource/azuremonitor/datasource.ts +++ b/public/app/plugins/datasource/azuremonitor/datasource.ts @@ -8,6 +8,7 @@ import { DataQueryResponse, DataSourceInstanceSettings, LoadingState, + QueryFixAction, ScopedVars, } from '@grafana/data'; import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; @@ -197,6 +198,27 @@ export default class Datasource extends DataSourceWithBackend