diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index 2569c480cfc..d10491950ef 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -50,6 +50,12 @@ func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response { return ApiError(500, "Metric request error", err) } + for _, res := range resp.Results { + if res.Error != nil { + res.ErrorString = res.Error.Error() + } + } + return Json(200, &resp) } diff --git a/pkg/models/test_data.go b/pkg/models/test_data.go index 314f09965c9..f56694cfd12 100644 --- a/pkg/models/test_data.go +++ b/pkg/models/test_data.go @@ -6,14 +6,13 @@ type InsertSqlTestDataCommand struct { } type SqlTestData struct { - Id int64 - Metric1 string - Metric2 string - ValueBigInt int64 - ValueDouble float64 - ValueFloat float32 - ValueInt int - TimeEpoch int64 - TimeDateTime time.Time - TimeTimeStamp time.Time + Id int64 + Metric1 string + Metric2 string + ValueBigInt int64 + ValueDouble float64 + ValueFloat float32 + ValueInt int + TimeEpoch int64 + TimeDateTime time.Time } diff --git a/pkg/services/sqlstore/migrations/stats_mig.go b/pkg/services/sqlstore/migrations/stats_mig.go index 467e35316cd..7e10eeb9f90 100644 --- a/pkg/services/sqlstore/migrations/stats_mig.go +++ b/pkg/services/sqlstore/migrations/stats_mig.go @@ -46,8 +46,8 @@ func addTestDataMigrations(mg *Migrator) { {Name: "value_float", Type: DB_Float, Nullable: true}, {Name: "value_int", Type: DB_Int, Nullable: true}, {Name: "time_epoch", Type: DB_BigInt, Nullable: false}, - {Name: "time_datetime", Type: DB_DateTime, Nullable: false}, - {Name: "time_timestamp", Type: DB_TimeStamp, Nullable: false}, + {Name: "time_date_time", Type: DB_DateTime, Nullable: false}, + {Name: "time_time_stamp", Type: DB_TimeStamp, Nullable: false}, }, } diff --git a/pkg/services/sqlstore/sql_test_data.go b/pkg/services/sqlstore/sql_test_data.go index ea70c30877b..f6c57428d20 100644 --- a/pkg/services/sqlstore/sql_test_data.go +++ b/pkg/services/sqlstore/sql_test_data.go @@ -1,6 +1,7 @@ package sqlstore import ( + "math/rand" "time" "github.com/grafana/grafana/pkg/bus" @@ -11,23 +12,53 @@ func init() { bus.AddHandler("sql", InsertSqlTestData) } -func InsertSqlTestData(cmd *m.InsertSqlTestDataCommand) error { - return inTransaction2(func(sess *session) error { +func sqlRandomWalk(m1 string, m2 string, intWalker int64, floatWalker float64, sess *session) error { - row := &m.SqlTestData{ - Metric1: "server1", - Metric2: "frontend", - ValueBigInt: 123123, - ValueDouble: 3.14159265359, - ValueFloat: 3.14159265359, - TimeEpoch: time.Now().Unix(), - TimeDateTime: time.Now(), - } + timeWalker := time.Now().Add(time.Hour * -1) + now := time.Now() + step := time.Minute + row := &m.SqlTestData{ + Metric1: m1, + Metric2: m2, + TimeEpoch: timeWalker.Unix(), + TimeDateTime: timeWalker, + } + + for timeWalker.Unix() < now.Unix() { + timeWalker = timeWalker.Add(step) + + row.Id = 0 + row.ValueBigInt += rand.Int63n(100) - 100 + row.ValueDouble += rand.Float64() - 0.5 + row.ValueFloat += rand.Float32() - 0.5 + row.TimeEpoch = timeWalker.Unix() + row.TimeDateTime = timeWalker + + sqlog.Info("Writing SQL test data row") if _, err := sess.Table("test_data").Insert(row); err != nil { return err } + } - return nil + return nil +} + +func InsertSqlTestData(cmd *m.InsertSqlTestDataCommand) error { + return inTransaction2(func(sess *session) error { + var err error + + sqlog.Info("SQL TestData: Clearing previous test data") + res, err := sess.Exec("TRUNCATE test_data") + if err != nil { + return err + } + + rows, _ := res.RowsAffected() + sqlog.Info("SQL TestData: Truncate done", "rows", rows) + + sqlRandomWalk("server1", "frontend", 100, 1.123, sess) + + return err }) } diff --git a/pkg/tsdb/models.go b/pkg/tsdb/models.go index ade9b1c75f2..838767dd5d9 100644 --- a/pkg/tsdb/models.go +++ b/pkg/tsdb/models.go @@ -45,9 +45,10 @@ func (br *BatchResult) WithError(err error) *BatchResult { } type QueryResult struct { - Error error `json:"error"` - RefId string `json:"refId"` - Series TimeSeriesSlice `json:"series"` + Error error `json:"-"` + ErrorString string `json:"error"` + RefId string `json:"refId"` + Series TimeSeriesSlice `json:"series"` } type TimeSeries struct { diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index 912150a5d14..ff85d8b1810 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -4,9 +4,12 @@ import ( "context" "database/sql" "fmt" + "strconv" "sync" + "github.com/go-xorm/core" "github.com/go-xorm/xorm" + "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/tsdb" @@ -74,19 +77,14 @@ func (e *MysqlExecutor) initEngine() error { } func (e *MysqlExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult { - result := &tsdb.BatchResult{} + result := &tsdb.BatchResult{ + QueryResults: make(map[string]*tsdb.QueryResult), + } session := e.engine.NewSession() defer session.Close() - db := session.DB() - // queries := strings.Split(req.Query, ";") - // - // data := dataStruct{} - // data.Results = make([]resultsStruct, 1) - // data.Results[0].Series = make([]seriesStruct, 0) - for _, query := range queries { rawSql := query.Model.Get("rawSql").MustString() if rawSql == "" { @@ -100,118 +98,119 @@ func (e *MysqlExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, co } defer rows.Close() - columnNames, err := rows.Columns() + result.QueryResults[query.RefId] = e.TransformToTimeSeries(query, rows) + } + + for _, value := range result.QueryResults { + if value.Error != nil { + e.log.Error("error", "error", value.Error) + } + } + + return result +} + +func (e MysqlExecutor) TransformToTimeSeries(query *tsdb.Query, rows *core.Rows) *tsdb.QueryResult { + result := &tsdb.QueryResult{RefId: query.RefId} + pointsBySeries := make(map[string]*tsdb.TimeSeries) + columnNames, err := rows.Columns() + + if err != nil { + result.Error = err + return result + } + + rowData := NewStringStringScan(columnNames) + for rows.Next() { + err := rowData.Update(rows.Rows) if err != nil { + e.log.Error("Mysql response parsing", "error", err) result.Error = err return result } - rc := NewStringStringScan(columnNames) - for rows.Next() { - err := rc.Update(rows.Rows) - if err != nil { - e.log.Error("Mysql response parsing", "error", err) - result.Error = err - return result - } - - rowValues := rc.Get() - e.log.Info("Rows", "row", rowValues) + if rowData.metric == "" { + rowData.metric = "Unknown" } - // for rows.Next() { - // columnValues := make([]interface{}, len(columnNames)) - // - // err = rows.ScanSlice(&columnValues) - // if err != nil { - // result.Error = err - // return result - // } - // - // // bytes -> string - // for i := range columnValues { - // rowType := reflect.TypeOf(columnValues[i]) - // e.log.Info("row", "type", rowType) - // - // rawValue := reflect.Indirect(reflect.ValueOf(columnValues[i])) - // - // // if rawValue is null then ignore - // if rawValue.Interface() == nil { - // continue - // } - // - // rawValueType := reflect.TypeOf(rawValue.Interface()) - // vv := reflect.ValueOf(rawValue.Interface()) - // e.log.Info("column type", "name", columnNames[i], "type", rawValueType, "vv", vv) - // } - // } + e.log.Info("Rows", "metric", rowData.metric, "time", rowData.time, "value", rowData.value) + + if !rowData.time.Valid { + result.Error = fmt.Errorf("Found row with no time value") + return result + } + + if series, exist := pointsBySeries[rowData.metric]; exist { + series.Points = append(series.Points, tsdb.TimePoint{rowData.value, rowData.time}) + } else { + series := &tsdb.TimeSeries{Name: rowData.metric} + series.Points = append(series.Points, tsdb.TimePoint{rowData.value, rowData.time}) + pointsBySeries[rowData.metric] = series + } + } + + for _, value := range pointsBySeries { + result.Series = append(result.Series, value) } return result } type stringStringScan struct { - // cp are the column pointers - cp []interface{} - // row contains the final result - row []string - colCount int - colNames []string + rowPtrs []interface{} + rowValues []string + columnNames []string + columnCount int + + time null.Float + value null.Float + metric string } func NewStringStringScan(columnNames []string) *stringStringScan { - lenCN := len(columnNames) s := &stringStringScan{ - cp: make([]interface{}, lenCN), - row: make([]string, lenCN*2), - colCount: lenCN, - colNames: columnNames, + columnCount: len(columnNames), + columnNames: columnNames, + rowPtrs: make([]interface{}, len(columnNames)), + rowValues: make([]string, len(columnNames)), } - j := 0 - for i := 0; i < lenCN; i++ { - s.cp[i] = new(sql.RawBytes) - s.row[j] = s.colNames[i] - j = j + 2 + + for i := 0; i < s.columnCount; i++ { + s.rowPtrs[i] = new(sql.RawBytes) } + return s } func (s *stringStringScan) Update(rows *sql.Rows) error { - if err := rows.Scan(s.cp...); err != nil { + if err := rows.Scan(s.rowPtrs...); err != nil { return err } - j := 0 - for i := 0; i < s.colCount; i++ { - if rb, ok := s.cp[i].(*sql.RawBytes); ok { - s.row[j+1] = string(*rb) + + for i := 0; i < s.columnCount; i++ { + if rb, ok := s.rowPtrs[i].(*sql.RawBytes); ok { + s.rowValues[i] = string(*rb) + fmt.Printf("column %s = %s", s.columnNames[i], s.rowValues[i]) + + switch s.columnNames[i] { + case "time_sec": + if sec, err := strconv.ParseInt(s.rowValues[i], 10, 64); err == nil { + s.time = null.FloatFrom(float64(sec * 1000)) + } + case "value": + if value, err := strconv.ParseFloat(s.rowValues[i], 64); err == nil { + s.value = null.FloatFrom(value) + } + case "metric": + if value, err := strconv.ParseFloat(s.rowValues[i], 64); err == nil { + s.value = null.FloatFrom(value) + } + } + *rb = nil // reset pointer to discard current value to avoid a bug } else { - return fmt.Errorf("Cannot convert index %d column %s to type *sql.RawBytes", i, s.colNames[i]) + return fmt.Errorf("Cannot convert index %d column %s to type *sql.RawBytes", i, s.columnNames[i]) } - j = j + 2 } return nil } - -func (s *stringStringScan) Get() []string { - return s.row -} - -// type sqlDataRequest struct { -// Query string `json:"query"` -// Body []byte `json:"-"` -// } -// -// type seriesStruct struct { -// Columns []string `json:"columns"` -// Name string `json:"name"` -// Values [][]interface{} `json:"values"` -// } -// -// type resultsStruct struct { -// Series []seriesStruct `json:"series"` -// } -// -// type dataStruct struct { -// Results []resultsStruct `json:"results"` -// } diff --git a/pkg/tsdb/request.go b/pkg/tsdb/request.go index 2934443bc53..ed10a023e9c 100644 --- a/pkg/tsdb/request.go +++ b/pkg/tsdb/request.go @@ -1,6 +1,8 @@ package tsdb -import "context" +import ( + "context" +) type HandleRequestFunc func(ctx context.Context, req *Request) (*Response, error) diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index 7b22e150b54..9da41fcca0a 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -39,6 +39,11 @@ export class MysqlDatasource { var data = []; if (res.results) { _.forEach(res.results, queryRes => { + + if (queryRes.error) { + throw {error: queryRes.error, message: queryRes.error}; + } + for (let series of queryRes.series) { data.push({ target: series.name, diff --git a/public/app/plugins/datasource/mysql/module.ts b/public/app/plugins/datasource/mysql/module.ts index 7d9a65b1609..8cff67ff1af 100644 --- a/public/app/plugins/datasource/mysql/module.ts +++ b/public/app/plugins/datasource/mysql/module.ts @@ -6,6 +6,21 @@ import {QueryCtrl} from 'app/plugins/sdk'; class MysqlQueryCtrl extends QueryCtrl { static templateUrl = 'partials/query.editor.html'; + + resultFormats: any; + target: any; + + constructor($scope, $injector) { + super($scope, $injector); + + this.target.resultFormat = 'time_series'; + this.target.alias = "{{table}}{{col_3}}"; + this.resultFormats = [ + {text: 'Time series', value: 'time_series'}, + {text: 'Table', value: 'table'}, + ]; + + } } class MysqlConfigCtrl { diff --git a/public/app/plugins/datasource/mysql/partials/query.editor.html b/public/app/plugins/datasource/mysql/partials/query.editor.html index 0e8a76bd21c..1a22bec1e75 100644 --- a/public/app/plugins/datasource/mysql/partials/query.editor.html +++ b/public/app/plugins/datasource/mysql/partials/query.editor.html @@ -1,7 +1,25 @@
- +
+ +
+
+ +
+ +
+
+
+ + +
+ +
+
+
+
+