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

     1  package expr
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"strings"
     7  	"time"
     8  
     9  	"bosun.org/cmd/bosun/expr/parse"
    10  	"bosun.org/graphite"
    11  	"bosun.org/models"
    12  	"bosun.org/opentsdb"
    13  	"github.com/MiniProfiler/go/miniprofiler"
    14  )
    15  
    16  // Graphite defines functions for use with a Graphite backend.
    17  var Graphite = map[string]parse.Func{
    18  	"graphiteBand": {
    19  		Args:   []models.FuncType{models.TypeString, models.TypeString, models.TypeString, models.TypeString, models.TypeScalar},
    20  		Return: models.TypeSeriesSet,
    21  		Tags:   graphiteTagQuery,
    22  		F:      GraphiteBand,
    23  	},
    24  	"graphite": {
    25  		Args:   []models.FuncType{models.TypeString, models.TypeString, models.TypeString, models.TypeString},
    26  		Return: models.TypeSeriesSet,
    27  		Tags:   graphiteTagQuery,
    28  		F:      GraphiteQuery,
    29  	},
    30  }
    31  
    32  func parseGraphiteResponse(req *graphite.Request, s *graphite.Response, formatTags []string) ([]*Result, error) {
    33  	const parseErrFmt = "graphite ParseError (%s): %s"
    34  	if len(*s) == 0 {
    35  		return nil, fmt.Errorf(parseErrFmt, req.URL, "empty response")
    36  	}
    37  	seen := make(map[string]bool)
    38  	results := make([]*Result, 0)
    39  	for _, res := range *s {
    40  		// build tag set
    41  		tags := make(opentsdb.TagSet)
    42  		if len(formatTags) == 1 && formatTags[0] == "" {
    43  			tags["key"] = res.Target
    44  		} else {
    45  			nodes := strings.Split(res.Target, ".")
    46  			if len(nodes) < len(formatTags) {
    47  				msg := fmt.Sprintf("returned target '%s' does not match format '%s'", res.Target, strings.Join(formatTags, ","))
    48  				return nil, fmt.Errorf(parseErrFmt, req.URL, msg)
    49  			}
    50  			for i, key := range formatTags {
    51  				if len(key) > 0 {
    52  					tags[key] = nodes[i]
    53  				}
    54  			}
    55  		}
    56  		if !tags.Valid() {
    57  			msg := fmt.Sprintf("returned target '%s' would make an invalid tag '%s'", res.Target, tags.String())
    58  			return nil, fmt.Errorf(parseErrFmt, req.URL, msg)
    59  		}
    60  		if ts := tags.String(); !seen[ts] {
    61  			seen[ts] = true
    62  		} else {
    63  			return nil, fmt.Errorf(parseErrFmt, req.URL, fmt.Sprintf("More than 1 series identified by tagset '%v'", ts))
    64  		}
    65  		// build data
    66  		dps := make(Series)
    67  		for _, dp := range res.Datapoints {
    68  			if len(dp) != 2 {
    69  				return nil, fmt.Errorf(parseErrFmt, req.URL, fmt.Sprintf("Datapoint has != 2 fields: %v", dp))
    70  			}
    71  			if len(dp[0].String()) == 0 {
    72  				// none value. skip this record
    73  				continue
    74  			}
    75  			val, err := dp[0].Float64()
    76  			if err != nil {
    77  				msg := fmt.Sprintf("value '%s' cannot be decoded to Float64: %s", dp[0], err.Error())
    78  				return nil, fmt.Errorf(parseErrFmt, req.URL, msg)
    79  			}
    80  			unixTS, err := dp[1].Int64()
    81  			if err != nil {
    82  				msg := fmt.Sprintf("timestamp '%s' cannot be decoded to Int64: %s", dp[1], err.Error())
    83  				return nil, fmt.Errorf(parseErrFmt, req.URL, msg)
    84  			}
    85  			t := time.Unix(unixTS, 0)
    86  			dps[t] = val
    87  		}
    88  		results = append(results, &Result{
    89  			Value: dps,
    90  			Group: tags,
    91  		})
    92  	}
    93  	return results, nil
    94  }
    95  
    96  func GraphiteBand(e *State, query, duration, period, format string, num float64) (r *Results, err error) {
    97  	r = new(Results)
    98  	r.IgnoreOtherUnjoined = true
    99  	r.IgnoreUnjoined = true
   100  	e.Timer.Step("graphiteBand", func(T miniprofiler.Timer) {
   101  		var d, p opentsdb.Duration
   102  		d, err = opentsdb.ParseDuration(duration)
   103  		if err != nil {
   104  			return
   105  		}
   106  		p, err = opentsdb.ParseDuration(period)
   107  		if err != nil {
   108  			return
   109  		}
   110  		if num < 1 || num > 100 {
   111  			err = fmt.Errorf("expr: Band: num out of bounds")
   112  		}
   113  		req := &graphite.Request{
   114  			Targets: []string{query},
   115  		}
   116  		now := e.now
   117  		req.End = &now
   118  		st := e.now.Add(-time.Duration(d))
   119  		req.Start = &st
   120  		for i := 0; i < int(num); i++ {
   121  			now = now.Add(time.Duration(-p))
   122  			req.End = &now
   123  			st := now.Add(time.Duration(-d))
   124  			req.Start = &st
   125  			var s graphite.Response
   126  			s, err = timeGraphiteRequest(e, req)
   127  			if err != nil {
   128  				return
   129  			}
   130  			formatTags := strings.Split(format, ".")
   131  			var results []*Result
   132  			results, err = parseGraphiteResponse(req, &s, formatTags)
   133  			if err != nil {
   134  				return
   135  			}
   136  			if i == 0 {
   137  				r.Results = results
   138  			} else {
   139  				// different graphite requests might return series with different id's.
   140  				// i.e. a different set of tagsets.  merge the data of corresponding tagsets
   141  				for _, result := range results {
   142  					updateKey := -1
   143  					for j, existing := range r.Results {
   144  						if result.Group.Equal(existing.Group) {
   145  							updateKey = j
   146  							break
   147  						}
   148  					}
   149  					if updateKey == -1 {
   150  						// result tagset is new
   151  						r.Results = append(r.Results, result)
   152  						updateKey = len(r.Results) - 1
   153  					}
   154  					for k, v := range result.Value.(Series) {
   155  						r.Results[updateKey].Value.(Series)[k] = v
   156  					}
   157  				}
   158  			}
   159  		}
   160  	})
   161  	if err != nil {
   162  		return nil, fmt.Errorf("graphiteBand: %v", err)
   163  	}
   164  	return
   165  }
   166  
   167  func GraphiteQuery(e *State, query string, sduration, eduration, format string) (r *Results, err error) {
   168  	sd, err := opentsdb.ParseDuration(sduration)
   169  	if err != nil {
   170  		return
   171  	}
   172  	ed := opentsdb.Duration(0)
   173  	if eduration != "" {
   174  		ed, err = opentsdb.ParseDuration(eduration)
   175  		if err != nil {
   176  			return
   177  		}
   178  	}
   179  	st := e.now.Add(-time.Duration(sd))
   180  	et := e.now.Add(-time.Duration(ed))
   181  	req := &graphite.Request{
   182  		Targets: []string{query},
   183  		Start:   &st,
   184  		End:     &et,
   185  	}
   186  	s, err := timeGraphiteRequest(e, req)
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  	formatTags := strings.Split(format, ".")
   191  	r = new(Results)
   192  	results, err := parseGraphiteResponse(req, &s, formatTags)
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  	r.Results = results
   197  
   198  	return
   199  }
   200  
   201  func graphiteTagQuery(args []parse.Node) (parse.Tags, error) {
   202  	t := make(parse.Tags)
   203  	n := args[3].(*parse.StringNode)
   204  	for _, s := range strings.Split(n.Text, ".") {
   205  		if s != "" {
   206  			t[s] = struct{}{}
   207  		}
   208  	}
   209  	return t, nil
   210  }
   211  
   212  func timeGraphiteRequest(e *State, req *graphite.Request) (resp graphite.Response, err error) {
   213  	e.graphiteQueries = append(e.graphiteQueries, *req)
   214  	b, _ := json.MarshalIndent(req, "", "  ")
   215  	e.Timer.StepCustomTiming("graphite", "query", string(b), func() {
   216  		key := req.CacheKey()
   217  		getFn := func() (interface{}, error) {
   218  			return e.GraphiteContext.Query(req)
   219  		}
   220  		var val interface{}
   221  		var hit bool
   222  		val, err, hit = e.Cache.Get(key, getFn)
   223  		collectCacheHit(e.Cache, "graphite", hit)
   224  		resp = val.(graphite.Response)
   225  	})
   226  	return
   227  }