bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/cmd/bosun/expr/influx.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/models"
    11  	"bosun.org/opentsdb"
    12  	"github.com/influxdata/influxdb/client/v2"
    13  	influxModels "github.com/influxdata/influxdb/models"
    14  	"github.com/influxdata/influxql"
    15  )
    16  
    17  // Influx is a map of functions to query InfluxDB.
    18  var Influx = map[string]parse.Func{
    19  	"influx": {
    20  		Args:   []models.FuncType{models.TypeString, models.TypeString, models.TypeString, models.TypeString, models.TypeString},
    21  		Return: models.TypeSeriesSet,
    22  		Tags:   influxTag,
    23  		F:      InfluxQuery,
    24  	},
    25  }
    26  
    27  func influxTag(args []parse.Node) (parse.Tags, error) {
    28  	st, err := influxql.ParseStatement(args[1].(*parse.StringNode).Text)
    29  	if err != nil {
    30  		return nil, err
    31  	}
    32  	s, ok := st.(*influxql.SelectStatement)
    33  	if !ok {
    34  		return nil, fmt.Errorf("influx: expected select statement")
    35  	}
    36  
    37  	t := make(parse.Tags, len(s.Dimensions))
    38  	for _, d := range s.Dimensions {
    39  		if _, ok := d.Expr.(*influxql.Call); ok {
    40  			continue
    41  		}
    42  		t[d.String()] = struct{}{}
    43  	}
    44  	return t, nil
    45  }
    46  
    47  func InfluxQuery(e *State, db, query, startDuration, endDuration, groupByInterval string) (*Results, error) {
    48  	qres, err := timeInfluxRequest(e, db, query, startDuration, endDuration, groupByInterval)
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  	r := new(Results)
    53  	for _, row := range qres {
    54  		tags := opentsdb.TagSet(row.Tags)
    55  		if e.Squelched(tags) {
    56  			continue
    57  		}
    58  		if len(row.Columns) != 2 {
    59  			return nil, fmt.Errorf("influx: expected exactly one result column")
    60  		}
    61  		values := make(Series, len(row.Values))
    62  		for _, v := range row.Values {
    63  			if len(v) != 2 {
    64  				return nil, fmt.Errorf("influx: expected exactly one result column")
    65  			}
    66  			ts, ok := v[0].(string)
    67  			if !ok {
    68  				return nil, fmt.Errorf("influx: expected time string column")
    69  			}
    70  			t, err := time.Parse(time.RFC3339, ts)
    71  			if err != nil {
    72  				return nil, err
    73  			}
    74  			n, ok := v[1].(json.Number)
    75  			if !ok {
    76  				return nil, fmt.Errorf("influx: expected json.Number")
    77  			}
    78  			f, err := n.Float64()
    79  			if err != nil {
    80  				return nil, fmt.Errorf("influx: bad number: %v", err)
    81  			}
    82  			values[t] = f
    83  		}
    84  		r.Results = append(r.Results, &Result{
    85  			Value: values,
    86  			Group: tags,
    87  		})
    88  	}
    89  	_ = r
    90  	return r, nil
    91  }
    92  
    93  // influxQueryDuration adds time WHERE clauses to query for the given start and end durations.
    94  func influxQueryDuration(now time.Time, query, start, end, groupByInterval string) (string, error) {
    95  	sd, err := opentsdb.ParseDuration(start)
    96  	if err != nil {
    97  		return "", err
    98  	}
    99  	ed, err := opentsdb.ParseDuration(end)
   100  	if end == "" {
   101  		ed = 0
   102  	} else if err != nil {
   103  		return "", err
   104  	}
   105  	st, err := influxql.ParseStatement(query)
   106  	if err != nil {
   107  		return "", err
   108  	}
   109  	s, ok := st.(*influxql.SelectStatement)
   110  	if !ok {
   111  		return "", fmt.Errorf("influx: expected select statement")
   112  	}
   113  	isTime := func(n influxql.Node) bool {
   114  		v, ok := n.(*influxql.VarRef)
   115  		if !ok {
   116  			return false
   117  		}
   118  		s := strings.ToLower(v.Val)
   119  		return s == "time"
   120  	}
   121  	influxql.WalkFunc(s.Condition, func(n influxql.Node) {
   122  		b, ok := n.(*influxql.BinaryExpr)
   123  		if !ok {
   124  			return
   125  		}
   126  		if isTime(b.LHS) || isTime(b.RHS) {
   127  			err = fmt.Errorf("influx query must not contain time in WHERE")
   128  		}
   129  	})
   130  	if err != nil {
   131  		return "", err
   132  	}
   133  
   134  	//Add New BinaryExpr for time clause
   135  	startExpr := &influxql.BinaryExpr{
   136  		Op:  influxql.GTE,
   137  		LHS: &influxql.VarRef{Val: "time"},
   138  		RHS: &influxql.TimeLiteral{Val: now.Add(time.Duration(-sd))},
   139  	}
   140  
   141  	stopExpr := &influxql.BinaryExpr{
   142  		Op:  influxql.LTE,
   143  		LHS: &influxql.VarRef{Val: "time"},
   144  		RHS: &influxql.TimeLiteral{Val: now.Add(time.Duration(-ed))},
   145  	}
   146  
   147  	if s.Condition != nil {
   148  		s.Condition = &influxql.BinaryExpr{
   149  			Op:  influxql.AND,
   150  			LHS: s.Condition,
   151  			RHS: &influxql.BinaryExpr{
   152  				Op:  influxql.AND,
   153  				LHS: startExpr,
   154  				RHS: stopExpr,
   155  			},
   156  		}
   157  	} else {
   158  		s.Condition = &influxql.BinaryExpr{
   159  			Op:  influxql.AND,
   160  			LHS: startExpr,
   161  			RHS: stopExpr,
   162  		}
   163  	}
   164  
   165  	// parse last argument
   166  	if len(groupByInterval) > 0 {
   167  		gbi, err := time.ParseDuration(groupByInterval)
   168  		if err != nil {
   169  			return "", err
   170  		}
   171  		s.Dimensions = append(s.Dimensions,
   172  			&influxql.Dimension{Expr: &influxql.Call{
   173  				Name: "time",
   174  				Args: []influxql.Expr{&influxql.DurationLiteral{Val: gbi}},
   175  			},
   176  			})
   177  	}
   178  
   179  	// emtpy aggregate windows should be purged from the result
   180  	// this default resembles the opentsdb results.
   181  	if s.Fill == influxql.NullFill {
   182  		s.Fill = influxql.NoFill
   183  		s.FillValue = nil
   184  	}
   185  
   186  	return s.String(), nil
   187  }
   188  
   189  func timeInfluxRequest(e *State, db, query, startDuration, endDuration, groupByInterval string) (s []influxModels.Row, err error) {
   190  	q, err := influxQueryDuration(e.now, query, startDuration, endDuration, groupByInterval)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  	conn, err := client.NewHTTPClient(e.InfluxConfig)
   195  	if err != nil {
   196  		return nil, err
   197  	}
   198  	defer conn.Close()
   199  	q_key := fmt.Sprintf("%s: %s", db, q)
   200  	e.Timer.StepCustomTiming("influx", "query", q_key, func() {
   201  		getFn := func() (interface{}, error) {
   202  			res, err := conn.Query(client.Query{
   203  				Command:  q,
   204  				Database: db,
   205  			})
   206  			if err != nil {
   207  				return nil, err
   208  			}
   209  			if res.Error() != nil {
   210  				return nil, res.Error()
   211  			}
   212  			if len(res.Results) != 1 {
   213  				return nil, fmt.Errorf("influx: expected one result")
   214  			}
   215  
   216  			r := res.Results[0]
   217  			if r.Err == "" {
   218  				return r.Series, nil
   219  			}
   220  			err = fmt.Errorf(r.Err)
   221  			return r.Series, err
   222  		}
   223  		var val interface{}
   224  		var ok bool
   225  		var hit bool
   226  		val, err, hit = e.Cache.Get(q_key, getFn)
   227  		collectCacheHit(e.Cache, "influx", hit)
   228  		if s, ok = val.([]influxModels.Row); !ok {
   229  			err = fmt.Errorf("influx: did not get a valid result from InfluxDB")
   230  		}
   231  	})
   232  	return
   233  }