diff --git a/CHANGELOG.md b/CHANGELOG.md index 39dc18a..1cc5694 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 2.0.0 (not released yet) +### ⭐ Added +- Add a query tag that includes relevant Grafana context information. + + ## 1.9.1 ### 🔨 Changed diff --git a/pkg/check_health.go b/pkg/check_health.go index a1e4550..26dfe88 100644 --- a/pkg/check_health.go +++ b/pkg/check_health.go @@ -4,6 +4,8 @@ import ( "context" "database/sql" "fmt" + "github.com/michelin/snowflake-grafana-datasource/pkg/data" + "github.com/michelin/snowflake-grafana-datasource/pkg/utils" "github.com/grafana/grafana-plugin-sdk-go/backend" _ "github.com/snowflakedb/gosnowflake" @@ -32,7 +34,7 @@ func (td *SnowflakeDatasource) CheckHealth(ctx context.Context, req *backend.Che } defer td.db.Close() - row, err := td.db.QueryContext(ctx, "SELECT 1") + row, err := td.db.QueryContext(utils.AddQueryTagInfos(ctx, &data.QueryConfigStruct{}), "SELECT 1") if err != nil { return &backend.CheckHealthResult{ Status: backend.HealthStatusError, diff --git a/pkg/data/type.go b/pkg/data/type.go new file mode 100644 index 0000000..013db63 --- /dev/null +++ b/pkg/data/type.go @@ -0,0 +1,51 @@ +package data + +import ( + "database/sql" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "time" +) + +type QueryResult struct { + Tables []Table +} + +// DataTable structure containing columns and rows +type Table struct { + Columns []*sql.ColumnType + Rows [][]interface{} +} + +const TimeSeriesType = "time series" + +func (qc *QueryConfigStruct) IsTimeSeriesType() bool { + return qc.QueryType == TimeSeriesType +} + +type QueryConfigStruct struct { + FinalQuery string + QueryType string + RawQuery string + TimeColumns []string + TimeRange backend.TimeRange + Interval time.Duration + MaxDataPoints int64 + FillMode string + FillValue float64 +} + +type QueryTagStruct struct { + PluginVersion string `json:"pluginVersion,omitempty"` + QueryType string `json:"queryType,omitempty"` + From string `json:"from,omitempty"` + To string `json:"to,omitempty"` + Grafana QueryTagGrafanaStruct `json:"grafana,omitempty"` +} + +type QueryTagGrafanaStruct struct { + Version string `json:"version,omitempty"` + Host string `json:"host,omitempty"` + OrgId int64 `json:"orgId,omitempty"` + User string `json:"user,omitempty"` + DatasourceId string `json:"datasourceId,omitempty"` +} diff --git a/pkg/macros.go b/pkg/macros.go index 5eb8db7..d3d1b27 100644 --- a/pkg/macros.go +++ b/pkg/macros.go @@ -2,6 +2,8 @@ package main import ( "fmt" + "github.com/michelin/snowflake-grafana-datasource/pkg/data" + "github.com/michelin/snowflake-grafana-datasource/pkg/utils" "math" "regexp" "strconv" @@ -30,7 +32,7 @@ func ReplaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]str return result + str[lastIndex:] } -func Interpolate(configStruct *queryConfigStruct) (string, error) { +func Interpolate(configStruct *data.QueryConfigStruct) (string, error) { rExp, _ := regexp.Compile(sExpr) var macroError error @@ -58,7 +60,7 @@ func Interpolate(configStruct *queryConfigStruct) (string, error) { return sql, nil } -func SetupFillmode(configStruct *queryConfigStruct, fillmode string) error { +func SetupFillmode(configStruct *data.QueryConfigStruct, fillmode string) error { switch fillmode { case "NULL": configStruct.FillMode = NullFill @@ -77,7 +79,7 @@ func SetupFillmode(configStruct *queryConfigStruct, fillmode string) error { } // evaluateMacro convert macro expression to sql expression -func evaluateMacro(name string, args []string, configStruct *queryConfigStruct) (string, error) { +func evaluateMacro(name string, args []string, configStruct *data.QueryConfigStruct) (string, error) { timeRange := configStruct.TimeRange switch name { @@ -146,7 +148,7 @@ func evaluateMacro(name string, args []string, configStruct *queryConfigStruct) if len(args) < 2 { return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name) } - interval, err := ParseInterval(strings.Trim(args[1], `'`)) + interval, err := utils.ParseInterval(strings.Trim(args[1], `'`)) if err != nil { return "", fmt.Errorf("error parsing interval %v", args[1]) } @@ -182,7 +184,7 @@ func evaluateMacro(name string, args []string, configStruct *queryConfigStruct) if len(args) < 2 { return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name) } - interval, err := ParseInterval(strings.Trim(args[1], `'`)) + interval, err := utils.ParseInterval(strings.Trim(args[1], `'`)) if err != nil { return "", fmt.Errorf("error parsing interval %v", args[1]) } diff --git a/pkg/macros_test.go b/pkg/macros_test.go index 0109d6c..a1e8e29 100644 --- a/pkg/macros_test.go +++ b/pkg/macros_test.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "github.com/michelin/snowflake-grafana-datasource/pkg/data" "testing" "time" @@ -16,14 +17,14 @@ func TestEvaluateMacro(t *testing.T) { To: time.Now().Add(time.Minute), } - configStruct := queryConfigStruct{ + configStruct := data.QueryConfigStruct{ TimeRange: timeRange, } tcs := []struct { args []string name string - config queryConfigStruct + config data.QueryConfigStruct response string err string fillMode string @@ -128,13 +129,13 @@ func TestEvaluateMacro(t *testing.T) { func TestInterpolate(t *testing.T) { tests := []struct { name string - configStruct *queryConfigStruct + configStruct *data.QueryConfigStruct expectedSQL string expectedError string }{ { name: "valid macro", - configStruct: &queryConfigStruct{ + configStruct: &data.QueryConfigStruct{ RawQuery: "SELECT * FROM table WHERE $__timeFilter(col)", TimeRange: backend.TimeRange{ From: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), @@ -146,7 +147,7 @@ func TestInterpolate(t *testing.T) { }, { name: "missing time column argument", - configStruct: &queryConfigStruct{ + configStruct: &data.QueryConfigStruct{ RawQuery: "SELECT * FROM table WHERE $__timeFilter()", }, expectedSQL: "", @@ -154,7 +155,7 @@ func TestInterpolate(t *testing.T) { }, { name: "valid snowflake system macro", - configStruct: &queryConfigStruct{ + configStruct: &data.QueryConfigStruct{ RawQuery: "SELECT SYSTEM$TYPEOF('a')", }, expectedSQL: "SELECT SYSTEM$TYPEOF('a')", @@ -162,7 +163,7 @@ func TestInterpolate(t *testing.T) { }, { name: "unknown macro", - configStruct: &queryConfigStruct{ + configStruct: &data.QueryConfigStruct{ RawQuery: "SELECT * FROM table WHERE $__unknownMacro(col)", }, expectedSQL: "", @@ -170,7 +171,7 @@ func TestInterpolate(t *testing.T) { }, { name: "check __timeRoundTo with default 15min", - configStruct: &queryConfigStruct{ + configStruct: &data.QueryConfigStruct{ RawQuery: "SELECT * FROM table WHERE $__timeRoundTo()", TimeRange: backend.TimeRange{ From: time.Date(2020, 1, 1, 0, 7, 0, 0, time.UTC), @@ -182,7 +183,7 @@ func TestInterpolate(t *testing.T) { }, { name: "check __timeRoundFrom with default 15min", - configStruct: &queryConfigStruct{ + configStruct: &data.QueryConfigStruct{ RawQuery: "SELECT * FROM table WHERE $__timeRoundFrom()", TimeRange: backend.TimeRange{ From: time.Date(2020, 1, 1, 0, 7, 0, 0, time.UTC), @@ -194,7 +195,7 @@ func TestInterpolate(t *testing.T) { }, { name: "check __timeRoundTo with 5min", - configStruct: &queryConfigStruct{ + configStruct: &data.QueryConfigStruct{ RawQuery: "SELECT * FROM table WHERE $__timeRoundTo(5)", TimeRange: backend.TimeRange{ From: time.Date(2020, 1, 1, 0, 7, 0, 0, time.UTC), @@ -206,7 +207,7 @@ func TestInterpolate(t *testing.T) { }, { name: "check __timeRoundFrom with 5min", - configStruct: &queryConfigStruct{ + configStruct: &data.QueryConfigStruct{ RawQuery: "SELECT * FROM table WHERE $__timeRoundFrom(5)", TimeRange: backend.TimeRange{ From: time.Date(2020, 1, 1, 0, 7, 0, 0, time.UTC), @@ -218,7 +219,7 @@ func TestInterpolate(t *testing.T) { }, { name: "check __timeRoundTo with 10min", - configStruct: &queryConfigStruct{ + configStruct: &data.QueryConfigStruct{ RawQuery: "SELECT * FROM table WHERE $__timeRoundTo(10)", TimeRange: backend.TimeRange{ From: time.Date(2020, 1, 1, 0, 7, 0, 0, time.UTC), @@ -230,7 +231,7 @@ func TestInterpolate(t *testing.T) { }, { name: "check __timeRoundFrom with 10min", - configStruct: &queryConfigStruct{ + configStruct: &data.QueryConfigStruct{ RawQuery: "SELECT * FROM table WHERE $__timeRoundFrom(10)", TimeRange: backend.TimeRange{ From: time.Date(2020, 1, 1, 0, 7, 0, 0, time.UTC), @@ -242,7 +243,7 @@ func TestInterpolate(t *testing.T) { }, { name: "check __timeRoundTo with 30min", - configStruct: &queryConfigStruct{ + configStruct: &data.QueryConfigStruct{ RawQuery: "SELECT * FROM table WHERE $__timeRoundTo(30)", TimeRange: backend.TimeRange{ From: time.Date(2020, 1, 1, 0, 7, 0, 0, time.UTC), @@ -254,7 +255,7 @@ func TestInterpolate(t *testing.T) { }, { name: "check __timeRoundFrom with 30min", - configStruct: &queryConfigStruct{ + configStruct: &data.QueryConfigStruct{ RawQuery: "SELECT * FROM table WHERE $__timeRoundFrom(30)", TimeRange: backend.TimeRange{ From: time.Date(2020, 1, 1, 0, 59, 0, 0, time.UTC), @@ -266,7 +267,7 @@ func TestInterpolate(t *testing.T) { }, { name: "check __timeRoundTo with 1440min", - configStruct: &queryConfigStruct{ + configStruct: &data.QueryConfigStruct{ RawQuery: "SELECT * FROM table WHERE $__timeRoundTo(1440)", TimeRange: backend.TimeRange{ From: time.Date(2020, 1, 1, 0, 7, 0, 0, time.UTC), @@ -278,7 +279,7 @@ func TestInterpolate(t *testing.T) { }, { name: "check __timeRoundFrom with 1440min", - configStruct: &queryConfigStruct{ + configStruct: &data.QueryConfigStruct{ RawQuery: "SELECT * FROM table WHERE $__timeRoundFrom(1440)", TimeRange: backend.TimeRange{ From: time.Date(2020, 1, 1, 0, 59, 0, 0, time.UTC), diff --git a/pkg/query.go b/pkg/query.go index d98dcad..3e18174 100644 --- a/pkg/query.go +++ b/pkg/query.go @@ -5,6 +5,8 @@ import ( "database/sql" "encoding/json" "fmt" + _data "github.com/michelin/snowflake-grafana-datasource/pkg/data" + "github.com/michelin/snowflake-grafana-datasource/pkg/utils" "math/big" "reflect" "strconv" @@ -18,24 +20,6 @@ import ( const rowLimit = 1000000 -const timeSeriesType = "time series" - -func (qc *queryConfigStruct) isTimeSeriesType() bool { - return qc.QueryType == timeSeriesType -} - -type queryConfigStruct struct { - FinalQuery string - QueryType string - RawQuery string - TimeColumns []string - TimeRange backend.TimeRange - Interval time.Duration - MaxDataPoints int64 - FillMode string - FillValue float64 -} - // type var boolean bool var tim time.Time @@ -57,7 +41,7 @@ type queryModel struct { FillMode string `json:"fillMode"` } -func (qc *queryConfigStruct) fetchData(ctx context.Context, config *pluginConfig, password string, privateKey string) (result DataQueryResult, err error) { +func fetchData(ctx context.Context, qc *_data.QueryConfigStruct, config *pluginConfig, password string, privateKey string) (result _data.QueryResult, err error) { connectionString := getConnectionString(config, password, privateKey) db, err := sql.Open("snowflake", connectionString) @@ -68,7 +52,7 @@ func (qc *queryConfigStruct) fetchData(ctx context.Context, config *pluginConfig defer db.Close() log.DefaultLogger.Info("Query", "finalQuery", qc.FinalQuery) - rows, err := db.QueryContext(ctx, qc.FinalQuery) + rows, err := db.QueryContext(utils.AddQueryTagInfos(ctx, qc), qc.FinalQuery) if err != nil { if strings.Contains(err.Error(), "000605") { log.DefaultLogger.Info("Query got cancelled", "query", qc.FinalQuery, "err", err) @@ -91,7 +75,7 @@ func (qc *queryConfigStruct) fetchData(ctx context.Context, config *pluginConfig return result, nil } - table := DataTable{ + table := _data.Table{ Columns: columnTypes, Rows: make([][]interface{}, 0), } @@ -101,7 +85,7 @@ func (qc *queryConfigStruct) fetchData(ctx context.Context, config *pluginConfig if rowCount > rowLimit { return result, fmt.Errorf("query row limit exceeded, limit %d", rowLimit) } - values, err := qc.transformQueryResult(columnTypes, rows) + values, err := transformQueryResult(*qc, columnTypes, rows) if err != nil { return result, err } @@ -118,7 +102,7 @@ func (qc *queryConfigStruct) fetchData(ctx context.Context, config *pluginConfig return result, nil } -func (qc *queryConfigStruct) transformQueryResult(columnTypes []*sql.ColumnType, rows *sql.Rows) ([]interface{}, error) { +func transformQueryResult(qc _data.QueryConfigStruct, columnTypes []*sql.ColumnType, rows *sql.Rows) ([]interface{}, error) { values := make([]interface{}, len(columnTypes)) valuePtrs := make([]interface{}, len(columnTypes)) @@ -137,7 +121,7 @@ func (qc *queryConfigStruct) transformQueryResult(columnTypes []*sql.ColumnType, log.DefaultLogger.Debug("Type", fmt.Sprintf("%T %v ", values[i], values[i]), columnTypes[i].DatabaseTypeName()) // Convert time columns when query mode is time series - if qc.isTimeSeriesType() && equalsIgnoreCase(qc.TimeColumns, columnTypes[i].Name()) && reflect.TypeOf(values[i]) == reflect.TypeOf(str) { + if qc.IsTimeSeriesType() && utils.EqualsIgnoreCase(qc.TimeColumns, columnTypes[i].Name()) && reflect.TypeOf(values[i]) == reflect.TypeOf(str) { if v, err := strconv.ParseFloat(values[i].(string), 64); err == nil { values[i] = time.Unix(int64(v), 0) } else { @@ -195,7 +179,7 @@ func (td *SnowflakeDatasource) query(ctx context.Context, dataQuery backend.Data return response } - queryConfig := queryConfigStruct{ + queryConfig := _data.QueryConfigStruct{ FinalQuery: qm.QueryText, RawQuery: qm.QueryText, TimeColumns: qm.TimeColumns, @@ -219,7 +203,7 @@ func (td *SnowflakeDatasource) query(ctx context.Context, dataQuery backend.Data queryConfig.FinalQuery = strings.TrimSuffix(strings.TrimSpace(queryConfig.FinalQuery), ";") frame := data.NewFrame("") - dataResponse, err := queryConfig.fetchData(ctx, &config, password, privateKey) + dataResponse, err := fetchData(ctx, &queryConfig, &config, password, privateKey) if err != nil { response.Error = err return response @@ -232,7 +216,7 @@ func (td *SnowflakeDatasource) query(ctx context.Context, dataQuery backend.Data return backend.DataResponse{} } // Check time column - if queryConfig.isTimeSeriesType() && equalsIgnoreCase(queryConfig.TimeColumns, column.Name()) { + if queryConfig.IsTimeSeriesType() && utils.EqualsIgnoreCase(queryConfig.TimeColumns, column.Name()) { if strings.EqualFold(column.Name(), "Time") { timeColumnIndex = i } @@ -269,17 +253,17 @@ func (td *SnowflakeDatasource) query(ctx context.Context, dataQuery backend.Data for j, row := range table.Rows { // Handle fill mode when the time column exist if timeColumnIndex != -1 { - fillTimesSeries(queryConfig, intervalStart, row[Max(int64(timeColumnIndex), 0)].(time.Time).UnixNano()/1e6, timeColumnIndex, frame, len(table.Columns), &count, previousRow(table.Rows, j)) + fillTimesSeries(queryConfig, intervalStart, row[utils.Max(int64(timeColumnIndex), 0)].(time.Time).UnixNano()/1e6, timeColumnIndex, frame, len(table.Columns), &count, utils.PreviousRow(table.Rows, j)) } // without fill mode for i, v := range row { - insertFrameField(frame, v, i) + utils.InsertFrameField(frame, v, i) } count++ } - fillTimesSeries(queryConfig, intervalStart, intervalEnd, timeColumnIndex, frame, len(table.Columns), &count, previousRow(table.Rows, len(table.Rows))) + fillTimesSeries(queryConfig, intervalStart, intervalEnd, timeColumnIndex, frame, len(table.Columns), &count, utils.PreviousRow(table.Rows, len(table.Rows))) } - if queryConfig.isTimeSeriesType() { + if queryConfig.IsTimeSeriesType() { frame, err = td.longToWide(frame, queryConfig, dataResponse) if err != nil { response.Error = err @@ -298,7 +282,7 @@ func (td *SnowflakeDatasource) query(ctx context.Context, dataQuery backend.Data return response } -func (td *SnowflakeDatasource) longToWide(frame *data.Frame, queryConfig queryConfigStruct, dataResponse DataQueryResult) (*data.Frame, error) { +func (td *SnowflakeDatasource) longToWide(frame *data.Frame, queryConfig _data.QueryConfigStruct, dataResponse _data.QueryResult) (*data.Frame, error) { tsSchema := frame.TimeSeriesSchema() if tsSchema.Type == data.TimeSeriesTypeLong { fillMode := &data.FillMissing{Mode: mapFillMode(queryConfig.FillMode), Value: queryConfig.FillValue} @@ -336,8 +320,8 @@ func mapFillMode(fillModeString string) data.FillMode { return fillMode } -func fillTimesSeries(queryConfig queryConfigStruct, intervalStart int64, intervalEnd int64, timeColumnIndex int, frame *data.Frame, columnSize int, count *int, previousRow []interface{}) { - if queryConfig.isTimeSeriesType() && queryConfig.FillMode != "" && timeColumnIndex != -1 { +func fillTimesSeries(queryConfig _data.QueryConfigStruct, intervalStart int64, intervalEnd int64, timeColumnIndex int, frame *data.Frame, columnSize int, count *int, previousRow []interface{}) { + if queryConfig.IsTimeSeriesType() && queryConfig.FillMode != "" && timeColumnIndex != -1 { for stepTime := intervalStart + queryConfig.Interval.Nanoseconds()/1e6*int64(*count); stepTime < intervalEnd; stepTime = stepTime + (queryConfig.Interval.Nanoseconds() / 1e6) { for i := 0; i < columnSize; i++ { if i == timeColumnIndex { @@ -352,9 +336,9 @@ func fillTimesSeries(queryConfig queryConfigStruct, intervalStart int64, interva frame.Fields[i].Append(nil) case PreviousFill: if previousRow == nil { - insertFrameField(frame, nil, i) + utils.InsertFrameField(frame, nil, i) } else { - insertFrameField(frame, previousRow[i], i) + utils.InsertFrameField(frame, previousRow[i], i) } default: } diff --git a/pkg/query_test.go b/pkg/query_test.go index e8ef4c5..6689da2 100644 --- a/pkg/query_test.go +++ b/pkg/query_test.go @@ -2,6 +2,7 @@ package main import ( "github.com/grafana/grafana-plugin-sdk-go/data" + _data "github.com/michelin/snowflake-grafana-datasource/pkg/data" sf "github.com/snowflakedb/gosnowflake" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -10,18 +11,18 @@ import ( ) func TestIsTimeSeriesType_TrueWhenQueryTypeIsTimeSeries(t *testing.T) { - qc := queryConfigStruct{QueryType: "time series"} - assert.True(t, qc.isTimeSeriesType()) + qc := _data.QueryConfigStruct{QueryType: "time series"} + assert.True(t, qc.IsTimeSeriesType()) } func TestIsTimeSeriesType_FalseWhenQueryTypeIsNotTimeSeries(t *testing.T) { - qc := queryConfigStruct{QueryType: "table"} - assert.False(t, qc.isTimeSeriesType()) + qc := _data.QueryConfigStruct{QueryType: "table"} + assert.False(t, qc.IsTimeSeriesType()) } func TestIsTimeSeriesType_FalseWhenQueryTypeIsEmpty(t *testing.T) { - qc := queryConfigStruct{QueryType: ""} - assert.False(t, qc.isTimeSeriesType()) + qc := _data.QueryConfigStruct{QueryType: ""} + assert.False(t, qc.IsTimeSeriesType()) } // Helper functions to create pointers @@ -44,10 +45,10 @@ func TestMapFillMode(t *testing.T) { func TestFillTimesSeries_AppendsCorrectTimeValues(t *testing.T) { frame := data.NewFrame("") frame.Fields = append(frame.Fields, data.NewField("time", nil, []*time.Time{})) - queryConfig := queryConfigStruct{ + queryConfig := _data.QueryConfigStruct{ Interval: time.Minute, FillMode: NullFill, - QueryType: timeSeriesType, + QueryType: _data.TimeSeriesType, } fillTimesSeries(queryConfig, 0, 60000, 0, frame, 1, new(int), nil) assert.Equal(t, 1, frame.Fields[0].Len()) @@ -58,11 +59,11 @@ func TestFillTimesSeries_AppendsFillValue(t *testing.T) { frame := data.NewFrame("") frame.Fields = append(frame.Fields, data.NewField("time", nil, []*time.Time{})) frame.Fields = append(frame.Fields, data.NewField("value", nil, []*float64{})) - queryConfig := queryConfigStruct{ + queryConfig := _data.QueryConfigStruct{ Interval: time.Minute, FillMode: ValueFill, FillValue: 42.0, - QueryType: timeSeriesType, + QueryType: _data.TimeSeriesType, } fillTimesSeries(queryConfig, 0, 60000, 0, frame, 2, new(int), nil) assert.Equal(t, 1, frame.Fields[1].Len()) @@ -73,10 +74,10 @@ func TestFillTimesSeries_AppendsNilForNullFill(t *testing.T) { frame := data.NewFrame("") frame.Fields = append(frame.Fields, data.NewField("time", nil, []*time.Time{})) frame.Fields = append(frame.Fields, data.NewField("value", nil, []*float64{})) - queryConfig := queryConfigStruct{ + queryConfig := _data.QueryConfigStruct{ Interval: time.Minute, FillMode: NullFill, - QueryType: timeSeriesType, + QueryType: _data.TimeSeriesType, } fillTimesSeries(queryConfig, 0, 60000, 0, frame, 2, new(int), nil) assert.Equal(t, 1, frame.Fields[1].Len()) @@ -87,10 +88,10 @@ func TestFillTimesSeries_AppendsPreviousValue(t *testing.T) { frame := data.NewFrame("") frame.Fields = append(frame.Fields, data.NewField("time", nil, []*time.Time{})) frame.Fields = append(frame.Fields, data.NewField("value", nil, []*float64{})) - queryConfig := queryConfigStruct{ + queryConfig := _data.QueryConfigStruct{ Interval: time.Minute, FillMode: PreviousFill, - QueryType: timeSeriesType, + QueryType: _data.TimeSeriesType, } previousRow := []interface{}{time.Unix(0, 0), 42.0} fillTimesSeries(queryConfig, 0, 60000, 0, frame, 2, new(int), previousRow) @@ -102,7 +103,7 @@ func TestFillTimesSeries_DoesNotAppendWhenNotTimeSeries(t *testing.T) { frame := data.NewFrame("") frame.Fields = append(frame.Fields, data.NewField("time", nil, []*time.Time{})) frame.Fields = append(frame.Fields, data.NewField("value", nil, []*float64{})) - queryConfig := queryConfigStruct{ + queryConfig := _data.QueryConfigStruct{ Interval: time.Minute, FillMode: NullFill, QueryType: "table", @@ -115,10 +116,10 @@ func TestAppendsNilWhenPreviousRowIsNil(t *testing.T) { frame := data.NewFrame("") frame.Fields = append(frame.Fields, data.NewField("time", nil, []*time.Time{})) frame.Fields = append(frame.Fields, data.NewField("value", nil, []*float64{})) - queryConfig := queryConfigStruct{ + queryConfig := _data.QueryConfigStruct{ Interval: time.Minute, FillMode: PreviousFill, - QueryType: timeSeriesType, + QueryType: _data.TimeSeriesType, } fillTimesSeries(queryConfig, 0, 60000, 0, frame, 2, new(int), nil) assert.Equal(t, 1, frame.Fields[1].Len()) diff --git a/pkg/type.go b/pkg/type.go deleted file mode 100644 index f02916d..0000000 --- a/pkg/type.go +++ /dev/null @@ -1,15 +0,0 @@ -package main - -import ( - "database/sql" -) - -type DataQueryResult struct { - Tables []DataTable -} - -// DataTable structure containing columns and rows -type DataTable struct { - Columns []*sql.ColumnType - Rows [][]interface{} -} diff --git a/pkg/gtime.go b/pkg/utils/gtime.go similarity index 99% rename from pkg/gtime.go rename to pkg/utils/gtime.go index 92350b5..c32d3a4 100644 --- a/pkg/gtime.go +++ b/pkg/utils/gtime.go @@ -1,4 +1,4 @@ -package main +package utils import ( "fmt" diff --git a/pkg/gtime_test.go b/pkg/utils/gtime_test.go similarity index 99% rename from pkg/gtime_test.go rename to pkg/utils/gtime_test.go index 9f55093..c42ea06 100644 --- a/pkg/gtime_test.go +++ b/pkg/utils/gtime_test.go @@ -1,4 +1,4 @@ -package main +package utils import ( "fmt" diff --git a/pkg/utils/queryInfo.go b/pkg/utils/queryInfo.go new file mode 100644 index 0000000..ad9e611 --- /dev/null +++ b/pkg/utils/queryInfo.go @@ -0,0 +1,61 @@ +package utils + +import ( + "context" + "encoding/json" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/michelin/snowflake-grafana-datasource/pkg/data" + "github.com/snowflakedb/gosnowflake" + "time" +) + +// AddQueryTagInfos Add Query Tag Infos to the context +func AddQueryTagInfos(ctx context.Context, qc *data.QueryConfigStruct) context.Context { + // Extract plugin config + pluginConfig := backend.PluginConfigFromContext(ctx) + + // User Agent + var grafanaVersion = "" + if pluginConfig.UserAgent != nil { + grafanaVersion = pluginConfig.UserAgent.GrafanaVersion() + } + + // Grafana Host + var grafanaHost = "" + if pluginConfig.GrafanaConfig != nil { + grafanaHost = pluginConfig.GrafanaConfig.Get("GF_APP_URL") + } + + // Datasource ID + var grafanaDatasourceID = "" + if pluginConfig.DataSourceInstanceSettings != nil { + grafanaDatasourceID = pluginConfig.DataSourceInstanceSettings.UID + } + + // User + var grafanaUser = "" + if pluginConfig.User != nil { + grafanaUser = pluginConfig.User.Login + } + + queryTagData := data.QueryTagStruct{ + PluginVersion: pluginConfig.PluginVersion, + QueryType: qc.QueryType, + From: qc.TimeRange.From.Format(time.RFC3339), + To: qc.TimeRange.To.Format(time.RFC3339), + Grafana: data.QueryTagGrafanaStruct{ + Version: grafanaVersion, + Host: grafanaHost, + OrgId: pluginConfig.OrgID, + User: grafanaUser, + DatasourceId: grafanaDatasourceID, + }, + } + queryTag, err := json.Marshal(queryTagData) + if err != nil { + log.DefaultLogger.Error("could not marshal json: %s\n", err) + return ctx + } + return gosnowflake.WithQueryTag(ctx, string(queryTag)) +} diff --git a/pkg/utils/queryInfo_test.go b/pkg/utils/queryInfo_test.go new file mode 100644 index 0000000..3583c5f --- /dev/null +++ b/pkg/utils/queryInfo_test.go @@ -0,0 +1,81 @@ +package utils + +import ( + "context" + "fmt" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/useragent" + "github.com/michelin/snowflake-grafana-datasource/pkg/data" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func TestAddQueryTagInfosWithValidPluginConfig(t *testing.T) { + ctx := context.Background() + + useragent, _ := useragent.New("8.0.0", "darwin", "amd64") + + config := map[string]string{ + "GF_APP_URL": "http://localhost:3000", + } + pluginConfig := &backend.PluginContext{ + PluginVersion: "1.0.0", + UserAgent: useragent, + GrafanaConfig: backend.NewGrafanaCfg(config), + OrgID: 1, + DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ + UID: "datasource-uid", + }, + User: &backend.User{ + Login: "test-user", + }, + } + + timeRange := backend.TimeRange{ + From: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + To: time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC), + } + + qc := &data.QueryConfigStruct{ + QueryType: "table", + TimeRange: timeRange, + FinalQuery: "SELECT * FROM test_table", + } + + ctx = backend.WithPluginContext(ctx, *pluginConfig) + ctx = AddQueryTagInfos(ctx, qc) + queryTag := fmt.Sprint(ctx) + expectedTag := `{"pluginVersion":"1.0.0","queryType":"table","from":"2024-01-01T00:00:00Z","to":"2024-01-02T00:00:00Z","grafana":{"version":"8.0.0","host":"http://localhost:3000","orgId":1,"user":"test-user","datasourceId":"datasource-uid"}}` + require.Contains(t, queryTag, expectedTag) +} + +func TestAddQueryTagInfosWithNilConfig(t *testing.T) { + ctx := context.Background() + + pluginConfig := &backend.PluginContext{ + PluginVersion: "1.0.0", + UserAgent: nil, + GrafanaConfig: nil, + OrgID: 1, + DataSourceInstanceSettings: nil, + User: nil, + } + + timeRange := backend.TimeRange{ + From: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + To: time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC), + } + + qc := &data.QueryConfigStruct{ + QueryType: "table", + TimeRange: timeRange, + FinalQuery: "SELECT * FROM test_table", + } + + ctx = backend.WithPluginContext(ctx, *pluginConfig) + ctx = AddQueryTagInfos(ctx, qc) + queryTag := fmt.Sprint(ctx) + expectedTag := `{"pluginVersion":"1.0.0","queryType":"table","from":"2024-01-01T00:00:00Z","to":"2024-01-02T00:00:00Z","grafana":{"orgId":1}}` + require.Contains(t, queryTag, expectedTag) +} diff --git a/pkg/utils.go b/pkg/utils/utils.go similarity index 80% rename from pkg/utils.go rename to pkg/utils/utils.go index f2ef088..680effe 100644 --- a/pkg/utils.go +++ b/pkg/utils/utils.go @@ -1,13 +1,12 @@ -package main +package utils import ( + "github.com/grafana/grafana-plugin-sdk-go/data" "strings" "time" - - "github.com/grafana/grafana-plugin-sdk-go/data" ) -func equalsIgnoreCase(s []string, str string) bool { +func EqualsIgnoreCase(s []string, str string) bool { for _, v := range s { if strings.EqualFold(v, str) { return true @@ -32,7 +31,7 @@ func Min(x, y int64) int64 { return y } -func insertFrameField(frame *data.Frame, value interface{}, index int) { +func InsertFrameField(frame *data.Frame, value interface{}, index int) { switch v := value.(type) { case string: frame.Fields[index].Append(&v) @@ -49,7 +48,7 @@ func insertFrameField(frame *data.Frame, value interface{}, index int) { } } -func previousRow(rows [][]interface{}, index int) []interface{} { +func PreviousRow(rows [][]interface{}, index int) []interface{} { if len(rows) > 0 { return rows[Max(int64(index-1), 0)] } diff --git a/pkg/utils_test.go b/pkg/utils/utils_test.go similarity index 88% rename from pkg/utils_test.go rename to pkg/utils/utils_test.go index 7be9d1c..6a3eefd 100644 --- a/pkg/utils_test.go +++ b/pkg/utils/utils_test.go @@ -1,4 +1,4 @@ -package main +package utils import ( "fmt" @@ -24,9 +24,9 @@ func TestContainsIgnoreCase(t *testing.T) { for i, tc := range tcs { t.Run(fmt.Sprintf("testcase %d", i), func(t *testing.T) { if tc.success { - require.True(t, equalsIgnoreCase(tc.array, tc.str)) + require.True(t, EqualsIgnoreCase(tc.array, tc.str)) } else { - require.False(t, equalsIgnoreCase(tc.array, tc.str)) + require.False(t, EqualsIgnoreCase(tc.array, tc.str)) } }) } @@ -70,7 +70,7 @@ func TestMax(t *testing.T) { func TestPreviousRowWithEmptyRows(t *testing.T) { rows := [][]interface{}{} - result := previousRow(rows, 1) + result := PreviousRow(rows, 1) require.Nil(t, result) } @@ -79,7 +79,7 @@ func TestPreviousRowWithNonEmptyRowsAndIndexZero(t *testing.T) { {"row1"}, {"row2"}, } - result := previousRow(rows, 0) + result := PreviousRow(rows, 0) require.Equal(t, rows[0], result) } @@ -89,7 +89,7 @@ func TestPreviousRowWithNonEmptyRowsAndIndexGreaterThanZero(t *testing.T) { {"row2"}, {"row3"}, } - result := previousRow(rows, 2) + result := PreviousRow(rows, 2) require.Equal(t, rows[1], result) } @@ -97,7 +97,7 @@ func TestAppendsStringValueToFrameField(t *testing.T) { frame := data.NewFrame("test") frame.Fields = append(frame.Fields, data.NewField("field1", nil, []*string{})) value := "testString" - insertFrameField(frame, value, 0) + InsertFrameField(frame, value, 0) require.Equal(t, &value, frame.Fields[0].At(0)) } @@ -105,7 +105,7 @@ func TestAppendsFloat64ValueToFrameField(t *testing.T) { frame := data.NewFrame("test") frame.Fields = append(frame.Fields, data.NewField("field1", nil, []*float64{})) value := float64(123.45) - insertFrameField(frame, value, 0) + InsertFrameField(frame, value, 0) require.Equal(t, &value, frame.Fields[0].At(0)) } @@ -113,7 +113,7 @@ func TestAppendsInt64ValueToFrameField(t *testing.T) { frame := data.NewFrame("test") frame.Fields = append(frame.Fields, data.NewField("field1", nil, []*int64{})) value := int64(123) - insertFrameField(frame, value, 0) + InsertFrameField(frame, value, 0) require.Equal(t, &value, frame.Fields[0].At(0)) } @@ -121,7 +121,7 @@ func TestAppendsBoolValueToFrameField(t *testing.T) { frame := data.NewFrame("test") frame.Fields = append(frame.Fields, data.NewField("field1", nil, []*bool{})) value := true - insertFrameField(frame, value, 0) + InsertFrameField(frame, value, 0) require.Equal(t, &value, frame.Fields[0].At(0)) } @@ -129,6 +129,6 @@ func TestAppendsTimeValueToFrameField(t *testing.T) { frame := data.NewFrame("test") frame.Fields = append(frame.Fields, data.NewField("field1", nil, []*time.Time{})) value := time.Now() - insertFrameField(frame, value, 0) + InsertFrameField(frame, value, 0) require.Equal(t, &value, frame.Fields[0].At(0)) }