mirror of
https://github.com/grafana/grafana.git
synced 2024-11-29 20:24:18 -06:00
fb5ff6a70f
fixes #26473
315 lines
9.7 KiB
Go
315 lines
9.7 KiB
Go
package azuremonitor
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
|
)
|
|
|
|
// InsightsMetricsResultToFrame converts a MetricsResult (an Application Insights metrics query response) to a dataframe.
|
|
// Due to the dynamic nature of the MetricsResult object, the name of the metric, aggregation,
|
|
// and requested dimensions are used to determine the expected shape of the object.
|
|
// This builds all series into a single data.Frame with one time index (a wide formatted time series frame).
|
|
func InsightsMetricsResultToFrame(mr MetricsResult, metric, agg string, dimensions []string) (*data.Frame, error) {
|
|
dimLen := len(dimensions)
|
|
|
|
// The Response has both Start and End times, so we name the column "StartTime".
|
|
frame := data.NewFrame("", data.NewField("StartTime", nil, []time.Time{}))
|
|
|
|
fieldIdxMap := map[string]int{} // a map of a string representation of the labels to the Field index in the frame.
|
|
|
|
rowCounter := 0 // row in the resulting frame
|
|
|
|
if mr.Value == nil { // never seen this response, but to ensure there is no panic
|
|
return nil, fmt.Errorf("unexpected nil response or response value in metrics result")
|
|
}
|
|
|
|
for _, seg := range *mr.Value.Segments { // each top level segment in the response shares timestamps.
|
|
frame.Extend(1)
|
|
frame.Set(0, rowCounter, seg.Start) // field 0 is the time field
|
|
labels := data.Labels{}
|
|
|
|
// handleLeafSegment is for the leaf MetricsSegmentInfo nodes in the response.
|
|
// A leaf node contains an aggregated value, and when there are multiple dimensions, a label key/value pair.
|
|
handleLeafSegment := func(s MetricsSegmentInfo) error {
|
|
// since this is a dynamic response, everything we are interested in here from JSON
|
|
// is Marshalled (mapped) into the AdditionalProperties property.
|
|
v, err := valFromLeafAP(s.AdditionalProperties, metric, agg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if dimLen != 0 { // when there are dimensions, the final dimension is in this inner segment.
|
|
dimension := dimensions[dimLen-1]
|
|
dimVal, err := dimValueFromAP(s.AdditionalProperties, dimension)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
labels[dimension] = dimVal
|
|
}
|
|
|
|
if _, ok := fieldIdxMap[labels.String()]; !ok {
|
|
// When we find a new combination of labels for the metric, a new Field is appended.
|
|
frame.Fields = append(frame.Fields, data.NewField(metric, labels.Copy(), make([]*float64, rowCounter+1)))
|
|
fieldIdxMap[labels.String()] = len(frame.Fields) - 1
|
|
}
|
|
|
|
frame.Set(fieldIdxMap[labels.String()], rowCounter, v)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Simple case with no segments/dimensions
|
|
if dimLen == 0 {
|
|
if err := handleLeafSegment(seg); err != nil {
|
|
return nil, err
|
|
}
|
|
rowCounter++
|
|
continue
|
|
}
|
|
|
|
// Multiple dimension case
|
|
var traverse func(segments *[]MetricsSegmentInfo, depth int) error
|
|
|
|
// traverse walks segments collecting dimensions into labels until leaf segments are
|
|
// reached, and then handleInnerSegment is called. The final k/v label pair is
|
|
// in the leaf segment.
|
|
// A non-recursive implementation would probably be better.
|
|
traverse = func(segments *[]MetricsSegmentInfo, depth int) error {
|
|
if segments == nil {
|
|
return nil
|
|
}
|
|
for _, seg := range *segments {
|
|
if seg.Segments == nil {
|
|
if err := handleLeafSegment(seg); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
dimension := dimensions[depth]
|
|
dimVal, err := dimValueFromAP(seg.AdditionalProperties, dimension)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
labels[dimension] = dimVal
|
|
if err := traverse(seg.Segments, depth+1); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if err := traverse(seg.Segments, 0); err != nil {
|
|
return nil, err
|
|
}
|
|
rowCounter++
|
|
}
|
|
|
|
if len(frame.Fields) == 1 { // No data, only a time column, no sort
|
|
return frame, nil
|
|
}
|
|
|
|
if err := data.SortWideFrameFields(frame, dimensions...); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return frame, nil
|
|
}
|
|
|
|
// valFromLeafAP extracts value for the given metric and aggregation (agg)
|
|
// from the dynamic AdditionalProperties properties of a leaf node. It is for use in the InsightsMetricsResultToFrame
|
|
// function.
|
|
func valFromLeafAP(ap map[string]interface{}, metric, agg string) (*float64, error) {
|
|
if ap == nil {
|
|
return nil, fmt.Errorf("expected additional properties for metric %v not found in leaf segment", metric)
|
|
}
|
|
met, ok := ap[metric]
|
|
if !ok {
|
|
return nil, fmt.Errorf("expected additional properties for metric %v not found in leaf segment", metric)
|
|
}
|
|
|
|
metMap, ok := met.(map[string]interface{})
|
|
if !ok {
|
|
return nil, fmt.Errorf("unexpected type for additional properties not found in leaf segment, want map[string]interface{}, but got %T", met)
|
|
}
|
|
metVal, ok := metMap[agg]
|
|
if !ok {
|
|
return nil, fmt.Errorf("expected value for aggregation %v not found in leaf segment", agg)
|
|
}
|
|
var v *float64
|
|
if val, ok := metVal.(float64); ok {
|
|
v = &val
|
|
}
|
|
|
|
return v, nil
|
|
}
|
|
|
|
// dimValueFromAP fetches the value as a string for the corresponding dimension from the dynamic AdditionalProperties properties of a leaf node. It is for use in the InsightsMetricsResultToFrame
|
|
// function.
|
|
func dimValueFromAP(ap map[string]interface{}, dimension string) (string, error) {
|
|
rawDimValue, ok := ap[dimension]
|
|
if !ok {
|
|
return "", fmt.Errorf("expected dimension key %v not found in response", dimension)
|
|
}
|
|
dimValue, ok := rawDimValue.(string)
|
|
if !ok {
|
|
return "", fmt.Errorf("unexpected non-string value for the value for dimension %v, got type %T with a value of %v", dimension, rawDimValue, dimValue)
|
|
}
|
|
return dimValue, nil
|
|
}
|
|
|
|
// MetricsResult a metric result.
|
|
// This is copied from azure-sdk-for-go/services/preview/appinsights/v1/insights.
|
|
type MetricsResult struct {
|
|
Value *MetricsResultInfo `json:"value,omitempty"`
|
|
}
|
|
|
|
// MetricsResultInfo a metric result data.
|
|
// This is copied from azure-sdk-for-go/services/preview/appinsights/v1/insights (except time Type is changed).
|
|
type MetricsResultInfo struct {
|
|
// AdditionalProperties - Unmatched properties from the message are deserialized this collection
|
|
AdditionalProperties map[string]interface{} `json:""`
|
|
// Start - Start time of the metric.
|
|
Start time.Time `json:"start,omitempty"`
|
|
// End - Start time of the metric.
|
|
End time.Time `json:"end,omitempty"`
|
|
// Interval - The interval used to segment the metric data.
|
|
Interval *string `json:"interval,omitempty"`
|
|
// Segments - Segmented metric data (if segmented).
|
|
Segments *[]MetricsSegmentInfo `json:"segments,omitempty"`
|
|
}
|
|
|
|
// MetricsSegmentInfo is a metric segment.
|
|
// This is copied from azure-sdk-for-go/services/preview/appinsights/v1/insights (except time Type is changed).
|
|
type MetricsSegmentInfo struct {
|
|
// AdditionalProperties - Unmatched properties from the message are deserialized this collection
|
|
AdditionalProperties map[string]interface{} `json:""`
|
|
// Start - Start time of the metric segment (only when an interval was specified).
|
|
Start time.Time `json:"start,omitempty"`
|
|
// End - Start time of the metric segment (only when an interval was specified).
|
|
End time.Time `json:"end,omitempty"`
|
|
// Segments - Segmented metric data (if further segmented).
|
|
Segments *[]MetricsSegmentInfo `json:"segments,omitempty"`
|
|
}
|
|
|
|
// UnmarshalJSON is the custom unmarshaler for MetricsSegmentInfo struct.
|
|
// This is copied from azure-sdk-for-go/services/preview/appinsights/v1/insights (except time Type is changed).
|
|
func (mri *MetricsSegmentInfo) UnmarshalJSON(body []byte) error {
|
|
var m map[string]*json.RawMessage
|
|
err := json.Unmarshal(body, &m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for k, v := range m {
|
|
switch k {
|
|
default:
|
|
if v != nil {
|
|
var additionalProperties interface{}
|
|
err = json.Unmarshal(*v, &additionalProperties)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if mri.AdditionalProperties == nil {
|
|
mri.AdditionalProperties = make(map[string]interface{})
|
|
}
|
|
mri.AdditionalProperties[k] = additionalProperties
|
|
}
|
|
case "start":
|
|
if v != nil {
|
|
var start time.Time
|
|
err = json.Unmarshal(*v, &start)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mri.Start = start
|
|
}
|
|
case "end":
|
|
if v != nil {
|
|
var end time.Time
|
|
err = json.Unmarshal(*v, &end)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mri.End = end
|
|
}
|
|
case "segments":
|
|
if v != nil {
|
|
var segments []MetricsSegmentInfo
|
|
err = json.Unmarshal(*v, &segments)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mri.Segments = &segments
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UnmarshalJSON is the custom unmarshaler for MetricsResultInfo struct.
|
|
// This is copied from azure-sdk-for-go/services/preview/appinsights/v1/insights (except time Type is changed).
|
|
func (mri *MetricsResultInfo) UnmarshalJSON(body []byte) error {
|
|
var m map[string]*json.RawMessage
|
|
err := json.Unmarshal(body, &m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for k, v := range m {
|
|
switch k {
|
|
default:
|
|
if v != nil {
|
|
var additionalProperties interface{}
|
|
err = json.Unmarshal(*v, &additionalProperties)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if mri.AdditionalProperties == nil {
|
|
mri.AdditionalProperties = make(map[string]interface{})
|
|
}
|
|
mri.AdditionalProperties[k] = additionalProperties
|
|
}
|
|
case "start":
|
|
if v != nil {
|
|
var start time.Time
|
|
err = json.Unmarshal(*v, &start)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mri.Start = start
|
|
}
|
|
case "end":
|
|
if v != nil {
|
|
var end time.Time
|
|
err = json.Unmarshal(*v, &end)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mri.End = end
|
|
}
|
|
case "interval":
|
|
if v != nil {
|
|
var interval string
|
|
err = json.Unmarshal(*v, &interval)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mri.Interval = &interval
|
|
}
|
|
case "segments":
|
|
if v != nil {
|
|
var segments []MetricsSegmentInfo
|
|
err = json.Unmarshal(*v, &segments)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mri.Segments = &segments
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|