bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/cmd/bosun/web/chart.go (about) 1 package web 2 3 import ( 4 "encoding/base64" 5 "encoding/json" 6 "fmt" 7 "image/color" 8 "net/http" 9 "strconv" 10 "strings" 11 "time" 12 13 "bosun.org/annotate" 14 "bosun.org/cmd/bosun/expr" 15 "bosun.org/cmd/bosun/sched" 16 "bosun.org/metadata" 17 "bosun.org/models" 18 "bosun.org/opentsdb" 19 "github.com/MiniProfiler/go/miniprofiler" 20 svg "github.com/ajstarks/svgo" 21 "github.com/bradfitz/slice" 22 "github.com/gorilla/mux" 23 "github.com/vdobler/chart" 24 "github.com/vdobler/chart/svgg" 25 ) 26 27 // Graph takes an OpenTSDB request data structure and queries OpenTSDB. Use the 28 // json parameter to pass JSON. Use the b64 parameter to pass base64-encoded 29 // JSON. 30 func Graph(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) { 31 j := []byte(r.FormValue("json")) 32 if bs := r.FormValue("b64"); bs != "" { 33 b, err := base64.StdEncoding.DecodeString(bs) 34 if err != nil { 35 return nil, err 36 } 37 j = b 38 } 39 if len(j) == 0 { 40 return nil, fmt.Errorf("either json or b64 required") 41 } 42 oreq, err := opentsdb.RequestFromJSON(j) 43 if err != nil { 44 return nil, err 45 } 46 if ads_v := r.FormValue("autods"); ads_v != "" { 47 ads_i, err := strconv.Atoi(ads_v) 48 if err != nil { 49 return nil, err 50 } 51 if err := oreq.AutoDownsample(ads_i); err != nil { 52 return nil, err 53 } 54 } 55 ar := make(map[int]bool) 56 for _, v := range r.Form["autorate"] { 57 if i, err := strconv.Atoi(v); err == nil { 58 ar[i] = true 59 } 60 } 61 queries := make([]string, len(oreq.Queries)) 62 var start, end string 63 var startT, endT time.Time 64 if s, ok := oreq.Start.(string); ok && strings.Contains(s, "-ago") { 65 startT, err = opentsdb.ParseTime(s) 66 if err != nil { 67 return nil, err 68 } 69 start = strings.TrimSuffix(s, "-ago") 70 } 71 if s, ok := oreq.End.(string); ok && strings.Contains(s, "-ago") { 72 endT, err = opentsdb.ParseTime(s) 73 if err != nil { 74 return nil, err 75 } 76 end = strings.TrimSuffix(s, "-ago") 77 } 78 if start == "" && end == "" { 79 s, sok := oreq.Start.(int64) 80 e, eok := oreq.End.(int64) 81 if sok && eok { 82 start = fmt.Sprintf("%vs", e-s) 83 startT = time.Unix(s, 0) 84 endT = time.Unix(e, 0) 85 if err != nil { 86 return nil, err 87 } 88 } 89 } 90 if endT.Equal(time.Time{}) { 91 endT = time.Now().UTC() 92 } 93 m_units := make(map[string]string) 94 for i, q := range oreq.Queries { 95 if ar[i] { 96 97 meta, err := schedule.MetadataMetrics(q.Metric) 98 if err != nil { 99 return nil, err 100 } 101 if meta == nil { 102 return nil, fmt.Errorf("no metadata for %s: cannot use auto rate", q) 103 } 104 if meta.Unit != "" { 105 m_units[q.Metric] = meta.Unit 106 } 107 if meta.Rate != "" { 108 switch meta.Rate { 109 case metadata.Gauge: 110 // ignore 111 case metadata.Rate: 112 q.Rate = true 113 case metadata.Counter: 114 q.Rate = true 115 q.RateOptions = opentsdb.RateOptions{ 116 Counter: true, 117 ResetValue: 1, 118 } 119 default: 120 return nil, fmt.Errorf("unknown metadata rate: %s", meta.Rate) 121 } 122 } 123 } 124 queries[i] = fmt.Sprintf(`q("%v", "%v", "%v")`, q, start, end) 125 if !schedule.SystemConf.GetTSDBContext().Version().FilterSupport() { 126 if err := schedule.Search.Expand(q); err != nil { 127 return nil, err 128 } 129 } 130 } 131 var tr opentsdb.ResponseSet 132 b, _ := json.MarshalIndent(oreq, "", " ") 133 t.StepCustomTiming("tsdb", "query", string(b), func() { 134 h := schedule.SystemConf.GetTSDBHost() 135 if h == "" { 136 err = fmt.Errorf("tsdbHost not set") 137 return 138 } 139 tr, err = oreq.Query(h) 140 }) 141 if err != nil { 142 return nil, err 143 } 144 cs, err := makeChart(tr, m_units) 145 if err != nil { 146 return nil, err 147 } 148 if _, present := r.Form["png"]; present { 149 c := chart.ScatterChart{ 150 Title: fmt.Sprintf("%v - %v", oreq.Start, queries), 151 } 152 c.XRange.Time = true 153 if min, err := strconv.ParseFloat(r.FormValue("min"), 64); err == nil { 154 c.YRange.MinMode.Fixed = true 155 c.YRange.MinMode.Value = min 156 } 157 if max, err := strconv.ParseFloat(r.FormValue("max"), 64); err == nil { 158 c.YRange.MaxMode.Fixed = true 159 c.YRange.MaxMode.Value = max 160 } 161 for ri, r := range cs { 162 pts := make([]chart.EPoint, len(r.Data)) 163 for idx, v := range r.Data { 164 pts[idx].X = v[0] 165 pts[idx].Y = v[1] 166 } 167 slice.Sort(pts, func(i, j int) bool { 168 return pts[i].X < pts[j].X 169 }) 170 c.AddData(r.Name, pts, chart.PlotStyleLinesPoints, sched.Autostyle(ri)) 171 } 172 w.Header().Set("Content-Type", "image/svg+xml") 173 white := color.RGBA{0xff, 0xff, 0xff, 0xff} 174 const width = 800 175 const height = 600 176 s := svg.New(w) 177 s.Start(width, height) 178 s.Rect(0, 0, width, height, "fill: #ffffff") 179 sgr := svgg.AddTo(s, 0, 0, width, height, "", 12, white) 180 c.Plot(sgr) 181 s.End() 182 return nil, nil 183 } 184 var a []annotate.Annotation 185 warnings := []string{} 186 if schedule.SystemConf.AnnotateEnabled() { 187 a, err = AnnotateBackend.GetAnnotations(&startT, &endT) 188 if err != nil { 189 warnings = append(warnings, fmt.Sprintf("unable to get annotations: %v", err)) 190 } 191 } 192 return struct { 193 Queries []string 194 Series []*chartSeries 195 Annotations []annotate.Annotation 196 Warnings []string 197 }{ 198 queries, 199 cs, 200 a, 201 warnings, 202 }, nil 203 } 204 205 // ExprGraph returns an svg graph. 206 // The basename of the requested svg file should be a base64 encoded expression. 207 func ExprGraph(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) { 208 vars := mux.Vars(r) 209 bs := vars["bs"] 210 format := vars["format"] 211 b, err := base64.StdEncoding.DecodeString(bs) 212 if err != nil { 213 return nil, err 214 } 215 q := string(b) 216 if len(q) == 0 { 217 return nil, fmt.Errorf("missing expression") 218 } 219 autods := 1000 220 if a := r.FormValue("autods"); a != "" { 221 i, err := strconv.Atoi(a) 222 if err != nil { 223 return nil, err 224 } 225 autods = i 226 } 227 now := time.Now().UTC() 228 if n := r.FormValue("now"); n != "" { 229 i, err := strconv.ParseInt(n, 10, 64) 230 if err != nil { 231 return nil, err 232 } 233 now = time.Unix(i, 0).UTC() 234 } 235 e, err := expr.New(q, schedule.RuleConf.GetFuncs(schedule.SystemConf.EnabledBackends())) 236 if err != nil { 237 return nil, err 238 } else if e.Root.Return() != models.TypeSeriesSet { 239 return nil, fmt.Errorf("egraph: requires an expression that returns a series") 240 } 241 // it may not strictly be necessary to recreate the contexts each time, but we do to be safe 242 backends := &expr.Backends{ 243 TSDBContext: schedule.SystemConf.GetTSDBContext(), 244 GraphiteContext: schedule.SystemConf.GetGraphiteContext(), 245 InfluxConfig: schedule.SystemConf.GetInfluxContext(), 246 ElasticHosts: schedule.SystemConf.GetElasticContext(), 247 AzureMonitor: schedule.SystemConf.GetAzureMonitorContext(), 248 PromConfig: schedule.SystemConf.GetPromContext(), 249 CloudWatchContext: schedule.SystemConf.GetCloudWatchContext(), 250 } 251 providers := &expr.BosunProviders{ 252 Cache: cacheObj, 253 Search: schedule.Search, 254 Annotate: AnnotateBackend, 255 Squelched: nil, 256 History: nil, 257 } 258 res, _, err := e.Execute(backends, providers, t, now, autods, false, "Web: chart creation") 259 if err != nil { 260 return nil, err 261 } 262 switch format { 263 case "svg": 264 if err := schedule.ExprSVG(t, w, 800, 600, "", res.Results); err != nil { 265 return nil, err 266 } 267 case "png": 268 if err := schedule.ExprPNG(t, w, 800, 600, "", res.Results); err != nil { 269 return nil, err 270 } 271 } 272 return nil, nil 273 } 274 275 func makeChart(r opentsdb.ResponseSet, m_units map[string]string) ([]*chartSeries, error) { 276 var series []*chartSeries 277 for _, resp := range r { 278 dps := make([][2]float64, 0) 279 for k, v := range resp.DPS { 280 ki, err := strconv.ParseInt(k, 10, 64) 281 if err != nil { 282 return nil, err 283 } 284 dps = append(dps, [2]float64{float64(ki), float64(v)}) 285 } 286 if len(dps) > 0 { 287 slice.Sort(dps, func(i, j int) bool { 288 return dps[i][0] < dps[j][0] 289 }) 290 name := resp.Metric 291 if len(resp.Tags) > 0 { 292 name += resp.Tags.String() 293 } 294 series = append(series, &chartSeries{ 295 Name: name, 296 Metric: resp.Metric, 297 Tags: resp.Tags, 298 Data: dps, 299 Unit: m_units[resp.Metric], 300 }) 301 } 302 } 303 return series, nil 304 } 305 306 type chartSeries struct { 307 Name string 308 Metric string 309 Tags opentsdb.TagSet 310 Data [][2]float64 311 Unit string 312 }