mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 03:41:50 +08:00
mysql: added support for tables in mysql queries
This commit is contained in:
@ -51,14 +51,25 @@ type QueryResult struct {
|
|||||||
RefId string `json:"refId"`
|
RefId string `json:"refId"`
|
||||||
Meta *simplejson.Json `json:"meta,omitempty"`
|
Meta *simplejson.Json `json:"meta,omitempty"`
|
||||||
Series TimeSeriesSlice `json:"series"`
|
Series TimeSeriesSlice `json:"series"`
|
||||||
|
Tables []*Table `json:"tables"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TimeSeries struct {
|
type TimeSeries struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Points TimeSeriesPoints `json:"points"`
|
Points TimeSeriesPoints `json:"points"`
|
||||||
Tags map[string]string `json:"tags"`
|
Tags map[string]string `json:"tags,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Table struct {
|
||||||
|
Columns []TableColumn `json:"columns"`
|
||||||
|
Rows []RowValues `json:"rows"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TableColumn struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RowValues []interface{}
|
||||||
type TimePoint [2]null.Float
|
type TimePoint [2]null.Float
|
||||||
type TimeSeriesPoints []TimePoint
|
type TimeSeriesPoints []TimePoint
|
||||||
type TimeSeriesSlice []*TimeSeries
|
type TimeSeriesSlice []*TimeSeries
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/go-sql-driver/mysql"
|
||||||
"github.com/go-xorm/core"
|
"github.com/go-xorm/core"
|
||||||
"github.com/go-xorm/xorm"
|
"github.com/go-xorm/xorm"
|
||||||
"github.com/grafana/grafana/pkg/components/null"
|
"github.com/grafana/grafana/pkg/components/null"
|
||||||
@ -114,34 +115,97 @@ func (e *MysqlExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, co
|
|||||||
|
|
||||||
format := query.Model.Get("format").MustString("time_series")
|
format := query.Model.Get("format").MustString("time_series")
|
||||||
|
|
||||||
if format == "time_series" {
|
switch format {
|
||||||
res, err := e.TransformToTimeSeries(query, rows)
|
case "time_series":
|
||||||
|
err := e.TransformToTimeSeries(query, rows, queryResult)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
queryResult.Error = err
|
queryResult.Error = err
|
||||||
return result
|
continue
|
||||||
|
}
|
||||||
|
case "table":
|
||||||
|
err := e.TransformToTable(query, rows, queryResult)
|
||||||
|
if err != nil {
|
||||||
|
queryResult.Error = err
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
queryResult.Series = res
|
|
||||||
queryResult.Meta.Set("rowCount", countPointsInAllSeries(res))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func countPointsInAllSeries(seriesList tsdb.TimeSeriesSlice) (count int) {
|
func (e MysqlExecutor) TransformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult) error {
|
||||||
for _, series := range seriesList {
|
columnNames, err := rows.Columns()
|
||||||
count += len(series.Points)
|
columnCount := len(columnNames)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return count
|
|
||||||
|
table := &tsdb.Table{
|
||||||
|
Columns: make([]tsdb.TableColumn, columnCount),
|
||||||
|
Rows: make([]tsdb.RowValues, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, name := range columnNames {
|
||||||
|
table.Columns[i].Text = name
|
||||||
|
}
|
||||||
|
|
||||||
|
columnTypes, err := rows.ColumnTypes()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rowLimit := 1000000
|
||||||
|
rowCount := 0
|
||||||
|
|
||||||
|
for ; rows.Next(); rowCount += 1 {
|
||||||
|
if rowCount > rowLimit {
|
||||||
|
return fmt.Errorf("MySQL query row limit exceeded, limit %d", rowLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
values, err := e.getTypedRowData(columnTypes, rows)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Rows = append(table.Rows, values)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Tables = append(result.Tables, table)
|
||||||
|
result.Meta.Set("rowCount", rowCount)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e MysqlExecutor) TransformToTimeSeries(query *tsdb.Query, rows *core.Rows) (tsdb.TimeSeriesSlice, error) {
|
func (e MysqlExecutor) getTypedRowData(types []*sql.ColumnType, rows *core.Rows) (tsdb.RowValues, error) {
|
||||||
|
values := make([]interface{}, len(types))
|
||||||
|
|
||||||
|
for i, stype := range types {
|
||||||
|
switch stype.DatabaseTypeName() {
|
||||||
|
case mysql.FieldTypeNameVarString:
|
||||||
|
values[i] = new(string)
|
||||||
|
case mysql.FieldTypeNameLongLong:
|
||||||
|
values[i] = new(int64)
|
||||||
|
case mysql.FieldTypeNameDouble:
|
||||||
|
values[i] = new(float64)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("Database type %s not supported", stype)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Scan(values...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return values, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e MysqlExecutor) TransformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult) error {
|
||||||
pointsBySeries := make(map[string]*tsdb.TimeSeries)
|
pointsBySeries := make(map[string]*tsdb.TimeSeries)
|
||||||
columnNames, err := rows.Columns()
|
columnNames, err := rows.Columns()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
rowData := NewStringStringScan(columnNames)
|
rowData := NewStringStringScan(columnNames)
|
||||||
@ -150,13 +214,13 @@ func (e MysqlExecutor) TransformToTimeSeries(query *tsdb.Query, rows *core.Rows)
|
|||||||
|
|
||||||
for ; rows.Next(); rowCount += 1 {
|
for ; rows.Next(); rowCount += 1 {
|
||||||
if rowCount > rowLimit {
|
if rowCount > rowLimit {
|
||||||
return nil, fmt.Errorf("MySQL query row limit exceeded, limit %d", rowLimit)
|
return fmt.Errorf("MySQL query row limit exceeded, limit %d", rowLimit)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := rowData.Update(rows.Rows)
|
err := rowData.Update(rows.Rows)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.log.Error("MySQL response parsing", "error", err)
|
e.log.Error("MySQL response parsing", "error", err)
|
||||||
return nil, fmt.Errorf("MySQL response parsing error %v", err)
|
return fmt.Errorf("MySQL response parsing error %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if rowData.metric == "" {
|
if rowData.metric == "" {
|
||||||
@ -166,7 +230,7 @@ func (e MysqlExecutor) TransformToTimeSeries(query *tsdb.Query, rows *core.Rows)
|
|||||||
//e.log.Debug("Rows", "metric", rowData.metric, "time", rowData.time, "value", rowData.value)
|
//e.log.Debug("Rows", "metric", rowData.metric, "time", rowData.time, "value", rowData.value)
|
||||||
|
|
||||||
if !rowData.time.Valid {
|
if !rowData.time.Valid {
|
||||||
return nil, fmt.Errorf("Found row with no time value")
|
return fmt.Errorf("Found row with no time value")
|
||||||
}
|
}
|
||||||
|
|
||||||
if series, exist := pointsBySeries[rowData.metric]; exist {
|
if series, exist := pointsBySeries[rowData.metric]; exist {
|
||||||
@ -178,13 +242,12 @@ func (e MysqlExecutor) TransformToTimeSeries(query *tsdb.Query, rows *core.Rows)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
seriesList := make(tsdb.TimeSeriesSlice, 0)
|
|
||||||
for _, value := range pointsBySeries {
|
for _, value := range pointsBySeries {
|
||||||
seriesList = append(seriesList, value)
|
result.Series = append(result.Series, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
e.log.Debug("TransformToTimeSeries", "rowCount", rowCount, "timeSeriesCount", len(seriesList))
|
result.Meta.Set("rowCount", rowCount)
|
||||||
return seriesList, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type stringStringScan struct {
|
type stringStringScan struct {
|
||||||
|
@ -31,6 +31,7 @@ class MetricsPanelCtrl extends PanelCtrl {
|
|||||||
skipDataOnInit: boolean;
|
skipDataOnInit: boolean;
|
||||||
dataStream: any;
|
dataStream: any;
|
||||||
dataSubscription: any;
|
dataSubscription: any;
|
||||||
|
dataList: any;
|
||||||
|
|
||||||
constructor($scope, $injector) {
|
constructor($scope, $injector) {
|
||||||
super($scope, $injector);
|
super($scope, $injector);
|
||||||
|
@ -38,14 +38,20 @@ export class MysqlDatasource {
|
|||||||
to: options.range.to.valueOf().toString(),
|
to: options.range.to.valueOf().toString(),
|
||||||
queries: queries,
|
queries: queries,
|
||||||
}
|
}
|
||||||
}).then(res => {
|
}).then(this.processQueryResult.bind(this));
|
||||||
var data = [];
|
}
|
||||||
|
|
||||||
if (!res.data.results) {
|
processQueryResult(res) {
|
||||||
return {data: data};
|
var data = [];
|
||||||
}
|
|
||||||
|
|
||||||
_.forEach(res.data.results, queryRes => {
|
if (!res.data.results) {
|
||||||
|
return {data: data};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let key in res.data.results) {
|
||||||
|
let queryRes = res.data.results[key];
|
||||||
|
|
||||||
|
if (queryRes.series) {
|
||||||
for (let series of queryRes.series) {
|
for (let series of queryRes.series) {
|
||||||
data.push({
|
data.push({
|
||||||
target: series.name,
|
target: series.name,
|
||||||
@ -54,10 +60,19 @@ export class MysqlDatasource {
|
|||||||
meta: queryRes.meta,
|
meta: queryRes.meta,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
return {data: data};
|
if (queryRes.tables) {
|
||||||
});
|
for (let table of queryRes.tables) {
|
||||||
|
table.type = 'table';
|
||||||
|
table.refId = queryRes.refId;
|
||||||
|
table.meta = queryRes.meta;
|
||||||
|
data.push(table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {data: data};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ class MysqlQueryCtrl extends QueryCtrl {
|
|||||||
constructor($scope, $injector) {
|
constructor($scope, $injector) {
|
||||||
super($scope, $injector);
|
super($scope, $injector);
|
||||||
|
|
||||||
this.target.format = 'time_series';
|
this.target.format = this.target.format || 'time_series';
|
||||||
this.target.alias = "";
|
this.target.alias = "";
|
||||||
this.formats = [
|
this.formats = [
|
||||||
{text: 'Time series', value: 'time_series'},
|
{text: 'Time series', value: 'time_series'},
|
||||||
|
@ -16,24 +16,17 @@
|
|||||||
<label class="gf-form-label query-keyword">Name by</label>
|
<label class="gf-form-label query-keyword">Name by</label>
|
||||||
<input type="text" class="gf-form-input" ng-model="ctrl.target.alias" spellcheck='false' placeholder="pattern" ng-blur="ctrl.refresh()">
|
<input type="text" class="gf-form-input" ng-model="ctrl.target.alias" spellcheck='false' placeholder="pattern" ng-blur="ctrl.refresh()">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="gf-form" ng-show="ctrl.lastQueryMeta">
|
||||||
<div class="gf-form gf-form--grow">
|
|
||||||
<div class="gf-form-label gf-form-label--grow"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form-inline" ng-show="ctrl.lastQueryMeta">
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label query-keyword" ng-click="ctrl.showLastQuerySQL = !ctrl.showLastQuerySQL">
|
<label class="gf-form-label query-keyword" ng-click="ctrl.showLastQuerySQL = !ctrl.showLastQuerySQL">
|
||||||
Generated SQL
|
Generated SQL
|
||||||
<i class="fa fa-caret-down" ng-show="ctrl.showLastQuerySQL"></i>
|
<i class="fa fa-caret-down" ng-show="ctrl.showLastQuerySQL"></i>
|
||||||
<i class="fa fa-caret-right" ng-hide="ctrl.showLastQuerySQL"></i>
|
<i class="fa fa-caret-right" ng-hide="ctrl.showLastQuerySQL"></i>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form gf-form--grow">
|
<div class="gf-form gf-form--grow">
|
||||||
<div class="gf-form-label gf-form-label--grow"></div>
|
<div class="gf-form-label gf-form-label--grow"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<pre class="small" ng-show="ctrl.showLastQuerySQL">{{ctrl.lastQueryMeta.sql}}</pre>
|
<pre class="small" ng-show="ctrl.showLastQuerySQL">{{ctrl.lastQueryMeta.sql}}</pre>
|
||||||
<pre class="small alert alert-error" ng-show="ctrl.lastQueryError">{{ctrl.lastQueryError}}</pre>
|
<pre class="small alert alert-error" ng-show="ctrl.lastQueryError">{{ctrl.lastQueryError}}</pre>
|
||||||
|
78
vendor/github.com/go-sql-driver/mysql/row_columntypes.go
generated
vendored
Normal file
78
vendor/github.com/go-sql-driver/mysql/row_columntypes.go
generated
vendored
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
const (
|
||||||
|
// In case we get something unexpected
|
||||||
|
FieldTypeUnknown = "UNKNOWN"
|
||||||
|
|
||||||
|
// Human-readable names for each distinct type byte
|
||||||
|
FieldTypeNameDecimal = "DECIMAL"
|
||||||
|
FieldTypeNameTiny = "TINY"
|
||||||
|
FieldTypeNameShort = "SHORT"
|
||||||
|
FieldTypeNameLong = "LONG"
|
||||||
|
FieldTypeNameFloat = "FLOAT"
|
||||||
|
FieldTypeNameDouble = "DOUBLE"
|
||||||
|
FieldTypeNameNULL = "NULL"
|
||||||
|
FieldTypeNameTimestamp = "TIMESTAMP"
|
||||||
|
FieldTypeNameLongLong = "LONGLONG"
|
||||||
|
FieldTypeNameInt24 = "INT24"
|
||||||
|
FieldTypeNameDate = "DATE"
|
||||||
|
FieldTypeNameTime = "TIME"
|
||||||
|
FieldTypeNameDateTime = "DATETIME"
|
||||||
|
FieldTypeNameYear = "YEAR"
|
||||||
|
FieldTypeNameNewDate = "NEWDATE"
|
||||||
|
FieldTypeNameVarChar = "VARCHAR"
|
||||||
|
FieldTypeNameBit = "BIT"
|
||||||
|
FieldTypeNameJSON = "JSON"
|
||||||
|
FieldTypeNameNewDecimal = "NEWDECIMAL"
|
||||||
|
FieldTypeNameEnum = "ENUM"
|
||||||
|
FieldTypeNameSet = "SET"
|
||||||
|
FieldTypeNameTinyBLOB = "TINYBLOB"
|
||||||
|
FieldTypeNameMediumBLOB = "MEDIUMBLOB"
|
||||||
|
FieldTypeNameLongBLOB = "LONGBLOB"
|
||||||
|
FieldTypeNameBLOB = "BLOB"
|
||||||
|
FieldTypeNameVarString = "VARSTRING"
|
||||||
|
FieldTypeNameString = "STRING"
|
||||||
|
FieldTypeNameGeometry = "GEOMETRY"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mapping from each type identifier to human readable string
|
||||||
|
var mysqlTypeMap = map[byte]string{
|
||||||
|
fieldTypeDecimal: FieldTypeNameDecimal,
|
||||||
|
fieldTypeTiny: FieldTypeNameTiny,
|
||||||
|
fieldTypeShort: FieldTypeNameShort,
|
||||||
|
fieldTypeLong: FieldTypeNameLong,
|
||||||
|
fieldTypeFloat: FieldTypeNameFloat,
|
||||||
|
fieldTypeDouble: FieldTypeNameDouble,
|
||||||
|
fieldTypeNULL: FieldTypeNameNULL,
|
||||||
|
fieldTypeTimestamp: FieldTypeNameTimestamp,
|
||||||
|
fieldTypeLongLong: FieldTypeNameLongLong,
|
||||||
|
fieldTypeInt24: FieldTypeNameInt24,
|
||||||
|
fieldTypeDate: FieldTypeNameDate,
|
||||||
|
fieldTypeTime: FieldTypeNameTime,
|
||||||
|
fieldTypeDateTime: FieldTypeNameDateTime,
|
||||||
|
fieldTypeYear: FieldTypeNameYear,
|
||||||
|
fieldTypeNewDate: FieldTypeNameNewDate,
|
||||||
|
fieldTypeVarChar: FieldTypeNameVarChar,
|
||||||
|
fieldTypeBit: FieldTypeNameBit,
|
||||||
|
fieldTypeJSON: FieldTypeNameJSON,
|
||||||
|
fieldTypeNewDecimal: FieldTypeNameNewDecimal,
|
||||||
|
fieldTypeEnum: FieldTypeNameEnum,
|
||||||
|
fieldTypeSet: FieldTypeNameSet,
|
||||||
|
fieldTypeTinyBLOB: FieldTypeNameTinyBLOB,
|
||||||
|
fieldTypeMediumBLOB: FieldTypeNameMediumBLOB,
|
||||||
|
fieldTypeLongBLOB: FieldTypeNameLongBLOB,
|
||||||
|
fieldTypeBLOB: FieldTypeNameBLOB,
|
||||||
|
fieldTypeVarString: FieldTypeNameVarString,
|
||||||
|
fieldTypeString: FieldTypeNameString,
|
||||||
|
fieldTypeGeometry: FieldTypeNameGeometry,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make Rows implement the optional RowsColumnTypeDatabaseTypeName interface.
|
||||||
|
// See https://github.com/golang/go/commit/2a85578b0ecd424e95b29d810b7a414a299fd6a7
|
||||||
|
// - (go 1.8 required for this to have any effect)
|
||||||
|
func (rows *mysqlRows) ColumnTypeDatabaseTypeName(index int) string {
|
||||||
|
if typeName, ok := mysqlTypeMap[rows.rs.columns[index].fieldType]; ok {
|
||||||
|
return typeName
|
||||||
|
}
|
||||||
|
return FieldTypeUnknown
|
||||||
|
}
|
Reference in New Issue
Block a user