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 }