package loganalytics import ( "compress/flate" "compress/gzip" "fmt" "io" "regexp" "strconv" "strings" "time" "github.com/andybalholm/brotli" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/tsdb/azuremonitor/kinds/dataquery" ) func AddCustomDataLink(frame data.Frame, dataLink data.DataLink, singleField bool) data.Frame { for i := range frame.Fields { if frame.Fields[i].Config == nil { frame.Fields[i].Config = &data.FieldConfig{} } frame.Fields[i].Config.Links = append(frame.Fields[i].Config.Links, dataLink) // Queries using the trace viz only need the link added to a single field if singleField { break } } return frame } const SingleField bool = true const MultiField bool = false func AddConfigLinks(frame data.Frame, dl string, title *string) data.Frame { linkTitle := "View query in Azure Portal" if title != nil { linkTitle = *title } deepLink := data.DataLink{ Title: linkTitle, TargetBlank: true, URL: dl, } frame = AddCustomDataLink(frame, deepLink, MultiField) return frame } // Check whether a query should be handled as basic logs query // 1. resource selected is a workspace // 2. query is not an alerts query // 3. number of selected resources is exactly one // 4. the ds toggle is set to true func meetsBasicLogsCriteria(resources []string, fromAlert bool, basicLogsEnabled bool) (bool, error) { if fromAlert { return false, backend.DownstreamError(fmt.Errorf("basic Logs queries cannot be used for alerts")) } if len(resources) != 1 { return false, backend.DownstreamError(fmt.Errorf("basic logs queries cannot be run against multiple resources")) } if !strings.Contains(strings.ToLower(resources[0]), "microsoft.operationalinsights/workspaces") { return false, backend.DownstreamError(fmt.Errorf("basic logs queries may only be run against Log Analytics workspaces")) } if !basicLogsEnabled { return false, backend.DownstreamError(fmt.Errorf("basic Logs queries are disabled for this data source")) } return true, nil } // This function should be part of migration function func ParseResultFormat(queryResultFormat *dataquery.ResultFormat, queryType dataquery.AzureQueryType) dataquery.ResultFormat { if queryResultFormat != nil && *queryResultFormat != "" { return *queryResultFormat } if queryType == dataquery.AzureQueryTypeLogAnalytics { // Default to time series format for logs queries. It was time series before this change return dataquery.ResultFormatTimeSeries } if queryType == dataquery.AzureQueryTypeAzureTraces { // Default to table format for traces queries as many traces may be returned return dataquery.ResultFormatTable } return "" } func getApiURL(resourceOrWorkspace string, isAppInsightsQuery bool, basicLogsQuery bool) string { matchesResourceURI, _ := regexp.MatchString("^/subscriptions/", resourceOrWorkspace) queryOrSearch := "query" if basicLogsQuery { queryOrSearch = "search" } if matchesResourceURI { if isAppInsightsQuery { componentName := resourceOrWorkspace[strings.LastIndex(resourceOrWorkspace, "/")+1:] return fmt.Sprintf("v1/apps/%s/query", componentName) } return fmt.Sprintf("v1%s/%s", resourceOrWorkspace, queryOrSearch) } else { return fmt.Sprintf("v1/workspaces/%s/%s", resourceOrWorkspace, queryOrSearch) } } // 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. func retrieveResources(query dataquery.AzureLogsQuery) ([]string, string) { resources := []string{} var resourceOrWorkspace string if len(query.Resources) > 0 { resources = query.Resources resourceOrWorkspace = query.Resources[0] } else if query.Resource != nil && *query.Resource != "" { resources = []string{*query.Resource} resourceOrWorkspace = *query.Resource } else if query.Workspace != nil { resourceOrWorkspace = *query.Workspace } return resources, resourceOrWorkspace } func ConvertTime(timeStamp string) (time.Time, error) { // Convert the timestamp string to an int64 timestampInt, err := strconv.ParseInt(timeStamp, 10, 64) if err != nil { // Handle error return time.Time{}, err } // Convert the Unix timestamp (in milliseconds) to a time.Time convTimeStamp := time.Unix(0, timestampInt*int64(time.Millisecond)) return convTimeStamp, nil } func GetDataVolumeRawQuery(table string) string { return fmt.Sprintf("Usage \n| where DataType == \"%s\"\n| where IsBillable == true\n| summarize BillableDataGB = round(sum(Quantity) / 1000, 3)", table) } // This function handles various compression mechanisms that may have been used on a response body func decode(encoding string, original io.ReadCloser) ([]byte, error) { var reader io.Reader var err error switch encoding { case "gzip": reader, err = gzip.NewReader(original) if err != nil { return nil, err } defer func() { if err := reader.(io.ReadCloser).Close(); err != nil { backend.Logger.Warn("Failed to close reader body", "err", err) } }() case "deflate": reader = flate.NewReader(original) defer func() { if err := reader.(io.ReadCloser).Close(); err != nil { backend.Logger.Warn("Failed to close reader body", "err", err) } }() case "br": reader = brotli.NewReader(original) case "": reader = original default: return nil, fmt.Errorf("unexpected encoding type %v", err) } body, err := io.ReadAll(reader) if err != nil { return nil, err } return body, nil }