bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/cmd/bosun/expr/tsdb.go (about)

     1  package expr
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"math"
     7  	"math/rand"
     8  	"reflect"
     9  	"strconv"
    10  	"time"
    11  
    12  	"bosun.org/cmd/bosun/expr/parse"
    13  	"bosun.org/models"
    14  	"bosun.org/opentsdb"
    15  	"bosun.org/slog"
    16  	"github.com/MiniProfiler/go/miniprofiler"
    17  )
    18  
    19  // TSDB defines functions for use with an OpenTSDB backend.
    20  var TSDB = map[string]parse.Func{
    21  	"band": {
    22  		Args:   []models.FuncType{models.TypeString, models.TypeString, models.TypeString, models.TypeScalar},
    23  		Return: models.TypeSeriesSet,
    24  		Tags:   tagQuery,
    25  		F:      Band,
    26  	},
    27  	"bandQuery": {
    28  		Args:   []models.FuncType{models.TypeString, models.TypeString, models.TypeString, models.TypeString, models.TypeScalar},
    29  		Return: models.TypeSeriesSet,
    30  		Tags:   tagQuery,
    31  		F:      BandQuery,
    32  	},
    33  	"shiftBand": {
    34  		Args:   []models.FuncType{models.TypeString, models.TypeString, models.TypeString, models.TypeScalar},
    35  		Return: models.TypeSeriesSet,
    36  		Tags:   tagQuery,
    37  		F:      ShiftBand,
    38  	},
    39  	"over": {
    40  		Args:   []models.FuncType{models.TypeString, models.TypeString, models.TypeString, models.TypeScalar},
    41  		Return: models.TypeSeriesSet,
    42  		Tags:   tagQuery,
    43  		F:      Over,
    44  	},
    45  	"overQuery": {
    46  		Args:   []models.FuncType{models.TypeString, models.TypeString, models.TypeString, models.TypeString, models.TypeScalar},
    47  		Return: models.TypeSeriesSet,
    48  		Tags:   tagQuery,
    49  		F:      OverQuery,
    50  	},
    51  	"change": {
    52  		Args:   []models.FuncType{models.TypeString, models.TypeString, models.TypeString},
    53  		Return: models.TypeNumberSet,
    54  		Tags:   tagQuery,
    55  		F:      Change,
    56  	},
    57  	"count": {
    58  		Args:   []models.FuncType{models.TypeString, models.TypeString, models.TypeString},
    59  		Return: models.TypeScalar,
    60  		F:      Count,
    61  	},
    62  	"q": {
    63  		Args:   []models.FuncType{models.TypeString, models.TypeString, models.TypeString},
    64  		Return: models.TypeSeriesSet,
    65  		Tags:   tagQuery,
    66  		F:      Query,
    67  	},
    68  	"window": {
    69  		Args:   []models.FuncType{models.TypeString, models.TypeString, models.TypeString, models.TypeScalar, models.TypeString},
    70  		Return: models.TypeSeriesSet,
    71  		Tags:   tagQuery,
    72  		F:      Window,
    73  		Check:  windowCheck,
    74  	},
    75  }
    76  
    77  const tsdbMaxTries = 3
    78  
    79  func timeTSDBRequest(e *State, req *opentsdb.Request) (s opentsdb.ResponseSet, err error) {
    80  	e.tsdbQueries = append(e.tsdbQueries, *req)
    81  	if e.autods > 0 {
    82  		for _, q := range req.Queries {
    83  			if q.Downsample == "" {
    84  				if err := req.AutoDownsample(e.autods); err != nil {
    85  					return nil, err
    86  				}
    87  			}
    88  		}
    89  	}
    90  	b, _ := json.MarshalIndent(req, "", "  ")
    91  	tries := 1
    92  	for {
    93  		e.Timer.StepCustomTiming("tsdb", "query", string(b), func() {
    94  			getFn := func() (interface{}, error) {
    95  				return e.TSDBContext.Query(req)
    96  			}
    97  			var val interface{}
    98  			var hit bool
    99  			val, err, hit = e.Cache.Get(string(b), getFn)
   100  			collectCacheHit(e.Cache, "opentsdb", hit)
   101  			rs := val.(opentsdb.ResponseSet)
   102  			s = rs.Copy()
   103  			for _, r := range rs {
   104  				if r.SQL != "" {
   105  					e.Timer.AddCustomTiming("sql", "query", time.Now(), time.Now(), r.SQL)
   106  				}
   107  			}
   108  		})
   109  		if err == nil || tries == tsdbMaxTries {
   110  			break
   111  		}
   112  		slog.Errorf("Error on tsdb query %d: %s", tries, err.Error())
   113  		tries++
   114  		rand.Seed(time.Now().UnixNano())
   115  		minSleep := 1000
   116  		maxSleep := 3000
   117  		time.Sleep(time.Millisecond * time.Duration(rand.Intn(maxSleep-minSleep)+minSleep))
   118  	}
   119  	return
   120  }
   121  
   122  func bandTSDB(e *State, query, duration, period, eduration string, num float64, rfunc func(*Results, *opentsdb.Response, time.Duration) error) (r *Results, err error) {
   123  	r = new(Results)
   124  	r.IgnoreOtherUnjoined = true
   125  	r.IgnoreUnjoined = true
   126  	e.Timer.Step("band", func(T miniprofiler.Timer) {
   127  		var d, p opentsdb.Duration
   128  		d, err = opentsdb.ParseDuration(duration)
   129  		if err != nil {
   130  			return
   131  		}
   132  		p, err = opentsdb.ParseDuration(period)
   133  		if err != nil {
   134  			return
   135  		}
   136  		if num < 1 || num > 100 {
   137  			err = fmt.Errorf("num out of bounds")
   138  		}
   139  		var q *opentsdb.Query
   140  		q, err = opentsdb.ParseQuery(query, e.TSDBContext.Version())
   141  		if err != nil {
   142  			return
   143  		}
   144  		if !e.TSDBContext.Version().FilterSupport() {
   145  			if err = e.Search.Expand(q); err != nil {
   146  				return
   147  			}
   148  		}
   149  		req := opentsdb.Request{
   150  			Queries: []*opentsdb.Query{q},
   151  		}
   152  		end := e.now
   153  		if eduration != "" {
   154  			var ed opentsdb.Duration
   155  			ed, err = opentsdb.ParseDuration(eduration)
   156  			if err != nil {
   157  				return
   158  			}
   159  			end = end.Add(time.Duration(-ed))
   160  		}
   161  		req.End = end.Unix()
   162  		req.Start = end.Add(time.Duration(-d)).Unix()
   163  		if err = req.SetTime(e.now); err != nil {
   164  			return
   165  		}
   166  		for i := 0; i < int(num); i++ {
   167  			req.End = end.Unix()
   168  			req.Start = end.Add(time.Duration(-d)).Unix()
   169  			var s opentsdb.ResponseSet
   170  			s, err = timeTSDBRequest(e, &req)
   171  			if err != nil {
   172  				return
   173  			}
   174  			for _, res := range s {
   175  				if e.Squelched(res.Tags) {
   176  					continue
   177  				}
   178  				//offset := e.now.Sub(now.Add(time.Duration(p-d)))
   179  				offset := e.now.Sub(end)
   180  				if err = rfunc(r, res, offset); err != nil {
   181  					return
   182  				}
   183  			}
   184  			end = end.Add(time.Duration(-p))
   185  		}
   186  	})
   187  	return
   188  }
   189  
   190  func Window(e *State, query, duration, period string, num float64, rfunc string) (*Results, error) {
   191  	var isPerc bool
   192  	var percValue float64
   193  	if len(rfunc) > 0 && rfunc[0] == 'p' {
   194  		var err error
   195  		percValue, err = strconv.ParseFloat(rfunc[1:], 10)
   196  		isPerc = err == nil
   197  	}
   198  	if isPerc {
   199  		if percValue < 0 || percValue > 1 {
   200  			return nil, fmt.Errorf("expr: window: percentile number must be greater than or equal to zero 0 and less than or equal 1")
   201  		}
   202  		rfunc = "percentile"
   203  	}
   204  	fn, ok := e.GetFunction(rfunc)
   205  	if !ok {
   206  		return nil, fmt.Errorf("expr: Window: no %v function", rfunc)
   207  	}
   208  	windowFn := reflect.ValueOf(fn.F)
   209  	bandFn := func(results *Results, resp *opentsdb.Response, offset time.Duration) error {
   210  		values := make(Series)
   211  		min := int64(math.MaxInt64)
   212  		for k, v := range resp.DPS {
   213  			i, e := strconv.ParseInt(k, 10, 64)
   214  			if e != nil {
   215  				return e
   216  			}
   217  			if i < min {
   218  				min = i
   219  			}
   220  			values[time.Unix(i, 0).UTC()] = float64(v)
   221  		}
   222  		if len(values) == 0 {
   223  			return nil
   224  		}
   225  		callResult := &Results{
   226  			Results: ResultSlice{
   227  				&Result{
   228  					Group: resp.Tags,
   229  					Value: values,
   230  				},
   231  			},
   232  		}
   233  		fnArgs := []reflect.Value{reflect.ValueOf(e), reflect.ValueOf(callResult)}
   234  		if isPerc {
   235  			fnArgs = append(fnArgs, reflect.ValueOf(fromScalar(percValue)))
   236  		}
   237  		fnResult := windowFn.Call(fnArgs)
   238  		if !fnResult[1].IsNil() {
   239  			if err := fnResult[1].Interface().(error); err != nil {
   240  				return err
   241  			}
   242  		}
   243  		minTime := time.Unix(min, 0).UTC()
   244  		fres := float64(fnResult[0].Interface().(*Results).Results[0].Value.(Number))
   245  		found := false
   246  		for _, result := range results.Results {
   247  			if result.Group.Equal(resp.Tags) {
   248  				found = true
   249  				v := result.Value.(Series)
   250  				v[minTime] = fres
   251  				break
   252  			}
   253  		}
   254  		if !found {
   255  			results.Results = append(results.Results, &Result{
   256  				Group: resp.Tags,
   257  				Value: Series{
   258  					minTime: fres,
   259  				},
   260  			})
   261  		}
   262  		return nil
   263  	}
   264  	r, err := bandTSDB(e, query, duration, period, period, num, bandFn)
   265  	if err != nil {
   266  		err = fmt.Errorf("expr: Window: %v", err)
   267  	}
   268  	return r, err
   269  }
   270  
   271  func windowCheck(t *parse.Tree, f *parse.FuncNode) error {
   272  	name := f.Args[4].(*parse.StringNode).Text
   273  	var isPerc bool
   274  	var percValue float64
   275  	if len(name) > 0 && name[0] == 'p' {
   276  		var err error
   277  		percValue, err = strconv.ParseFloat(name[1:], 10)
   278  		isPerc = err == nil
   279  	}
   280  	if isPerc {
   281  		if percValue < 0 || percValue > 1 {
   282  			return fmt.Errorf("expr: window: percentile number must be greater than or equal to zero 0 and less than or equal 1")
   283  		}
   284  		return nil
   285  	}
   286  	v, ok := t.GetFunction(name)
   287  	if !ok {
   288  		return fmt.Errorf("expr: Window: unknown function %v", name)
   289  	}
   290  	if len(v.Args) != 1 || v.Args[0] != models.TypeSeriesSet || v.Return != models.TypeNumberSet {
   291  		return fmt.Errorf("expr: Window: %v is not a reduction function", name)
   292  	}
   293  	return nil
   294  }
   295  
   296  func BandQuery(e *State, query, duration, period, eduration string, num float64) (r *Results, err error) {
   297  	r, err = bandTSDB(e, query, duration, period, eduration, num, func(r *Results, res *opentsdb.Response, offset time.Duration) error {
   298  		newarr := true
   299  		for _, a := range r.Results {
   300  			if !a.Group.Equal(res.Tags) {
   301  				continue
   302  			}
   303  			newarr = false
   304  			values := a.Value.(Series)
   305  			for k, v := range res.DPS {
   306  				i, e := strconv.ParseInt(k, 10, 64)
   307  				if e != nil {
   308  					return e
   309  				}
   310  				values[time.Unix(i, 0).UTC()] = float64(v)
   311  			}
   312  		}
   313  		if newarr {
   314  			values := make(Series)
   315  			a := &Result{Group: res.Tags}
   316  			for k, v := range res.DPS {
   317  				i, e := strconv.ParseInt(k, 10, 64)
   318  				if e != nil {
   319  					return e
   320  				}
   321  				values[time.Unix(i, 0).UTC()] = float64(v)
   322  			}
   323  			a.Value = values
   324  			r.Results = append(r.Results, a)
   325  		}
   326  		return nil
   327  	})
   328  	if err != nil {
   329  		err = fmt.Errorf("expr: Band: %v", err)
   330  	}
   331  	return
   332  }
   333  
   334  func OverQuery(e *State, query, duration, period, eduration string, num float64) (r *Results, err error) {
   335  	r, err = bandTSDB(e, query, duration, period, eduration, num, func(r *Results, res *opentsdb.Response, offset time.Duration) error {
   336  		values := make(Series)
   337  		a := &Result{Group: res.Tags.Merge(opentsdb.TagSet{"shift": offset.String()})}
   338  		for k, v := range res.DPS {
   339  			i, e := strconv.ParseInt(k, 10, 64)
   340  			if e != nil {
   341  				return e
   342  			}
   343  			values[time.Unix(i, 0).Add(offset).UTC()] = float64(v)
   344  		}
   345  		a.Value = values
   346  		r.Results = append(r.Results, a)
   347  		return nil
   348  	})
   349  	if err != nil {
   350  		err = fmt.Errorf("expr: Band: %v", err)
   351  	}
   352  	return
   353  }
   354  
   355  func Band(e *State, query, duration, period string, num float64) (r *Results, err error) {
   356  	// existing Band behaviour is to end 'period' ago, so pass period as eduration.
   357  	return BandQuery(e, query, duration, period, period, num)
   358  }
   359  
   360  func ShiftBand(e *State, query, duration, period string, num float64) (r *Results, err error) {
   361  	return OverQuery(e, query, duration, period, period, num)
   362  }
   363  
   364  func Over(e *State, query, duration, period string, num float64) (r *Results, err error) {
   365  	return OverQuery(e, query, duration, period, "", num)
   366  }
   367  
   368  func Query(e *State, query, sduration, eduration string) (r *Results, err error) {
   369  	r = new(Results)
   370  	q, err := opentsdb.ParseQuery(query, e.TSDBContext.Version())
   371  	if q == nil && err != nil {
   372  		return
   373  	}
   374  	if !e.TSDBContext.Version().FilterSupport() {
   375  		if err = e.Search.Expand(q); err != nil {
   376  			return
   377  		}
   378  	}
   379  	sd, err := opentsdb.ParseDuration(sduration)
   380  	if err != nil {
   381  		return
   382  	}
   383  	req := opentsdb.Request{
   384  		Queries: []*opentsdb.Query{q},
   385  		Start:   fmt.Sprintf("%s-ago", sd),
   386  	}
   387  	if eduration != "" {
   388  		var ed opentsdb.Duration
   389  		ed, err = opentsdb.ParseDuration(eduration)
   390  		if err != nil {
   391  			return
   392  		}
   393  		req.End = fmt.Sprintf("%s-ago", ed)
   394  	}
   395  	var s opentsdb.ResponseSet
   396  	if err = req.SetTime(e.now); err != nil {
   397  		return
   398  	}
   399  	s, err = timeTSDBRequest(e, &req)
   400  	if err != nil {
   401  		return
   402  	}
   403  	for _, res := range s {
   404  		if e.Squelched(res.Tags) {
   405  			continue
   406  		}
   407  		values := make(Series)
   408  		for k, v := range res.DPS {
   409  			i, err := strconv.ParseInt(k, 10, 64)
   410  			if err != nil {
   411  				return nil, err
   412  			}
   413  			values[time.Unix(i, 0).UTC()] = float64(v)
   414  		}
   415  		r.Results = append(r.Results, &Result{
   416  			Value: values,
   417  			Group: res.Tags,
   418  		})
   419  	}
   420  	return
   421  }
   422  
   423  func Change(e *State, query, sduration, eduration string) (r *Results, err error) {
   424  	r = new(Results)
   425  	sd, err := opentsdb.ParseDuration(sduration)
   426  	if err != nil {
   427  		return
   428  	}
   429  	var ed opentsdb.Duration
   430  	if eduration != "" {
   431  		ed, err = opentsdb.ParseDuration(eduration)
   432  		if err != nil {
   433  			return
   434  		}
   435  	}
   436  	r, err = Query(e, query, sduration, eduration)
   437  	if err != nil {
   438  		return
   439  	}
   440  	r, err = reduce(e, r, change, fromScalar((sd - ed).Seconds()))
   441  	return
   442  }
   443  
   444  func change(dps Series, args ...float64) float64 {
   445  	return avg(dps) * args[0]
   446  }