Azure monitor: Support Logs visualization (#76594)

* logs view

* cant add a library

* gzip doesnt work

* remove link code :(

* clean up

* linter

* linter

* betterer. cant run this locally??

* Remove unneeded conversions

* Return portal link for log queries from backend

* Display portal link

* Update tests

* Lint

* Prettier

* Unused import

* Add Azure Monitor to explore docs

---------

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>
This commit is contained in:
Andrew Hackmann 2023-10-18 10:19:35 -05:00 committed by GitHub
parent 17fe1d3fc7
commit 91ad07719f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 236 additions and 111 deletions

View File

@ -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.

View File

@ -258,6 +258,7 @@ export const defaultAzureTracesFilter: Partial<AzureTracesFilter> = {
};
export enum ResultFormat {
Logs = 'logs',
Table = 'table',
TimeSeries = 'time_series',
Trace = 'trace',

View File

@ -75,6 +75,7 @@ const (
// Defines values for ResultFormat.
const (
ResultFormatLogs ResultFormat = "logs"
ResultFormatTable ResultFormat = "table"
ResultFormatTimeSeries ResultFormat = "time_series"
ResultFormatTrace ResultFormat = "trace"

View File

@ -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.

View File

@ -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,

View File

@ -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(
<LogsQueryEditor
query={query}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={onChange}
setError={() => {}}
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();
});
});
});

View File

@ -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 = (
<>
<LinkButton
size="md"
target="_blank"
style={{ marginTop: '22px' }}
href={querySeries.meta?.custom?.azurePortalLink}
>
View query in Azure Portal
</LinkButton>
</>
);
}
}
return (
<span data-testid={selectors.components.queryEditor.logsQueryEditor.container.input}>
<EditorRows>
@ -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 && <Alert title={migrationError.title}>{migrationError.message}</Alert>}
</EditorFieldGroup>
</EditorRow>

View File

@ -142,12 +142,14 @@ const EditorForQueryType = ({
case AzureQueryType.LogAnalytics:
return (
<LogsQueryEditor
data={data}
subscriptionId={subscriptionId}
query={query}
datasource={datasource}
onChange={onChange}
variableOptionGroup={variableOptionGroup}
setError={setError}
timeRange={range}
/>
);

View File

@ -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.

View File

@ -255,6 +255,7 @@ export const defaultAzureTracesFilter: Partial<AzureTracesFilter> = {
};
export enum ResultFormat {
Logs = 'logs',
Table = 'table',
TimeSeries = 'time_series',
Trace = 'trace',

View File

@ -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<AzureMonitorQuery,
getVariablesRaw() {
return this.templateSrv.getVariables();
}
modifyQuery(query: AzureMonitorQuery, action: QueryFixAction): AzureMonitorQuery {
if (!action.options) {
return query;
}
let expression = query.azureLogAnalytics?.query;
if (expression === undefined) {
return query;
}
switch (action.type) {
case 'ADD_FILTER': {
expression += `\n| where ${action.options.key} == "${action.options.value}"`;
break;
}
case 'ADD_FILTER_OUT': {
expression += `\n| where ${action.options.key} != "${action.options.value}"`;
break;
}
}
return { ...query, azureLogAnalytics: { ...query.azureLogAnalytics, query: expression } };
}
}
function hasQueryForType(query: AzureMonitorQuery): boolean {

View File

@ -99,5 +99,6 @@
"metrics": true,
"annotations": true,
"alerting": true,
"backend": true
"backend": true,
"logs": true
}