Code

server: Add support for GroupBy in graphs.
[sysdb/webui.git] / server / query.go
index cb74f22f479dcd4bbec5dcd787b82f0fdb992c5c..22385bf07de13e576e03007b179ede5fd6e97000 100644 (file)
@@ -30,13 +30,12 @@ package server
 import (
        "errors"
        "fmt"
-       "log"
        "strings"
        "time"
        "unicode"
 
+       "github.com/sysdb/go/client"
        "github.com/sysdb/go/proto"
-       "github.com/sysdb/go/sysdb"
 )
 
 func listAll(req request, s *Server) (*page, error) {
@@ -44,7 +43,11 @@ func listAll(req request, s *Server) (*page, error) {
                return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
        }
 
-       res, err := s.query("LIST %s", identifier(req.cmd))
+       q, err := client.QueryString("LIST %s", client.Identifier(req.cmd))
+       if err != nil {
+               return nil, err
+       }
+       res, err := s.c.Query(q)
        if err != nil {
                return nil, err
        }
@@ -56,52 +59,39 @@ func lookup(req request, s *Server) (*page, error) {
        if req.r.Method != "POST" {
                return nil, errors.New("Method not allowed")
        }
-       tokens, err := tokenize(req.r.PostForm.Get("query"))
+       raw, err := parseQuery(req.r.PostForm.Get("query"))
        if err != nil {
                return nil, err
        }
-       if len(tokens) == 0 {
-               return nil, errors.New("Empty query")
-       }
 
-       typ := "hosts"
+       if raw.typ == "" {
+               raw.typ = "hosts"
+       }
        var args string
-       for i, tok := range tokens {
+       for name, value := range raw.args {
                if len(args) > 0 {
                        args += " AND"
                }
 
-               if fields := strings.SplitN(tok, ":", 2); len(fields) == 2 {
-                       // Query: [<type>:] [<sibling-type>.]<attribute>:<value> ...
-                       if i == 0 && fields[1] == "" {
-                               typ = fields[0]
-                       } else if elems := strings.Split(fields[0], "."); len(elems) > 1 {
-                               objs := elems[:len(elems)-1]
-                               for _, o := range objs {
-                                       if o != "host" && o != "service" && o != "metric" {
-                                               return nil, fmt.Errorf("Invalid object type %q", o)
-                                       }
-                               }
-                               args += fmt.Sprintf(" %s.attribute[%s] = %s",
-                                       strings.Join(objs, "."), proto.EscapeString(elems[len(elems)-1]),
-                                       proto.EscapeString(fields[1]))
-                       } else {
-                               args += fmt.Sprintf(" attribute[%s] = %s",
-                                       proto.EscapeString(fields[0]), proto.EscapeString(fields[1]))
-                       }
+               if name == "name" {
+                       args += fmt.Sprintf(" name =~ %s", value)
                } else {
-                       args += fmt.Sprintf(" name =~ %s", proto.EscapeString(tok))
+                       args += fmt.Sprintf(" %s = %s", name, value)
                }
        }
 
-       res, err := s.query("LOOKUP %s MATCHING"+args, identifier(typ))
+       q, err := client.QueryString("LOOKUP %s MATCHING"+args, client.Identifier(raw.typ))
+       if err != nil {
+               return nil, err
+       }
+       res, err := s.c.Query(q)
        if err != nil {
                return nil, err
        }
-       if t, ok := s.results[typ]; ok {
+       if t, ok := s.results[raw.typ]; ok {
                return tmpl(t, res)
        }
-       return nil, fmt.Errorf("Unsupported type %s", typ)
+       return nil, fmt.Errorf("Unsupported type %s", raw.typ)
 }
 
 func fetch(req request, s *Server) (*page, error) {
@@ -109,31 +99,55 @@ func fetch(req request, s *Server) (*page, error) {
                return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
        }
 
-       var res interface{}
+       var q string
        var err error
        switch req.cmd {
        case "host":
                if len(req.args) != 1 {
                        return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
                }
-               res, err = s.query("FETCH host %s", req.args[0])
+               q, err = client.QueryString("FETCH host %s", req.args[0])
        case "service", "metric":
                if len(req.args) != 2 {
                        return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
                }
-               res, err = s.query("FETCH %s %s.%s", identifier(req.cmd), req.args[0], req.args[1])
-               if err == nil && req.cmd == "metric" {
-                       return metric(req, res, s)
-               }
+               q, err = client.QueryString("FETCH %s %s.%s", client.Identifier(req.cmd), req.args[0], req.args[1])
        default:
                panic("Unknown request: fetch(" + req.cmd + ")")
        }
        if err != nil {
                return nil, err
        }
+
+       res, err := s.c.Query(q)
+       if err != nil {
+               return nil, err
+       }
+       if req.cmd == "metric" {
+               return metric(req, res, s)
+       }
        return tmpl(s.results[req.cmd], res)
 }
 
+func graphs(req request, s *Server) (*page, error) {
+       p := struct {
+               Query, Metrics string
+               QueryOptions   string
+               GroupBy        string
+       }{
+               Query:   req.r.PostForm.Get("metrics-query"),
+               GroupBy: req.r.PostForm.Get("group-by"),
+       }
+
+       if req.r.Method == "POST" {
+               p.Metrics = p.Query
+               if p.GroupBy != "" {
+                       p.QueryOptions += "/g=" + strings.Join(strings.Fields(p.GroupBy), ",")
+               }
+       }
+       return tmpl(s.results["graphs"], &p)
+}
+
 var datetime = "2006-01-02 15:04:05"
 
 func metric(req request, res interface{}, s *Server) (*page, error) {
@@ -170,6 +184,69 @@ func metric(req request, res interface{}, s *Server) (*page, error) {
        return tmpl(s.results["metric"], &p)
 }
 
+type query struct {
+       typ  string
+       args map[string]string
+}
+
+func (q *query) arg(name, value string) error {
+       if _, ok := q.args[name]; ok {
+               return fmt.Errorf("Duplicate key %q", name)
+       }
+       q.args[name] = proto.EscapeString(value)
+       return nil
+}
+
+func (q *query) attr(parent, name, value string) error {
+       var k string
+       if parent != "" {
+               k = fmt.Sprintf("%s.attribute[%s]", parent, proto.EscapeString(name))
+       } else {
+               k = fmt.Sprintf("attribute[%s]", proto.EscapeString(name))
+       }
+
+       return q.arg(k, value)
+}
+
+func parseQuery(s string) (*query, error) {
+       tokens, err := tokenize(s)
+       if err != nil {
+               return nil, err
+       }
+       if len(tokens) == 0 {
+               return nil, errors.New("Empty query")
+       }
+
+       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> ...
+                       if i == 0 && fields[1] == "" {
+                               q.typ = fields[0]
+                       } else if elems := strings.Split(fields[0], "."); len(elems) > 1 {
+                               objs := elems[:len(elems)-1]
+                               for _, o := range objs {
+                                       if o != "host" && o != "service" && o != "metric" {
+                                               return nil, fmt.Errorf("Invalid object type %q", o)
+                                       }
+                               }
+                               if err := q.attr(strings.Join(objs, "."), elems[len(elems)-1], fields[1]); err != nil {
+                                       return nil, err
+                               }
+                       } else {
+                               if err := q.attr("", fields[0], fields[1]); err != nil {
+                                       return nil, err
+                               }
+                       }
+               } else {
+                       if err := q.arg("name", tok); err != nil {
+                               return nil, err
+                       }
+               }
+       }
+       return q, nil
+}
+
 // tokenize split the string s into its tokens where a token is either a quoted
 // string or surrounded by one or more consecutive whitespace characters.
 func tokenize(s string) ([]string, error) {
@@ -267,73 +344,4 @@ func (s *scanner) inField(r rune) bool {
        return !unicode.IsSpace(r)
 }
 
-type identifier string
-
-func (s *Server) query(cmd string, args ...interface{}) (interface{}, error) {
-       c := <-s.conns
-       defer func() { s.conns <- c }()
-
-       for i, arg := range args {
-               switch v := arg.(type) {
-               case identifier:
-                       // Nothing to do.
-               case string:
-                       args[i] = proto.EscapeString(v)
-               case time.Time:
-                       args[i] = v.Format(datetime)
-               default:
-                       panic(fmt.Sprintf("query: invalid type %T", arg))
-               }
-       }
-
-       cmd = fmt.Sprintf(cmd, args...)
-       m := &proto.Message{
-               Type: proto.ConnectionQuery,
-               Raw:  []byte(cmd),
-       }
-       if err := c.Send(m); err != nil {
-               return nil, fmt.Errorf("Query %q: %v", cmd, err)
-       }
-
-       for {
-               m, err := c.Receive()
-               if err != nil {
-                       return nil, fmt.Errorf("Failed to receive server response: %v", err)
-               }
-               if m.Type == proto.ConnectionLog {
-                       log.Println(string(m.Raw[4:]))
-                       continue
-               } else if m.Type == proto.ConnectionError {
-                       return nil, errors.New(string(m.Raw))
-               }
-
-               t, err := m.DataType()
-               if err != nil {
-                       return nil, fmt.Errorf("Failed to unmarshal response: %v", err)
-               }
-
-               var res interface{}
-               switch t {
-               case proto.HostList:
-                       var hosts []sysdb.Host
-                       err = proto.Unmarshal(m, &hosts)
-                       res = hosts
-               case proto.Host:
-                       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)
-               }
-               if err != nil {
-                       return nil, fmt.Errorf("Failed to unmarshal response: %v", err)
-               }
-               return res, nil
-       }
-}
-
 // vim: set tw=78 sw=4 sw=4 noexpandtab :