Gomain.go -rw-r--r-- 7.6 KiB
1package main
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "image/color"
8 "log"
9 "math"
10 "net/http"
11 "net/url"
12 "os"
13 "sort"
14 "strconv"
15 "strings"
16 "text/template"
17 "time"
18
19 "github.com/dustin/go-humanize"
20 "github.com/go-chi/chi"
21 "github.com/go-chi/chi/middleware"
22 "gonum.org/v1/plot"
23 "gonum.org/v1/plot/plotter"
24 "gonum.org/v1/plot/plotutil"
25 "gonum.org/v1/plot/vg"
26)
27
28var (
29 upstream string
30)
31
32type PromResponse struct {
33 Status string `json:"status"`
34 Data struct {
35 ResultType string `json:"resultType"`
36 Result []struct {
37 Metric map[string]string `json:"metric"`
38 Values [][]interface{} `json:"values"`
39 } `json:"result"`
40 } `json:"data"`
41}
42
43type Datapoint struct {
44 Time time.Time
45 Value float64
46}
47
48type PromResult struct {
49 Metric map[string]string
50 Values []Datapoint
51}
52
53func Query(q string, start time.Time, end time.Time, step int) ([]PromResult, error) {
54 body := url.Values{}
55 body.Set("query", q)
56 body.Set("start", fmt.Sprintf("%d", start.Unix()))
57 body.Set("end", fmt.Sprintf("%d", end.Unix()))
58 body.Set("step", fmt.Sprintf("%d", step))
59 resp, err := http.Post(fmt.Sprintf("%s/api/v1/query_range", upstream),
60 "application/x-www-form-urlencoded", strings.NewReader(body.Encode()))
61 if err != nil {
62 return nil, err
63 }
64 defer resp.Body.Close()
65
66 if resp.StatusCode != 200 {
67 return nil, fmt.Errorf("Received %d response from upstream", resp.StatusCode)
68 }
69
70 var data PromResponse
71 if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
72 return nil, err
73 }
74
75 if data.Data.ResultType != "matrix" {
76 return nil, fmt.Errorf("result type isn't of type matrix: %s",
77 data.Data.ResultType)
78 }
79
80 if len(data.Data.Result) == 0 {
81 return nil, fmt.Errorf("No data")
82 }
83
84 var results []PromResult
85 for _, res := range data.Data.Result {
86 r := PromResult{}
87 r.Metric = res.Metric
88
89 var values []Datapoint
90 isValid := true
91 for _, vals := range res.Values {
92 timestamp := vals[0].(float64)
93 value := vals[1].(string)
94 fv, _ := strconv.ParseFloat(value, 64)
95 if math.IsNaN(fv) || math.IsInf(fv, 0) {
96 isValid = false
97 break
98 }
99 values = append(values, Datapoint{
100 time.Unix(int64(timestamp), 0),
101 fv,
102 })
103 }
104 r.Values = values
105
106 if isValid {
107 results = append(results, r)
108 }
109 }
110 return results, nil
111}
112
113func metricName(metric map[string]string) string {
114 if len(metric) == 0 {
115 return "{}"
116 }
117
118 out := ""
119 var inner []string
120 for key, value := range metric {
121 if key == "__name__" {
122 out = value
123 continue
124 }
125 inner = append(inner, fmt.Sprintf(`%s="%s"`, key, value))
126 }
127
128 if len(inner) == 0 {
129 return out
130 }
131
132 sort.Slice(inner, func(i, j int) bool {
133 return inner[i] < inner[j]
134 })
135
136 return out + "{" + strings.Join(inner, ",") + "}"
137}
138
139func handleLabel(p *plot.Plot, l *plotter.Line, label string, metric map[string]string) {
140 tmpl, err := template.New("label").Parse(label)
141 if err != nil {
142 log.Printf("Failed to parse label template: %v", err)
143 } else {
144 var label_out bytes.Buffer
145 tmpl.Execute(&label_out, metric)
146 p.Legend.Add(label_out.String(), l)
147 }
148}
149
150func registerExtension(router chi.Router, extension string, mime string) {
151 router.Get("/chart."+extension, func(w http.ResponseWriter, r *http.Request) {
152 args := r.URL.Query()
153 var query string
154 if q, ok := args["query"]; !ok {
155 w.WriteHeader(400)
156 w.Write([]byte("Expected ?query=... parameter"))
157 return
158 } else {
159 query = q[0]
160 }
161
162 start := time.Now().Add(-24 * 60 * time.Minute)
163 end := time.Now()
164 if s, ok := args["since"]; ok {
165 d, _ := time.ParseDuration(s[0])
166 start = time.Now().Add(-d)
167 }
168 if u, ok := args["until"]; ok {
169 d, _ := time.ParseDuration(u[0])
170 end = time.Now().Add(-d)
171 }
172
173 width := 12 * vg.Inch
174 height := 6 * vg.Inch
175 if ws, ok := args["width"]; ok {
176 w, _ := strconv.ParseFloat(ws[0], 32)
177 width = vg.Length(w) * vg.Inch
178 }
179 if hs, ok := args["height"]; ok {
180 h, _ := strconv.ParseFloat(hs[0], 32)
181 height = vg.Length(h) * vg.Inch
182 }
183
184 // Label template
185 var label string
186 if l, ok := args["label"]; ok {
187 label = l[0]
188 }
189
190 // Set step so that there's approximately 25 data points per inch
191 step := int(end.Sub(start).Seconds() / (25 * float64(width/vg.Inch)))
192 if s, ok := args["step"]; ok {
193 d, _ := strconv.ParseInt(s[0], 10, 32)
194 step = int(d)
195 }
196 _, stacked := args["stacked"]
197
198 data, err := Query(query, start, end, step)
199 if err != nil {
200 w.WriteHeader(400)
201 w.Write([]byte(fmt.Sprintf("%v", err)))
202 return
203 }
204
205 p, err := plot.New()
206 if err != nil {
207 panic(err)
208 }
209 if t, ok := args["title"]; ok {
210 p.Title.Text = t[0]
211 }
212 p.X.Label.Text = "Time"
213 p.X.Tick.Marker = dateTicks{start, end}
214 if ms, ok := args["max"]; ok {
215 m, _ := strconv.ParseFloat(ms[0], 64)
216 p.Y.Max = m
217 }
218
219 p.Y.Tick.Marker = humanTicks{}
220 if ms, ok := args["min"]; ok {
221 m, _ := strconv.ParseFloat(ms[0], 64)
222 p.Y.Min = m
223 }
224 p.Legend.Top = true
225
226 sums := make([]float64, len(data[0].Values))
227
228 plotters := make([]plot.Plotter, len(data))
229 var nextColor int
230 colors := plotutil.SoftColors
231 for i, res := range data {
232 var points plotter.XYs
233 for j, d := range res.Values {
234 value := d.Value
235 if stacked {
236 value += sums[j]
237 }
238 points = append(points, plotter.XY{
239 float64(d.Time.Unix()),
240 value,
241 })
242 sums[j] += d.Value
243 }
244
245 l, _, err := plotter.NewLinePoints(points)
246 if err != nil {
247 w.WriteHeader(400)
248 w.Write([]byte(fmt.Sprintf("%v", err)))
249 return
250 }
251 if stacked {
252 l.FillColor = colors[nextColor]
253 if i != len(data)-1 {
254 l.Color = color.RGBA{0, 0, 0, 0}
255 }
256 } else {
257 l.Color = colors[nextColor]
258 }
259 nextColor += 1
260 if nextColor >= len(colors) {
261 nextColor = 0
262 }
263 plotters[i] = l
264 if label != "" {
265 handleLabel(p, l, label, res.Metric)
266 } else {
267 p.Legend.Add(metricName(res.Metric), l)
268 }
269 }
270 for i := len(plotters) - 1; i >= 0; i-- {
271 p.Add(plotters[i])
272 }
273
274 writer, err := p.WriterTo(width, height, extension)
275 if err != nil {
276 w.WriteHeader(400)
277 w.Write([]byte(fmt.Sprintf("%v", err)))
278 return
279 }
280
281 w.Header().Add("Content-Type", mime)
282 writer.WriteTo(w)
283 })
284
285}
286
287func main() {
288 plotutil.DefaultDashes = [][]vg.Length{{}}
289
290 if len(os.Args) < 2 {
291 fmt.Printf("Usage: %s server\n", os.Args[0])
292 os.Exit(1)
293 }
294 upstream = os.Args[1]
295 router := chi.NewRouter()
296
297 router.Use(middleware.RealIP)
298 router.Use(middleware.Logger)
299
300 registerExtension(router, "svg", "image/svg+xml")
301 registerExtension(router, "png", "image/png")
302
303 staticDir := os.Getenv("CHARTSRV_STATICDIR")
304 if staticDir == "" {
305 staticDir = "static"
306 }
307
308 router.Get("/*", func(w http.ResponseWriter, r *http.Request) {
309 fs := http.FileServer(http.Dir(staticDir))
310 fs.ServeHTTP(w, r)
311 })
312
313 addr := ":8142"
314 if len(os.Args) > 2 {
315 addr = os.Args[2]
316 }
317 log.Printf("Listening on %s", addr)
318 http.ListenAndServe(addr, router)
319}
320
321type dateTicks struct {
322 Start time.Time
323 End time.Time
324}
325
326// Ticks computes the default tick marks, but inserts commas
327// into the labels for the major tick marks.
328func (dt dateTicks) Ticks(min, max float64) []plot.Tick {
329 fmt := "15:04:05"
330 if dt.End.Sub(dt.Start).Hours() >= 24 {
331 fmt = "Jan 2 15:04:05"
332 }
333
334 tks := plot.DefaultTicks{}.Ticks(min, max)
335 for i, t := range tks {
336 if t.Label == "" { // Skip minor ticks, they are fine.
337 continue
338 }
339 d, _ := strconv.ParseFloat(t.Label, 64)
340 tm := time.Unix(int64(d), 0)
341 tks[i].Label = tm.Format(fmt)
342 }
343 return tks
344}
345
346type humanTicks struct{}
347
348func (ht humanTicks) Ticks(min, max float64) []plot.Tick {
349 tks := plot.DefaultTicks{}.Ticks(min, max)
350 for i, t := range tks {
351 if t.Label == "" { // Skip minor ticks, they are fine.
352 continue
353 }
354 d, _ := strconv.ParseFloat(t.Label, 64)
355 tks[i].Label = humanize.SI(d, "")
356 }
357 return tks
358}