Gomain.go -rw-r--r-- 5.8 KiB
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "image/color"
7 "log"
8 "net/http"
9 "net/url"
10 "os"
11 "sort"
12 "strconv"
13 "strings"
14 "time"
15
16 "github.com/go-chi/chi"
17 "github.com/go-chi/chi/middleware"
18 "gonum.org/v1/plot"
19 "gonum.org/v1/plot/plotter"
20 "gonum.org/v1/plot/plotutil"
21 "gonum.org/v1/plot/vg"
22)
23
24var (
25 upstream string
26)
27
28type PromResponse struct {
29 Status string `json:"status"`
30 Data struct {
31 ResultType string `json:"resultType"`
32 Result []struct {
33 Metric map[string]string `json:"metric"`
34 Values [][]interface{} `json:"values"`
35 } `json:"result"`
36 } `json:"data"`
37}
38
39type Datapoint struct {
40 Time time.Time
41 Value float64
42}
43
44type PromResult struct {
45 Metric string
46 Values []Datapoint
47}
48
49func Query(q string, start time.Time, end time.Time, step int) ([]PromResult, error) {
50 body := url.Values{}
51 body.Set("query", q)
52 body.Set("start", fmt.Sprintf("%d", start.Unix()))
53 body.Set("end", fmt.Sprintf("%d", end.Unix()))
54 body.Set("step", fmt.Sprintf("%d", step))
55 resp, err := http.Post(fmt.Sprintf("%s/api/v1/query_range", upstream),
56 "application/x-www-form-urlencoded", strings.NewReader(body.Encode()))
57 if err != nil {
58 return nil, err
59 }
60 defer resp.Body.Close()
61
62 if resp.StatusCode != 200 {
63 return nil, fmt.Errorf("Received %d response from upstream", resp.StatusCode)
64 }
65
66 var data PromResponse
67 if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
68 return nil, err
69 }
70
71 if data.Data.ResultType != "matrix" {
72 return nil, fmt.Errorf("result type isn't of type matrix: %s",
73 data.Data.ResultType)
74 }
75
76 if len(data.Data.Result) == 0 {
77 return nil, fmt.Errorf("No data")
78 }
79
80 var results []PromResult
81 for _, res := range data.Data.Result {
82 r := PromResult{}
83 r.Metric = metricName(res.Metric)
84
85 var values []Datapoint
86 for _, vals := range res.Values {
87 timestamp := vals[0].(float64)
88 value := vals[1].(string)
89 fv, _ := strconv.ParseFloat(value, 64)
90 values = append(values, Datapoint{
91 time.Unix(int64(timestamp), 0),
92 fv,
93 })
94 }
95 r.Values = values
96
97 results = append(results, r)
98 }
99 return results, nil
100}
101
102func metricName(metric map[string]string) string {
103 if len(metric) == 0 {
104 return "{}"
105 }
106
107 out := ""
108 var inner []string
109 for key, value := range metric {
110 if key == "__name__" {
111 out = value
112 continue
113 }
114 inner = append(inner, fmt.Sprintf(`%s="%s"`, key, value))
115 }
116
117 if len(inner) == 0 {
118 return out
119 }
120
121 sort.Slice(inner, func(i, j int) bool {
122 return inner[i] < inner[j]
123 })
124
125 return out + "{" + strings.Join(inner, ",") + "}"
126}
127
128func main() {
129 plotutil.DefaultDashes = [][]vg.Length{{}}
130
131 upstream = os.Args[1]
132 router := chi.NewRouter()
133 router.Use(middleware.RealIP)
134 router.Use(middleware.Logger)
135
136 router.Get("/chart.svg", func(w http.ResponseWriter, r *http.Request) {
137 args := r.URL.Query()
138 var query string
139 if q, ok := args["query"]; !ok {
140 w.WriteHeader(400)
141 w.Write([]byte("Expected ?query=... parameter"))
142 return
143 } else {
144 query = q[0]
145 }
146
147 start := time.Now().Add(-24 * 60 * time.Minute)
148 end := time.Now()
149 if s, ok := args["since"]; ok {
150 d, _ := time.ParseDuration(s[0])
151 start = time.Now().Add(-d)
152 }
153
154 width := 12*vg.Inch
155 height := 6*vg.Inch
156 if ws, ok := args["width"]; ok {
157 w, _ := strconv.ParseFloat(ws[0], 32)
158 width = vg.Length(w)*vg.Inch
159 }
160 if hs, ok := args["height"]; ok {
161 h, _ := strconv.ParseFloat(hs[0], 32)
162 height = vg.Length(h)*vg.Inch
163 }
164
165 // Set step so that there's approximately 25 data points per inch
166 step := int(end.Sub(start).Seconds() / (25 * float64(width / vg.Inch)))
167 if s, ok := args["step"]; ok {
168 d, _ := strconv.ParseInt(s[0], 10, 32)
169 step = int(d)
170 }
171 _, stacked := args["stacked"]
172
173 data, err := Query(query, start, end, step)
174 if err != nil {
175 w.WriteHeader(400)
176 w.Write([]byte(fmt.Sprintf("%v", err)))
177 return
178 }
179
180 p, err := plot.New()
181 if err != nil {
182 panic(err)
183 }
184 if t, ok := args["title"]; ok {
185 p.Title.Text = t[0]
186 }
187 p.X.Label.Text = "Time"
188 p.X.Tick.Marker = dateTicks{}
189 if ms, ok := args["max"]; ok {
190 m, _ := strconv.ParseFloat(ms[0], 64)
191 p.Y.Max = m
192 }
193 if ms, ok := args["min"]; ok {
194 m, _ := strconv.ParseFloat(ms[0], 64)
195 p.Y.Min = m
196 }
197 p.Legend.Top = true
198
199 sums := make([]float64, len(data[0].Values))
200
201 plotters := make([]plot.Plotter, len(data))
202 var nextColor int
203 colors := plotutil.SoftColors
204 for i, res := range data {
205 var points plotter.XYs
206 for j, d := range res.Values {
207 value := d.Value
208 if stacked {
209 value += sums[j]
210 }
211 points = append(points, plotter.XY{
212 float64(d.Time.Unix()),
213 value,
214 })
215 sums[j] += d.Value
216 }
217
218 l, _, err := plotter.NewLinePoints(points)
219 if err != nil {
220 w.WriteHeader(400)
221 w.Write([]byte(fmt.Sprintf("%v", err)))
222 return
223 }
224 if stacked {
225 l.FillColor = colors[nextColor]
226 if i != len(data) - 1 {
227 l.Color = color.RGBA{0, 0, 0, 0}
228 }
229 } else {
230 l.Color = colors[nextColor]
231 }
232 nextColor += 1
233 if nextColor >= len(colors) {
234 nextColor = 0
235 }
236 plotters[i] = l
237 p.Legend.Add(res.Metric, l)
238 }
239 for i := len(plotters) - 1; i >= 0; i-- {
240 p.Add(plotters[i])
241 }
242
243 writer, err := p.WriterTo(width, height, "svg")
244 if err != nil {
245 w.WriteHeader(400)
246 w.Write([]byte(fmt.Sprintf("%v", err)))
247 return
248 }
249
250 w.Header().Add("Content-Type", "image/svg+xml")
251 writer.WriteTo(w)
252 })
253
254 addr := ":8142"
255 if len(os.Args) > 2 {
256 addr = os.Args[2]
257 }
258 log.Printf("Listening on %s", addr)
259 http.ListenAndServe(addr, router)
260}
261
262type dateTicks struct{}
263// Ticks computes the default tick marks, but inserts commas
264// into the labels for the major tick marks.
265func (dateTicks) Ticks(min, max float64) []plot.Tick {
266 tks := plot.DefaultTicks{}.Ticks(min, max)
267 for i, t := range tks {
268 if t.Label == "" { // Skip minor ticks, they are fine.
269 continue
270 }
271 d, _ := strconv.ParseFloat(t.Label, 64)
272 tm := time.Unix(int64(d), 0)
273 tks[i].Label = tm.Format("15:04:05")
274 }
275 return tks
276}