bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/opentsdb/tsdb.go (about) 1 // Package opentsdb defines structures for interacting with an OpenTSDB server. 2 package opentsdb // import "bosun.org/opentsdb" 3 4 import ( 5 "bytes" 6 "encoding/json" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "math" 11 "math/big" 12 "net/http" 13 "net/url" 14 "regexp" 15 "sort" 16 "strconv" 17 "strings" 18 "time" 19 20 "bosun.org/slog" 21 "github.com/pkg/errors" 22 ) 23 24 // ResponseSet is a Multi-Set Response: 25 // http://opentsdb.net/docs/build/html/api_http/query/index.html#example-multi-set-response. 26 type ResponseSet []*Response 27 28 func (r ResponseSet) Copy() ResponseSet { 29 newSet := make(ResponseSet, len(r)) 30 for i, resp := range r { 31 newSet[i] = resp.Copy() 32 } 33 return newSet 34 } 35 36 // Point is the Response data point type. 37 type Point float64 38 39 // Response is a query response: 40 // http://opentsdb.net/docs/build/html/api_http/query/index.html#response. 41 type Response struct { 42 Metric string `json:"metric"` 43 Tags TagSet `json:"tags"` 44 AggregateTags []string `json:"aggregateTags"` 45 DPS map[string]Point `json:"dps"` 46 47 // fields added by translating proxy 48 SQL string `json:"sql,omitempty"` 49 } 50 51 func (r *Response) Copy() *Response { 52 newR := Response{} 53 newR.Metric = r.Metric 54 newR.Tags = r.Tags.Copy() 55 copy(newR.AggregateTags, r.AggregateTags) 56 newR.DPS = map[string]Point{} 57 for k, v := range r.DPS { 58 newR.DPS[k] = v 59 } 60 return &newR 61 } 62 63 // DataPoint is a data point for the /api/put route: 64 // http://opentsdb.net/docs/build/html/api_http/put.html#example-single-data-point-put. 65 type DataPoint struct { 66 Metric string `json:"metric"` 67 Timestamp int64 `json:"timestamp"` 68 Value interface{} `json:"value"` 69 Tags TagSet `json:"tags"` 70 } 71 72 // MarshalJSON verifies d is valid and converts it to JSON. 73 func (d *DataPoint) MarshalJSON() ([]byte, error) { 74 if err := d.Clean(); err != nil { 75 return nil, err 76 } 77 return json.Marshal(struct { 78 Metric string `json:"metric"` 79 Timestamp int64 `json:"timestamp"` 80 Value interface{} `json:"value"` 81 Tags TagSet `json:"tags"` 82 }{ 83 d.Metric, 84 d.Timestamp, 85 d.Value, 86 d.Tags, 87 }) 88 } 89 90 // Valid returns whether d contains valid data (populated fields, valid tags) 91 // for submission to OpenTSDB. 92 func (d *DataPoint) Valid() bool { 93 if d.Metric == "" || !ValidTSDBString(d.Metric) || d.Timestamp == 0 || d.Value == nil || !d.Tags.Valid() { 94 return false 95 } 96 f, err := strconv.ParseFloat(fmt.Sprint(d.Value), 64) 97 if err != nil || math.IsNaN(f) { 98 return false 99 } 100 return true 101 } 102 103 // MultiDataPoint holds multiple DataPoints: 104 // http://opentsdb.net/docs/build/html/api_http/put.html#example-multiple-data-point-put. 105 type MultiDataPoint []*DataPoint 106 107 // TagSet is a helper class for tags. 108 type TagSet map[string]string 109 110 // Copy creates a new TagSet from t. 111 func (t TagSet) Copy() TagSet { 112 n := make(TagSet) 113 for k, v := range t { 114 n[k] = v 115 } 116 return n 117 } 118 119 // Merge adds or overwrites everything from o into t and returns t. 120 func (t TagSet) Merge(o TagSet) TagSet { 121 for k, v := range o { 122 t[k] = v 123 } 124 return t 125 } 126 127 // Equal returns true if t and o contain only the same k=v pairs. 128 func (t TagSet) Equal(o TagSet) bool { 129 if len(t) != len(o) { 130 return false 131 } 132 for k, v := range t { 133 if ov, ok := o[k]; !ok || ov != v { 134 return false 135 } 136 } 137 return true 138 } 139 140 // Subset returns true if all k=v pairs in o are in t. 141 func (t TagSet) Subset(o TagSet) bool { 142 if len(o) > len(t) { 143 return false 144 } 145 for k, v := range o { 146 if tv, ok := t[k]; !ok || tv != v { 147 return false 148 } 149 } 150 return true 151 } 152 153 // Compatible returns true if all keys that are in both o and t, have the same value. 154 func (t TagSet) Compatible(o TagSet) bool { 155 for k, v := range o { 156 if tv, ok := t[k]; ok && tv != v { 157 return false 158 } 159 } 160 return true 161 } 162 163 // Intersection returns the intersection of t and o. 164 func (t TagSet) Intersection(o TagSet) TagSet { 165 r := make(TagSet) 166 for k, v := range t { 167 if o[k] == v { 168 r[k] = v 169 } 170 } 171 return r 172 } 173 174 // String converts t to an OpenTSDB-style {a=b,c=b} string, alphabetized by key. 175 func (t TagSet) String() string { 176 return fmt.Sprintf("{%s}", t.Tags()) 177 } 178 179 // Tags is identical to String() but without { and }. 180 func (t TagSet) Tags() string { 181 var keys []string 182 for k := range t { 183 keys = append(keys, k) 184 } 185 sort.Strings(keys) 186 b := &bytes.Buffer{} 187 for i, k := range keys { 188 if i > 0 { 189 fmt.Fprint(b, ",") 190 } 191 fmt.Fprintf(b, "%s=%s", k, t[k]) 192 } 193 return b.String() 194 } 195 196 func (t TagSet) AllSubsets() []string { 197 var keys []string 198 for k := range t { 199 keys = append(keys, k) 200 } 201 sort.Strings(keys) 202 return t.allSubsets("", 0, keys) 203 } 204 205 func (t TagSet) allSubsets(base string, start int, keys []string) []string { 206 subs := []string{} 207 for i := start; i < len(keys); i++ { 208 part := base 209 if part != "" { 210 part += "," 211 } 212 part += fmt.Sprintf("%s=%s", keys[i], t[keys[i]]) 213 subs = append(subs, part) 214 subs = append(subs, t.allSubsets(part, i+1, keys)...) 215 } 216 return subs 217 } 218 219 // Returns true if the two tagsets "overlap". 220 // Two tagsets overlap if they: 221 // 1. Have at least one key/value pair that matches 222 // 2. Have no keys in common where the values do not match 223 func (a TagSet) Overlaps(b TagSet) bool { 224 anyMatch := false 225 for k, v := range a { 226 v2, ok := b[k] 227 if !ok { 228 continue 229 } 230 if v2 != v { 231 return false 232 } 233 anyMatch = true 234 } 235 return anyMatch 236 } 237 238 // Valid returns whether t contains OpenTSDB-submittable tags. 239 func (t TagSet) Valid() bool { 240 if len(t) == 0 { 241 return true 242 } 243 _, err := ParseTags(t.Tags()) 244 return err == nil 245 } 246 247 func (d *DataPoint) Clean() error { 248 if err := d.Tags.Clean(); err != nil { 249 return fmt.Errorf("cleaning tags for metric %s: %s", d.Metric, err) 250 } 251 m, err := Clean(d.Metric) 252 if err != nil { 253 return fmt.Errorf("cleaning metric %s: %s", d.Metric, err) 254 } 255 if d.Metric != m { 256 d.Metric = m 257 } 258 switch v := d.Value.(type) { 259 case string: 260 if i, err := strconv.ParseInt(v, 10, 64); err == nil { 261 d.Value = i 262 } else if f, err := strconv.ParseFloat(v, 64); err == nil { 263 d.Value = f 264 } else { 265 return fmt.Errorf("Unparseable number %v", v) 266 } 267 case uint64: 268 if v > math.MaxInt64 { 269 d.Value = float64(v) 270 } 271 case *big.Int: 272 if bigMaxInt64.Cmp(v) < 0 { 273 if f, err := strconv.ParseFloat(v.String(), 64); err == nil { 274 d.Value = f 275 } 276 } 277 } 278 // if timestamp bigger than 32 bits, likely in milliseconds 279 if d.Timestamp > 0xffffffff { 280 d.Timestamp /= 1000 281 } 282 if !d.Valid() { 283 return fmt.Errorf("datapoint is invalid") 284 } 285 return nil 286 } 287 288 var bigMaxInt64 = big.NewInt(math.MaxInt64) 289 290 // Clean removes characters from t that are invalid for OpenTSDB metric and tag 291 // values. An error is returned if a resulting tag is empty. 292 func (t TagSet) Clean() error { 293 for k, v := range t { 294 kc, err := Clean(k) 295 if err != nil { 296 return fmt.Errorf("cleaning tag %s: %s", k, err) 297 } 298 vc, err := Clean(v) 299 if err != nil { 300 return fmt.Errorf("cleaning value %s for tag %s: %s", v, k, err) 301 } 302 if kc == "" || vc == "" { 303 return fmt.Errorf("cleaning value [%s] for tag [%s] result in an empty string", v, k) 304 } 305 if kc != k || vc != v { 306 delete(t, k) 307 t[kc] = vc 308 } 309 } 310 return nil 311 } 312 313 // Clean is Replace with an empty replacement string. 314 func Clean(s string) (string, error) { 315 return Replace(s, "") 316 } 317 318 // Replace removes characters from s that are invalid for OpenTSDB metric and 319 // tag values and replaces them. 320 // See: http://opentsdb.net/docs/build/html/user_guide/writing.html#metrics-and-tags 321 func Replace(s, replacement string) (string, error) { 322 323 // constructing a name processor isn't too expensive but we need to refactor this file so that it's possible to 324 // inject instances so that we don't have to keep newing up. 325 // For the moment I prefer to constructing like this to holding onto a global instance 326 val, err := NewOpenTsdbNameProcessor(replacement) 327 if err != nil { 328 return "", errors.Wrap(err, "Failed to create name processor") 329 } 330 331 result, err := val.FormatName(s) 332 if err != nil { 333 return "", errors.Wrap(err, "Failed to format string") 334 } 335 336 return result, nil 337 } 338 339 // MustReplace is like Replace, but returns an empty string on error. 340 func MustReplace(s, replacement string) string { 341 r, err := Replace(s, replacement) 342 if err != nil { 343 return "" 344 } 345 return r 346 } 347 348 // Request holds query objects: 349 // http://opentsdb.net/docs/build/html/api_http/query/index.html#requests. 350 type Request struct { 351 Start interface{} `json:"start"` 352 End interface{} `json:"end,omitempty"` 353 Queries []*Query `json:"queries"` 354 NoAnnotations bool `json:"noAnnotations,omitempty"` 355 GlobalAnnotations bool `json:"globalAnnotations,omitempty"` 356 MsResolution bool `json:"msResolution,omitempty"` 357 ShowTSUIDs bool `json:"showTSUIDs,omitempty"` 358 Delete bool `json:"delete,omitempty"` 359 } 360 361 // RequestFromJSON creates a new request from JSON. 362 func RequestFromJSON(b []byte) (*Request, error) { 363 var r Request 364 if err := json.Unmarshal(b, &r); err != nil { 365 return nil, err 366 } 367 r.Start = TryParseAbsTime(r.Start) 368 r.End = TryParseAbsTime(r.End) 369 return &r, nil 370 } 371 372 // Query is a query for a request: 373 // http://opentsdb.net/docs/build/html/api_http/query/index.html#sub-queries. 374 type Query struct { 375 Aggregator string `json:"aggregator"` 376 Metric string `json:"metric"` 377 Rate bool `json:"rate,omitempty"` 378 RateOptions RateOptions `json:"rateOptions,omitempty"` 379 Downsample string `json:"downsample,omitempty"` 380 Tags TagSet `json:"tags,omitempty"` 381 Filters Filters `json:"filters,omitempty"` 382 GroupByTags TagSet `json:"-"` 383 } 384 385 type Filter struct { 386 Type string `json:"type"` 387 TagK string `json:"tagk"` 388 Filter string `json:"filter"` 389 GroupBy bool `json:"groupBy"` 390 } 391 392 func (f Filter) String() string { 393 return fmt.Sprintf("%s=%s(%s)", f.TagK, f.Type, f.Filter) 394 } 395 396 type Filters []Filter 397 398 func (filters Filters) String() string { 399 s := "" 400 gb := make(Filters, 0) 401 nGb := make(Filters, 0) 402 for _, filter := range filters { 403 if filter.GroupBy { 404 gb = append(gb, filter) 405 continue 406 } 407 nGb = append(nGb, filter) 408 } 409 s += "{" 410 for i, filter := range gb { 411 s += filter.String() 412 if i != len(gb)-1 { 413 s += "," 414 } 415 } 416 s += "}" 417 for i, filter := range nGb { 418 if i == 0 { 419 s += "{" 420 } 421 s += filter.String() 422 if i == len(nGb)-1 { 423 s += "}" 424 } else { 425 s += "," 426 } 427 } 428 return s 429 } 430 431 // RateOptions are rate options for a query. 432 type RateOptions struct { 433 Counter bool `json:"counter,omitempty"` 434 CounterMax int64 `json:"counterMax,omitempty"` 435 ResetValue int64 `json:"resetValue,omitempty"` 436 DropResets bool `json:"dropResets,omitempty"` 437 } 438 439 // ParseRequest parses OpenTSDB requests of the form: start=1h-ago&m=avg:cpu. 440 func ParseRequest(req string, version Version) (*Request, error) { 441 v, err := url.ParseQuery(req) 442 if err != nil { 443 return nil, err 444 } 445 r := Request{} 446 s := v.Get("start") 447 if s == "" { 448 return nil, fmt.Errorf("opentsdb: missing start: %s", req) 449 } 450 r.Start = s 451 for _, m := range v["m"] { 452 q, err := ParseQuery(m, version) 453 if err != nil { 454 return nil, err 455 } 456 r.Queries = append(r.Queries, q) 457 } 458 if len(r.Queries) == 0 { 459 return nil, fmt.Errorf("opentsdb: missing m: %s", req) 460 } 461 return &r, nil 462 } 463 464 var qRE2_1 = regexp.MustCompile(`^(?P<aggregator>\w+):(?:(?P<downsample>\w+-\w+):)?(?:(?P<rate>rate.*):)?(?P<metric>[\w./-]+)(?:\{([\w./,=*-|]+)\})?$`) 465 var qRE2_2 = regexp.MustCompile(`^(?P<aggregator>\w+):(?:(?P<downsample>\w+-\w+(?:-(?:\w+))?):)?(?:(?P<rate>rate.*):)?(?P<metric>[\w./-]+)(?:\{([^}]+)?\})?(?:\{([^}]+)?\})?$`) 466 467 // ParseQuery parses OpenTSDB queries of the form: avg:rate:cpu{k=v}. Validation 468 // errors will be returned along with a valid Query. 469 func ParseQuery(query string, version Version) (q *Query, err error) { 470 var regExp = qRE2_1 471 q = new(Query) 472 if version.FilterSupport() { 473 regExp = qRE2_2 474 } 475 476 m := regExp.FindStringSubmatch(query) 477 478 if m == nil { 479 return nil, fmt.Errorf("opentsdb: bad query format: %s", query) 480 } 481 482 result := make(map[string]string) 483 for i, name := range regExp.SubexpNames() { 484 if i != 0 { 485 result[name] = m[i] 486 } 487 } 488 489 q.Aggregator = result["aggregator"] 490 q.Downsample = result["downsample"] 491 q.Rate = strings.HasPrefix(result["rate"], "rate") 492 if q.Rate && len(result["rate"]) > 4 { 493 s := result["rate"][4:] 494 if !strings.HasSuffix(s, "}") || !strings.HasPrefix(s, "{") { 495 err = fmt.Errorf("opentsdb: invalid rate options") 496 return 497 } 498 sp := strings.Split(s[1:len(s)-1], ",") 499 q.RateOptions.Counter = sp[0] == "counter" || sp[0] == "dropcounter" 500 q.RateOptions.DropResets = sp[0] == "dropcounter" 501 if len(sp) > 1 { 502 if sp[1] != "" { 503 if q.RateOptions.CounterMax, err = strconv.ParseInt(sp[1], 10, 64); err != nil { 504 return 505 } 506 } 507 } 508 if len(sp) > 2 { 509 if q.RateOptions.ResetValue, err = strconv.ParseInt(sp[2], 10, 64); err != nil { 510 return 511 } 512 } 513 } 514 q.Metric = result["metric"] 515 516 if !version.FilterSupport() && len(m) > 5 && m[5] != "" { 517 tags, e := ParseTags(m[5]) 518 if e != nil { 519 err = e 520 if tags == nil { 521 return 522 } 523 } 524 q.Tags = tags 525 } 526 527 if !version.FilterSupport() { 528 return 529 } 530 531 // OpenTSDB Greater than 2.2, treating as filters 532 q.GroupByTags = make(TagSet) 533 q.Filters = make([]Filter, 0) 534 if m[5] != "" { 535 f, err := ParseFilters(m[5], true, q) 536 if err != nil { 537 return nil, fmt.Errorf("Failed to parse filter(s): %s", m[5]) 538 } 539 q.Filters = append(q.Filters, f...) 540 } 541 if m[6] != "" { 542 f, err := ParseFilters(m[6], false, q) 543 if err != nil { 544 return nil, fmt.Errorf("Failed to parse filter(s): %s", m[6]) 545 } 546 q.Filters = append(q.Filters, f...) 547 } 548 549 return 550 } 551 552 var filterValueRe = regexp.MustCompile(`([a-z_]+)\((.*)\)$`) 553 554 // ParseFilters parses filters in the form of `tagk=filterFunc(...),...` 555 // It also mimics OpenTSDB's promotion of queries with a * or no 556 // function to iwildcard and literal_or respectively 557 func ParseFilters(rawFilters string, grouping bool, q *Query) ([]Filter, error) { 558 var filters []Filter 559 for _, rawFilter := range strings.Split(rawFilters, ",") { 560 splitRawFilter := strings.SplitN(rawFilter, "=", 2) 561 if len(splitRawFilter) != 2 { 562 return nil, fmt.Errorf("opentsdb: bad filter format: %s", rawFilter) 563 } 564 filter := Filter{} 565 filter.TagK = splitRawFilter[0] 566 if grouping { 567 q.GroupByTags[filter.TagK] = "" 568 } 569 // See if we have a filter function, if not we have to use legacy parsing defined in 570 // filter conversions of http://opentsdb.net/docs/build/html/api_http/query/index.html 571 m := filterValueRe.FindStringSubmatch(splitRawFilter[1]) 572 if m != nil { 573 filter.Type = m[1] 574 filter.Filter = m[2] 575 } else { 576 // Legacy Conversion 577 filter.Type = "literal_or" 578 if strings.Contains(splitRawFilter[1], "*") { 579 filter.Type = "iwildcard" 580 } 581 if splitRawFilter[1] == "*" { 582 filter.Type = "wildcard" 583 } 584 filter.Filter = splitRawFilter[1] 585 } 586 filter.GroupBy = grouping 587 filters = append(filters, filter) 588 } 589 return filters, nil 590 } 591 592 // ParseTags parses OpenTSDB tagk=tagv pairs of the form: k=v,m=o. Validation 593 // errors do not stop processing, and will return a non-nil TagSet. 594 func ParseTags(t string) (TagSet, error) { 595 ts := make(TagSet) 596 var err error 597 for _, v := range strings.Split(t, ",") { 598 sp := strings.SplitN(v, "=", 2) 599 if len(sp) != 2 { 600 return nil, fmt.Errorf("opentsdb: bad tag: %s", v) 601 } 602 for i, s := range sp { 603 sp[i] = strings.TrimSpace(s) 604 if i > 0 { 605 continue 606 } 607 if !ValidTSDBString(sp[i]) { 608 err = fmt.Errorf("invalid character in %s", sp[i]) 609 } 610 } 611 for _, s := range strings.Split(sp[1], "|") { 612 if s == "*" { 613 continue 614 } 615 if !ValidTSDBString(s) { 616 err = fmt.Errorf("invalid character in %s", sp[1]) 617 } 618 } 619 if _, present := ts[sp[0]]; present { 620 return nil, fmt.Errorf("opentsdb: duplicated tag: %s", v) 621 } 622 ts[sp[0]] = sp[1] 623 } 624 return ts, err 625 } 626 627 // ValidTSDBString returns true if s is a valid metric or tag. 628 func ValidTSDBString(s string) bool { 629 630 // constructing a name processor isn't too expensive but we need to refactor this file so that it's possible to 631 // inject instances so that we don't have to keep newing up. 632 // For the moment I prefer to constructing like this to holding onto a global instance 633 val, err := NewOpenTsdbNameProcessor("") 634 if err != nil { 635 return false 636 } 637 638 return val.IsValid(s) 639 } 640 641 var groupRE = regexp.MustCompile("{[^}]+}") 642 643 // ReplaceTags replaces all tag-like strings with tags from the given 644 // group. For example, given the string "test.metric{host=*}" and a TagSet 645 // with host=test.com, this returns "test.metric{host=test.com}". 646 func ReplaceTags(text string, group TagSet) string { 647 return groupRE.ReplaceAllStringFunc(text, func(s string) string { 648 tags, err := ParseTags(s[1 : len(s)-1]) 649 if err != nil { 650 return s 651 } 652 for k := range tags { 653 if group[k] != "" { 654 tags[k] = group[k] 655 } 656 } 657 return fmt.Sprintf("{%s}", tags.Tags()) 658 }) 659 } 660 661 func (q Query) String() string { 662 s := q.Aggregator + ":" 663 if q.Downsample != "" { 664 s += q.Downsample + ":" 665 } 666 if q.Rate { 667 s += "rate" 668 if q.RateOptions.Counter { 669 s += "{" 670 if q.RateOptions.DropResets { 671 s += "dropcounter" 672 } else { 673 s += "counter" 674 } 675 if q.RateOptions.CounterMax != 0 { 676 s += "," 677 s += strconv.FormatInt(q.RateOptions.CounterMax, 10) 678 } 679 if q.RateOptions.ResetValue != 0 { 680 if q.RateOptions.CounterMax == 0 { 681 s += "," 682 } 683 s += "," 684 s += strconv.FormatInt(q.RateOptions.ResetValue, 10) 685 } 686 s += "}" 687 } 688 s += ":" 689 } 690 s += q.Metric 691 if len(q.Tags) > 0 { 692 s += q.Tags.String() 693 } 694 if len(q.Filters) > 0 { 695 s += q.Filters.String() 696 } 697 return s 698 } 699 700 func (r *Request) String() string { 701 v := make(url.Values) 702 for _, q := range r.Queries { 703 v.Add("m", q.String()) 704 } 705 if start, err := CanonicalTime(r.Start); err == nil { 706 v.Add("start", start) 707 } 708 if end, err := CanonicalTime(r.End); err == nil { 709 v.Add("end", end) 710 } 711 return v.Encode() 712 } 713 714 // Search returns a string suitable for OpenTSDB's `/` route. 715 func (r *Request) Search() string { 716 // OpenTSDB uses the URL hash, not search parameters, to do this. The values are 717 // not URL encoded. So it's the same as a url.Values just left as normal 718 // strings. 719 v, err := url.ParseQuery(r.String()) 720 if err != nil { 721 return "" 722 } 723 buf := &bytes.Buffer{} 724 for k, values := range v { 725 for _, value := range values { 726 fmt.Fprintf(buf, "%s=%s&", k, value) 727 } 728 } 729 return buf.String() 730 } 731 732 // TSDBTimeFormat is the OpenTSDB-required time format for the time package. 733 const TSDBTimeFormat = "2006/01/02-15:04:05" 734 735 // CanonicalTime converts v to a string for use with OpenTSDB's `/` route. 736 func CanonicalTime(v interface{}) (string, error) { 737 if s, ok := v.(string); ok { 738 if strings.HasSuffix(s, "-ago") { 739 return s, nil 740 } 741 } 742 t, err := ParseTime(v) 743 if err != nil { 744 return "", err 745 } 746 return t.Format(TSDBTimeFormat), nil 747 } 748 749 // TryParseAbsTime attempts to parse v as an absolute time. It may be a string 750 // in the format of TSDBTimeFormat or a float64 of seconds since epoch. If so, 751 // the epoch as an int64 is returned. Otherwise, v is returned. 752 func TryParseAbsTime(v interface{}) interface{} { 753 switch v := v.(type) { 754 case string: 755 d, err := ParseAbsTime(v) 756 if err == nil { 757 return d.Unix() 758 } 759 case float64: 760 return int64(v) 761 } 762 return v 763 } 764 765 // ParseAbsTime returns the time of s, which must be of any non-relative (not 766 // "X-ago") format supported by OpenTSDB. 767 func ParseAbsTime(s string) (time.Time, error) { 768 var t time.Time 769 tFormats := [4]string{ 770 "2006/01/02-15:04:05", 771 "2006/01/02-15:04", 772 "2006/01/02-15", 773 "2006/01/02", 774 } 775 for _, f := range tFormats { 776 if t, err := time.Parse(f, s); err == nil { 777 return t, nil 778 } 779 } 780 i, err := strconv.ParseInt(s, 10, 64) 781 if err != nil { 782 return t, err 783 } 784 return time.Unix(i, 0), nil 785 } 786 787 // ParseTime returns the time of v, which can be of any format supported by 788 // OpenTSDB. 789 func ParseTime(v interface{}) (time.Time, error) { 790 now := time.Now().UTC() 791 const max32 int64 = 0xffffffff 792 switch i := v.(type) { 793 case string: 794 if i != "" { 795 if strings.HasSuffix(i, "-ago") { 796 s := strings.TrimSuffix(i, "-ago") 797 d, err := ParseDuration(s) 798 if err != nil { 799 return now, err 800 } 801 return now.Add(time.Duration(-d)), nil 802 } 803 return ParseAbsTime(i) 804 } 805 return now, nil 806 case int64: 807 if i > max32 { 808 i /= 1000 809 } 810 return time.Unix(i, 0).UTC(), nil 811 case float64: 812 i2 := int64(i) 813 if i2 > max32 { 814 i2 /= 1000 815 } 816 return time.Unix(i2, 0).UTC(), nil 817 default: 818 return time.Time{}, fmt.Errorf("type must be string or int64, got: %v", v) 819 } 820 } 821 822 // GetDuration returns the duration from the request's start to end. 823 func GetDuration(r *Request) (Duration, error) { 824 var t Duration 825 if v, ok := r.Start.(string); ok && v == "" { 826 return t, errors.New("start time must be provided") 827 } 828 start, err := ParseTime(r.Start) 829 if err != nil { 830 return t, err 831 } 832 var end time.Time 833 if r.End != nil { 834 end, err = ParseTime(r.End) 835 if err != nil { 836 return t, err 837 } 838 } else { 839 end = time.Now() 840 } 841 t = Duration(end.Sub(start)) 842 return t, nil 843 } 844 845 // AutoDownsample sets the avg downsample aggregator to produce l points. 846 func (r *Request) AutoDownsample(l int) error { 847 if l == 0 { 848 return errors.New("opentsdb: target length must be > 0") 849 } 850 cd, err := GetDuration(r) 851 if err != nil { 852 return err 853 } 854 d := cd / Duration(l) 855 ds := "" 856 if d > Duration(time.Second)*15 { 857 ds = fmt.Sprintf("%ds-avg", int64(d.Seconds())) 858 } 859 for _, q := range r.Queries { 860 q.Downsample = ds 861 } 862 return nil 863 } 864 865 // SetTime adjusts the start and end time of the request to assume t is now. 866 // Relative times ("1m-ago") are changed to absolute times. Existing absolute 867 // times are adjusted by the difference between time.Now() and t. 868 func (r *Request) SetTime(t time.Time) error { 869 diff := -time.Since(t) 870 start, err := ParseTime(r.Start) 871 if err != nil { 872 return err 873 } 874 r.Start = start.Add(diff).Unix() 875 if r.End != nil { 876 end, err := ParseTime(r.End) 877 if err != nil { 878 return err 879 } 880 r.End = end.Add(diff).Unix() 881 } else { 882 r.End = t.UTC().Unix() 883 } 884 return nil 885 } 886 887 // Query performs a v2 OpenTSDB request to the given host. host should be of the 888 // form hostname:port. Uses DefaultClient. Can return a RequestError. 889 func (r *Request) Query(host string) (ResponseSet, error) { 890 resp, err := r.QueryResponse(host, nil) 891 if err != nil { 892 return nil, err 893 } 894 defer resp.Body.Close() 895 var tr ResponseSet 896 if err := json.NewDecoder(resp.Body).Decode(&tr); err != nil { 897 return nil, err 898 } 899 return tr, nil 900 } 901 902 // DefaultClient is the default http client for requests. 903 var DefaultClient = &http.Client{ 904 Timeout: time.Minute, 905 } 906 907 // QueryResponse performs a v2 OpenTSDB request to the given host. host should 908 // be of the form hostname:port. A nil client uses DefaultClient. 909 func (r *Request) QueryResponse(host string, client *http.Client) (*http.Response, error) { 910 911 u := url.URL{ 912 Scheme: "http", 913 Host: host, 914 Path: "/api/query", 915 } 916 917 pu, err := url.Parse(host) 918 if err == nil && pu.Scheme != "" && pu.Host != "" { 919 u.Scheme = pu.Scheme 920 u.Host = pu.Host 921 if pu.Path != "" { 922 u.Path = pu.Path 923 } 924 } 925 926 b, err := json.Marshal(&r) 927 if err != nil { 928 return nil, err 929 } 930 if client == nil { 931 client = DefaultClient 932 } 933 resp, err := client.Post(u.String(), "application/json", bytes.NewReader(b)) 934 if err != nil { 935 return nil, err 936 } 937 if resp.StatusCode != http.StatusOK { 938 e := RequestError{Request: string(b)} 939 defer resp.Body.Close() 940 body, _ := ioutil.ReadAll(resp.Body) 941 if err := json.NewDecoder(bytes.NewBuffer(body)).Decode(&e); err == nil { 942 return nil, &e 943 } 944 s := fmt.Sprintf("opentsdb: %s", resp.Status) 945 if len(body) > 0 { 946 s = fmt.Sprintf("%s: %s", s, body) 947 } 948 return nil, errors.New(s) 949 } 950 return resp, nil 951 } 952 953 // RequestError is the error structure for request errors. 954 type RequestError struct { 955 Request string 956 Err struct { 957 Code int `json:"code"` 958 Message string `json:"message"` 959 Details string `json:"details"` 960 } `json:"error"` 961 } 962 963 func (r *RequestError) Error() string { 964 return fmt.Sprintf("opentsdb: %s: %s", r.Request, r.Err.Message) 965 } 966 967 // Context is the interface for querying an OpenTSDB server. 968 type Context interface { 969 Query(*Request) (ResponseSet, error) 970 Version() Version 971 } 972 973 // Host is a simple OpenTSDB Context with no additional features. 974 type Host string 975 976 // Query performs the request to the OpenTSDB server. 977 func (h Host) Query(r *Request) (ResponseSet, error) { 978 return r.Query(string(h)) 979 } 980 981 // OpenTSDB 2.1 version struct 982 var Version2_1 = Version{2, 1} 983 984 // OpenTSDB 2.2 version struct 985 var Version2_2 = Version{2, 2} 986 987 type Version struct { 988 Major int64 989 Minor int64 990 } 991 992 func (v *Version) UnmarshalText(text []byte) error { 993 var err error 994 split := strings.Split(string(text), ".") 995 if len(split) != 2 { 996 return fmt.Errorf("invalid opentsdb version, expected number.number, (i.e 2.2) got %v", text) 997 } 998 v.Major, err = strconv.ParseInt(split[0], 10, 64) 999 if err != nil { 1000 return fmt.Errorf("could not parse major version number for opentsdb version: %v", split[0]) 1001 } 1002 v.Minor, err = strconv.ParseInt(split[0], 10, 64) 1003 if err != nil { 1004 return fmt.Errorf("could not parse minor version number for opentsdb version: %v", split[1]) 1005 } 1006 return nil 1007 } 1008 1009 func (v Version) FilterSupport() bool { 1010 return v.Major >= 2 && v.Minor >= 2 1011 } 1012 1013 // LimitContext is a context that enables limiting response size and filtering tags 1014 type LimitContext struct { 1015 Host string 1016 // Limit limits response size in bytes 1017 Limit int64 1018 // FilterTags removes tagks from results if that tagk was not in the request 1019 FilterTags bool 1020 // Use the version to see if groupby and filters are supported 1021 TSDBVersion Version 1022 } 1023 1024 // NewLimitContext returns a new context for the given host with response sizes limited 1025 // to limit bytes. 1026 func NewLimitContext(host string, limit int64, version Version) *LimitContext { 1027 return &LimitContext{ 1028 Host: host, 1029 Limit: limit, 1030 FilterTags: true, 1031 TSDBVersion: version, 1032 } 1033 } 1034 1035 func (c *LimitContext) Version() Version { 1036 return c.TSDBVersion 1037 } 1038 1039 // Query returns the result of the request. r may be cached. The request is 1040 // byte-limited and filtered by c's properties. 1041 func (c *LimitContext) Query(r *Request) (tr ResponseSet, err error) { 1042 resp, err := r.QueryResponse(c.Host, nil) 1043 if err != nil { 1044 return 1045 } 1046 defer resp.Body.Close() 1047 lr := &io.LimitedReader{R: resp.Body, N: c.Limit} 1048 err = json.NewDecoder(lr).Decode(&tr) 1049 if lr.N == 0 { 1050 err = fmt.Errorf("TSDB response too large: limited to %E bytes", float64(c.Limit)) 1051 slog.Error(err) 1052 return 1053 } 1054 if err != nil { 1055 return 1056 } 1057 if c.FilterTags { 1058 FilterTags(r, tr) 1059 } 1060 return 1061 } 1062 1063 // FilterTags removes tagks in tr not present in r. Does nothing in the event of 1064 // multiple queries in the request. 1065 func FilterTags(r *Request, tr ResponseSet) { 1066 if len(r.Queries) != 1 { 1067 return 1068 } 1069 for _, resp := range tr { 1070 for k := range resp.Tags { 1071 _, inTags := r.Queries[0].Tags[k] 1072 inGroupBy := false 1073 for _, filter := range r.Queries[0].Filters { 1074 if filter.GroupBy && filter.TagK == k { 1075 inGroupBy = true 1076 break 1077 } 1078 } 1079 if inTags || inGroupBy { 1080 continue 1081 } 1082 delete(resp.Tags, k) 1083 } 1084 } 1085 }