Code

Add rudimentary support for dynamic graphs based on a metrics query.
authorSebastian Harl <sh@tokkee.org>
Thu, 14 May 2015 10:42:51 +0000 (12:42 +0200)
committerSebastian Harl <sh@tokkee.org>
Thu, 14 May 2015 10:46:13 +0000 (12:46 +0200)
A new page now allows to query metrics using the simple query language of the
webUI. All returned metrics will be thrown into a single graph.

server/graph.go
server/query.go
server/server.go
static/style/main.css
templates/graphs.tmpl [new file with mode: 0644]
templates/main.tmpl

index 7509485930e23f60f1dc140e8776d3d0b473c8b5..ddc8c4d17e5f6aec59531fbcf2a5e4f33b50c852 100644 (file)
@@ -35,6 +35,7 @@ import (
        "time"
 
        "github.com/gonum/plot/vg"
+       "github.com/sysdb/go/sysdb"
        "github.com/sysdb/webui/graph"
 )
 
@@ -61,11 +62,20 @@ func (s *Server) graph(w http.ResponseWriter, req request) {
                        return
                }
        }
+
        g := &graph.Graph{
-               Start:   start,
-               End:     end,
-               Metrics: []graph.Metric{{Hostname: req.args[0], Identifier: req.args[1]}},
+               Start: start,
+               End:   end,
+       }
+       if req.args[0] == "q" {
+               if g.Metrics, err = s.queryMetrics(req.args[1]); err != nil {
+                       s.badrequest(w, fmt.Errorf("Failed to query metrics: %v", err))
+                       return
+               }
+       } else {
+               g.Metrics = []graph.Metric{{Hostname: req.args[0], Identifier: req.args[1]}}
        }
+
        p, err := g.Plot(s.c)
        if err != nil {
                s.internal(w, err)
@@ -88,4 +98,43 @@ func (s *Server) graph(w http.ResponseWriter, req request) {
        io.Copy(w, &buf)
 }
 
+func (s *Server) queryMetrics(q string) ([]graph.Metric, error) {
+       raw, err := parseQuery(q)
+       if err != nil {
+               return nil, err
+       }
+       if raw.typ != "" && raw.typ != "metrics" {
+               return nil, fmt.Errorf("Invalid object type %q for graphs", raw.typ)
+       }
+
+       var args string
+       for name, value := range raw.args {
+               if len(args) > 0 {
+                       args += " AND"
+               }
+
+               if name == "name" {
+                       args += fmt.Sprintf(" name =~ %s", value)
+               } else {
+                       args += fmt.Sprintf(" %s = %s", name, value)
+               }
+       }
+
+       res, err := s.c.Query("LOOKUP metrics MATCHING" + args)
+       if err != nil {
+               return nil, err
+       }
+       hosts, ok := res.([]sysdb.Host)
+       if !ok {
+               return nil, fmt.Errorf("LOOKUP did not return a list of hosts but %T", res)
+       }
+       var metrics []graph.Metric
+       for _, h := range hosts {
+               for _, m := range h.Metrics {
+                       metrics = append(metrics, graph.Metric{Hostname: h.Name, Identifier: m.Name})
+               }
+       }
+       return metrics, nil
+}
+
 // vim: set tw=78 sw=4 sw=4 noexpandtab :
index 962e3ef56c704ae3548abf52155da114954f1dcd..bde450fef97dc74911e4aa516b859a3dba85afe7 100644 (file)
@@ -64,6 +64,9 @@ func lookup(req request, s *Server) (*page, error) {
                return nil, err
        }
 
+       if raw.typ == "" {
+               raw.typ = "hosts"
+       }
        var args string
        for name, value := range raw.args {
                if len(args) > 0 {
@@ -126,6 +129,19 @@ func fetch(req request, s *Server) (*page, error) {
        return tmpl(s.results[req.cmd], res)
 }
 
+func graphs(req request, s *Server) (*page, error) {
+       p := struct {
+               Query, Metrics string
+       }{
+               Query: req.r.PostForm.Get("metrics-query"),
+       }
+
+       if req.r.Method == "POST" {
+               p.Metrics = p.Query
+       }
+       return tmpl(s.results["graphs"], &p)
+}
+
 var datetime = "2006-01-02 15:04:05"
 
 func metric(req request, res interface{}, s *Server) (*page, error) {
@@ -195,7 +211,7 @@ func parseQuery(s string) (*query, error) {
                return nil, errors.New("Empty query")
        }
 
-       q := &query{typ: "hosts", args: make(map[string]string)}
+       q := &query{args: make(map[string]string)}
        for i, tok := range tokens {
                if fields := strings.SplitN(tok, ":", 2); len(fields) == 2 {
                        // Query: [<type>:] [<sibling-type>.]<attribute>:<value> ...
index 8d0d72dc191770f6810896306ea3f0f3beca64b5..1f5ea94a3879f8a26ddb98a27455393e5eeebca6 100644 (file)
@@ -79,7 +79,7 @@ func New(addr, user string, cfg Config) (*Server, error) {
        if s.main, err = cfg.parse("main.tmpl"); err != nil {
                return nil, err
        }
-       types := []string{"host", "hosts", "service", "services", "metric", "metrics"}
+       types := []string{"graphs", "host", "hosts", "service", "services", "metric", "metrics"}
        for _, t := range types {
                s.results[t], err = cfg.parse(t + ".tmpl")
                if err != nil {
@@ -120,6 +120,7 @@ var content = map[string]func(request, *Server) (*page, error){
        "": index,
 
        // Queries
+       "graphs":   graphs,
        "host":     fetch,
        "service":  fetch,
        "metric":   fetch,
index 4127602aa6de6c352091f2c43f73abf5964f51e1..1bcae8641f89521ca6ba6c8693e29c0ca17918fc 100644 (file)
@@ -173,6 +173,13 @@ input[type=text].datetime {
        padding: 0px 3px;
 }
 
+input[type=text].query {
+       width: 25em;
+       height: 25px;
+       border: 1px solid #000;
+       padding: 0px 3px;
+}
+
 button {
        background-color: #000;
        color: #fff;
diff --git a/templates/graphs.tmpl b/templates/graphs.tmpl
new file mode 100644 (file)
index 0000000..43ec622
--- /dev/null
@@ -0,0 +1,12 @@
+<section>
+       <h1>Graphs</h1>
+       <form action="/graphs" method="POST">
+               <input type="text" name="metrics-query" value="{{.Query}}"
+                      class="query" placeholder="Search metrics" required />
+               <button type="submit">GO</button>
+       </form><br />
+{{if .Metrics}}
+       <img src="/graph/q/{{.Metrics}}" border="0" />
+{{end}}
+       <p>&nbsp;</p>
+</section>
index 254a22ab714cbeb97f0a13bae3197d09338a0d99..e6049ebd41bc31b6cf04c049886b4b4db6daeba9 100644 (file)
@@ -38,6 +38,7 @@
                        <a href="/hosts">Hosts</a>
                        <a href="/services">Services</a>
                        <a href="/metrics">Metrics</a>
+                       <a href="/graphs">Graphs</a>
                </nav></aside>
 
                <div class="content">