Code

Added support for simple SVG graphs for metrics.
authorSebastian Harl <sh@tokkee.org>
Fri, 21 Nov 2014 20:23:57 +0000 (21:23 +0100)
committerSebastian Harl <sh@tokkee.org>
Fri, 21 Nov 2014 20:23:57 +0000 (21:23 +0100)
The graph will be displayed when querying a metric. The graph itself mostly
uses default settings of the Plotinum package for now.

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

diff --git a/server/graph.go b/server/graph.go
new file mode 100644 (file)
index 0000000..054fe8e
--- /dev/null
@@ -0,0 +1,105 @@
+//
+// Copyright (C) 2014 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 server
+
+// Helper functions for handling and plotting graphs.
+
+import (
+       "bytes"
+       "errors"
+       "fmt"
+       "io"
+       "net/http"
+       "strings"
+       "time"
+
+       "code.google.com/p/plotinum/plot"
+       "code.google.com/p/plotinum/plotter"
+       "code.google.com/p/plotinum/plotutil"
+       "code.google.com/p/plotinum/vg"
+       "code.google.com/p/plotinum/vg/vgsvg"
+       "github.com/sysdb/go/proto"
+       "github.com/sysdb/go/sysdb"
+)
+
+func (s *Server) graph(w http.ResponseWriter, req request) {
+       if len(req.args) < 2 {
+               s.internal(w, fmt.Errorf("Missing host/metric information"))
+       }
+
+       host := proto.EscapeString(req.args[0])
+       metric := proto.EscapeString(strings.Join(req.args[1:], "/"))
+       res, err := s.query(fmt.Sprintf("TIMESERIES %s.%s", host, metric))
+       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
+       }
+
+       p, err := plot.New()
+       if err != nil {
+               s.internal(w, fmt.Errorf("Failed to create plot: %v", err))
+               return
+       }
+       p.Add(plotter.NewGrid())
+
+       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++
+       }
+
+       c := vgsvg.New(vg.Length(500), vg.Length(200))
+       p.Draw(plot.MakeDrawArea(c))
+
+       var buf bytes.Buffer
+       if _, err := c.WriteTo(&buf); err != nil {
+               s.internal(w, fmt.Errorf("Failed to write plot: %v", err))
+               return
+       }
+       w.Header().Set("Content-Type", "image/svg+xml")
+       w.WriteHeader(http.StatusOK)
+       io.Copy(w, &buf)
+}
+
+// vim: set tw=78 sw=4 sw=4 noexpandtab :
index 33a2536f3b6fd491604cea59dbde8099460d91cc..59c1e4215a468bea1a3a36813eafc97014bfac99 100644 (file)
@@ -104,6 +104,7 @@ func New(cfg Config) (*Server, error) {
        s.mux = map[string]handler{
                "images": s.static,
                "style":  s.static,
+               "graph":  s.graph,
        }
        return s, nil
 }
@@ -317,6 +318,10 @@ func (s *Server) query(cmd string) (interface{}, error) {
                        var host sysdb.Host
                        err = proto.Unmarshal(m, &host)
                        res = host
+               case proto.Timeseries:
+                       var ts sysdb.Timeseries
+                       err = proto.Unmarshal(m, &ts)
+                       res = ts
                default:
                        return nil, fmt.Errorf("Unsupported data type %d", t)
                }
index e4a0efb7c7417d75a621564f0ef69ea36a02593f..a2648017ec47b18e2678dc03979e33a832b245be 100644 (file)
@@ -1,5 +1,6 @@
 <section>{{$m := index .Metrics 0}}
        <h1>Metric {{$.Name}} &mdash; {{$m.Name}}</h1>
+       <img src="/graph/{{urlquery $.Name}}/{{urlquery $m.Name}}" border="0" />
        <table class="results">
                <tr><td><b>Host</b></td><td><a href="/host/{{urlquery $.Name}}">{{$.Name}}</a></td></tr>
                <tr><td><b>Last update</b></td><td>{{$m.LastUpdate}}</td></tr>