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  }