The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasou...

877 lines
30 KiB

package loganalytics
import (
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path"
"regexp"
"sort"
"strings"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
"github.com/grafana/grafana-plugin-sdk-go/data"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"k8s.io/utils/strings/slices"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/kinds/dataquery"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/macros"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/types"
)
// AzureLogAnalyticsDatasource calls the Azure Log Analytics API's
type AzureLogAnalyticsDatasource struct {
Proxy types.ServiceProxy
Logger log.Logger
}
// AzureLogAnalyticsQuery is the query request that is built from the saved values for
// from the UI
type AzureLogAnalyticsQuery struct {
RefID string
ResultFormat dataquery.ResultFormat
URL string
TraceExploreQuery string
TraceParentExploreQuery string
TraceLogsExploreQuery string
JSON json.RawMessage
TimeRange backend.TimeRange
Query string
Resources []string
QueryType dataquery.AzureQueryType
AppInsightsQuery bool
DashboardTime bool
TimeColumn string
}
func (e *AzureLogAnalyticsDatasource) ResourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) (http.ResponseWriter, error) {
return e.Proxy.Do(rw, req, cli)
}
// 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) {
result := backend.NewQueryDataResponse()
queries, err := e.buildQueries(ctx, originalQueries, dsInfo)
if err != nil {
return nil, err
}
for _, query := range queries {
res, err := e.executeQuery(ctx, query, dsInfo, client, url)
if err != nil {
result.Responses[query.RefID] = backend.DataResponse{Error: err}
continue
}
result.Responses[query.RefID] = *res
}
return result, nil
}
func getApiURL(resourceOrWorkspace string, isAppInsightsQuery bool) string {
matchesResourceURI, _ := regexp.MatchString("^/subscriptions/", resourceOrWorkspace)
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)
} else {
return fmt.Sprintf("v1/workspaces/%s/query", resourceOrWorkspace)
}
}
func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, queries []backend.DataQuery, dsInfo types.DatasourceInfo) ([]*AzureLogAnalyticsQuery, error) {
azureLogAnalyticsQueries := []*AzureLogAnalyticsQuery{}
appInsightsRegExp, err := regexp.Compile("providers/Microsoft.Insights/components")
if err != nil {
return nil, fmt.Errorf("failed to compile Application Insights regex")
}
for _, query := range queries {
resources := []string{}
var resourceOrWorkspace string
var queryString string
var resultFormat dataquery.ResultFormat
appInsightsQuery := false
traceExploreQuery := ""
traceParentExploreQuery := ""
traceLogsExploreQuery := ""
dashboardTime := false
timeColumn := ""
if query.QueryType == string(dataquery.AzureQueryTypeAzureLogAnalytics) {
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)
}
azureLogAnalyticsTarget := queryJSONModel.AzureLogAnalytics
if azureLogAnalyticsTarget.ResultFormat != nil {
resultFormat = *azureLogAnalyticsTarget.ResultFormat
}
if resultFormat == "" {
resultFormat = types.TimeSeries
}
// Legacy queries only specify a Workspace GUID, which we need to use the old workspace-centric
// API URL for, and newer queries specifying a resource URI should use resource-centric API.
// However, legacy workspace queries using a `workspaces()` template variable will be resolved
// to a resource URI, so they should use the new resource-centric.
if len(azureLogAnalyticsTarget.Resources) > 0 {
resources = azureLogAnalyticsTarget.Resources
resourceOrWorkspace = azureLogAnalyticsTarget.Resources[0]
appInsightsQuery = appInsightsRegExp.Match([]byte(resourceOrWorkspace))
} else if azureLogAnalyticsTarget.Resource != nil && *azureLogAnalyticsTarget.Resource != "" {
resources = []string{*azureLogAnalyticsTarget.Resource}
resourceOrWorkspace = *azureLogAnalyticsTarget.Resource
} else if azureLogAnalyticsTarget.Workspace != nil {
resourceOrWorkspace = *azureLogAnalyticsTarget.Workspace
}
if azureLogAnalyticsTarget.Query != nil {
queryString = *azureLogAnalyticsTarget.Query
}
if azureLogAnalyticsTarget.DashboardTime != nil {
dashboardTime = *azureLogAnalyticsTarget.DashboardTime
if dashboardTime {
if azureLogAnalyticsTarget.TimeColumn != nil {
timeColumn = *azureLogAnalyticsTarget.TimeColumn
} else {
// Final fallback to TimeGenerated if no column is provided
timeColumn = "TimeGenerated"
}
}
}
}
if query.QueryType == string(dataquery.AzureQueryTypeAzureTraces) {
queryJSONModel := types.TracesJSONQuery{}
err := json.Unmarshal(query.JSON, &queryJSONModel)
if err != nil {
return nil, fmt.Errorf("failed to decode the Azure Traces query object from JSON: %w", err)
}
azureTracesTarget := queryJSONModel.AzureTraces
if azureTracesTarget.ResultFormat == nil {
resultFormat = types.Table
} else {
resultFormat = *azureTracesTarget.ResultFormat
if resultFormat == "" {
resultFormat = types.Table
}
}
resources = azureTracesTarget.Resources
resourceOrWorkspace = azureTracesTarget.Resources[0]
appInsightsQuery = appInsightsRegExp.Match([]byte(resourceOrWorkspace))
resourcesMap := make(map[string]bool, 0)
if len(resources) > 1 {
for _, resource := range resources {
resourcesMap[strings.ToLower(resource)] = true
}
// Remove the base resource as that's where the query is run anyway
delete(resourcesMap, strings.ToLower(resourceOrWorkspace))
}
operationId := ""
if queryJSONModel.AzureTraces.OperationId != nil && *queryJSONModel.AzureTraces.OperationId != "" {
operationId = *queryJSONModel.AzureTraces.OperationId
resourcesMap, err = getCorrelationWorkspaces(ctx, resourceOrWorkspace, resourcesMap, dsInfo, operationId)
if err != nil {
return nil, fmt.Errorf("failed to retrieve correlation resources for operation ID - %s: %s", operationId, err)
}
}
queryResources := make([]string, 0)
for resource := range resourcesMap {
queryResources = append(queryResources, resource)
}
sort.Strings(queryResources)
queryString = buildTracesQuery(operationId, nil, queryJSONModel.AzureTraces.TraceTypes, queryJSONModel.AzureTraces.Filters, &resultFormat, queryResources)
traceIdVariable := "${__data.fields.traceID}"
parentSpanIdVariable := "${__data.fields.parentSpanID}"
if operationId == "" {
traceExploreQuery = buildTracesQuery(traceIdVariable, nil, queryJSONModel.AzureTraces.TraceTypes, queryJSONModel.AzureTraces.Filters, &resultFormat, queryResources)
traceParentExploreQuery = buildTracesQuery(traceIdVariable, &parentSpanIdVariable, queryJSONModel.AzureTraces.TraceTypes, queryJSONModel.AzureTraces.Filters, &resultFormat, queryResources)
traceLogsExploreQuery = buildTracesLogsQuery(traceIdVariable, queryResources)
} else {
traceExploreQuery = queryString
traceParentExploreQuery = buildTracesQuery(operationId, &parentSpanIdVariable, queryJSONModel.AzureTraces.TraceTypes, queryJSONModel.AzureTraces.Filters, &resultFormat, queryResources)
traceLogsExploreQuery = buildTracesLogsQuery(operationId, queryResources)
}
traceExploreQuery, err = macros.KqlInterpolate(query, dsInfo, traceExploreQuery, "TimeGenerated")
if err != nil {
return nil, fmt.Errorf("failed to create traces explore query: %s", err)
}
traceParentExploreQuery, err = macros.KqlInterpolate(query, dsInfo, traceParentExploreQuery, "TimeGenerated")
if err != nil {
return nil, fmt.Errorf("failed to create parent span traces explore query: %s", err)
}
traceLogsExploreQuery, err = macros.KqlInterpolate(query, dsInfo, traceLogsExploreQuery, "TimeGenerated")
if err != nil {
return nil, fmt.Errorf("failed to create traces logs explore query: %s", err)
}
dashboardTime = true
timeColumn = "timestamp"
}
apiURL := getApiURL(resourceOrWorkspace, appInsightsQuery)
rawQuery, err := macros.KqlInterpolate(query, dsInfo, queryString, "TimeGenerated")
if err != nil {
return nil, err
}
azureLogAnalyticsQueries = append(azureLogAnalyticsQueries, &AzureLogAnalyticsQuery{
RefID: query.RefID,
ResultFormat: resultFormat,
URL: apiURL,
JSON: query.JSON,
TimeRange: query.TimeRange,
Query: rawQuery,
Resources: resources,
QueryType: dataquery.AzureQueryType(query.QueryType),
TraceExploreQuery: traceExploreQuery,
TraceParentExploreQuery: traceParentExploreQuery,
TraceLogsExploreQuery: traceLogsExploreQuery,
AppInsightsQuery: appInsightsQuery,
DashboardTime: dashboardTime,
TimeColumn: timeColumn,
})
}
return azureLogAnalyticsQueries, nil
}
func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *AzureLogAnalyticsQuery, dsInfo types.DatasourceInfo, client *http.Client, url string) (*backend.DataResponse, error) {
// If azureLogAnalyticsSameAs is defined and set to false, return an error
if sameAs, ok := dsInfo.JSONData["azureLogAnalyticsSameAs"]; ok && !sameAs.(bool) {
return nil, fmt.Errorf("credentials for Log Analytics are no longer supported. Go to the data source configuration to update Azure Monitor credentials")
}
queryJSONModel := dataquery.AzureMonitorQuery{}
err := json.Unmarshal(query.JSON, &queryJSONModel)
if err != nil {
return nil, err
}
if query.QueryType == dataquery.AzureQueryTypeAzureTraces {
if query.ResultFormat == dataquery.ResultFormatTrace && query.Query == "" {
return nil, fmt.Errorf("cannot visualise trace events using the trace visualiser")
}
}
req, err := e.createRequest(ctx, url, query)
if err != nil {
return nil, err
}
_, span := tracing.DefaultTracer().Start(ctx, "azure log analytics query", trace.WithAttributes(
attribute.String("target", query.Query),
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),
attribute.Int64("org_id", dsInfo.OrgID),
))
defer span.End()
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer func() {
if err := res.Body.Close(); err != nil {
e.Logger.Warn("Failed to close response body", "err", err)
}
}()
logResponse, err := e.unmarshalResponse(res)
if err != nil {
return nil, err
}
t, err := logResponse.GetPrimaryResultTable()
if err != nil {
return nil, err
}
frame, err := ResponseTableToFrame(t, query.RefID, query.Query, query.QueryType, query.ResultFormat)
if err != nil {
return nil, err
}
frame = appendErrorNotice(frame, logResponse.Error)
if frame == nil {
dataResponse := backend.DataResponse{}
return &dataResponse, nil
}
queryUrl, err := getQueryUrl(query.Query, query.Resources, dsInfo.Routes["Azure Portal"].URL, query.TimeRange)
if err != nil {
return nil, err
}
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 {
tsSchema := frame.TimeSeriesSchema()
if tsSchema.Type == data.TimeSeriesTypeLong {
wideFrame, err := data.LongToWide(frame, nil)
if err == nil {
frame = wideFrame
} else {
frame.AppendNotices(data.Notice{Severity: data.NoticeSeverityWarning, Text: "could not convert frame to time series, returning raw table: " + err.Error()})
}
}
}
// Use the parent span query for the parent span data link
err = addDataLinksToFields(query, dsInfo.Routes["Azure Portal"].URL, frame, dsInfo, queryUrl)
if err != nil {
return nil, err
}
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 err
}
return nil
}
if query.ResultFormat == dataquery.ResultFormatLogs {
return nil
}
AddConfigLinks(*frame, queryUrl, nil)
return nil
}
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{
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: ${__data.fields.traceID}",
URL: "",
Internal: &data.InternalDataLink{
DatasourceUID: dsInfo.DatasourceUID,
DatasourceName: dsInfo.DatasourceName,
Query: queryJSONModel,
},
})
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)
}
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 {
if err == nil {
return frame
}
if frame == nil {
frame = &data.Frame{}
}
frame.AppendNotices(apiErrorToNotice(err))
return frame
}
func (e *AzureLogAnalyticsDatasource) createRequest(ctx context.Context, queryURL string, query *AzureLogAnalyticsQuery) (*http.Request, error) {
body := map[string]interface{}{
"query": query.Query,
}
if query.DashboardTime {
from := query.TimeRange.From.Format(time.RFC3339)
to := query.TimeRange.To.Format(time.RFC3339)
timespan := fmt.Sprintf("%s/%s", from, to)
body["timespan"] = timespan
body["query_datetimescope_from"] = from
body["query_datetimescope_to"] = to
body["query_datetimescope_column"] = query.TimeColumn
}
if len(query.Resources) > 1 && query.QueryType == dataquery.AzureQueryTypeAzureLogAnalytics && !query.AppInsightsQuery {
str := strings.ToLower(query.Resources[0])
if strings.Contains(str, "microsoft.operationalinsights/workspaces") {
body["workspaces"] = query.Resources
} else {
body["resources"] = query.Resources
}
}
if query.AppInsightsQuery {
body["applications"] = query.Resources
}
jsonValue, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("%v: %w", "failed to create request", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, queryURL, bytes.NewBuffer(jsonValue))
if err != nil {
return nil, fmt.Errorf("%v: %w", "failed to create request", err)
}
req.URL.Path = "/"
req.Header.Set("Content-Type", "application/json")
req.URL.Path = path.Join(req.URL.Path, query.URL)
return req, nil
}
type AzureLogAnalyticsURLResources struct {
Resources []AzureLogAnalyticsURLResource `json:"resources"`
}
type AzureLogAnalyticsURLResource struct {
ResourceID string `json:"resourceId"`
}
func getQueryUrl(query string, resources []string, azurePortalUrl string, timeRange backend.TimeRange) (string, error) {
encodedQuery, err := encodeQuery(query)
if err != nil {
return "", fmt.Errorf("failed to encode the query: %s", err)
}
portalUrl := azurePortalUrl
if err != nil {
return "", fmt.Errorf("failed to parse base portal URL: %s", err)
}
portalUrl += "/#blade/Microsoft_OperationsManagementSuite_Workspace/AnalyticsBlade/initiator/AnalyticsShareLinkToQuery/isQueryEditorVisible/true/scope/"
resourcesJson := AzureLogAnalyticsURLResources{
Resources: make([]AzureLogAnalyticsURLResource, 0),
}
for _, resource := range resources {
resourcesJson.Resources = append(resourcesJson.Resources, AzureLogAnalyticsURLResource{
ResourceID: resource,
})
}
resourcesMarshalled, err := json.Marshal(resourcesJson)
if err != nil {
return "", fmt.Errorf("failed to marshal log analytics resources: %s", err)
}
from := timeRange.From.Format(time.RFC3339)
to := timeRange.To.Format(time.RFC3339)
timespan := url.QueryEscape(fmt.Sprintf("%s/%s", from, to))
portalUrl += url.QueryEscape(string(resourcesMarshalled))
portalUrl += "/query/" + url.PathEscape(encodedQuery) + "/isQueryBase64Compressed/true/timespan/" + timespan
return portalUrl, nil
}
func getTracesQueryUrl(resources []string, azurePortalUrl string) (string, error) {
portalUrl := azurePortalUrl
portalUrl += "/#view/AppInsightsExtension/DetailsV2Blade/ComponentId~/"
resource := struct {
ResourceId string `json:"ResourceId"`
}{
resources[0],
}
resourceMarshalled, err := json.Marshal(resource)
if err != nil {
return "", fmt.Errorf("failed to marshal application insights resource: %s", err)
}
portalUrl += url.PathEscape(string(resourceMarshalled))
portalUrl += "/DataModel~/"
// We're making use of data link variables to select the necessary fields in the frontend
eventId := "%22eventId%22%3A%22${__data.fields.itemId}%22%2C"
timestamp := "%22timestamp%22%3A%22${__data.fields.startTime}%22%2C"
eventTable := "%22eventTable%22%3A%22${__data.fields.itemType}%22"
traceObject := fmt.Sprintf("%%7B%s%s%s%%7D", eventId, timestamp, eventTable)
portalUrl += traceObject
return portalUrl, nil
}
func getCorrelationWorkspaces(ctx context.Context, baseResource string, resourcesMap map[string]bool, dsInfo types.DatasourceInfo, operationId string) (map[string]bool, error) {
azMonService := dsInfo.Services["Azure Monitor"]
correlationUrl := azMonService.URL + fmt.Sprintf("%s/providers/microsoft.insights/transactions/%s", baseResource, operationId)
callCorrelationAPI := func(url string) (AzureCorrelationAPIResponse, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer([]byte{}))
if err != nil {
return AzureCorrelationAPIResponse{}, fmt.Errorf("%v: %w", "failed to create request", err)
}
req.URL.Path = url
req.Header.Set("Content-Type", "application/json")
values := req.URL.Query()
values.Add("api-version", "2019-10-17-preview")
req.URL.RawQuery = values.Encode()
req.Method = "GET"
_, span := tracing.DefaultTracer().Start(ctx, "azure traces correlation request", trace.WithAttributes(
attribute.String("target", req.URL.String()),
attribute.Int64("datasource_id", dsInfo.DatasourceID),
attribute.Int64("org_id", dsInfo.OrgID),
))
defer span.End()
res, err := azMonService.HTTPClient.Do(req)
if err != nil {
return AzureCorrelationAPIResponse{}, err
}
body, err := io.ReadAll(res.Body)
if err != nil {
return AzureCorrelationAPIResponse{}, err
}
defer func() {
if err := res.Body.Close(); err != nil {
azMonService.Logger.Warn("Failed to close response body", "err", err)
}
}()
if res.StatusCode/100 != 2 {
return AzureCorrelationAPIResponse{}, fmt.Errorf("request failed, status: %s, body: %s", res.Status, string(body))
}
var data AzureCorrelationAPIResponse
d := json.NewDecoder(bytes.NewReader(body))
d.UseNumber()
err = d.Decode(&data)
if err != nil {
return AzureCorrelationAPIResponse{}, err
}
for _, resource := range data.Properties.Resources {
lowerCaseResource := strings.ToLower(resource)
if _, ok := resourcesMap[lowerCaseResource]; !ok {
resourcesMap[lowerCaseResource] = true
}
}
return data, nil
}
var nextLink *string
var correlationResponse AzureCorrelationAPIResponse
correlationResponse, err := callCorrelationAPI(correlationUrl)
if err != nil {
return nil, err
}
nextLink = correlationResponse.Properties.NextLink
for nextLink != nil {
correlationResponse, err := callCorrelationAPI(correlationUrl)
if err != nil {
return nil, err
}
nextLink = correlationResponse.Properties.NextLink
}
// Remove the base element as that's where the query is run anyway
delete(resourcesMap, strings.ToLower(baseResource))
return resourcesMap, nil
}
// Error definition has been inferred from real data and other model definitions like
// https://github.com/Azure/azure-sdk-for-go/blob/3640559afddbad452d265b54fb1c20b30be0b062/services/preview/virtualmachineimagebuilder/mgmt/2019-05-01-preview/virtualmachineimagebuilder/models.go
type AzureLogAnalyticsAPIError struct {
Details *[]AzureLogAnalyticsAPIErrorBase `json:"details,omitempty"`
Code *string `json:"code,omitempty"`
Message *string `json:"message,omitempty"`
}
type AzureLogAnalyticsAPIErrorBase struct {
Code *string `json:"code,omitempty"`
Message *string `json:"message,omitempty"`
Innererror *AzureLogAnalyticsInnerError `json:"innererror,omitempty"`
}
type AzureLogAnalyticsInnerError struct {
Code *string `json:"code,omitempty"`
Message *string `json:"message,omitempty"`
Severity *int `json:"severity,omitempty"`
SeverityName *string `json:"severityName,omitempty"`
}
// AzureLogAnalyticsResponse is the json response object from the Azure Log Analytics API.
type AzureLogAnalyticsResponse struct {
Tables []types.AzureResponseTable `json:"tables"`
Error *AzureLogAnalyticsAPIError `json:"error,omitempty"`
}
type AzureCorrelationAPIResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Properties AzureCorrelationAPIResponseProperties `json:"properties"`
Error *AzureLogAnalyticsAPIError `json:"error,omitempty"`
}
type AzureCorrelationAPIResponseProperties struct {
Resources []string `json:"resources"`
NextLink *string `json:"nextLink,omitempty"`
}
// GetPrimaryResultTable returns the first table in the response named "PrimaryResult", or an
// error if there is no table by that name.
func (ar *AzureLogAnalyticsResponse) GetPrimaryResultTable() (*types.AzureResponseTable, error) {
for _, t := range ar.Tables {
if t.Name == "PrimaryResult" {
return &t, nil
}
}
return nil, fmt.Errorf("no data as PrimaryResult table is missing from the response")
}
func (e *AzureLogAnalyticsDatasource) unmarshalResponse(res *http.Response) (AzureLogAnalyticsResponse, error) {
body, err := io.ReadAll(res.Body)
if err != nil {
return AzureLogAnalyticsResponse{}, err
}
defer func() {
if err := res.Body.Close(); err != nil {
e.Logger.Warn("Failed to close response body", "err", err)
}
}()
if res.StatusCode/100 != 2 {
return AzureLogAnalyticsResponse{}, fmt.Errorf("request failed, status: %s, body: %s", res.Status, string(body))
}
var data AzureLogAnalyticsResponse
d := json.NewDecoder(bytes.NewReader(body))
d.UseNumber()
err = d.Decode(&data)
if err != nil {
return AzureLogAnalyticsResponse{}, err
}
return data, nil
}
// LogAnalyticsMeta is a type for the a Frame's Meta's Custom property.
type LogAnalyticsMeta struct {
ColumnTypes []string `json:"azureColumnTypes"`
AzurePortalLink string `json:"azurePortalLink,omitempty"`
}
// encodeQuery encodes the query in gzip so the frontend can build links.
func encodeQuery(rawQuery string) (string, error) {
var b bytes.Buffer
gz := gzip.NewWriter(&b)
if _, err := gz.Write([]byte(rawQuery)); err != nil {
return "", err
}
if err := gz.Close(); err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(b.Bytes()), nil
}
func buildTracesQuery(operationId string, parentSpanID *string, traceTypes []string, filters []dataquery.AzureTracesFilter, resultFormat *dataquery.ResultFormat, resources []string) string {
types := traceTypes
if len(types) == 0 {
types = Tables
}
filteredTypes := make([]string, 0)
// If the result format is set to trace then we filter out all events that are of the type traces as they don't make sense when visualised as a span
if resultFormat != nil && *resultFormat == dataquery.ResultFormatTrace {
filteredTypes = slices.Filter(filteredTypes, types, func(s string) bool { return s != "traces" })
} else {
filteredTypes = types
}
sort.Strings(filteredTypes)
if len(filteredTypes) == 0 {
return ""
}
resourcesQuery := strings.Join(filteredTypes, ",")
if len(resources) > 0 {
intermediate := make([]string, 0)
for _, resource := range resources {
for _, table := range filteredTypes {
intermediate = append(intermediate, fmt.Sprintf("app('%s').%s", resource, table))
}
}
resourcesQuery += "," + strings.Join(intermediate, ",")
}
tagsMap := make(map[string]bool)
var tags []string
for _, t := range filteredTypes {
tableTags := getTagsForTable(t)
for _, i := range tableTags {
if tagsMap[i] {
continue
}
if i == "cloud_RoleInstance" || i == "cloud_RoleName" || i == "customDimensions" || i == "customMeasurements" {
continue
}
tags = append(tags, i)
tagsMap[i] = true
}
}
sort.Strings(tags)
whereClause := ""
if operationId != "" {
whereClause = fmt.Sprintf("| where (operation_Id != '' and operation_Id == '%s') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == '%s')", operationId, operationId)
}
parentWhereClause := ""
if parentSpanID != nil && *parentSpanID != "" {
parentWhereClause = fmt.Sprintf("| where (operation_ParentId != '' and operation_ParentId == '%s')", *parentSpanID)
}
filtersClause := ""
if len(filters) > 0 {
for _, filter := range filters {
if len(filter.Filters) == 0 {
continue
}
operation := "in"
if filter.Operation == "ne" {
operation = "!in"
}
filterValues := []string{}
for _, val := range filter.Filters {
filterValues = append(filterValues, fmt.Sprintf(`"%s"`, val))
}
filtersClause += fmt.Sprintf("| where %s %s (%s)", filter.Property, operation, strings.Join(filterValues, ","))
}
}
propertiesFunc := "bag_merge(customDimensions, customMeasurements)"
if len(tags) > 0 {
propertiesFunc = fmt.Sprintf("bag_merge(bag_pack_columns(%s), customDimensions, customMeasurements)", strings.Join(tags, ","))
}
errorProperty := `| extend error = todynamic(iff(itemType == "exception", "true", "false"))`
baseQuery := fmt.Sprintf(`set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true %s`, resourcesQuery)
propertiesStaticQuery := `| extend duration = iff(isnull(column_ifexists("duration", real(null))), toreal(0), column_ifexists("duration", real(null)))` +
`| extend spanID = iff(itemType == "pageView" or isempty(column_ifexists("id", "")), tostring(new_guid()), column_ifexists("id", ""))` +
`| extend operationName = iff(isempty(column_ifexists("name", "")), column_ifexists("problemId", ""), column_ifexists("name", ""))` +
`| extend serviceName = cloud_RoleName` +
`| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)`
propertiesQuery := fmt.Sprintf(`| extend tags = %s`, propertiesFunc)
projectClause := `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` +
`| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` +
`| order by startTime asc`
return baseQuery + whereClause + parentWhereClause + propertiesStaticQuery + errorProperty + propertiesQuery + filtersClause + projectClause
}
func buildTracesLogsQuery(operationId string, resources []string) string {
types := Tables
sort.Strings(types)
selectors := "union " + strings.Join(types, ",\n") + "\n"
if len(resources) > 0 {
intermediate := make([]string, 0)
for _, resource := range resources {
for _, table := range types {
intermediate = append(intermediate, fmt.Sprintf("app('%s').%s", resource, table))
}
}
sort.Strings(intermediate)
types = intermediate
selectors = strings.Join(append([]string{"union *"}, types...), ",\n") + "\n"
}
query := selectors
query += fmt.Sprintf(`| where operation_Id == "%s"`, operationId)
return query
}