mirror of https://github.com/grafana/grafana
Azure Monitor: Log Analytics response to data frames (#25297)
Co-authored-by: Ryan McKinley <ryantxu@gmail.com>pull/25382/head^2
parent
c3549f845e
commit
ef61a64c46
@ -0,0 +1,181 @@ |
||||
package azuremonitor |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"strconv" |
||||
"time" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data" |
||||
) |
||||
|
||||
// LogTableToFrame converts an AzureLogAnalyticsTable to a data.Frame.
|
||||
func LogTableToFrame(table *AzureLogAnalyticsTable) (*data.Frame, error) { |
||||
converterFrame, err := converterFrameForTable(table) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
for rowIdx, row := range table.Rows { |
||||
for fieldIdx, field := range row { |
||||
err = converterFrame.Set(fieldIdx, rowIdx, field) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
} |
||||
return converterFrame.Frame, nil |
||||
} |
||||
|
||||
func converterFrameForTable(t *AzureLogAnalyticsTable) (*data.FrameInputConverter, error) { |
||||
converters := []data.FieldConverter{} |
||||
colNames := make([]string, len(t.Columns)) |
||||
colTypes := make([]string, len(t.Columns)) // for metadata
|
||||
|
||||
for i, col := range t.Columns { |
||||
colNames[i] = col.Name |
||||
colTypes[i] = col.Type |
||||
converter, ok := converterMap[col.Type] |
||||
if !ok { |
||||
return nil, fmt.Errorf("unsupported analytics column type %v", col.Type) |
||||
} |
||||
converters = append(converters, converter) |
||||
} |
||||
|
||||
fic, err := data.NewFrameInputConverter(converters, len(t.Rows)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
err = fic.Frame.SetFieldNames(colNames...) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
fic.Frame.Meta = &data.FrameMeta{ |
||||
Custom: map[string]interface{}{"azureColumnTypes": colTypes}, |
||||
} |
||||
|
||||
return fic, nil |
||||
} |
||||
|
||||
var converterMap = map[string]data.FieldConverter{ |
||||
"string": stringConverter, |
||||
"guid": stringConverter, |
||||
"timespan": stringConverter, |
||||
"dynamic": stringConverter, |
||||
"datetime": timeConverter, |
||||
"int": intConverter, |
||||
"long": longConverter, |
||||
"real": realConverter, |
||||
"bool": boolConverter, |
||||
} |
||||
|
||||
var stringConverter = data.FieldConverter{ |
||||
OutputFieldType: data.FieldTypeNullableString, |
||||
Converter: func(v interface{}) (interface{}, error) { |
||||
var as *string |
||||
if v == nil { |
||||
return as, nil |
||||
} |
||||
s, ok := v.(string) |
||||
if !ok { |
||||
return nil, fmt.Errorf("unexpected type, expected string but got %T", v) |
||||
} |
||||
as = &s |
||||
return as, nil |
||||
}, |
||||
} |
||||
|
||||
var timeConverter = data.FieldConverter{ |
||||
OutputFieldType: data.FieldTypeNullableTime, |
||||
Converter: func(v interface{}) (interface{}, error) { |
||||
var at *time.Time |
||||
if v == nil { |
||||
return at, nil |
||||
} |
||||
s, ok := v.(string) |
||||
if !ok { |
||||
return nil, fmt.Errorf("unexpected type, expected string but got %T", v) |
||||
} |
||||
t, err := time.Parse(time.RFC3339Nano, s) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &t, nil |
||||
}, |
||||
} |
||||
|
||||
var realConverter = data.FieldConverter{ |
||||
OutputFieldType: data.FieldTypeNullableFloat64, |
||||
Converter: func(v interface{}) (interface{}, error) { |
||||
var af *float64 |
||||
if v == nil { |
||||
return af, nil |
||||
} |
||||
jN, ok := v.(json.Number) |
||||
if !ok { |
||||
return nil, fmt.Errorf("unexpected type, expected json.Number but got %T", v) |
||||
} |
||||
f, err := jN.Float64() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &f, err |
||||
}, |
||||
} |
||||
|
||||
var boolConverter = data.FieldConverter{ |
||||
OutputFieldType: data.FieldTypeNullableBool, |
||||
Converter: func(v interface{}) (interface{}, error) { |
||||
var ab *bool |
||||
if v == nil { |
||||
return ab, nil |
||||
} |
||||
b, ok := v.(bool) |
||||
if !ok { |
||||
return nil, fmt.Errorf("unexpected type, expected bool but got %T", v) |
||||
} |
||||
return &b, nil |
||||
}, |
||||
} |
||||
|
||||
var intConverter = data.FieldConverter{ |
||||
OutputFieldType: data.FieldTypeNullableInt32, |
||||
Converter: func(v interface{}) (interface{}, error) { |
||||
var ai *int32 |
||||
if v == nil { |
||||
return ai, nil |
||||
} |
||||
jN, ok := v.(json.Number) |
||||
if !ok { |
||||
return nil, fmt.Errorf("unexpected type, expected json.Number but got %T", v) |
||||
} |
||||
var err error |
||||
iv, err := strconv.ParseInt(jN.String(), 10, 32) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
aInt := int32(iv) |
||||
return &aInt, nil |
||||
}, |
||||
} |
||||
|
||||
var longConverter = data.FieldConverter{ |
||||
OutputFieldType: data.FieldTypeNullableInt64, |
||||
Converter: func(v interface{}) (interface{}, error) { |
||||
var ai *int64 |
||||
if v == nil { |
||||
return ai, nil |
||||
} |
||||
jN, ok := v.(json.Number) |
||||
if !ok { |
||||
return nil, fmt.Errorf("unexpected type, expected json.Number but got %T", v) |
||||
} |
||||
out, err := jN.Int64() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &out, err |
||||
}, |
||||
} |
||||
@ -0,0 +1,153 @@ |
||||
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 TestLogTableToFrame(t *testing.T) { |
||||
tests := []struct { |
||||
name string |
||||
testFile string |
||||
expectedFrame func() *data.Frame |
||||
}{ |
||||
{ |
||||
name: "single series", |
||||
testFile: "loganalytics/1-log-analytics-response-metrics-single-series.json", |
||||
expectedFrame: func() *data.Frame { |
||||
frame := data.NewFrame("", |
||||
data.NewField("TimeGenerated", nil, []*time.Time{ |
||||
pointer.Time(time.Date(2020, 4, 19, 19, 16, 6, 5e8, time.UTC)), |
||||
pointer.Time(time.Date(2020, 4, 19, 19, 16, 16, 5e8, time.UTC)), |
||||
pointer.Time(time.Date(2020, 4, 19, 19, 16, 26, 5e8, time.UTC)), |
||||
}), |
||||
data.NewField("Computer", nil, []*string{ |
||||
pointer.String("grafana-vm"), |
||||
pointer.String("grafana-vm"), |
||||
pointer.String("grafana-vm"), |
||||
}), |
||||
data.NewField("avg_CounterValue", nil, []*float64{ |
||||
pointer.Float64(1.1), |
||||
pointer.Float64(2.2), |
||||
pointer.Float64(3.3), |
||||
}), |
||||
) |
||||
frame.Meta = &data.FrameMeta{ |
||||
Custom: map[string]interface{}{"azureColumnTypes": []string{"datetime", "string", "real"}}, |
||||
} |
||||
return frame |
||||
}, |
||||
}, |
||||
{ |
||||
name: "response table", |
||||
testFile: "loganalytics/6-log-analytics-response-table.json", |
||||
expectedFrame: func() *data.Frame { |
||||
frame := data.NewFrame("", |
||||
data.NewField("TenantId", nil, []*string{ |
||||
pointer.String("a2c1b44e-3e57-4410-b027-6cc0ae6dee67"), |
||||
pointer.String("a2c1b44e-3e57-4410-b027-6cc0ae6dee67"), |
||||
pointer.String("a2c1b44e-3e57-4410-b027-6cc0ae6dee67"), |
||||
}), |
||||
data.NewField("Computer", nil, []*string{ |
||||
pointer.String("grafana-vm"), |
||||
pointer.String("grafana-vm"), |
||||
pointer.String("grafana-vm"), |
||||
}), |
||||
data.NewField("ObjectName", nil, []*string{ |
||||
pointer.String("Memory"), |
||||
pointer.String("Memory"), |
||||
pointer.String("Memory"), |
||||
}), |
||||
data.NewField("CounterName", nil, []*string{ |
||||
pointer.String("Available MBytes Memory"), |
||||
pointer.String("Available MBytes Memory"), |
||||
pointer.String("Available MBytes Memory"), |
||||
}), |
||||
data.NewField("InstanceName", nil, []*string{ |
||||
pointer.String("Memory"), |
||||
pointer.String("Memory"), |
||||
pointer.String("Memory"), |
||||
}), |
||||
data.NewField("Min", nil, []*float64{nil, nil, nil}), |
||||
data.NewField("Max", nil, []*float64{nil, nil, nil}), |
||||
data.NewField("SampleCount", nil, []*int32{nil, nil, nil}), |
||||
data.NewField("CounterValue", nil, []*float64{ |
||||
pointer.Float64(2040), |
||||
pointer.Float64(2066), |
||||
pointer.Float64(2066), |
||||
}), |
||||
data.NewField("TimeGenerated", nil, []*time.Time{ |
||||
pointer.Time(time.Date(2020, 4, 23, 11, 46, 3, 857e6, time.UTC)), |
||||
pointer.Time(time.Date(2020, 4, 23, 11, 46, 13, 857e6, time.UTC)), |
||||
pointer.Time(time.Date(2020, 4, 23, 11, 46, 23, 857e6, time.UTC)), |
||||
}), |
||||
) |
||||
frame.Meta = &data.FrameMeta{ |
||||
Custom: map[string]interface{}{"azureColumnTypes": []string{"string", "string", "string", |
||||
"string", "string", "real", "real", "int", "real", "datetime"}}, |
||||
} |
||||
return frame |
||||
}, |
||||
}, |
||||
{ |
||||
name: "all supported field types", |
||||
testFile: "loganalytics/7-log-analytics-all-types-table.json", |
||||
expectedFrame: func() *data.Frame { |
||||
frame := data.NewFrame("", |
||||
data.NewField("XBool", nil, []*bool{pointer.Bool(true)}), |
||||
data.NewField("XString", nil, []*string{pointer.String("Grafana")}), |
||||
data.NewField("XDateTime", nil, []*time.Time{pointer.Time(time.Date(2006, 1, 2, 22, 4, 5, 1*1e8, time.UTC))}), |
||||
data.NewField("XDynamic", nil, []*string{pointer.String(`[{"person":"Daniel"},{"cats":23},{"diagnosis":"cat problem"}]`)}), |
||||
data.NewField("XGuid", nil, []*string{pointer.String("74be27de-1e4e-49d9-b579-fe0b331d3642")}), |
||||
data.NewField("XInt", nil, []*int32{pointer.Int32(2147483647)}), |
||||
data.NewField("XLong", nil, []*int64{pointer.Int64(9223372036854775807)}), |
||||
data.NewField("XReal", nil, []*float64{pointer.Float64(1.797693134862315708145274237317043567981e+308)}), |
||||
data.NewField("XTimeSpan", nil, []*string{pointer.String("00:00:00.0000001")}), |
||||
) |
||||
frame.Meta = &data.FrameMeta{ |
||||
Custom: map[string]interface{}{"azureColumnTypes": []string{"bool", "string", "datetime", |
||||
"dynamic", "guid", "int", "long", "real", "timespan"}}, |
||||
} |
||||
return frame |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
res, err := loadLogAnalyticsTestFileWithNumber(tt.testFile) |
||||
require.NoError(t, err) |
||||
frame, err := LogTableToFrame(&res.Tables[0]) |
||||
require.NoError(t, err) |
||||
|
||||
if diff := cmp.Diff(tt.expectedFrame(), frame, data.FrameTestCompareOptions()...); diff != "" { |
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff) |
||||
} |
||||
|
||||
}) |
||||
} |
||||
|
||||
} |
||||
|
||||
func loadLogAnalyticsTestFileWithNumber(name string) (AzureLogAnalyticsResponse, error) { |
||||
var data AzureLogAnalyticsResponse |
||||
|
||||
path := filepath.Join("testdata", name) |
||||
f, err := os.Open(path) |
||||
if err != nil { |
||||
return data, err |
||||
} |
||||
defer f.Close() |
||||
d := json.NewDecoder(f) |
||||
d.UseNumber() |
||||
err = d.Decode(&data) |
||||
return data, err |
||||
} |
||||
@ -0,0 +1,59 @@ |
||||
{ |
||||
"tables": [ |
||||
{ |
||||
"name": "PrimaryResult", |
||||
"columns": [ |
||||
{ |
||||
"name": "XBool", |
||||
"type": "bool" |
||||
}, |
||||
{ |
||||
"name": "XString", |
||||
"type": "string" |
||||
}, |
||||
{ |
||||
"name": "XDateTime", |
||||
"type": "datetime" |
||||
}, |
||||
{ |
||||
"name": "XDynamic", |
||||
"type": "dynamic" |
||||
}, |
||||
{ |
||||
"name": "XGuid", |
||||
"type": "guid" |
||||
}, |
||||
{ |
||||
"name": "XInt", |
||||
"type": "int" |
||||
}, |
||||
{ |
||||
"name": "XLong", |
||||
"type": "long" |
||||
}, |
||||
{ |
||||
"name": "XReal", |
||||
"type": "real" |
||||
}, |
||||
{ |
||||
"name": "XTimeSpan", |
||||
"type": "timespan" |
||||
} |
||||
], |
||||
"rows": [ |
||||
[ |
||||
true, |
||||
"Grafana", |
||||
"2006-01-02T22:04:05.1Z", |
||||
"[{\"person\":\"Daniel\"},{\"cats\":23},{\"diagnosis\":\"cat problem\"}]", |
||||
"74be27de-1e4e-49d9-b579-fe0b331d3642", |
||||
2147483647, |
||||
9223372036854775807, |
||||
1.7976931348623157e+308, |
||||
"00:00:00.0000001" |
||||
] |
||||
] |
||||
} |
||||
] |
||||
} |
||||
|
||||
Loading…
Reference in new issue