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 }