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 }