Created
June 24, 2020 21:08
-
-
Save kylebrandt/bcd544df8f3071a5d09d1d6131015b36 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package azuremonitor | |
import ( | |
"encoding/json" | |
"fmt" | |
"time" | |
"github.com/grafana/grafana-plugin-sdk-go/data" | |
) | |
func (mr *MetricsResult) ToFrame(metric, agg string, dimensions []string) (*data.Frame, error) { | |
dimLen := len(dimensions) | |
frame := data.NewFrame("", data.NewField("StartTime", nil, []time.Time{})) | |
fieldIdxMap := map[string]int{} | |
rowCounter := 0 | |
for _, seg := range *mr.Value.Segments { | |
labels := data.Labels{} | |
handleInnerSegment := func(s MetricsSegmentInfo) error { | |
met, ok := s.AdditionalProperties[metric] | |
if !ok { | |
return fmt.Errorf("expected additional properties not found on inner segment while handling azure query") | |
} | |
metMap, ok := met.(map[string]interface{}) | |
if !ok { | |
return fmt.Errorf("unexpected type for additional properties not found on inner segment while handling azure query") | |
} | |
metVal, ok := metMap[agg] | |
if !ok { | |
return fmt.Errorf("expected aggregation value for aggregation %v not found on inner segment while handling azure query", agg) | |
} | |
if dimLen != 0 { | |
key := dimensions[len(dimensions)-1] | |
val, ok := s.AdditionalProperties[key] | |
if !ok { | |
return fmt.Errorf("unexpected dimension/segment key %v not found in response", key) | |
} | |
sVal, ok := val.(string) | |
if !ok { | |
return fmt.Errorf("unexpected dimension/segment value for key %v in response", key) | |
} | |
labels[key] = sVal | |
} | |
if _, ok := fieldIdxMap[labels.String()]; !ok { | |
frame.Fields = append(frame.Fields, data.NewField(metric, labels.Copy(), make([]*float64, 1))) | |
fieldIdxMap[labels.String()] = len(frame.Fields) - 1 | |
} | |
var v *float64 | |
if val, ok := metVal.(float64); ok { | |
v = &val | |
} | |
frame.Set(fieldIdxMap[labels.String()], rowCounter, v) | |
return nil | |
} | |
// Simple case with no Segments/Dimensions | |
if len(dimensions) == 0 { | |
frame.Extend(1) | |
frame.Set(0, rowCounter, seg.Start) | |
err := handleInnerSegment(seg) | |
rowCounter++ | |
if err != nil { | |
return nil, err | |
} | |
continue | |
} | |
// Case with Segments/Dimensions | |
next := &seg | |
// decend (fast forward) to the next nested MetricsSegmentInfo by moving the 'next' pointer | |
decend := func(dim string) error { | |
if next == nil || next.Segments == nil || len(*next.Segments) == 0 { | |
return fmt.Errorf("unexpected insights response while handling dimension %s", dim) | |
} | |
next = &(*next.Segments)[0] | |
return nil | |
} | |
if dimLen > 1 { | |
if err := decend("root-level"); err != nil { | |
return nil, err | |
} | |
} | |
// When multiple dimensions are requests, there are nested MetricsSegmentInfo objects | |
// The higher levels just contain all the dimension key-value pairs except the last. | |
// So we fast forward to the depth that has the last tag pair and the metric values | |
// collect tags along the way | |
for i := 0; i < dimLen-1; i++ { | |
segStr := dimensions[i] | |
labels[segStr] = next.AdditionalProperties[segStr].(string) | |
if i != dimLen-2 { // the last dimension/segment will be in same []MetricsSegmentInfo slice as the metric value | |
if err := decend(string(dimensions[i])); err != nil { | |
return nil, err | |
} | |
} | |
} | |
if next == nil { | |
return nil, fmt.Errorf("unexpected dimension in insights response") | |
} | |
frame.Extend(1) | |
frame.Set(0, rowCounter, seg.Start) | |
for _, innerSeg := range *next.Segments { | |
err := handleInnerSegment(innerSeg) | |
if err != nil { | |
return nil, err | |
} | |
} | |
rowCounter++ | |
} | |
return frame, nil | |
} | |
// MetricsResult a metric result. | |
type MetricsResult struct { | |
Value *MetricsResultInfo `json:"value,omitempty"` | |
} | |
// MetricsResultInfo a metric result data. | |
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 a metric segment | |
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 MetricsResultInfo struct. | |
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. | |
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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package azuremonitor | |
import ( | |
"encoding/json" | |
"os" | |
"path/filepath" | |
"testing" | |
"time" | |
"github.com/google/go-cmp/cmp" | |
"github.com/grafana/grafana-plugin-sdk-go/data" | |
"github.com/stretchr/testify/require" | |
"github.com/xorcare/pointer" | |
) | |
func TestInsightsMetricsToFrame(t *testing.T) { | |
tests := []struct { | |
name string | |
testFile string | |
metric string | |
agg string | |
dimensions []string | |
expectedFrame func() *data.Frame | |
}{ | |
{ | |
name: "single series", | |
testFile: "applicationinsights/4-application-insights-response-metrics-no-segment.json", | |
metric: "value", | |
agg: "avg", | |
expectedFrame: func() *data.Frame { | |
frame := data.NewFrame("", | |
data.NewField("StartTime", nil, []time.Time{ | |
time.Date(2019, 9, 13, 1, 2, 3, 456789000, time.UTC), | |
time.Date(2019, 9, 13, 2, 2, 3, 456789000, time.UTC), | |
}), | |
data.NewField("value", nil, []*float64{ | |
pointer.Float64(1), | |
pointer.Float64(2), | |
}), | |
) | |
return frame | |
}, | |
}, | |
{ | |
name: "segmented series", | |
testFile: "applicationinsights/4-application-insights-response-metrics-segmented.json", | |
metric: "value", | |
agg: "avg", | |
dimensions: []string{"blob"}, | |
expectedFrame: func() *data.Frame { | |
frame := data.NewFrame("", | |
data.NewField("StartTime", nil, []time.Time{ | |
time.Date(2019, 9, 13, 1, 2, 3, 456789000, time.UTC), | |
time.Date(2019, 9, 13, 2, 2, 3, 456789000, time.UTC), | |
}), | |
data.NewField("value", data.Labels{"blob": "a"}, []*float64{ | |
pointer.Float64(1), | |
pointer.Float64(2), | |
}), | |
data.NewField("value", data.Labels{"blob": "b"}, []*float64{ | |
pointer.Float64(3), | |
pointer.Float64(4), | |
}), | |
) | |
return frame | |
}, | |
}, | |
} | |
for _, tt := range tests { | |
t.Run(tt.name, func(t *testing.T) { | |
res, err := loadInsightsMetricsResponse(tt.testFile) | |
require.NoError(t, err) | |
frame, err := res.ToFrame(tt.metric, tt.agg, tt.dimensions) | |
require.NoError(t, err) | |
if diff := cmp.Diff(tt.expectedFrame(), frame, data.FrameTestCompareOptions()...); diff != "" { | |
t.Errorf("Result mismatch (-want +got):\n%s", diff) | |
} | |
}) | |
} | |
} | |
func loadInsightsMetricsResponse(name string) (MetricsResult, error) { | |
var mr MetricsResult | |
path := filepath.Join("testdata", name) | |
f, err := os.Open(path) | |
if err != nil { | |
return mr, err | |
} | |
defer f.Close() | |
d := json.NewDecoder(f) | |
err = d.Decode(&mr) | |
return mr, err | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment