Code

Add a separate package for handling graphs and plots.
authorSebastian Harl <sh@tokkee.org>
Tue, 12 May 2015 21:13:01 +0000 (23:13 +0200)
committerSebastian Harl <sh@tokkee.org>
Tue, 12 May 2015 21:13:01 +0000 (23:13 +0200)
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).

graph/graph.go [new file with mode: 0644]
server/graph.go

diff --git a/graph/graph.go b/graph/graph.go
new file mode 100644 (file)
index 0000000..cb1852c
--- /dev/null
@@ -0,0 +1,128 @@
+//
+// Copyright (C) 2014-2015 Sebastian 'tokkee' Harl <sh@tokkee.org>
+// 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: {<hostname>, <identifier>}
+       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 :
index ed5c7fd8338cedb694944674e0cdfe1c440cdca0..9c679bc5a0386a9ed48a4900a8a5c7d9a7f7af27 100644 (file)
@@ -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 :