From: Sebastian Harl Date: Tue, 12 May 2015 21:13:01 +0000 (+0200) Subject: Add a separate package for handling graphs and plots. X-Git-Url: https://git.tokkee.org/?p=sysdb%2Fwebui.git;a=commitdiff_plain;h=ac8367fc6973d9856cc88c83c1ccc9a78c978723 Add a separate package for handling graphs and plots. For now, the package is a simple wrapper around github.com/gonum/plot. It supports graphing multiple metrics in the same graph (even though that's not used by the server yet). --- diff --git a/graph/graph.go b/graph/graph.go new file mode 100644 index 0000000..cb1852c --- /dev/null +++ b/graph/graph.go @@ -0,0 +1,128 @@ +// +// Copyright (C) 2014-2015 Sebastian 'tokkee' Harl +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Package graph handles time-series data provided by SysDB. It supports +// querying and post-processing of the data. +package graph + +import ( + "fmt" + "time" + + "github.com/gonum/plot" + "github.com/gonum/plot/plotter" + "github.com/gonum/plot/plotutil" + "github.com/sysdb/go/client" + "github.com/sysdb/go/sysdb" +) + +// A Graph represents a single graph. It may reference multiple time-series. +type Graph struct { + // Time range of the graph. + Start, End time.Time + + // Metrics: {, } + Metrics [][2]string +} + +type pl struct { + *plot.Plot + + ts int // Index of the current time-series. +} + +func (p *pl) addTimeseries(c *client.Client, metric [2]string, start, end time.Time) error { + q, err := client.QueryString("TIMESERIES %s.%s START %s END %s", metric[0], metric[1], start, end) + if err != nil { + return fmt.Errorf("Failed to retrieve graph data: %v", err) + } + res, err := c.Query(q) + if err != nil { + return fmt.Errorf("Failed to retrieve graph data: %v", err) + } + + ts, ok := res.(*sysdb.Timeseries) + if !ok { + return fmt.Errorf("TIMESERIES did not return a time-series but %T", res) + } + + for name, data := range ts.Data { + pts := make(plotter.XYs, len(data)) + for i, p := range data { + pts[i].X = float64(time.Time(p.Timestamp).UnixNano()) + pts[i].Y = p.Value + } + + l, err := plotter.NewLine(pts) + if err != nil { + return fmt.Errorf("Failed to create line plotter: %v", err) + } + l.LineStyle.Color = plotutil.DarkColors[p.ts%len(plotutil.DarkColors)] + + p.Add(l) + p.Legend.Add(name, l) + p.ts++ + } + return nil +} + +// Plot fetches a graph's time-series data using the specified client and +// plots it. +func (g *Graph) Plot(c *client.Client) (*plot.Plot, error) { + var err error + + p := &pl{} + p.Plot, err = plot.New() + if err != nil { + return nil, fmt.Errorf("Failed to create plot: %v", err) + } + p.Add(plotter.NewGrid()) + p.X.Tick.Marker = dateTicks{} + + for _, m := range g.Metrics { + if err := p.addTimeseries(c, m, g.Start, g.End); err != nil { + return nil, err + } + } + return p.Plot, nil +} + +type dateTicks struct{} + +func (dateTicks) Ticks(min, max float64) []plot.Tick { + // TODO: this is surely not the best we can do + // but it'll distribute ticks evenly. + ticks := plot.DefaultTicks{}.Ticks(min, max) + for i, t := range ticks { + if t.Label == "" { + // Skip minor ticks. + continue + } + ticks[i].Label = time.Unix(0, int64(t.Value)).Format(time.RFC822) + } + return ticks +} + +// vim: set tw=78 sw=4 sw=4 noexpandtab : diff --git a/server/graph.go b/server/graph.go index ed5c7fd..9c679bc 100644 --- a/server/graph.go +++ b/server/graph.go @@ -29,18 +29,13 @@ package server import ( "bytes" - "errors" "fmt" "io" "net/http" "time" - "github.com/gonum/plot" - "github.com/gonum/plot/plotter" - "github.com/gonum/plot/plotutil" "github.com/gonum/plot/vg" - "github.com/sysdb/go/client" - "github.com/sysdb/go/sysdb" + "github.com/sysdb/webui/graph" ) var urldate = "20060102150405" @@ -66,53 +61,16 @@ func (s *Server) graph(w http.ResponseWriter, req request) { return } } - if start.Equal(end) || start.After(end) { - s.badrequest(w, fmt.Errorf("START(%v) is greater than or equal to END(%v)", start, end)) - return - } - - q, err := client.QueryString("TIMESERIES %s.%s START %s END %s", req.args[0], req.args[1], start, end) - if err != nil { - s.internal(w, fmt.Errorf("Failed to retrieve graph data: %v", err)) - return - } - res, err := s.c.Query(q) - if err != nil { - s.internal(w, fmt.Errorf("Failed to retrieve graph data: %v", err)) - return - } - - ts, ok := res.(*sysdb.Timeseries) - if !ok { - s.internal(w, errors.New("TIMESERIES did not return a time-series")) - return + g := &graph.Graph{ + Start: start, + End: end, + Metrics: [][2]string{{req.args[0], req.args[1]}}, } - - p, err := plot.New() + p, err := g.Plot(s.c) if err != nil { - s.internal(w, fmt.Errorf("Failed to create plot: %v", err)) + s.internal(w, err) return } - p.Add(plotter.NewGrid()) - p.X.Tick.Marker = dateTicks{} - - var i int - for name, data := range ts.Data { - pts := make(plotter.XYs, len(data)) - for i, p := range data { - pts[i].X = float64(time.Time(p.Timestamp).UnixNano()) - pts[i].Y = p.Value - } - l, err := plotter.NewLine(pts) - if err != nil { - s.internal(w, fmt.Errorf("Failed to create line plotter: %v", err)) - return - } - l.LineStyle.Color = plotutil.DarkColors[i%len(plotutil.DarkColors)] - p.Add(l) - p.Legend.Add(name, l) - i++ - } pw, err := p.WriterTo(vg.Length(500), vg.Length(200), "svg") if err != nil { @@ -130,20 +88,4 @@ func (s *Server) graph(w http.ResponseWriter, req request) { io.Copy(w, &buf) } -type dateTicks struct{} - -func (dateTicks) Ticks(min, max float64) []plot.Tick { - // TODO: this is surely not the best we can do - // but it'll distribute ticks evenly. - ticks := plot.DefaultTicks{}.Ticks(min, max) - for i, t := range ticks { - if t.Label == "" { - // Skip minor ticks. - continue - } - ticks[i].Label = time.Unix(0, int64(t.Value)).Format(time.RFC822) - } - return ticks -} - // vim: set tw=78 sw=4 sw=4 noexpandtab :