From cb15d9cbdd6d8de9675bbb37d196961f6ac2e2d4 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 10 Dec 2017 19:50:01 +0100 Subject: [PATCH] add missing value fill code to mysql datasource --- pkg/tsdb/mysql/macros.go | 18 +++++++++++++- pkg/tsdb/mysql/mysql.go | 54 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go index 7eb673e7aa2..d684fdc9d92 100644 --- a/pkg/tsdb/mysql/macros.go +++ b/pkg/tsdb/mysql/macros.go @@ -3,6 +3,7 @@ package mysql import ( "fmt" "regexp" + "strconv" "strings" "time" @@ -15,6 +16,7 @@ const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)` type MySqlMacroEngine struct { TimeRange *tsdb.TimeRange + Query *tsdb.Query } func NewMysqlMacroEngine() tsdb.SqlMacroEngine { @@ -23,6 +25,7 @@ func NewMysqlMacroEngine() tsdb.SqlMacroEngine { func (m *MySqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { m.TimeRange = timeRange + m.Query = query rExp, _ := regexp.Compile(sExpr) var macroError error @@ -76,13 +79,26 @@ func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, er case "__timeTo": return fmt.Sprintf("FROM_UNIXTIME(%d)", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil case "__timeGroup": - if len(args) != 2 { + if len(args) < 2 { return "", fmt.Errorf("macro %v needs time column and interval", name) } interval, err := time.ParseDuration(strings.Trim(args[1], `'" `)) if err != nil { return "", fmt.Errorf("error parsing interval %v", args[1]) } + if len(args) == 3 { + m.Query.Model.Set("fill", true) + m.Query.Model.Set("fillInterval", interval.Seconds()) + if strings.Trim(args[2], " ") == "NULL" { + m.Query.Model.Set("fillNull", true) + } else { + floatVal, err := strconv.ParseFloat(args[2], 64) + if err != nil { + return "", fmt.Errorf("error parsing fill value %v", args[2]) + } + m.Query.Model.Set("fillValue", floatVal) + } + } return fmt.Sprintf("cast(cast(UNIX_TIMESTAMP(%s)/(%.0f) as signed)*%.0f as signed)", args[0], interval.Seconds(), interval.Seconds()), nil case "__unixEpochFilter": if len(args) == 0 { diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index b3fec71aaf7..ddd90b77642 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -5,6 +5,7 @@ import ( "context" "database/sql" "fmt" + "math" "strconv" "time" @@ -176,6 +177,18 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core. rowLimit := 1000000 rowCount := 0 + fillMissing := query.Model.Get("fill").MustBool(false) + var fillInterval float64 + fillValue := null.Float{} + if fillMissing { + fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000 + if query.Model.Get("fillNull").MustBool(false) == false { + fillValue.Float64 = query.Model.Get("fillValue").MustFloat64() + fillValue.Valid = true + } + + } + for ; rows.Next(); rowCount++ { if rowCount > rowLimit { return fmt.Errorf("MySQL query row limit exceeded, limit %d", rowLimit) @@ -195,19 +208,50 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core. return fmt.Errorf("Found row with no time value") } - 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}) + series, exist := pointsBySeries[rowData.metric] + if exist == false { + series = &tsdb.TimeSeries{Name: rowData.metric} pointsBySeries[rowData.metric] = series seriesByQueryOrder.PushBack(rowData.metric) } + + if fillMissing { + var intervalStart float64 + if exist == false { + intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6) + } else { + intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval + } + + // align interval start + intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval + + for i := intervalStart; i < rowData.time.Float64; i += fillInterval { + series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) + rowCount++ + } + } + + series.Points = append(series.Points, tsdb.TimePoint{rowData.value, rowData.time}) } for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() { key := elem.Value.(string) result.Series = append(result.Series, pointsBySeries[key]) + + if fillMissing { + series := pointsBySeries[key] + // fill in values from last fetched value till interval end + intervalStart := series.Points[len(series.Points)-1][1].Float64 + intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) + + // align interval start + intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval + for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { + series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) + rowCount++ + } + } } result.Meta.Set("rowCount", rowCount)