| 
									
										
										
										
											2023-03-30 00:01:18 +08:00
										 |  |  | package loganalytics | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							| 
									
										
										
										
											2025-04-02 22:15:25 +08:00
										 |  |  | 	"compress/flate" | 
					
						
							|  |  |  | 	"compress/gzip" | 
					
						
							| 
									
										
										
										
											2024-05-11 00:11:54 +08:00
										 |  |  | 	"fmt" | 
					
						
							| 
									
										
										
										
											2025-04-02 22:15:25 +08:00
										 |  |  | 	"io" | 
					
						
							| 
									
										
										
										
											2024-05-11 00:11:54 +08:00
										 |  |  | 	"regexp" | 
					
						
							| 
									
										
										
										
											2024-05-29 01:06:27 +08:00
										 |  |  | 	"strconv" | 
					
						
							| 
									
										
										
										
											2024-05-11 00:11:54 +08:00
										 |  |  | 	"strings" | 
					
						
							| 
									
										
										
										
											2024-05-29 01:06:27 +08:00
										 |  |  | 	"time" | 
					
						
							| 
									
										
										
										
											2024-05-11 00:11:54 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-02 22:15:25 +08:00
										 |  |  | 	"github.com/andybalholm/brotli" | 
					
						
							| 
									
										
										
										
											2024-12-06 23:38:09 +08:00
										 |  |  | 	"github.com/grafana/grafana-plugin-sdk-go/backend" | 
					
						
							| 
									
										
										
										
											2023-03-30 00:01:18 +08:00
										 |  |  | 	"github.com/grafana/grafana-plugin-sdk-go/data" | 
					
						
							| 
									
										
										
										
											2024-05-11 00:11:54 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/tsdb/azuremonitor/kinds/dataquery" | 
					
						
							| 
									
										
										
										
											2023-03-30 00:01:18 +08:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-26 21:10:19 +08:00
										 |  |  | func AddCustomDataLink(frame data.Frame, dataLink data.DataLink, singleField bool) data.Frame { | 
					
						
							| 
									
										
										
										
											2023-03-30 00:01:18 +08:00
										 |  |  | 	for i := range frame.Fields { | 
					
						
							|  |  |  | 		if frame.Fields[i].Config == nil { | 
					
						
							|  |  |  | 			frame.Fields[i].Config = &data.FieldConfig{} | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-04-28 03:24:11 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		frame.Fields[i].Config.Links = append(frame.Fields[i].Config.Links, dataLink) | 
					
						
							| 
									
										
										
										
											2025-06-26 21:10:19 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Queries using the trace viz only need the link added to a single field
 | 
					
						
							|  |  |  | 		if singleField { | 
					
						
							|  |  |  | 			break | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-04-28 03:24:11 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 	return frame | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-26 21:10:19 +08:00
										 |  |  | const SingleField bool = true | 
					
						
							|  |  |  | const MultiField bool = false | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-28 03:24:11 +08:00
										 |  |  | func AddConfigLinks(frame data.Frame, dl string, title *string) data.Frame { | 
					
						
							| 
									
										
										
										
											2024-03-21 02:39:00 +08:00
										 |  |  | 	linkTitle := "View query in Azure Portal" | 
					
						
							| 
									
										
										
										
											2023-04-28 03:24:11 +08:00
										 |  |  | 	if title != nil { | 
					
						
							|  |  |  | 		linkTitle = *title | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	deepLink := data.DataLink{ | 
					
						
							|  |  |  | 		Title:       linkTitle, | 
					
						
							|  |  |  | 		TargetBlank: true, | 
					
						
							|  |  |  | 		URL:         dl, | 
					
						
							| 
									
										
										
										
											2023-03-30 00:01:18 +08:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2023-04-28 03:24:11 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-26 21:10:19 +08:00
										 |  |  | 	frame = AddCustomDataLink(frame, deepLink, MultiField) | 
					
						
							| 
									
										
										
										
											2023-04-28 03:24:11 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-30 00:01:18 +08:00
										 |  |  | 	return frame | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2024-05-11 00:11:54 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-29 01:06:27 +08:00
										 |  |  | // Check whether a query should be handled as basic logs query
 | 
					
						
							| 
									
										
										
										
											2024-09-10 03:35:56 +08:00
										 |  |  | // 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) { | 
					
						
							| 
									
										
										
										
											2024-05-29 01:06:27 +08:00
										 |  |  | 	if fromAlert { | 
					
						
							| 
									
										
										
										
											2024-12-06 23:38:09 +08:00
										 |  |  | 		return false, backend.DownstreamError(fmt.Errorf("basic Logs queries cannot be used for alerts")) | 
					
						
							| 
									
										
										
										
											2024-05-29 01:06:27 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 	if len(resources) != 1 { | 
					
						
							| 
									
										
										
										
											2024-12-06 23:38:09 +08:00
										 |  |  | 		return false, backend.DownstreamError(fmt.Errorf("basic logs queries cannot be run against multiple resources")) | 
					
						
							| 
									
										
										
										
											2024-05-29 01:06:27 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if !strings.Contains(strings.ToLower(resources[0]), "microsoft.operationalinsights/workspaces") { | 
					
						
							| 
									
										
										
										
											2024-12-06 23:38:09 +08:00
										 |  |  | 		return false, backend.DownstreamError(fmt.Errorf("basic logs queries may only be run against Log Analytics workspaces")) | 
					
						
							| 
									
										
										
										
											2024-09-10 03:35:56 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if !basicLogsEnabled { | 
					
						
							| 
									
										
										
										
											2024-12-06 23:38:09 +08:00
										 |  |  | 		return false, backend.DownstreamError(fmt.Errorf("basic Logs queries are disabled for this data source")) | 
					
						
							| 
									
										
										
										
											2024-05-29 01:06:27 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return true, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-20 07:30:03 +08:00
										 |  |  | // This function should be part of migration function
 | 
					
						
							| 
									
										
										
										
											2024-05-11 00:11:54 +08:00
										 |  |  | func ParseResultFormat(queryResultFormat *dataquery.ResultFormat, queryType dataquery.AzureQueryType) dataquery.ResultFormat { | 
					
						
							| 
									
										
										
										
											2024-09-20 07:30:03 +08:00
										 |  |  | 	if queryResultFormat != nil && *queryResultFormat != "" { | 
					
						
							|  |  |  | 		return *queryResultFormat | 
					
						
							| 
									
										
										
										
											2024-05-11 00:11:54 +08:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-01-14 22:58:38 +08:00
										 |  |  | 	if queryType == dataquery.AzureQueryTypeLogAnalytics { | 
					
						
							| 
									
										
										
										
											2024-09-20 07:30:03 +08:00
										 |  |  | 		// 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 | 
					
						
							| 
									
										
										
										
											2024-05-11 00:11:54 +08:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-09-20 07:30:03 +08:00
										 |  |  | 	return "" | 
					
						
							| 
									
										
										
										
											2024-05-11 00:11:54 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-29 01:06:27 +08:00
										 |  |  | func getApiURL(resourceOrWorkspace string, isAppInsightsQuery bool, basicLogsQuery bool) string { | 
					
						
							| 
									
										
										
										
											2024-05-11 00:11:54 +08:00
										 |  |  | 	matchesResourceURI, _ := regexp.MatchString("^/subscriptions/", resourceOrWorkspace) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-29 01:06:27 +08:00
										 |  |  | 	queryOrSearch := "query" | 
					
						
							|  |  |  | 	if basicLogsQuery { | 
					
						
							|  |  |  | 		queryOrSearch = "search" | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-11 00:11:54 +08:00
										 |  |  | 	if matchesResourceURI { | 
					
						
							|  |  |  | 		if isAppInsightsQuery { | 
					
						
							|  |  |  | 			componentName := resourceOrWorkspace[strings.LastIndex(resourceOrWorkspace, "/")+1:] | 
					
						
							|  |  |  | 			return fmt.Sprintf("v1/apps/%s/query", componentName) | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-05-29 01:06:27 +08:00
										 |  |  | 		return fmt.Sprintf("v1%s/%s", resourceOrWorkspace, queryOrSearch) | 
					
						
							| 
									
										
										
										
											2024-05-11 00:11:54 +08:00
										 |  |  | 	} else { | 
					
						
							| 
									
										
										
										
											2024-05-29 01:06:27 +08:00
										 |  |  | 		return fmt.Sprintf("v1/workspaces/%s/%s", resourceOrWorkspace, queryOrSearch) | 
					
						
							| 
									
										
										
										
											2024-05-11 00:11:54 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // 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 | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2024-05-29 01:06:27 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 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) | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2025-04-02 22:15:25 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | // 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 | 
					
						
							|  |  |  | } |