Commit
+395 -0 +/-5 browse
1 | diff --git a/LICENSE b/LICENSE |
2 | new file mode 100644 |
3 | index 0000000..2048d4d |
4 | --- /dev/null |
5 | +++ b/LICENSE |
6 | @@ -0,0 +1,19 @@ |
7 | + Copyright (c) 2020 Drew DeVault |
8 | + |
9 | + Permission is hereby granted, free of charge, to any person obtaining a copy of |
10 | + this software and associated documentation files (the "Software"), to deal in |
11 | + the Software without restriction, including without limitation the rights to |
12 | + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies |
13 | + of the Software, and to permit persons to whom the Software is furnished to do |
14 | + so, subject to the following conditions: |
15 | + |
16 | + The above copyright notice and this permission notice shall be included in all |
17 | + copies or substantial portions of the Software. |
18 | + |
19 | + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
20 | + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
21 | + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
22 | + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
23 | + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
24 | + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
25 | + SOFTWARE. |
26 | diff --git a/README.md b/README.md |
27 | new file mode 100644 |
28 | index 0000000..4d8f80e |
29 | --- /dev/null |
30 | +++ b/README.md |
31 | @@ -0,0 +1,33 @@ |
32 | + # chartsrv |
33 | + |
34 | + chartsrv is a dead-simple web application which runs [Prometheus][0] queries and |
35 | + charts the result as an SVG. |
36 | + |
37 | + [0]: https://prometheus.io/ |
38 | + |
39 | + ## Running the daemon |
40 | + |
41 | + ``` |
42 | + $ go build -o chartsrv main.go |
43 | + $ ./chartsrv https://prometheus.example.org |
44 | + Listening on :8142 |
45 | + ``` |
46 | + |
47 | + Forward `/chart.svg` to this address with your favorite reverse proxy. |
48 | + |
49 | + ## Usage |
50 | + |
51 | + Create a URL like `https://chartsrv.example.org/chart.svg?query=...&args...` and |
52 | + set the query parameters as appropriate: |
53 | + |
54 | + - **query**: required. Prometheus query to execute. |
55 | + - **title**: chart title |
56 | + - **stacked**: set to create an area chart instead of a line chart |
57 | + - **since**: [time.ParseDuration][1] to set distance in the past to start |
58 | + charting from |
59 | + - **width**: chart width in inches |
60 | + - **height**: chart height in inches |
61 | + - **step**: number of seconds between data points |
62 | + - **max**: maximum Y value |
63 | + |
64 | + [1]: https://golang.org/pkg/time/#ParseDuration |
65 | diff --git a/go.mod b/go.mod |
66 | new file mode 100644 |
67 | index 0000000..04ecdc6 |
68 | --- /dev/null |
69 | +++ b/go.mod |
70 | @@ -0,0 +1,8 @@ |
71 | + module git.sr.ht/~sircmpwn/promsvg |
72 | + |
73 | + go 1.15 |
74 | + |
75 | + require ( |
76 | + github.com/go-chi/chi v4.1.2+incompatible // indirect |
77 | + gonum.org/v1/plot v0.8.0 // indirect |
78 | + ) |
79 | diff --git a/go.sum b/go.sum |
80 | new file mode 100644 |
81 | index 0000000..817203a |
82 | --- /dev/null |
83 | +++ b/go.sum |
84 | @@ -0,0 +1,63 @@ |
85 | + dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= |
86 | + gioui.org v0.0.0-20200628203458-851255f7a67b/go.mod h1:jiUwifN9cRl/zmco43aAqh0aV+s9GbhG13KcD+gEpkU= |
87 | + github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= |
88 | + github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af h1:wVe6/Ea46ZMeNkQjjBW6xcqyQA/j5e0D6GytH95g0gQ= |
89 | + github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= |
90 | + github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= |
91 | + github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
92 | + github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= |
93 | + github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= |
94 | + github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= |
95 | + github.com/go-chi/chi v1.0.0 h1:s/kv1cTXfivYjdKJdyUzNGyAWZ/2t7duW1gKn5ivu+c= |
96 | + github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= |
97 | + github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= |
98 | + github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= |
99 | + github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= |
100 | + github.com/go-latex/latex v0.0.0-20200518072620-0806b477ea35 h1:uroDDLmuCK5Pz5J/Ef5vCL6F0sJmAtZFTm0/cF027F4= |
101 | + github.com/go-latex/latex v0.0.0-20200518072620-0806b477ea35/go.mod h1:PNI+CcWytn/2Z/9f1SGOOYn0eILruVyp0v2/iAs8asQ= |
102 | + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= |
103 | + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= |
104 | + github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= |
105 | + github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= |
106 | + github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc= |
107 | + github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0= |
108 | + github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= |
109 | + github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= |
110 | + github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
111 | + github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= |
112 | + github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= |
113 | + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
114 | + golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= |
115 | + golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= |
116 | + golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= |
117 | + golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= |
118 | + golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= |
119 | + golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= |
120 | + golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= |
121 | + golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= |
122 | + golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= |
123 | + golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= |
124 | + golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= |
125 | + golang.org/x/image v0.0.0-20200618115811-c13761719519 h1:1e2ufUJNM3lCHEY5jIgac/7UTjd6cgJNdatjPdFWf34= |
126 | + golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= |
127 | + golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= |
128 | + golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= |
129 | + golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
130 | + golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
131 | + golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
132 | + golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
133 | + golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
134 | + golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
135 | + golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
136 | + golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
137 | + golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
138 | + golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
139 | + golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= |
140 | + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
141 | + gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= |
142 | + gonum.org/v1/gonum v0.8.1/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= |
143 | + gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= |
144 | + gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= |
145 | + gonum.org/v1/plot v0.8.0 h1:dNgubmltsMoehfn6XgbutHpicbUfbkcGSxkICy1bC4o= |
146 | + gonum.org/v1/plot v0.8.0/go.mod h1:3GH8dTfoceRTELDnv+4HNwbvM/eMfdDUGHFG2bo3NeE= |
147 | + rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= |
148 | diff --git a/main.go b/main.go |
149 | new file mode 100644 |
150 | index 0000000..3ab820e |
151 | --- /dev/null |
152 | +++ b/main.go |
153 | @@ -0,0 +1,272 @@ |
154 | + package main |
155 | + |
156 | + import ( |
157 | + "encoding/json" |
158 | + "fmt" |
159 | + "image/color" |
160 | + "log" |
161 | + "net/http" |
162 | + "net/url" |
163 | + "os" |
164 | + "sort" |
165 | + "strconv" |
166 | + "strings" |
167 | + "time" |
168 | + |
169 | + "github.com/go-chi/chi" |
170 | + "github.com/go-chi/chi/middleware" |
171 | + "gonum.org/v1/plot" |
172 | + "gonum.org/v1/plot/plotter" |
173 | + "gonum.org/v1/plot/plotutil" |
174 | + "gonum.org/v1/plot/vg" |
175 | + ) |
176 | + |
177 | + var ( |
178 | + upstream string |
179 | + ) |
180 | + |
181 | + type PromResponse struct { |
182 | + Status string `json:"status"` |
183 | + Data struct { |
184 | + ResultType string `json:"resultType"` |
185 | + Result []struct { |
186 | + Metric map[string]string `json:"metric"` |
187 | + Values [][]interface{} `json:"values"` |
188 | + } `json:"result"` |
189 | + } `json:"data"` |
190 | + } |
191 | + |
192 | + type Datapoint struct { |
193 | + Time time.Time |
194 | + Value float64 |
195 | + } |
196 | + |
197 | + type PromResult struct { |
198 | + Metric string |
199 | + Values []Datapoint |
200 | + } |
201 | + |
202 | + func Query(q string, start time.Time, end time.Time, step int) ([]PromResult, error) { |
203 | + body := url.Values{} |
204 | + body.Set("query", q) |
205 | + body.Set("start", fmt.Sprintf("%d", start.Unix())) |
206 | + body.Set("end", fmt.Sprintf("%d", end.Unix())) |
207 | + body.Set("step", fmt.Sprintf("%d", step)) |
208 | + resp, err := http.Post(fmt.Sprintf("%s/api/v1/query_range", upstream), |
209 | + "application/x-www-form-urlencoded", strings.NewReader(body.Encode())) |
210 | + if err != nil { |
211 | + return nil, err |
212 | + } |
213 | + defer resp.Body.Close() |
214 | + |
215 | + if resp.StatusCode != 200 { |
216 | + return nil, fmt.Errorf("Received %d response from upstream", resp.StatusCode) |
217 | + } |
218 | + |
219 | + var data PromResponse |
220 | + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { |
221 | + return nil, err |
222 | + } |
223 | + |
224 | + if data.Data.ResultType != "matrix" { |
225 | + return nil, fmt.Errorf("result type isn't of type matrix: %s", |
226 | + data.Data.ResultType) |
227 | + } |
228 | + |
229 | + if len(data.Data.Result) == 0 { |
230 | + return nil, fmt.Errorf("No data") |
231 | + } |
232 | + |
233 | + var results []PromResult |
234 | + for _, res := range data.Data.Result { |
235 | + r := PromResult{} |
236 | + r.Metric = metricName(res.Metric) |
237 | + |
238 | + var values []Datapoint |
239 | + for _, vals := range res.Values { |
240 | + timestamp := vals[0].(float64) |
241 | + value := vals[1].(string) |
242 | + fv, _ := strconv.ParseFloat(value, 64) |
243 | + values = append(values, Datapoint{ |
244 | + time.Unix(int64(timestamp), 0), |
245 | + fv, |
246 | + }) |
247 | + } |
248 | + r.Values = values |
249 | + |
250 | + results = append(results, r) |
251 | + } |
252 | + return results, nil |
253 | + } |
254 | + |
255 | + func metricName(metric map[string]string) string { |
256 | + if len(metric) == 0 { |
257 | + return "{}" |
258 | + } |
259 | + |
260 | + out := "" |
261 | + var inner []string |
262 | + for key, value := range metric { |
263 | + if key == "__name__" { |
264 | + out = value |
265 | + continue |
266 | + } |
267 | + inner = append(inner, fmt.Sprintf(`%s="%s"`, key, value)) |
268 | + } |
269 | + |
270 | + if len(inner) == 0 { |
271 | + return out |
272 | + } |
273 | + |
274 | + sort.Slice(inner, func(i, j int) bool { |
275 | + return inner[i] < inner[j] |
276 | + }) |
277 | + |
278 | + return out + "{" + strings.Join(inner, ",") + "}" |
279 | + } |
280 | + |
281 | + func main() { |
282 | + plotutil.DefaultDashes = [][]vg.Length{{}} |
283 | + |
284 | + upstream = os.Args[1] |
285 | + router := chi.NewRouter() |
286 | + router.Use(middleware.RealIP) |
287 | + router.Use(middleware.Logger) |
288 | + |
289 | + router.Get("/chart.svg", func(w http.ResponseWriter, r *http.Request) { |
290 | + args := r.URL.Query() |
291 | + var query string |
292 | + if q, ok := args["query"]; !ok { |
293 | + w.WriteHeader(400) |
294 | + w.Write([]byte("Expected ?query=... parameter")) |
295 | + return |
296 | + } else { |
297 | + query = q[0] |
298 | + } |
299 | + |
300 | + start := time.Now().Add(-24 * 60 * time.Minute) |
301 | + end := time.Now() |
302 | + if s, ok := args["since"]; ok { |
303 | + d, _ := time.ParseDuration(s[0]) |
304 | + start = time.Now().Add(-d) |
305 | + } |
306 | + |
307 | + width := 12*vg.Inch |
308 | + height := 6*vg.Inch |
309 | + if ws, ok := args["width"]; ok { |
310 | + w, _ := strconv.ParseFloat(ws[0], 32) |
311 | + width = vg.Length(w)*vg.Inch |
312 | + } |
313 | + if hs, ok := args["height"]; ok { |
314 | + h, _ := strconv.ParseFloat(hs[0], 32) |
315 | + height = vg.Length(h)*vg.Inch |
316 | + } |
317 | + |
318 | + // Set step so that there's approximately 25 data points per inch |
319 | + step := int(end.Sub(start).Seconds() / (25 * float64(width / vg.Inch))) |
320 | + if s, ok := args["step"]; ok { |
321 | + d, _ := strconv.ParseInt(s[0], 10, 32) |
322 | + step = int(d) |
323 | + } |
324 | + _, stacked := args["stacked"] |
325 | + |
326 | + data, err := Query(query, start, end, step) |
327 | + if err != nil { |
328 | + w.WriteHeader(400) |
329 | + w.Write([]byte(fmt.Sprintf("%v", err))) |
330 | + return |
331 | + } |
332 | + |
333 | + p, err := plot.New() |
334 | + if err != nil { |
335 | + panic(err) |
336 | + } |
337 | + if t, ok := args["title"]; ok { |
338 | + p.Title.Text = t[0] |
339 | + } |
340 | + p.X.Label.Text = "Time" |
341 | + p.X.Tick.Marker = dateTicks{} |
342 | + if ms, ok := args["max"]; ok { |
343 | + m, _ := strconv.ParseFloat(ms[0], 64) |
344 | + p.Y.Max = m |
345 | + } |
346 | + p.Legend.Top = true |
347 | + |
348 | + sums := make([]float64, len(data[0].Values)) |
349 | + |
350 | + plotters := make([]plot.Plotter, len(data)) |
351 | + var nextColor int |
352 | + colors := plotutil.SoftColors |
353 | + for i, res := range data { |
354 | + var points plotter.XYs |
355 | + for j, d := range res.Values { |
356 | + value := d.Value |
357 | + if stacked { |
358 | + value += sums[j] |
359 | + } |
360 | + points = append(points, plotter.XY{ |
361 | + float64(d.Time.Unix()), |
362 | + value, |
363 | + }) |
364 | + sums[j] += d.Value |
365 | + } |
366 | + |
367 | + l, _, err := plotter.NewLinePoints(points) |
368 | + if err != nil { |
369 | + w.WriteHeader(400) |
370 | + w.Write([]byte(fmt.Sprintf("%v", err))) |
371 | + return |
372 | + } |
373 | + if stacked { |
374 | + l.FillColor = colors[nextColor] |
375 | + if i != len(data) - 1 { |
376 | + l.Color = color.RGBA{0, 0, 0, 0} |
377 | + } |
378 | + } else { |
379 | + l.Color = colors[nextColor] |
380 | + } |
381 | + nextColor += 1 |
382 | + if nextColor >= len(colors) { |
383 | + nextColor = 0 |
384 | + } |
385 | + plotters[i] = l |
386 | + p.Legend.Add(res.Metric, l) |
387 | + } |
388 | + for i := len(plotters) - 1; i >= 0; i-- { |
389 | + p.Add(plotters[i]) |
390 | + } |
391 | + |
392 | + writer, err := p.WriterTo(width, height, "svg") |
393 | + if err != nil { |
394 | + w.WriteHeader(400) |
395 | + w.Write([]byte(fmt.Sprintf("%v", err))) |
396 | + return |
397 | + } |
398 | + |
399 | + w.Header().Add("Content-Type", "image/svg+xml") |
400 | + writer.WriteTo(w) |
401 | + }) |
402 | + |
403 | + addr := ":8142" |
404 | + if len(os.Args) > 2 { |
405 | + addr = os.Args[2] |
406 | + } |
407 | + log.Printf("Listening on %s", addr) |
408 | + http.ListenAndServe(addr, router) |
409 | + } |
410 | + |
411 | + type dateTicks struct{} |
412 | + // Ticks computes the default tick marks, but inserts commas |
413 | + // into the labels for the major tick marks. |
414 | + func (dateTicks) Ticks(min, max float64) []plot.Tick { |
415 | + tks := plot.DefaultTicks{}.Ticks(min, max) |
416 | + for i, t := range tks { |
417 | + if t.Label == "" { // Skip minor ticks, they are fine. |
418 | + continue |
419 | + } |
420 | + d, _ := strconv.ParseFloat(t.Label, 64) |
421 | + tm := time.Unix(int64(d), 0) |
422 | + tks[i].Label = tm.Format("15:04:05") |
423 | + } |
424 | + return tks |
425 | + } |